diff --git a/net-cli/src/main/java/org/jline/builtins/Commands.java b/net-cli/src/main/java/org/jline/builtins/Commands.java new file mode 100644 index 0000000..995954b --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/Commands.java @@ -0,0 +1,1626 @@ +package org.jline.builtins; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jline.builtins.Completers.CompletionData; +import org.jline.builtins.Options.HelpException; +import org.jline.builtins.Source.StdInSource; +import org.jline.builtins.Source.URLSource; +import org.jline.keymap.KeyMap; +import org.jline.reader.Binding; +import org.jline.reader.Highlighter; +import org.jline.reader.History; +import org.jline.reader.LineReader; +import org.jline.reader.LineReader.Option; +import org.jline.reader.Macro; +import org.jline.reader.Reference; +import org.jline.reader.Widget; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.StyleResolver; +import static org.jline.builtins.SyntaxHighlighter.COMMAND_THEME; + +public class Commands { + + public static void tmux( + Terminal terminal, + PrintStream out, + PrintStream err, + Supplier getter, + Consumer setter, + Consumer runner, + String[] argv) + throws Exception { + final String[] usage = { + "tmux - terminal multiplexer", "Usage: tmux [command]", " -? --help Show help", + }; + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + // Tmux with no args + if (argv.length == 0) { + Object instance = getter.get(); + if (instance != null) { + err.println("tmux: can't run tmux inside itself"); + } else { + Tmux tmux = new Tmux(terminal, err, runner); + setter.accept(tmux); + try { + tmux.run(); + } finally { + setter.accept(null); + } + } + } else { + Object instance = getter.get(); + if (instance != null) { + ((Tmux) instance).execute(out, err, Arrays.asList(argv)); + } else { + err.println("tmux: no instance running"); + } + } + } + + public static void less( + Terminal terminal, InputStream in, PrintStream out, PrintStream err, Path currentDir, Object[] argv) + throws Exception { + less(terminal, in, out, err, currentDir, argv, null); + } + + public static void less( + Terminal terminal, + InputStream in, + PrintStream out, + PrintStream err, + Path currentDir, + Object[] argv, + ConfigurationPath configPath) + throws Exception { + Options opt = Options.compile(Less.usage()).parse(argv); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + Less less = new Less(terminal, currentDir, opt, configPath); + List sources = new ArrayList<>(); + if (opt.argObjects().isEmpty()) { + opt.argObjects().add("-"); + } + + for (Object o : opt.argObjects()) { + if (o instanceof String arg) { + arg = arg.startsWith("~") ? arg.replace("~", System.getProperty("user.home")) : arg; + if ("-".equals(arg)) { + sources.add(new StdInSource(in)); + } else if (arg.contains("*") || arg.contains("?")) { + for (Path p : findFiles(currentDir, arg)) { + sources.add(new URLSource(p.toUri().toURL(), p.toString())); + } + } else { + sources.add(new URLSource(currentDir.resolve(arg).toUri().toURL(), arg)); + } + } else if (o instanceof Source) { + sources.add((Source) o); + } else { + ByteArrayInputStream bais = null; + if (o instanceof String[]) { + bais = new ByteArrayInputStream( + String.join("\n", (String[]) o).getBytes()); + } else if (o instanceof ByteArrayInputStream) { + bais = (ByteArrayInputStream) o; + } else if (o instanceof byte[]) { + bais = new ByteArrayInputStream((byte[]) o); + } + if (bais != null) { + sources.add(new Source.InputStreamSource(bais, true, "Less")); + } + } + } + less.run(sources); + } + + protected static List findFiles(Path root, String files) throws IOException { + files = files.startsWith("~") ? files.replace("~", System.getProperty("user.home")) : files; + String regex = files; + Path searchRoot = Paths.get("/"); + if (new File(files).isAbsolute()) { + regex = regex.replaceAll("\\\\", "/").replaceAll("//", "/"); + if (regex.contains("/")) { + String sr = regex.substring(0, regex.lastIndexOf("/") + 1); + while (sr.contains("*") || sr.contains("?")) { + sr = sr.substring(0, sr.lastIndexOf("/")); + } + searchRoot = Paths.get(sr + "/"); + } + } else { + regex = (root.toString().length() == 0 ? "" : root + "/") + files; + regex = regex.replaceAll("\\\\", "/").replaceAll("//", "/"); + searchRoot = root; + } + PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + regex); + try (Stream pathStream = Files.walk(searchRoot)) { + return pathStream.filter(pathMatcher::matches).collect(Collectors.toList()); + } + } + + public static void history(LineReader reader, PrintStream out, PrintStream err, Path currentDir, String[] argv) + throws Exception { + final String[] usage = { + "history - list history of commands", + "Usage: history [-dnrfEie] [-m match] [first] [last]", + " history -ARWI [filename]", + " history -s [old=new] [command]", + " history --clear", + " history --save", + " -? --help Displays command help", + " --clear Clear history", + " --save Save history", + " -m match If option -m is present the first argument is taken as a pattern", + " and only the history events matching the pattern will be shown", + " -d Print timestamps for each event", + " -f Print full time date stamps in the US format", + " -E Print full time date stamps in the European format", + " -i Print full time date stamps in ISO8601 format", + " -n Suppresses command numbers", + " -r Reverses the order of the commands", + " -A Appends the history out to the given file", + " -R Reads the history from the given file", + " -W Writes the history out to the given file", + " -I If added to -R, only the events that are not contained within the internal list are added", + " If added to -W or -A, only the events that are new since the last incremental operation", + " to the file are added", + " [first] [last] These optional arguments may be specified as a number or as a string. A negative number", + " is used as an offset to the current history event number. A string specifies the most", + " recent event beginning with the given string.", + " -s Re-executes the command without invoking an editor" + }; + Options opt = Options.compile(usage).parse(argv); + + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + History history = reader.getHistory(); + boolean done = true; + boolean increment = opt.isSet("I"); + if (opt.isSet("clear")) { + history.purge(); + } else if (opt.isSet("save")) { + history.save(); + } else if (opt.isSet("A")) { + Path file = opt.args().size() > 0 ? currentDir.resolve(opt.args().get(0)) : null; + history.append(file, increment); + } else if (opt.isSet("R")) { + Path file = opt.args().size() > 0 ? currentDir.resolve(opt.args().get(0)) : null; + history.read(file, increment); + } else if (opt.isSet("W")) { + Path file = opt.args().size() > 0 ? currentDir.resolve(opt.args().get(0)) : null; + history.write(file, increment); + } else { + done = false; + } + if (done) { + return; + } + ReExecute execute = new ReExecute(history, opt); + int argId = execute.getArgId(); + + Pattern pattern = null; + if (opt.isSet("m") && opt.args().size() > argId) { + StringBuilder sb = new StringBuilder(); + char prev = '0'; + for (char c : opt.args().get(argId++).toCharArray()) { + if (c == '*' && prev != '\\' && prev != '.') { + sb.append('.'); + } + sb.append(c); + prev = c; + } + pattern = Pattern.compile(sb.toString(), Pattern.DOTALL); + } + boolean reverse = opt.isSet("r") || (opt.isSet("s") && opt.args().size() <= argId); + int firstId = opt.args().size() > argId + ? retrieveHistoryId(history, opt.args().get(argId++)) + : -17; + int lastId = opt.args().size() > argId + ? retrieveHistoryId(history, opt.args().get(argId++)) + : -1; + firstId = historyId(firstId, history.first(), history.last()); + lastId = historyId(lastId, history.first(), history.last()); + if (firstId > lastId) { + int tmpId = firstId; + firstId = lastId; + lastId = tmpId; + reverse = !reverse; + } + int tot = lastId - firstId + 1; + int listed = 0; + final Highlighter highlighter = reader.getHighlighter(); + Iterator iter = null; + if (reverse) { + iter = history.reverseIterator(lastId); + } else { + iter = history.iterator(firstId); + } + + while (iter.hasNext() && listed < tot) { + History.Entry entry = iter.next(); + listed++; + if (pattern != null && !pattern.matcher(entry.line()).matches()) { + continue; + } + if (execute.isExecute()) { + if (execute.isEdit()) { + execute.addCommandInFile(entry.line()); + } else { + execute.addCommandInBuffer(reader, entry.line()); + break; + } + } else { + AttributedStringBuilder sb = new AttributedStringBuilder(); + if (!opt.isSet("n")) { + sb.append(" "); + sb.styled(AttributedStyle::bold, String.format("%3d", entry.index())); + } + if (opt.isSet("d") || opt.isSet("f") || opt.isSet("E") || opt.isSet("i")) { + sb.append(" "); + if (opt.isSet("d")) { + LocalTime lt = LocalTime.from(entry.time().atZone(ZoneId.systemDefault())) + .truncatedTo(ChronoUnit.SECONDS); + DateTimeFormatter.ISO_LOCAL_TIME.formatTo(lt, sb); + } else { + LocalDateTime lt = LocalDateTime.from( + entry.time().atZone(ZoneId.systemDefault()).truncatedTo(ChronoUnit.MINUTES)); + String format = "yyyy-MM-dd hh:mm"; + if (opt.isSet("f")) { + format = "MM/dd/yy hh:mm"; + } else if (opt.isSet("E")) { + format = "dd.MM.yyyy hh:mm"; + } + DateTimeFormatter.ofPattern(format).formatTo(lt, sb); + } + } + sb.append(" "); + sb.append(highlighter.highlight(reader, entry.line())); + out.println(sb.toAnsi(reader.getTerminal())); + } + } + //execute.editCommandsAndClose(reader); + } + + private static int historyId(int id, int minId, int maxId) { + int out = id; + if (id < 0) { + out = maxId + id + 1; + } + if (out < minId) { + out = minId; + } else if (out > maxId) { + out = maxId; + } + return out; + } + + private static int retrieveHistoryId(History history, String s) throws IllegalArgumentException { + try { + return Integer.parseInt(s); + } catch (NumberFormatException ex) { + Iterator iter = history.iterator(); + while (iter.hasNext()) { + History.Entry entry = iter.next(); + if (entry.line().startsWith(s)) { + return entry.index(); + } + } + throw new IllegalArgumentException("history: event not found: " + s); + } + } + + public static void complete( + LineReader reader, + PrintStream out, + PrintStream err, + Map> completions, + String[] argv) + throws HelpException { + final String[] usage = { + "complete - edit command specific tab-completions", + "Usage: complete", + " -? --help Displays command help", + " -c --command=COMMAND Command to add completion to", + " -d --description=DESCRIPTION Description of this completions", + " -e --erase Erase the completions", + " -s --short-option=SHORT_OPTION Posix-style option to complete", + " -l --long-option=LONG_OPTION GNU-style option to complete", + " -a --argument=ARGUMENTS A list of possible arguments", + " -n --condition=CONDITION The completion should only be used if the", + " specified command has a zero exit status" + }; + + Options opt = Options.compile(usage).parse(argv); + + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + + String command = opt.get("command"); + + if (opt.isSet("erase")) { + completions.remove(command); + return; + } + + List cmdCompletions = completions.computeIfAbsent(command, s -> new ArrayList<>()); + List options = null; + if (opt.isSet("short-option")) { + for (String op : opt.getList("short-option")) { + if (options == null) { + options = new ArrayList<>(); + } + options.add("-" + op); + } + } + if (opt.isSet("long-option")) { + for (String op : opt.getList("long-option")) { + if (options == null) { + options = new ArrayList<>(); + } + options.add("--" + op); + } + } + String description = opt.isSet("description") ? opt.get("description") : null; + String argument = opt.isSet("argument") ? opt.get("argument") : null; + String condition = opt.isSet("condition") ? opt.get("condition") : null; + cmdCompletions.add(new CompletionData(options, description, argument, condition)); + } + + public static void widget( + LineReader reader, PrintStream out, PrintStream err, Function widgetCreator, String[] argv) + throws Exception { + final String[] usage = { + "widget - manipulate widgets", + "Usage: widget -N new-widget [function-name]", + " widget -D widget ...", + " widget -A old-widget new-widget", + " widget -U string ...", + " widget -l [options]", + " -? --help Displays command help", + " -A Create alias to widget", + " -N Create new widget", + " -D Delete widgets", + " -U Push characters to the stack", + " -l List user-defined widgets", + " -a With -l, list all widgets" + }; + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + + int actions = (opt.isSet("N") ? 1 : 0) + + (opt.isSet("D") ? 1 : 0) + + (opt.isSet("U") ? 1 : 0) + + (opt.isSet("l") ? 1 : 0) + + (opt.isSet("A") ? 1 : 0); + if (actions > 1) { + err.println("widget: incompatible operation selection options"); + return; + } + if (opt.isSet("l")) { + TreeSet ws = new TreeSet<>(reader.getWidgets().keySet()); + if (opt.isSet("a")) { + Set temp = new HashSet<>(ws); + for (String s : temp) { + ws.add(reader.getWidgets().get(s).toString()); + } + } + for (String s : ws) { + if (opt.isSet("a")) { + out.println(s); + } else if (!reader.getWidgets().get(s).toString().startsWith(".")) { + out.println(s + " (" + reader.getWidgets().get(s) + ")"); + } + } + } else if (opt.isSet("N")) { + if (opt.args().size() < 1) { + err.println("widget: not enough arguments for -N"); + return; + } + if (opt.args().size() > 2) { + err.println("widget: too many arguments for -N"); + return; + } + final String name = opt.args().get(0); + final String func = opt.args().size() == 2 ? opt.args().get(1) : name; + reader.getWidgets().put(name, widgetCreator.apply(func)); + } else if (opt.isSet("D")) { + for (String name : opt.args()) { + reader.getWidgets().remove(name); + } + } else if (opt.isSet("A")) { + if (opt.args().size() < 2) { + err.println("widget: not enough arguments for -A"); + return; + } + if (opt.args().size() > 2) { + err.println("widget: too many arguments for -A"); + return; + } + Widget org = null; + if (opt.args().get(0).startsWith(".")) { + org = reader.getBuiltinWidgets().get(opt.args().get(0).substring(1)); + } else { + org = reader.getWidgets().get(opt.args().get(0)); + } + if (org == null) { + err.println("widget: no such widget `" + opt.args().get(0) + "'"); + return; + } + reader.getWidgets().put(opt.args().get(1), org); + } else if (opt.isSet("U")) { + for (String arg : opt.args()) { + reader.runMacro(KeyMap.translate(arg)); + } + } else if (opt.args().size() == 1) { + reader.callWidget(opt.args().get(0)); + } + } + + public static void keymap(LineReader reader, PrintStream out, PrintStream err, String[] argv) throws HelpException { + final String[] usage = { + "keymap - manipulate keymaps", + "Usage: keymap [options] -l [-L] [keymap ...]", + " keymap [options] -d", + " keymap [options] -D keymap ...", + " keymap [options] -A old-keymap new-keymap", + " keymap [options] -N new-keymap [old-keymap]", + " keymap [options] -M", + " keymap [options] -r in-string ...", + " keymap [options] -s in-string out-string ...", + " keymap [options] in-string command ...", + " keymap [options] [in-string]", + " -? --help Displays command help", + " -A Create alias to keymap", + " -D Delete named keymaps", + " -L Output in form of keymap commands", + " -M (default=main) Specify keymap to select", + " -N Create new keymap", + " -R Interpret in-strings as ranges", + " -a Select vicmd keymap", + " -d Delete existing keymaps and reset to default state", + " -e Select emacs keymap and bind it to main", + " -l List existing keymap names", + " -p List bindings which have given key sequence as a a prefix", + " -r Unbind specified in-strings ", + " -s Bind each in-string to each out-string ", + " -v Select viins keymap and bind it to main", + }; + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + + Map> keyMaps = reader.getKeyMaps(); + + int actions = (opt.isSet("N") ? 1 : 0) + + (opt.isSet("d") ? 1 : 0) + + (opt.isSet("D") ? 1 : 0) + + (opt.isSet("l") ? 1 : 0) + + (opt.isSet("r") ? 1 : 0) + + (opt.isSet("s") ? 1 : 0) + + (opt.isSet("A") ? 1 : 0); + if (actions > 1) { + err.println("keymap: incompatible operation selection options"); + return; + } + if (opt.isSet("l")) { + boolean commands = opt.isSet("L"); + // TODO: handle commands + if (opt.args().size() > 0) { + for (String arg : opt.args()) { + KeyMap map = keyMaps.get(arg); + if (map == null) { + err.println("keymap: no such keymap: `" + arg + "'"); + } else { + out.println(arg); + } + } + } else { + keyMaps.keySet().forEach(out::println); + } + } else if (opt.isSet("N")) { + if (opt.isSet("e") || opt.isSet("v") || opt.isSet("a") || opt.isSet("M")) { + err.println("keymap: keymap can not be selected with -N"); + return; + } + if (opt.args().size() < 1) { + err.println("keymap: not enough arguments for -N"); + return; + } + if (opt.args().size() > 2) { + err.println("keymap: too many arguments for -N"); + return; + } + KeyMap org = null; + if (opt.args().size() == 2) { + org = keyMaps.get(opt.args().get(1)); + if (org == null) { + err.println("keymap: no such keymap `" + opt.args().get(1) + "'"); + return; + } + } + KeyMap map = new KeyMap<>(); + if (org != null) { + for (Entry bound : org.getBoundKeys().entrySet()) { + map.bind(bound.getValue(), bound.getKey()); + } + } + keyMaps.put(opt.args().get(0), map); + } else if (opt.isSet("A")) { + if (opt.isSet("e") || opt.isSet("v") || opt.isSet("a") || opt.isSet("M")) { + err.println("keymap: keymap can not be selected with -N"); + return; + } + if (opt.args().size() < 2) { + err.println("keymap: not enough arguments for -A"); + return; + } + if (opt.args().size() > 2) { + err.println("keymap: too many arguments for -A"); + return; + } + KeyMap org = keyMaps.get(opt.args().get(0)); + if (org == null) { + err.println("keymap: no such keymap `" + opt.args().get(0) + "'"); + return; + } + keyMaps.put(opt.args().get(1), org); + } else if (opt.isSet("d")) { + if (opt.isSet("e") || opt.isSet("v") || opt.isSet("a") || opt.isSet("M")) { + err.println("keymap: keymap can not be selected with -N"); + return; + } + if (opt.args().size() > 0) { + err.println("keymap: too many arguments for -d"); + return; + } + keyMaps.clear(); + keyMaps.putAll(reader.defaultKeyMaps()); + } else if (opt.isSet("D")) { + if (opt.isSet("e") || opt.isSet("v") || opt.isSet("a") || opt.isSet("M")) { + err.println("keymap: keymap can not be selected with -N"); + return; + } + if (opt.args().size() < 1) { + err.println("keymap: not enough arguments for -A"); + return; + } + for (String name : opt.args()) { + if (keyMaps.remove(name) == null) { + err.println("keymap: no such keymap `" + name + "'"); + return; + } + } + } else if (opt.isSet("r")) { + // Select keymap + String keyMapName = LineReader.MAIN; + int sel = (opt.isSet("a") ? 1 : 0) + + (opt.isSet("e") ? 1 : 0) + + (opt.isSet("v") ? 1 : 0) + + (opt.isSet("M") ? 1 : 0); + if (sel > 1) { + err.println("keymap: incompatible keymap selection options"); + return; + } else if (opt.isSet("a")) { + keyMapName = LineReader.VICMD; + } else if (opt.isSet("e")) { + keyMapName = LineReader.EMACS; + } else if (opt.isSet("v")) { + keyMapName = LineReader.VIINS; + } else if (opt.isSet("M")) { + if (opt.args().isEmpty()) { + err.println("keymap: argument expected: -M"); + return; + } + keyMapName = opt.args().remove(0); + } + KeyMap map = keyMaps.get(keyMapName); + if (map == null) { + err.println("keymap: no such keymap `" + keyMapName + "'"); + return; + } + // Unbind + boolean range = opt.isSet("R"); + boolean prefix = opt.isSet("p"); + Set toRemove = new HashSet<>(); + Map bound = map.getBoundKeys(); + for (String arg : opt.args()) { + if (range) { + Collection r = KeyMap.range(opt.args().get(0)); + if (r == null) { + err.println("keymap: malformed key range `" + opt.args().get(0) + "'"); + return; + } + toRemove.addAll(r); + } else { + String seq = KeyMap.translate(arg); + for (String k : bound.keySet()) { + if (prefix && k.startsWith(seq) && k.length() > seq.length() || !prefix && k.equals(seq)) { + toRemove.add(k); + } + } + } + } + for (String seq : toRemove) { + map.unbind(seq); + } + if (opt.isSet("e") || opt.isSet("v")) { + keyMaps.put(LineReader.MAIN, map); + } + } else if (opt.isSet("s") || opt.args().size() > 1) { + // Select keymap + String keyMapName = LineReader.MAIN; + int sel = (opt.isSet("a") ? 1 : 0) + + (opt.isSet("e") ? 1 : 0) + + (opt.isSet("v") ? 1 : 0) + + (opt.isSet("M") ? 1 : 0); + if (sel > 1) { + err.println("keymap: incompatible keymap selection options"); + return; + } else if (opt.isSet("a")) { + keyMapName = LineReader.VICMD; + } else if (opt.isSet("e")) { + keyMapName = LineReader.EMACS; + } else if (opt.isSet("v")) { + keyMapName = LineReader.VIINS; + } else if (opt.isSet("M")) { + if (opt.args().isEmpty()) { + err.println("keymap: argument expected: -M"); + return; + } + keyMapName = opt.args().remove(0); + } + KeyMap map = keyMaps.get(keyMapName); + if (map == null) { + err.println("keymap: no such keymap `" + keyMapName + "'"); + return; + } + // Bind + boolean range = opt.isSet("R"); + if (opt.args().size() % 2 == 1) { + err.println("keymap: even number of arguments required"); + return; + } + for (int i = 0; i < opt.args().size(); i += 2) { + Binding bout = opt.isSet("s") + ? new Macro(KeyMap.translate(opt.args().get(i + 1))) + : new Reference(opt.args().get(i + 1)); + if (range) { + Collection r = KeyMap.range(opt.args().get(i)); + if (r == null) { + err.println("keymap: malformed key range `" + opt.args().get(i) + "'"); + return; + } + map.bind(bout, r); + } else { + String in = KeyMap.translate(opt.args().get(i)); + map.bind(bout, in); + } + } + if (opt.isSet("e") || opt.isSet("v")) { + keyMaps.put(LineReader.MAIN, map); + } + } else { + // Select keymap + String keyMapName = LineReader.MAIN; + int sel = (opt.isSet("a") ? 1 : 0) + + (opt.isSet("e") ? 1 : 0) + + (opt.isSet("v") ? 1 : 0) + + (opt.isSet("M") ? 1 : 0); + if (sel > 1) { + err.println("keymap: incompatible keymap selection options"); + return; + } else if (opt.isSet("a")) { + keyMapName = LineReader.VICMD; + } else if (opt.isSet("e")) { + keyMapName = LineReader.EMACS; + } else if (opt.isSet("v")) { + keyMapName = LineReader.VIINS; + } else if (opt.isSet("M")) { + if (opt.args().isEmpty()) { + err.println("keymap: argument expected: -M"); + return; + } + keyMapName = opt.args().remove(0); + } + KeyMap map = keyMaps.get(keyMapName); + if (map == null) { + err.println("keymap: no such keymap `" + keyMapName + "'"); + return; + } + // Display + boolean prefix = opt.isSet("p"); + boolean commands = opt.isSet("L"); + if (prefix && opt.args().isEmpty()) { + err.println("keymap: option -p requires a prefix string"); + return; + } + if (opt.args().size() > 0 || !opt.isSet("e") && !opt.isSet("v")) { + Map bound = map.getBoundKeys(); + String seq = opt.args().size() > 0 ? KeyMap.translate(opt.args().get(0)) : null; + Entry begin = null; + String last = null; + Iterator> iterator = bound.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + String key = entry.getKey(); + if (seq == null + || prefix && key.startsWith(seq) && !key.equals(seq) + || !prefix && key.equals(seq)) { + if (begin != null || !iterator.hasNext()) { + String n = (last.length() > 1 ? last.substring(0, last.length() - 1) : "") + + (char) (last.charAt(last.length() - 1) + 1); + if (key.equals(n) && entry.getValue().equals(begin.getValue())) { + last = key; + } else { + // We're not in a range, so we need to close the previous range + StringBuilder sb = new StringBuilder(); + if (commands) { + sb.append("keymap -M "); + sb.append(keyMapName); + sb.append(" "); + } + if (begin.getKey().equals(last)) { + sb.append(KeyMap.display(last)); + sb.append(" "); + displayValue(sb, begin.getValue()); + out.println(sb); + } else { + if (commands) { + sb.append("-R "); + } + sb.append(KeyMap.display(begin.getKey())); + sb.append("-"); + sb.append(KeyMap.display(last)); + sb.append(" "); + displayValue(sb, begin.getValue()); + out.println(sb); + } + begin = entry; + last = key; + } + } else { + begin = entry; + last = key; + } + } + } + } + if (opt.isSet("e") || opt.isSet("v")) { + keyMaps.put(LineReader.MAIN, map); + } + } + } + + public static void setopt(LineReader reader, PrintStream out, PrintStream err, String[] argv) throws HelpException { + final String[] usage = { + "setopt - set options", + "Usage: setopt [-m] option ...", + " setopt", + " -? --help Displays command help", + " -m Use pattern matching" + }; + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + if (opt.args().isEmpty()) { + for (Option option : Option.values()) { + if (reader.isSet(option) != option.isDef()) { + out.println((option.isDef() ? "no-" : "") + + option.toString().toLowerCase().replace('_', '-')); + } + } + } else { + boolean match = opt.isSet("m"); + doSetOpts(reader, out, err, opt.args(), match, true); + } + } + + public static void unsetopt(LineReader reader, PrintStream out, PrintStream err, String[] argv) + throws HelpException { + final String[] usage = { + "unsetopt - unset options", + "Usage: unsetopt [-m] option ...", + " unsetopt", + " -? --help Displays command help", + " -m Use pattern matching" + }; + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + if (opt.args().isEmpty()) { + for (Option option : Option.values()) { + if (reader.isSet(option) == option.isDef()) { + out.println((option.isDef() ? "no-" : "") + + option.toString().toLowerCase().replace('_', '-')); + } + } + } else { + boolean match = opt.isSet("m"); + doSetOpts(reader, out, err, opt.args(), match, false); + } + } + + private static void doSetOpts( + LineReader reader, PrintStream out, PrintStream err, List options, boolean match, boolean set) { + for (String name : options) { + String tname = name.toLowerCase().replaceAll("[-_]", ""); + if (match) { + tname = tname.replaceAll("\\*", "[a-z]*"); + tname = tname.replaceAll("\\?", "[a-z]"); + } + boolean found = false; + for (LineReader.Option option : LineReader.Option.values()) { + String optName = option.name().toLowerCase().replaceAll("[-_]", ""); + if (match ? optName.matches(tname) : optName.equals(tname)) { + if (set) { + reader.setOpt(option); + } else { + reader.unsetOpt(option); + } + found = true; + if (!match) { + break; + } + } else if (match ? ("no" + optName).matches(tname) : ("no" + optName).equals(tname)) { + if (set) { + reader.unsetOpt(option); + } else { + reader.setOpt(option); + } + if (!match) { + found = true; + } + break; + } + } + if (!found) { + err.println("No matching option: " + name); + } + } + } + + private static void displayValue(StringBuilder sb, Object value) { + if (value == null) { + sb.append("undefined-key"); + } else if (value instanceof Macro) { + sb.append(KeyMap.display(((Macro) value).getSequence())); + } else if (value instanceof Reference) { + sb.append(((Reference) value).name()); + } else { + sb.append(value); + } + } + + public static void setvar(LineReader lineReader, PrintStream out, PrintStream err, String[] argv) + throws HelpException { + final String[] usage = { + "setvar - set lineReader variable value", + "Usage: setvar [variable] [value]", + " -? --help Show help", + }; + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + if (opt.args().isEmpty()) { + for (Entry entry : lineReader.getVariables().entrySet()) { + out.println(entry.getKey() + ": " + entry.getValue()); + } + } else if (opt.args().size() == 1) { + out.println(lineReader.getVariable(opt.args().get(0))); + } else { + lineReader.setVariable(opt.args().get(0), opt.args().get(1)); + } + } + + public static void colors(Terminal terminal, PrintStream out, String[] argv) throws HelpException, IOException { + String[] usage = { + "colors - view 256-color table and ANSI-styles", + "Usage: colors [OPTIONS]", + " -? --help Displays command help", + " -a --ansistyles List ANSI-styles", + " -c --columns=COLUMNS Number of columns in name/rgb table", + " COLUMNS = 1, display columns: color, style, ansi and HSL", + " -f --find=NAME Find color names which contains NAME ", + " -l --lock=STYLE Lock fore- or background color", + " -n --name Color name table (default number table)", + " -r --rgb Use and display rgb value", + " -s --small View 16-color table (default 256-color)", + " -v --view=COLOR View 24bit color table of COLOR ", + " COLOR = , or hue" + }; + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + Colors colors = new Colors(terminal, out); + if (opt.isSet("ansistyles")) { + colors.printStyles(); + } else { + String style = null; + if (opt.isSet("lock")) { + style = opt.get("lock"); + if (style.length() - style.replace(":", "").length() > 1) { + style = null; + } + } + if (!opt.isSet("view")) { + boolean rgb = opt.isSet("rgb"); + int columns = terminal.getWidth() > (rgb ? 71 : 122) ? 6 : 5; + String findName = null; + boolean nameTable = opt.isSet("name"); + boolean table16 = opt.isSet("small"); + if (opt.isSet("find")) { + findName = opt.get("find").toLowerCase(); + nameTable = true; + table16 = false; + columns = 4; + } + if (table16) { + columns = columns + 2; + } + if (opt.isSet("columns")) { + columns = opt.getNumber("columns"); + } + colors.printColors(nameTable, rgb, table16, columns, findName, style); + } else { + colors.printColor(opt.get("view").toLowerCase(), style); + } + } + } + + private static void switchTheme(PrintStream err, Path config, String theme) { + try (Stream stream = Files.lines(config, StandardCharsets.UTF_8)) { + List list = stream.map(line -> + line.matches("\\s*" + COMMAND_THEME + "\\s+.*") ? COMMAND_THEME + " " + theme : line) + .collect(Collectors.toList()); + Files.write(config, list, StandardCharsets.UTF_8); + } catch (IOException e) { + err.println(e.getMessage()); + } + } + + private static String replaceFileName(Path path, String name) { + int nameLength = path.getFileName().toString().length(); + int pathLength = path.toString().length(); + return (path.toString().substring(0, pathLength - nameLength) + name).replace("\\", "\\\\"); + } + + private static AttributedStyle compileStyle(String reference, String colorDef) { + Map spec = new HashMap<>(); + spec.put(reference, colorDef); + Styles.StyleCompiler sh = new Styles.StyleCompiler(spec, true); + return new StyleResolver(sh::getStyle).resolve("." + reference); + } + + private static class ReExecute { + private final boolean execute; + private final boolean edit; + private String oldParam; + private String newParam; + private FileWriter cmdWriter; + private File cmdFile; + private int argId = 0; + + public ReExecute(History history, Options opt) throws IOException { + execute = opt.isSet("e") || opt.isSet("s"); + edit = opt.isSet("e"); + if (execute) { + Iterator iter = history.reverseIterator(history.last()); + if (iter.hasNext()) { + iter.next(); + iter.remove(); + } + if (edit) { + cmdFile = File.createTempFile("jline-history-", null); + cmdWriter = new FileWriter(cmdFile); + } else if (opt.args().size() > 0) { + String[] s = opt.args().get(argId).split("="); + if (s.length == 2) { + argId = argId + 1; + oldParam = s[0]; + newParam = s[1]; + } + } + } + } + + public int getArgId() { + return argId; + } + + public boolean isEdit() { + return edit; + } + + public boolean isExecute() { + return execute; + } + + public void addCommandInFile(String command) throws IOException { + cmdWriter.write(command + "\n"); + } + + public void addCommandInBuffer(LineReader reader, String command) { + reader.addCommandsInBuffer(Collections.singletonList(replaceParam(command))); + } + + private String replaceParam(String command) { + String out = command; + if (oldParam != null && newParam != null) { + out = command.replaceAll(oldParam, newParam); + } + return out; + } + } + + private static class Colors { + private static final String COLORS_24BIT = "[0-9a-fA-F]{6}"; + private static final List COLORS_16 = Arrays.asList( + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "!black", + "!red", + "!green", + "!yellow", + "!blue", + "!magenta", + "!cyan", + "!white"); + private final Terminal terminal; + private final PrintStream out; + boolean name; + boolean rgb; + int r, g, b; + private boolean fixedBg; + private String fixedStyle; + + public Colors(Terminal terminal, PrintStream out) { + this.terminal = terminal; + this.out = out; + } + + private String getAnsiStyle(String style) { + return style; + } + + public void printStyles() { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.tabs(13); + for (String s : Styles.ANSI_STYLES) { + AttributedStyle as = new StyleResolver(this::getAnsiStyle).resolve("." + s); + asb.style(as); + asb.append(s); + asb.style(AttributedStyle.DEFAULT); + asb.append("\t"); + asb.append(getAnsiStyle(s)); + asb.append("\t"); + asb.append(as.toAnsi()); + asb.append("\n"); + } + asb.toAttributedString().println(terminal); + } + + private String getStyle(String color) { + String out; + char fg = ' '; + if (name) { + out = (fixedBg ? "fg:" : "bg:") + "~" + color.substring(1); + fg = color.charAt(0); + } else if (rgb) { + out = (fixedBg ? "fg-rgb:" : "bg-rgb:") + "#" + color.substring(1); + fg = color.charAt(0); + } else if (color.substring(1).matches("\\d+")) { + out = (fixedBg ? "38;5;" : "48;5;") + color.substring(1); + fg = color.charAt(0); + } else { + out = (fixedBg ? "fg:" : "bg:") + color; + } + if (fixedStyle == null) { + if (color.startsWith("!") || color.equals("white") || fg == 'b') { + out += ",fg:black"; + } else { + out += ",fg:!white"; + } + } else { + out += "," + fixedStyle; + } + return out; + } + + private String foreground(int idx) { + String fg = "w"; + if ((idx > 6 && idx < 16) + || (idx > 33 && idx < 52) + || (idx > 69 && idx < 88) + || (idx > 105 && idx < 124) + || (idx > 141 && idx < 160) + || (idx > 177 && idx < 196) + || (idx > 213 && idx < 232) + || idx > 243) { + fg = "b"; + } + return fg; + } + + private String addPadding(int width, String field) { + int s = width - field.length(); + int left = s / 2; + StringBuilder lp = new StringBuilder(); + StringBuilder rp = new StringBuilder(); + for (int i = 0; i < left; i++) { + lp.append(" "); + } + for (int i = 0; i < s - left; i++) { + rp.append(" "); + } + return lp + field + rp; + } + + private String addLeftPadding(int width, String field) { + int s = width - field.length(); + StringBuilder lp = new StringBuilder(); + for (int i = 0; i < s; i++) { + lp.append(" "); + } + return lp + field; + } + + private void setFixedStyle(String style) { + this.fixedStyle = style; + if (style != null + && (style.contains("b:") + || style.contains("b-") + || style.contains("bg:") + || style.contains("bg-") + || style.contains("background"))) { + fixedBg = true; + } + } + + private List retrieveColorNames() throws IOException { + List out; + try (InputStream is = new Source.ResourceSource("/org/jline/utils/colors.txt", null).read(); + BufferedReader br = new BufferedReader(new InputStreamReader(is))) { + out = br.lines() + .map(String::trim) + .filter(s -> !s.startsWith("#")) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + } + return out; + } + + public void printColors(boolean name, boolean rgb, boolean small, int columns, String findName, String style) + throws IOException { + this.name = !rgb && name; + this.rgb = rgb; + setFixedStyle(style); + AttributedStringBuilder asb = new AttributedStringBuilder(); + int width = terminal.getWidth(); + String tableName = small ? " 16-color " : "256-color "; + if (!name && !rgb) { + out.print(tableName); + out.print("table, fg: "); + if (!small) { + out.print("/ 38;5;"); + } + out.println(); + out.print(" bg: "); + if (!small) { + out.print("/ 48;5;"); + } + out.println("\n"); + boolean narrow = width < 180; + for (String c : COLORS_16) { + AttributedStyle ss = new StyleResolver(this::getStyle).resolve('.' + c, null); + asb.style(ss); + asb.append(addPadding(11, c)); + asb.style(AttributedStyle.DEFAULT); + if (c.equals("white")) { + if (narrow || small) { + asb.append('\n'); + } else { + asb.append(" "); + } + } else if (c.equals("!white")) { + asb.append('\n'); + } + } + asb.append('\n'); + if (!small) { + for (int i = 16; i < 256; i++) { + String fg = foreground(i); + String code = Integer.toString(i); + AttributedStyle ss = new StyleResolver(this::getStyle).resolve("." + fg + code, null); + asb.style(ss); + String str = " "; + if (i < 100) { + str = " "; + } else if (i > 231) { + str = i % 2 == 0 ? " " : " "; + } + asb.append(str).append(code).append(' '); + if (i == 51 + || i == 87 + || i == 123 + || i == 159 + || i == 195 + || i == 231 + || narrow + && (i == 33 || i == 69 || i == 105 || i == 141 || i == 177 || i == 213 + || i == 243)) { + asb.style(AttributedStyle.DEFAULT); + asb.append('\n'); + if (i == 231) { + asb.append('\n'); + } + } + } + } + } else { + out.print(tableName); + if (name) { + asb.tabs(Arrays.asList(25, 60, 75)); + out.println("table, fg:~ OR 38;5;"); + out.println(" bg:~ OR 48;5;"); + } else { + asb.tabs(Arrays.asList(15, 45, 70)); + out.println("table, fg-rgb: OR 38;5;"); + out.println(" bg-rgb: OR 48;5;"); + } + out.println(); + int col = 0; + int idx = 0; + int colWidth = rgb ? 12 : 21; + int lb = 1; + if (findName != null && (findName.startsWith("#") || findName.startsWith("x"))) { + findName = findName.substring(1); + } + for (String line : retrieveColorNames()) { + if (rgb) { + // do nothing + } else if (findName != null) { + if (!line.toLowerCase().contains(findName)) { + idx++; + continue; + } + } else if (small) { + colWidth = 15; + lb = 1; + } else if (columns > 4) { + if (idx > 15 && idx < 232) { + colWidth = columns != 6 || col == 1 || col == 2 || col == 3 ? 21 : 20; + lb = 1; + } else { + colWidth = columns != 6 || idx % 2 == 0 || col == 7 ? 15 : 16; + lb = -1; + } + } + String fg = foreground(idx); + if (rgb) { + line = Integer.toHexString(org.jline.utils.Colors.DEFAULT_COLORS_256[idx]); + for (int p = line.length(); p < 6; p++) { + line = "0" + line; + } + if (findName != null) { + if (!line.toLowerCase().matches(findName)) { + idx++; + continue; + } + } + } + AttributedStyle ss = new StyleResolver(this::getStyle).resolve("." + fg + line, null); + if (rgb) { + line = "#" + line; + } + asb.style(ss); + String idxstr = Integer.toString(idx); + if (rgb) { + if (idx < 10) { + idxstr = " " + idxstr; + } else if (idx < 100) { + idxstr = " " + idxstr; + } + } + asb.append(idxstr).append(addPadding(colWidth - idxstr.length(), line)); + if (columns == 1) { + asb.style(AttributedStyle.DEFAULT); + asb.append("\t").append(getStyle(fg + line.substring(rgb ? 1 : 0))); + asb.append("\t").append(ss.toAnsi()); + int[] rgb1 = rgb(org.jline.utils.Colors.DEFAULT_COLORS_256[idx]); + int[] hsl = rgb2hsl(rgb1[0], rgb1[1], rgb1[2]); + asb.append("\t") + .append(addLeftPadding(6, hsl[0] + ", ")) + .append(addLeftPadding(4, hsl[1] + "%")) + .append(", ") + .append(addLeftPadding(4, hsl[2] + "%")); + } + col++; + idx++; + if ((col + 1) * colWidth > width || col + lb > columns) { + col = 0; + asb.style(AttributedStyle.DEFAULT); + asb.append('\n'); + } + if (findName == null) { + if (idx == 16) { + if (small) { + break; + } else if (col != 0) { + col = 0; + asb.style(AttributedStyle.DEFAULT); + asb.append('\n'); + } + } else if (idx == 232 && col != 0) { + col = 0; + asb.style(AttributedStyle.DEFAULT); + asb.append('\n'); + } + } + } + } + asb.toAttributedString().println(terminal); + } + + private int[] rgb(long color) { + int[] rgb = {0, 0, 0}; + rgb[0] = (int) ((color >> 16) & 0xFF); + rgb[1] = (int) ((color >> 8) & 0xFF); + rgb[2] = (int) (color & 0xFF); + return rgb; + } + + private int[] hue2rgb(int degree) { + int[] rgb = {0, 0, 0}; + double hue = degree / 60.0; + double a = Math.tan((degree / 360.0) * 2 * Math.PI) / Math.sqrt(3); + if (hue >= 0 && hue < 1) { + rgb[0] = 0xff; + rgb[1] = (int) (2 * a * 0xff / (1 + a)); + } else if (hue >= 1 && hue < 2) { + rgb[0] = (int) (0xff * (1 + a) / (2 * a)); + rgb[1] = 0xff; + } else if (hue >= 2 && hue < 3) { + rgb[1] = 0xff; + rgb[2] = (int) (0xff * (1 + a) / (1 - a)); + } else if (hue >= 3 && hue < 4) { + rgb[1] = (int) (0xff * (1 - a) / (1 + a)); + rgb[2] = 0xff; + } else if (hue >= 4 && hue <= 5) { + rgb[0] = (int) (0xff * (a - 1) / (2 * a)); + rgb[2] = 0xff; + } else if (hue > 5 && hue <= 6) { + rgb[0] = 0xff; + rgb[2] = (int) (0xff * 2 * a / (a - 1)); + } + return rgb; + } + + private int[] rgb2hsl(int r, int g, int b) { + int[] hsl = {0, 0, 0}; + if (r != 0 || g != 0 || b != 0) { + hsl[0] = (int) Math.round((180 / Math.PI) * Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b)); + while (hsl[0] < 0) { + hsl[0] += 360; + } + } + double mx = Math.max(Math.max(r, g), b) / 255.0; + double mn = Math.min(Math.min(r, g), b) / 255.0; + double l = (mx + mn) / 2; + hsl[1] = l == 0 || l == 1 ? 0 : (int) Math.round(100.0 * (mx - mn) / (1 - Math.abs(2 * l - 1))); + hsl[2] = (int) Math.round(100 * l); + return hsl; + } + + String getStyleRGB(String s) { + if (fixedStyle == null) { + double ry = Math.pow(r / 255.0, 2.2); + double by = Math.pow(b / 255.0, 2.2); + double gy = Math.pow(g / 255.0, 2.2); + double y = 0.2126 * ry + 0.7151 * gy + 0.0721 * by; + String fg = "black"; + if (1.05 / (y + 0.05) > (y + 0.05) / 0.05) { + fg = "white"; + } + return "bg-rgb:" + String.format("#%02x%02x%02x", r, g, b) + ",fg:" + fg; + } else { + return (fixedBg ? "fg-rgb:" : "bg-rgb:") + String.format("#%02x%02x%02x", r, g, b) + "," + fixedStyle; + } + } + + public void printColor(String name, String style) throws IOException { + setFixedStyle(style); + int hueAngle; + double zoom = 1; + int[] rgb = {0, 0, 0}; + if (name.matches(COLORS_24BIT)) { + rgb = rgb(Long.parseLong(name, 16)); + zoom = 2; + } else if ((name.startsWith("#") || name.startsWith("x")) + && name.substring(1).matches(COLORS_24BIT)) { + rgb = rgb(Long.parseLong(name.substring(1), 16)); + zoom = 2; + } else if (COLORS_16.contains(name)) { + for (int i = 0; i < 16; i++) { + if (COLORS_16.get(i).equals(name)) { + rgb = rgb(org.jline.utils.Colors.DEFAULT_COLORS_256[i]); + break; + } + } + } else if (name.matches("hue[1-3]?[0-9]{1,2}")) { + hueAngle = Integer.parseInt(name.substring(3)); + if (hueAngle > 360) { + throw new IllegalArgumentException("Color not found: " + name); + } + rgb = hue2rgb(hueAngle); + } else if (name.matches("[a-z0-9]+")) { + List colors = retrieveColorNames(); + if (colors.contains(name)) { + for (int i = 0; i < 256; i++) { + if (colors.get(i).equals(name)) { + rgb = rgb(org.jline.utils.Colors.DEFAULT_COLORS_256[i]); + break; + } + } + } else { + boolean found = false; + for (int i = 0; i < 256; i++) { + if (colors.get(i).startsWith(name)) { + rgb = rgb(org.jline.utils.Colors.DEFAULT_COLORS_256[i]); + found = true; + break; + } + } + if (!found) { + for (int i = 0; i < 256; i++) { + if (colors.get(i).contains(name)) { + rgb = rgb(org.jline.utils.Colors.DEFAULT_COLORS_256[i]); + found = true; + break; + } + } + } + if (!found) { + throw new IllegalArgumentException("Color not found: " + name); + } + } + } else { + throw new IllegalArgumentException("Color not found: " + name); + } + double step = 32; + int barSize = 14; + int width = terminal.getWidth(); + if (width > 287) { + step = 8; + barSize = 58; + } else if (width > 143) { + step = 16; + barSize = 29; + } else if (width > 98) { + step = 24; + barSize = 18; + } + r = rgb[0]; + g = rgb[1]; + b = rgb[2]; + int[] hsl = rgb2hsl(r, g, b); + hueAngle = hsl[0]; + out.println("HSL: " + hsl[0] + "deg, " + hsl[1] + "%, " + hsl[2] + "%"); + if (hsl[2] > 85 || hsl[2] < 15 || hsl[1] < 15) { + zoom = 1; + } + double div = zoom * 256.0 / step; + int ndiv = (int) (div / zoom); + double xrs = (0xFF - r) / div; + double xgs = (0xFF - g) / div; + double xbs = (0xFF - b) / div; + double[] yrs = new double[ndiv], ygs = new double[ndiv], ybs = new double[ndiv]; + double[] ro = new double[ndiv], go = new double[ndiv], bo = new double[ndiv]; + AttributedStringBuilder asb = new AttributedStringBuilder(); + for (int y = 0; y < ndiv; y++) { + for (int x = 0; x < ndiv; x++) { + if (y == 0) { + yrs[x] = (rgb[0] + x * xrs) / div; + ygs[x] = (rgb[1] + x * xgs) / div; + ybs[x] = (rgb[2] + x * xbs) / div; + ro[x] = rgb[0] + x * xrs; + go[x] = rgb[1] + x * xgs; + bo[x] = rgb[2] + x * xbs; + r = (int) ro[x]; + g = (int) go[x]; + b = (int) bo[x]; + } else { + r = (int) (ro[x] - y * yrs[x]); + g = (int) (go[x] - y * ygs[x]); + b = (int) (bo[x] - y * ybs[x]); + } + String col = String.format("%02x%02x%02x", r, g, b); + AttributedStyle s = new StyleResolver(this::getStyleRGB).resolve(".rgb" + col); + asb.style(s); + asb.append(" ").append("#").append(col).append(" "); + } + asb.style(AttributedStyle.DEFAULT).append("\n"); + } + asb.toAttributedString().println(terminal); + if (hueAngle != -1) { + int dAngle = 5; + int zero = (int) (hueAngle - (dAngle / 2.0) * (barSize - 1)); + zero = zero - zero % 5; + AttributedStringBuilder asb2 = new AttributedStringBuilder(); + for (int i = 0; i < barSize; i++) { + int angle = zero + dAngle * i; + while (angle < 0) { + angle += 360; + } + while (angle > 360) { + angle -= 360; + } + rgb = hue2rgb(angle); + r = rgb[0]; + g = rgb[1]; + b = rgb[2]; + AttributedStyle s = new StyleResolver(this::getStyleRGB).resolve(".hue" + angle); + asb2.style(s); + asb2.append(" ").append(addPadding(3, "" + angle)).append(" "); + } + asb2.style(AttributedStyle.DEFAULT).append("\n"); + asb2.toAttributedString().println(terminal); + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/Completers.java b/net-cli/src/main/java/org/jline/builtins/Completers.java new file mode 100644 index 0000000..e7de04c --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/Completers.java @@ -0,0 +1,1002 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Array; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Supplier; +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.jline.reader.LineReader.Option; +import org.jline.reader.ParsedLine; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.OSUtils; +import org.jline.utils.StyleResolver; + +public class Completers { + + public interface CompletionEnvironment { + Map> getCompletions(); + + Set getCommands(); + + String resolveCommand(String command); + + String commandName(String command); + + Object evaluate(LineReader reader, ParsedLine line, String func) throws Exception; + } + + public static class CompletionData { + public final List options; + public final String description; + public final String argument; + public final String condition; + + public CompletionData(List options, String description, String argument, String condition) { + this.options = options; + this.description = description; + this.argument = argument; + this.condition = condition; + } + } + + public static class Completer implements org.jline.reader.Completer { + + private final CompletionEnvironment environment; + + public Completer(CompletionEnvironment environment) { + this.environment = environment; + } + + public void complete(LineReader reader, ParsedLine line, List candidates) { + if (line.wordIndex() == 0) { + completeCommand(candidates); + } else { + tryCompleteArguments(reader, line, candidates); + } + } + + protected void tryCompleteArguments(LineReader reader, ParsedLine line, List candidates) { + String command = line.words().get(0); + String resolved = environment.resolveCommand(command); + Map> comp = environment.getCompletions(); + if (comp != null) { + List cmd = comp.get(resolved); + if (cmd != null) { + completeCommandArguments(reader, line, candidates, cmd); + } + } + } + + protected void completeCommandArguments( + LineReader reader, ParsedLine line, List candidates, List completions) { + for (CompletionData completion : completions) { + boolean isOption = line.word().startsWith("-"); + String prevOption = line.wordIndex() >= 2 + && line.words().get(line.wordIndex() - 1).startsWith("-") + ? line.words().get(line.wordIndex() - 1) + : null; + String key = UUID.randomUUID().toString(); + boolean conditionValue = true; + if (completion.condition != null) { + Object res = Boolean.FALSE; + try { + res = environment.evaluate(reader, line, completion.condition); + } catch (Throwable t) { + // Ignore + } + conditionValue = isTrue(res); + } + if (conditionValue && isOption && completion.options != null) { + for (String opt : completion.options) { + candidates.add(new Candidate(opt, opt, "options", completion.description, null, key, true)); + } + } else if (!isOption + && prevOption != null + && completion.argument != null + && (completion.options != null && completion.options.contains(prevOption))) { + Object res = null; + try { + res = environment.evaluate(reader, line, completion.argument); + } catch (Throwable t) { + // Ignore + } + if (res instanceof Candidate) { + candidates.add((Candidate) res); + } else if (res instanceof String) { + candidates.add(new Candidate((String) res, (String) res, null, null, null, null, true)); + } else if (res instanceof Collection) { + for (Object s : (Collection) res) { + if (s instanceof Candidate) { + candidates.add((Candidate) s); + } else if (s instanceof String) { + candidates.add(new Candidate((String) s, (String) s, null, null, null, null, true)); + } + } + } else if (res != null && res.getClass().isArray()) { + for (int i = 0, l = Array.getLength(res); i < l; i++) { + Object s = Array.get(res, i); + if (s instanceof Candidate) { + candidates.add((Candidate) s); + } else if (s instanceof String) { + candidates.add(new Candidate((String) s, (String) s, null, null, null, null, true)); + } + } + } + } else if (!isOption && completion.argument != null) { + Object res = null; + try { + res = environment.evaluate(reader, line, completion.argument); + } catch (Throwable t) { + // Ignore + } + if (res instanceof Candidate) { + candidates.add((Candidate) res); + } else if (res instanceof String) { + candidates.add(new Candidate( + (String) res, (String) res, null, completion.description, null, null, true)); + } else if (res instanceof Collection) { + for (Object s : (Collection) res) { + if (s instanceof Candidate) { + candidates.add((Candidate) s); + } else if (s instanceof String) { + candidates.add(new Candidate( + (String) s, (String) s, null, completion.description, null, null, true)); + } + } + } + } + } + } + + protected void completeCommand(List candidates) { + Set commands = environment.getCommands(); + for (String command : commands) { + String name = environment.commandName(command); + boolean resolved = command.equals(environment.resolveCommand(name)); + if (!name.startsWith("_")) { + String desc = null; + Map> comp = environment.getCompletions(); + if (comp != null) { + List completions = comp.get(command); + if (completions != null) { + for (CompletionData completion : completions) { + if (completion.description != null + && completion.options == null + && completion.argument == null + && completion.condition == null) { + desc = completion.description; + } + } + } + } + String key = UUID.randomUUID().toString(); + if (desc != null) { + candidates.add(new Candidate(command, command, null, desc, null, key, true)); + if (resolved) { + candidates.add(new Candidate(name, name, null, desc, null, key, true)); + } + } else { + candidates.add(new Candidate(command, command, null, null, null, key, true)); + if (resolved) { + candidates.add(new Candidate(name, name, null, null, null, key, true)); + } + } + } + } + } + + private boolean isTrue(Object result) { + if (result == null) return false; + if (result instanceof Boolean) return (Boolean) result; + if (result instanceof Number && 0 == ((Number) result).intValue()) { + return false; + } + return !("".equals(result) || "0".equals(result)); + } + } + + public static class DirectoriesCompleter extends FileNameCompleter { + + private final Supplier currentDir; + + public DirectoriesCompleter(File currentDir) { + this(currentDir.toPath()); + } + + public DirectoriesCompleter(Path currentDir) { + this.currentDir = () -> currentDir; + } + + public DirectoriesCompleter(Supplier currentDir) { + this.currentDir = currentDir; + } + + @Override + protected Path getUserDir() { + return currentDir.get(); + } + + @Override + protected boolean accept(Path path) { + return Files.isDirectory(path) && super.accept(path); + } + } + + public static class FilesCompleter extends FileNameCompleter { + + private final Supplier currentDir; + private final String namePattern; + + public FilesCompleter(File currentDir) { + this(currentDir.toPath(), null); + } + + public FilesCompleter(File currentDir, String namePattern) { + this(currentDir.toPath(), namePattern); + } + + public FilesCompleter(Path currentDir) { + this(currentDir, null); + } + + public FilesCompleter(Path currentDir, String namePattern) { + this.currentDir = () -> currentDir; + this.namePattern = compilePattern(namePattern); + } + + public FilesCompleter(Supplier currentDir) { + this(currentDir, null); + } + + public FilesCompleter(Supplier currentDir, String namePattern) { + this.currentDir = currentDir; + this.namePattern = compilePattern(namePattern); + } + + private String compilePattern(String pattern) { + if (pattern == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + if (ch == '\\') { + ch = pattern.charAt(++i); + sb.append(ch); + } else if (ch == '.') { + sb.append('\\').append('.'); + } else if (ch == '*') { + sb.append('.').append('*'); + } else { + sb.append(ch); + } + } + return sb.toString(); + } + + @Override + protected Path getUserDir() { + return currentDir.get(); + } + + @Override + protected boolean accept(Path path) { + if (namePattern == null || Files.isDirectory(path)) { + return super.accept(path); + } + return path.getFileName().toString().matches(namePattern) && super.accept(path); + } + } + + /** + * A file name completer takes the buffer and issues a list of + * potential completions. + *

+ * This completer tries to behave as similar as possible to + * bash's file name completion (using GNU readline) + * with the following exceptions: + *

    + *
  • Candidates that are directories will end with "/"
  • + *
  • Wildcard regular expressions are not evaluated or replaced
  • + *
  • The "~" character can be used to represent the user's home, + * but it cannot complete to other users' homes, since java does + * not provide any way of determining that easily
  • + *
+ * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @since 2.3 + */ + public static class FileNameCompleter implements org.jline.reader.Completer { + + public void complete(LineReader reader, ParsedLine commandLine, final List candidates) { + assert commandLine != null; + assert candidates != null; + + String buffer = commandLine.word().substring(0, commandLine.wordCursor()); + + Path current; + String curBuf; + String sep = getSeparator(reader.isSet(LineReader.Option.USE_FORWARD_SLASH)); + int lastSep = buffer.lastIndexOf(sep); + try { + if (lastSep >= 0) { + curBuf = buffer.substring(0, lastSep + 1); + if (curBuf.startsWith("~")) { + if (curBuf.startsWith("~" + sep)) { + current = getUserHome().resolve(curBuf.substring(2)); + } else { + current = getUserHome().getParent().resolve(curBuf.substring(1)); + } + } else { + current = getUserDir().resolve(curBuf); + } + } else { + curBuf = ""; + current = getUserDir(); + } + StyleResolver resolver = Styles.lsStyle(); + try (DirectoryStream directory = Files.newDirectoryStream(current, this::accept)) { + directory.forEach(p -> { + String value = curBuf + p.getFileName().toString(); + if (Files.isDirectory(p)) { + candidates.add(new Candidate( + value + (reader.isSet(LineReader.Option.AUTO_PARAM_SLASH) ? sep : ""), + getDisplay(reader.getTerminal(), p, resolver, sep), + null, + null, + reader.isSet(LineReader.Option.AUTO_REMOVE_SLASH) ? sep : null, + null, + false)); + } else { + candidates.add(new Candidate( + value, + getDisplay(reader.getTerminal(), p, resolver, sep), + null, + null, + null, + null, + true)); + } + }); + } catch (IOException e) { + // Ignore + } + } catch (Exception e) { + // Ignore + } + } + + protected boolean accept(Path path) { + try { + return !Files.isHidden(path); + } catch (IOException e) { + return false; + } + } + + protected Path getUserDir() { + return Paths.get(System.getProperty("user.dir")); + } + + protected Path getUserHome() { + return Paths.get(System.getProperty("user.home")); + } + + protected String getSeparator(boolean useForwardSlash) { + return useForwardSlash ? "/" : getUserDir().getFileSystem().getSeparator(); + } + + protected String getDisplay(Terminal terminal, Path p, StyleResolver resolver, String separator) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + String name = p.getFileName().toString(); + int idx = name.lastIndexOf("."); + String type = idx != -1 ? ".*" + name.substring(idx) : null; + if (Files.isSymbolicLink(p)) { + sb.styled(resolver.resolve(".ln"), name).append("@"); + } else if (Files.isDirectory(p)) { + sb.styled(resolver.resolve(".di"), name).append(separator); + } else if (Files.isExecutable(p) && !OSUtils.IS_WINDOWS) { + sb.styled(resolver.resolve(".ex"), name).append("*"); + } else if (type != null && resolver.resolve(type).getStyle() != 0) { + sb.styled(resolver.resolve(type), name); + } else if (Files.isRegularFile(p)) { + sb.styled(resolver.resolve(".fi"), name); + } else { + sb.append(name); + } + return sb.toAnsi(terminal); + } + } + + public static class TreeCompleter implements org.jline.reader.Completer { + + final Map completers = new HashMap<>(); + final RegexCompleter completer; + + public TreeCompleter(Node... nodes) { + this(Arrays.asList(nodes)); + } + + @SuppressWarnings("this-escape") + public TreeCompleter(List nodes) { + StringBuilder sb = new StringBuilder(); + addRoots(sb, nodes); + completer = new RegexCompleter(sb.toString(), completers::get); + } + + public static Node node(Object... objs) { + org.jline.reader.Completer comp = null; + List cands = new ArrayList<>(); + List nodes = new ArrayList<>(); + for (Object obj : objs) { + if (obj instanceof String) { + cands.add(new Candidate((String) obj)); + } else if (obj instanceof Candidate) { + cands.add((Candidate) obj); + } else if (obj instanceof Node) { + nodes.add((Node) obj); + } else if (obj instanceof org.jline.reader.Completer) { + comp = (org.jline.reader.Completer) obj; + } else { + throw new IllegalArgumentException(); + } + } + if (comp != null) { + if (!cands.isEmpty()) { + throw new IllegalArgumentException(); + } + return new Node(comp, nodes); + } else if (!cands.isEmpty()) { + return new Node((r, l, c) -> c.addAll(cands), nodes); + } else { + throw new IllegalArgumentException(); + } + } + + void addRoots(StringBuilder sb, List nodes) { + if (!nodes.isEmpty()) { + sb.append(" ( "); + boolean first = true; + for (Node n : nodes) { + if (first) { + first = false; + } else { + sb.append(" | "); + } + String name = "c" + completers.size(); + completers.put(name, n.completer); + sb.append(name); + addRoots(sb, n.nodes); + } + sb.append(" ) "); + } + } + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + completer.complete(reader, line, candidates); + } + + public static class Node { + final org.jline.reader.Completer completer; + final List nodes; + + public Node(org.jline.reader.Completer completer, List nodes) { + this.completer = completer; + this.nodes = nodes; + } + } + } + + public static class RegexCompleter implements org.jline.reader.Completer { + + private final NfaMatcher matcher; + private final Function completers; + private final ThreadLocal reader = new ThreadLocal<>(); + + public RegexCompleter(String syntax, Function completers) { + this.matcher = new NfaMatcher<>(syntax, this::doMatch); + this.completers = completers; + } + + @Override + public synchronized void complete(LineReader reader, ParsedLine line, List candidates) { + List words = line.words().subList(0, line.wordIndex()); + this.reader.set(reader); + Set next = matcher.matchPartial(words); + for (String n : next) { + completers.apply(n).complete(reader, new ArgumentLine(line.word(), line.wordCursor()), candidates); + } + this.reader.set(null); + } + + private boolean doMatch(String arg, String name) { + List candidates = new ArrayList<>(); + LineReader r = reader.get(); + boolean caseInsensitive = r != null && r.isSet(Option.CASE_INSENSITIVE); + completers.apply(name).complete(r, new ArgumentLine(arg, arg.length()), candidates); + return candidates.stream() + .anyMatch(c -> caseInsensitive + ? c.value().equalsIgnoreCase(arg) + : c.value().equals(arg)); + } + + public static class ArgumentLine implements ParsedLine { + private final String word; + private final int cursor; + + public ArgumentLine(String word, int cursor) { + this.word = word; + this.cursor = cursor; + } + + @Override + public String word() { + return word; + } + + @Override + public int wordCursor() { + return cursor; + } + + @Override + public int wordIndex() { + return 0; + } + + @Override + public List words() { + return Collections.singletonList(word); + } + + @Override + public String line() { + return word; + } + + @Override + public int cursor() { + return cursor; + } + } + } + + public static class OptDesc { + private String shortOption; + private String longOption; + private String description; + private org.jline.reader.Completer valueCompleter; + + /** + * Command option description. If option does not have short/long option assign to it null value. + * If option does not have value set valueCompleter = NullCompleter.INSTANCE + * + * @param shortOption short option + * @param longOption long option + * @param description short option description + * @param valueCompleter option value completer + */ + public OptDesc( + String shortOption, String longOption, String description, org.jline.reader.Completer valueCompleter) { + this.shortOption = shortOption; + this.longOption = longOption; + this.description = description; + this.valueCompleter = valueCompleter; + } + + /** + * Command option description. If option does not have short/long option assign to it null value. + * If option does not have value set valueCompleter = NullCompleter.INSTANCE + * + * @param shortOption short option + * @param longOption long option + * @param valueCompleter option value completer + */ + public OptDesc(String shortOption, String longOption, org.jline.reader.Completer valueCompleter) { + this(shortOption, longOption, null, valueCompleter); + } + + /** + * Command option description. If option does not have short/long option assign to it null value. + * + * @param shortOption short option + * @param longOption long option + * @param description short option description + */ + public OptDesc(String shortOption, String longOption, String description) { + this(shortOption, longOption, description, null); + } + + /** + * Command option description. If option does not have short/long option assign to it null value. + * + * @param shortOption short option + * @param longOption long option + */ + public OptDesc(String shortOption, String longOption) { + this(shortOption, longOption, null, null); + } + + protected OptDesc() { + } + + protected static List compile(Map> optionValues, Collection options) { + List out = new ArrayList<>(); + for (Map.Entry> entry : optionValues.entrySet()) { + if (entry.getKey().startsWith("--")) { + out.add(new OptDesc(null, entry.getKey(), new StringsCompleter(entry.getValue()))); + } else if (entry.getKey().matches("-[a-zA-Z]")) { + out.add(new OptDesc(entry.getKey(), null, new StringsCompleter(entry.getValue()))); + } + } + for (String o : options) { + if (o.startsWith("--")) { + out.add(new OptDesc(null, o)); + } else if (o.matches("-[a-zA-Z]")) { + out.add(new OptDesc(o, null)); + } + } + return out; + } + + public void setValueCompleter(org.jline.reader.Completer valueCompleter) { + this.valueCompleter = valueCompleter; + } + + public String longOption() { + return longOption; + } + + public String shortOption() { + return shortOption; + } + + public String description() { + return description; + } + + protected boolean hasValue() { + return valueCompleter != null && valueCompleter != NullCompleter.INSTANCE; + } + + protected org.jline.reader.Completer valueCompleter() { + return valueCompleter; + } + + protected void completeOption( + LineReader reader, final ParsedLine commandLine, List candidates, boolean longOpt) { + if (!longOpt) { + if (shortOption != null) { + candidates.add(new Candidate(shortOption, shortOption, null, description, null, null, false)); + } + } else if (longOption != null) { + if (hasValue()) { + candidates.add(new Candidate(longOption + "=", longOption, null, description, null, null, false)); + } else { + candidates.add(new Candidate(longOption, longOption, null, description, null, null, true)); + } + } + } + + protected boolean completeValue( + LineReader reader, + final ParsedLine commandLine, + List candidates, + String curBuf, + String partialValue) { + boolean out = false; + List temp = new ArrayList<>(); + ParsedLine pl = reader.getParser().parse(partialValue, partialValue.length()); + valueCompleter.complete(reader, pl, temp); + for (Candidate c : temp) { + String v = c.value(); + if (v.startsWith(partialValue)) { + out = true; + String val = c.value(); + if (valueCompleter instanceof FileNameCompleter cc) { + String sep = cc.getSeparator(reader.isSet(LineReader.Option.USE_FORWARD_SLASH)); + val = cc.getDisplay(reader.getTerminal(), Paths.get(c.value()), Styles.lsStyle(), sep); + } + candidates.add(new Candidate(curBuf + v, val, null, null, null, null, c.complete())); + } + } + return out; + } + + protected boolean match(String option) { + return (shortOption != null && shortOption.equals(option)) + || (longOption != null && longOption.equals(option)); + } + + protected boolean startsWith(String option) { + return (shortOption != null && shortOption.startsWith(option)) + || (longOption != null && longOption.startsWith(option)); + } + } + + public static class OptionCompleter implements org.jline.reader.Completer { + private Function> commandOptions; + private Collection options; + private List argsCompleters = new ArrayList<>(); + private int startPos; + + /** + * OptionCompleter completes command options and parameters. OptionCompleter should be used as an argument of ArgumentCompleter + * + * @param completer command parameter completer + * @param commandOptions command options descriptions + * @param startPos OptionCompleter position in ArgumentCompleter parameters + */ + public OptionCompleter( + org.jline.reader.Completer completer, + Function> commandOptions, + int startPos) { + this.startPos = startPos; + this.commandOptions = commandOptions; + this.argsCompleters.add(completer); + } + + /** + * OptionCompleter completes command options and parameters. OptionCompleter should be used as an argument of ArgumentCompleter + * + * @param completers command parameters completers + * @param commandOptions command options descriptions + * @param startPos OptionCompleter position in ArgumentCompleter parameters + */ + public OptionCompleter( + List completers, + Function> commandOptions, + int startPos) { + this.startPos = startPos; + this.commandOptions = commandOptions; + this.argsCompleters = new ArrayList<>(completers); + } + + /** + * OptionCompleter completes command options and parameters. OptionCompleter should be used as an argument of ArgumentCompleter + * + * @param completers command parameters completers + * @param optionValues command value options as map key and its possible values as map value + * @param options command options that do not have value + * @param startPos OptionCompleter position in ArgumentCompleter parameters + */ + public OptionCompleter( + List completers, + Map> optionValues, + Collection options, + int startPos) { + this(optionValues, options, startPos); + this.argsCompleters = new ArrayList<>(completers); + } + + /** + * OptionCompleter completes command options and parameters. OptionCompleter should be used as an argument of ArgumentCompleter + * + * @param completer command parameter completer + * @param optionValues command value options as map key and its possible values as map value + * @param options command options that do not have value + * @param startPos OptionCompleter position in ArgumentCompleter parameters + */ + public OptionCompleter( + org.jline.reader.Completer completer, + Map> optionValues, + Collection options, + int startPos) { + this(optionValues, options, startPos); + this.argsCompleters.add(completer); + } + + /** + * OptionCompleter completes command options and parameters. OptionCompleter should be used as an argument of ArgumentCompleter + * + * @param optionValues command value options as map key and its possible values as map value + * @param options command options that do not have value + * @param startPos OptionCompleter position in ArgumentCompleter parameters + */ + public OptionCompleter(Map> optionValues, Collection options, int startPos) { + this(OptDesc.compile(optionValues, options), startPos); + } + + /** + * OptionCompleter completes command options and parameters. OptionCompleter should be used as an argument of ArgumentCompleter + * + * @param completer command parameter completer + * @param options command options that do not have value + * @param startPos OptionCompleter position in ArgumentCompleter parameters + */ + public OptionCompleter(org.jline.reader.Completer completer, Collection options, int startPos) { + this(options, startPos); + this.argsCompleters.add(completer); + } + + /** + * OptionCompleter completes command options and parameters. OptionCompleter should be used as an argument of ArgumentCompleter + * + * @param completers command parameters completers + * @param options command options that do not have value + * @param startPos OptionCompleter position in ArgumentCompleter parameters + */ + public OptionCompleter(List completers, Collection options, int startPos) { + this(options, startPos); + this.argsCompleters = new ArrayList<>(completers); + } + + /** + * OptionCompleter completes command options and parameters. OptionCompleter should be used as an argument of ArgumentCompleter + * + * @param options command options that do not have value + * @param startPos OptionCompleter position in ArgumentCompleter parameters + */ + public OptionCompleter(Collection options, int startPos) { + this.options = options; + this.startPos = startPos; + } + + public void setStartPos(int startPos) { + this.startPos = startPos; + } + + @Override + public void complete(LineReader reader, final ParsedLine commandLine, List candidates) { + assert commandLine != null; + assert candidates != null; + List words = commandLine.words(); + String buffer = commandLine.word().substring(0, commandLine.wordCursor()); + if (startPos >= words.size()) { + candidates.add(new Candidate(buffer, buffer, null, null, null, null, true)); + return; + } + String command = reader.getParser().getCommand(words.get(startPos - 1)); + if (buffer.startsWith("-")) { + boolean addbuff = true; + boolean valueCandidates = false; + boolean longOption = buffer.startsWith("--"); + int eq = buffer.matches("-[a-zA-Z][a-zA-Z0-9]+") ? 2 : buffer.indexOf('='); + if (eq < 0) { + List usedOptions = new ArrayList<>(); + for (int i = startPos; i < words.size(); i++) { + if (words.get(i).startsWith("-")) { + String w = words.get(i); + int ind = w.indexOf('='); + if (ind < 0) { + usedOptions.add(w); + } else { + usedOptions.add(w.substring(0, ind)); + } + } + } + for (OptDesc o : commandOptions == null ? options : commandOptions.apply(command)) { + if (usedOptions.contains(o.shortOption()) || usedOptions.contains(o.longOption())) { + continue; + } + if (o.startsWith(buffer)) { + addbuff = false; + } + o.completeOption(reader, commandLine, candidates, longOption); + } + } else { + addbuff = false; + int nb = buffer.contains("=") ? 1 : 0; + String value = buffer.substring(eq + nb); + String curBuf = buffer.substring(0, eq + nb); + String opt = buffer.substring(0, eq); + OptDesc option = findOptDesc(command, opt); + if (option.hasValue()) { + valueCandidates = option.completeValue(reader, commandLine, candidates, curBuf, value); + } + } + if ((buffer.contains("=") && !buffer.endsWith("=") && !valueCandidates) || addbuff) { + candidates.add(new Candidate(buffer, buffer, null, null, null, null, true)); + } + } else if (words.size() > 1 && shortOptionValueCompleter(command, words.get(words.size() - 2)) != null) { + shortOptionValueCompleter(command, words.get(words.size() - 2)) + .complete(reader, commandLine, candidates); + } else if (words.size() > 1 && longOptionValueCompleter(command, words.get(words.size() - 2)) != null) { + longOptionValueCompleter(command, words.get(words.size() - 2)) + .complete(reader, commandLine, candidates); + } else if (!argsCompleters.isEmpty()) { + int args = -1; + for (int i = startPos; i < words.size(); i++) { + if (!words.get(i).startsWith("-")) { + if (i > 0 + && shortOptionValueCompleter(command, words.get(i - 1)) == null + && longOptionValueCompleter(command, words.get(i - 1)) == null) { + args++; + } + } + } + if (args == -1) { + candidates.add(new Candidate(buffer, buffer, null, null, null, null, true)); + } else if (args < argsCompleters.size()) { + argsCompleters.get(args).complete(reader, commandLine, candidates); + } else { + argsCompleters.get(argsCompleters.size() - 1).complete(reader, commandLine, candidates); + } + } + } + + private org.jline.reader.Completer longOptionValueCompleter(String command, String opt) { + if (!opt.matches("--[a-zA-Z]+")) { + return null; + } + Collection optDescs = commandOptions == null ? options : commandOptions.apply(command); + OptDesc option = findOptDesc(optDescs, opt); + return option.hasValue() ? option.valueCompleter() : null; + } + + private org.jline.reader.Completer shortOptionValueCompleter(String command, String opt) { + if (!opt.matches("-[a-zA-Z]+")) { + return null; + } + org.jline.reader.Completer out = null; + Collection optDescs = commandOptions == null ? options : commandOptions.apply(command); + if (opt.length() == 2) { + out = findOptDesc(optDescs, opt).valueCompleter(); + } else if (opt.length() > 2) { + for (int i = 1; i < opt.length(); i++) { + OptDesc o = findOptDesc(optDescs, "-" + opt.charAt(i)); + if (o.shortOption() == null) { + return null; + } else if (out == null) { + out = o.valueCompleter(); + } + } + } + return out; + } + + private OptDesc findOptDesc(String command, String opt) { + return findOptDesc(commandOptions == null ? options : commandOptions.apply(command), opt); + } + + private OptDesc findOptDesc(Collection optDescs, String opt) { + for (OptDesc o : optDescs) { + if (o.match(opt)) { + return o; + } + } + return new OptDesc(); + } + } + + public static class AnyCompleter implements org.jline.reader.Completer { + public static final AnyCompleter INSTANCE = new AnyCompleter(); + + @Override + public void complete(LineReader reader, ParsedLine commandLine, List candidates) { + assert commandLine != null; + assert candidates != null; + String buffer = commandLine.word().substring(0, commandLine.wordCursor()); + candidates.add(new Candidate(AttributedString.stripAnsi(buffer), buffer, null, null, null, null, true)); + } + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/ConfigurationPath.java b/net-cli/src/main/java/org/jline/builtins/ConfigurationPath.java new file mode 100644 index 0000000..2f8fe46 --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/ConfigurationPath.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2002-2019, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.io.IOException; +import java.nio.file.Path; + +public class ConfigurationPath { + private final Path appConfig; + private final Path userConfig; + + /** + * Configuration class constructor. + * + * @param appConfig Application configuration directory + * @param userConfig User private configuration directory + */ + public ConfigurationPath(Path appConfig, Path userConfig) { + this.appConfig = appConfig; + this.userConfig = userConfig; + } + + /** + * Search configuration file first from userConfig and then appConfig directory. Returns null if file is not found. + * + * @param name Configuration file name. + * @return Configuration file. + */ + public Path getConfig(String name) { + Path out = null; + if (userConfig != null && userConfig.resolve(name).toFile().exists()) { + out = userConfig.resolve(name); + } else if (appConfig != null && appConfig.resolve(name).toFile().exists()) { + out = appConfig.resolve(name); + } + return out; + } + + /** + * Search configuration file from userConfig directory. Returns null if file is not found. + * + * @param name Configuration file name. + * @return Configuration file. + * @throws IOException When we do not have read access to the file or directory. + */ + public Path getUserConfig(String name) throws IOException { + return getUserConfig(name, false); + } + + /** + * Search configuration file from userConfig directory. Returns null if file is not found. + * + * @param name Configuration file name + * @param create When true configuration file is created if not found. + * @return Configuration file. + * @throws IOException When we do not have read/write access to the file or directory. + */ + public Path getUserConfig(String name, boolean create) throws IOException { + Path out = null; + if (userConfig != null) { + if (!userConfig.resolve(name).toFile().exists() && create) { + userConfig.resolve(name).toFile().createNewFile(); + } + if (userConfig.resolve(name).toFile().exists()) { + out = userConfig.resolve(name); + } + } + return out; + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/ConsoleOptionGetter.java b/net-cli/src/main/java/org/jline/builtins/ConsoleOptionGetter.java new file mode 100644 index 0000000..9cf9b35 --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/ConsoleOptionGetter.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +public interface ConsoleOptionGetter { + + /** + * Return console option value + * + * @param name the option name + * @return option value + */ + Object consoleOption(String name); + + /** + * Read console option value + * + * @param option type + * @param option option name + * @param defval default value + * @return option value + */ + T consoleOption(String option, T defval); +} diff --git a/net-cli/src/main/java/org/jline/builtins/InputRC.java b/net-cli/src/main/java/org/jline/builtins/InputRC.java new file mode 100644 index 0000000..274f749 --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/InputRC.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.net.URL; +import org.jline.reader.LineReader; + +public final class InputRC { + + public static void configure(LineReader reader, URL url) throws IOException { + org.jline.reader.impl.InputRC.configure(reader, url); + } + + public static void configure(LineReader reader, InputStream is) throws IOException { + org.jline.reader.impl.InputRC.configure(reader, is); + } + + public static void configure(LineReader reader, Reader r) throws IOException { + org.jline.reader.impl.InputRC.configure(reader, r); + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/Less.java b/net-cli/src/main/java/org/jline/builtins/Less.java new file mode 100644 index 0000000..99d6a4a --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/Less.java @@ -0,0 +1,1587 @@ +/* + * Copyright (c) 2002-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Stream; +import org.jline.builtins.Source.ResourceSource; +import org.jline.builtins.Source.URLSource; +import org.jline.keymap.BindingReader; +import org.jline.keymap.KeyMap; +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.Terminal.Signal; +import org.jline.terminal.Terminal.SignalHandler; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.Display; +import org.jline.utils.InfoCmp.Capability; +import org.jline.utils.NonBlockingReader; +import org.jline.utils.Status; +import static org.jline.builtins.SyntaxHighlighter.COMMAND_INCLUDE; +import static org.jline.builtins.SyntaxHighlighter.COMMAND_THEME; +import static org.jline.builtins.SyntaxHighlighter.DEFAULT_LESSRC_FILE; +import static org.jline.builtins.SyntaxHighlighter.RuleSplitter; +import static org.jline.keymap.KeyMap.alt; +import static org.jline.keymap.KeyMap.ctrl; +import static org.jline.keymap.KeyMap.del; +import static org.jline.keymap.KeyMap.key; + +public class Less { + + private static final int ESCAPE = 27; + private static final String MESSAGE_FILE_INFO = "FILE_INFO"; + protected final Terminal terminal; + protected final Display display; + protected final BindingReader bindingReader; + protected final Path currentDir; + protected final StringBuilder buffer = new StringBuilder(); + protected final Map options = new TreeMap<>(); + protected final Size size = new Size(); + private final List syntaxFiles = new ArrayList<>(); + public boolean quitAtSecondEof; + public boolean quitAtFirstEof; + public boolean quitIfOneScreen; + public boolean printLineNumbers; + public boolean quiet; + public boolean veryQuiet; + public boolean chopLongLines; + public boolean ignoreCaseCond; + public boolean ignoreCaseAlways; + public boolean noKeypad; + public boolean noInit; + protected List tabs = Collections.singletonList(4); + protected String syntaxName; + protected List sources; + protected int sourceIdx; + protected BufferedReader reader; + protected KeyMap keys; + protected int firstLineInMemory = 0; + protected List lines = new ArrayList<>(); + protected int firstLineToDisplay = 0; + protected int firstColumnToDisplay = 0; + protected int offsetInLine = 0; + protected String message; + protected String errorMessage; + protected int window; + protected int halfWindow; + protected int nbEof; + protected String pattern; + protected String displayPattern; + SyntaxHighlighter syntaxHighlighter; + private String historyLog = null; + private boolean highlight = true; + private boolean nanorcIgnoreErrors; + + public Less(Terminal terminal, Path currentDir) { + this(terminal, currentDir, null); + } + + public Less(Terminal terminal, Path currentDir, Options opts) { + this(terminal, currentDir, opts, null); + } + + public Less(Terminal terminal, Path currentDir, Options opts, ConfigurationPath configPath) { + this.terminal = terminal; + this.display = new Display(terminal, true); + this.bindingReader = new BindingReader(terminal.reader()); + this.currentDir = currentDir; + Path lessrc = configPath != null ? configPath.getConfig(DEFAULT_LESSRC_FILE) : null; + boolean ignorercfiles = opts != null && opts.isSet("ignorercfiles"); + if (lessrc != null && !ignorercfiles) { + try { + parseConfig(lessrc); + } catch (IOException e) { + errorMessage = "Encountered error while reading config file: " + lessrc; + } + } else if (new File("/usr/share/nano").exists() && !ignorercfiles) { + PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:/usr/share/nano/*.nanorc"); + try (Stream pathStream = Files.walk(Paths.get("/usr/share/nano"))) { + pathStream.filter(pathMatcher::matches).forEach(syntaxFiles::add); + nanorcIgnoreErrors = true; + } catch (IOException e) { + errorMessage = "Encountered error while reading nanorc files"; + } + } + if (opts != null) { + if (opts.isSet("QUIT-AT-EOF")) { + quitAtFirstEof = true; + } + if (opts.isSet("quit-at-eof")) { + quitAtSecondEof = true; + } + if (opts.isSet("quit-if-one-screen")) { + quitIfOneScreen = true; + } + if (opts.isSet("quiet")) { + quiet = true; + } + if (opts.isSet("QUIET")) { + veryQuiet = true; + } + if (opts.isSet("chop-long-lines")) { + chopLongLines = true; + } + if (opts.isSet("IGNORE-CASE")) { + ignoreCaseAlways = true; + } + if (opts.isSet("ignore-case")) { + ignoreCaseCond = true; + } + if (opts.isSet("LINE-NUMBERS")) { + printLineNumbers = true; + } + if (opts.isSet("tabs")) { + doTabs(opts.get("tabs")); + } + if (opts.isSet("syntax")) { + syntaxName = opts.get("syntax"); + nanorcIgnoreErrors = false; + } + if (opts.isSet("no-init")) { + noInit = true; + } + if (opts.isSet("no-keypad")) { + noKeypad = true; + } + if (opts.isSet("historylog")) { + historyLog = opts.get("historylog"); + } + } + } + + public static String[] usage() { + return new String[]{ + "less - file pager", + "Usage: less [OPTIONS] [FILES]", + " -? --help Show help", + " -e --quit-at-eof Exit on second EOF", + " -E --QUIT-AT-EOF Exit on EOF", + " -F --quit-if-one-screen Exit if entire file fits on first screen", + " -q --quiet --silent Silent mode", + " -Q --QUIET --SILENT Completely silent", + " -S --chop-long-lines Do not fold long lines", + " -i --ignore-case Search ignores lowercase case", + " -I --IGNORE-CASE Search ignores all case", + " -x --tabs=N[,...] Set tab stops", + " -N --LINE-NUMBERS Display line number for each line", + " -Y --syntax=name The name of the syntax highlighting to use.", + " --no-init Disable terminal initialization", + " --no-keypad Disable keypad handling", + " --ignorercfiles Don't look at the system's lessrc nor at the user's lessrc.", + " -H --historylog=name Log search strings to file, so they can be retrieved in later sessions" + }; + } + + /** + * This is for long running commands to be interrupted by ctrl-c + * + * @throws InterruptedException if the thread has been interruped + */ + public static void checkInterrupted() throws InterruptedException { + Thread.yield(); + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + } + + private void parseConfig(Path file) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(file.toFile()))) { + String line = reader.readLine(); + while (line != null) { + line = line.trim(); + if (line.length() > 0 && !line.startsWith("#")) { + List parts = RuleSplitter.split(line); + if (parts.get(0).equals(COMMAND_INCLUDE)) { + SyntaxHighlighter.nanorcInclude(parts.get(1), syntaxFiles); + } else if (parts.get(0).equals(COMMAND_THEME)) { + SyntaxHighlighter.nanorcTheme(parts.get(1), syntaxFiles); + } else if (parts.size() == 2 + && (parts.get(0).equals("set") || parts.get(0).equals("unset"))) { + String option = parts.get(1); + boolean val = parts.get(0).equals("set"); + if (option.equals("QUIT-AT-EOF")) { + quitAtFirstEof = val; + } else if (option.equals("quit-at-eof")) { + quitAtSecondEof = val; + } else if (option.equals("quit-if-one-screen")) { + quitIfOneScreen = val; + } else if (option.equals("quiet") || option.equals("silent")) { + quiet = val; + } else if (option.equals("QUIET") || option.equals("SILENT")) { + veryQuiet = val; + } else if (option.equals("chop-long-lines")) { + chopLongLines = val; + } else if (option.equals("IGNORE-CASE")) { + ignoreCaseAlways = val; + } else if (option.equals("ignore-case")) { + ignoreCaseCond = val; + } else if (option.equals("LINE-NUMBERS")) { + printLineNumbers = val; + } else { + errorMessage = "Less config: Unknown or unsupported configuration option " + option; + } + } else if (parts.size() == 3 && parts.get(0).equals("set")) { + String option = parts.get(1); + String val = parts.get(2); + if (option.equals("tabs")) { + doTabs(val); + } else if (option.equals("historylog")) { + historyLog = val; + } else { + errorMessage = "Less config: Unknown or unsupported configuration option " + option; + } + } else if (parts.get(0).equals("bind") || parts.get(0).equals("unbind")) { + errorMessage = "Less config: Key bindings can not be changed!"; + } else { + errorMessage = "Less config: Bad configuration '" + line + "'"; + } + } + line = reader.readLine(); + } + } + } + + private void doTabs(String val) { + tabs = new ArrayList<>(); + for (String s : val.split(",")) { + try { + tabs.add(Integer.parseInt(s)); + } catch (Exception ex) { + errorMessage = "Less config: tabs option error parsing number: " + s; + } + } + } + + // to be removed + public Less tabs(List tabs) { + this.tabs = tabs; + return this; + } + + public void handle(Signal signal) { + size.copy(terminal.getSize()); + try { + display.clear(); + display(false); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void run(Source... sources) throws IOException, InterruptedException { + run(new ArrayList<>(Arrays.asList(sources))); + } + + public void run(List sources) throws IOException, InterruptedException { + if (sources == null || sources.isEmpty()) { + throw new IllegalArgumentException("No sources"); + } + sources.add(0, new ResourceSource("less-help.txt", "HELP -- Press SPACE for more, or q when done")); + this.sources = sources; + + sourceIdx = 1; + openSource(); + if (errorMessage != null) { + message = errorMessage; + errorMessage = null; + } + Status status = Status.getStatus(terminal, false); + + try { + if (status != null) { + status.suspend(); + } + size.copy(terminal.getSize()); + + if (quitIfOneScreen && sources.size() == 2) { + if (display(true)) { + return; + } + } + + SignalHandler prevHandler = terminal.handle(Signal.WINCH, this::handle); + Attributes attr = terminal.enterRawMode(); + try { + window = size.getRows() - 1; + halfWindow = window / 2; + keys = new KeyMap<>(); + bindKeys(keys); + + // Use alternate buffer + if (!noInit) { + terminal.puts(Capability.enter_ca_mode); + } + if (!noKeypad) { + terminal.puts(Capability.keypad_xmit); + } + terminal.writer().flush(); + + display.clear(); + display(false); + checkInterrupted(); + + options.put("-e", Operation.OPT_QUIT_AT_SECOND_EOF); + options.put("--quit-at-eof", Operation.OPT_QUIT_AT_SECOND_EOF); + options.put("-E", Operation.OPT_QUIT_AT_FIRST_EOF); + options.put("-QUIT-AT-EOF", Operation.OPT_QUIT_AT_FIRST_EOF); + options.put("-N", Operation.OPT_PRINT_LINES); + options.put("--LINE-NUMBERS", Operation.OPT_PRINT_LINES); + options.put("-q", Operation.OPT_QUIET); + options.put("--quiet", Operation.OPT_QUIET); + options.put("--silent", Operation.OPT_QUIET); + options.put("-Q", Operation.OPT_VERY_QUIET); + options.put("--QUIET", Operation.OPT_VERY_QUIET); + options.put("--SILENT", Operation.OPT_VERY_QUIET); + options.put("-S", Operation.OPT_CHOP_LONG_LINES); + options.put("--chop-long-lines", Operation.OPT_CHOP_LONG_LINES); + options.put("-i", Operation.OPT_IGNORE_CASE_COND); + options.put("--ignore-case", Operation.OPT_IGNORE_CASE_COND); + options.put("-I", Operation.OPT_IGNORE_CASE_ALWAYS); + options.put("--IGNORE-CASE", Operation.OPT_IGNORE_CASE_ALWAYS); + options.put("-Y", Operation.OPT_SYNTAX_HIGHLIGHT); + options.put("--syntax", Operation.OPT_SYNTAX_HIGHLIGHT); + + Operation op; + boolean forward = true; + do { + checkInterrupted(); + + op = null; + // + // Option edition + // + if (buffer.length() > 0 && buffer.charAt(0) == '-') { + int c = terminal.reader().read(); + message = null; + if (buffer.length() == 1) { + buffer.append((char) c); + if (c != '-') { + op = options.get(buffer.toString()); + if (op == null) { + message = "There is no " + printable(buffer.toString()) + " option"; + buffer.setLength(0); + } + } + } else if (c == '\r') { + op = options.get(buffer.toString()); + if (op == null) { + message = "There is no " + printable(buffer.toString()) + " option"; + buffer.setLength(0); + } + } else { + buffer.append((char) c); + Map matching = new HashMap<>(); + for (Map.Entry entry : options.entrySet()) { + if (entry.getKey().startsWith(buffer.toString())) { + matching.put(entry.getKey(), entry.getValue()); + } + } + switch (matching.size()) { + case 0: + buffer.setLength(0); + break; + case 1: + buffer.setLength(0); + buffer.append(matching.keySet().iterator().next()); + break; + } + } + } + // + // Pattern edition + // + else if (buffer.length() > 0 + && (buffer.charAt(0) == '/' || buffer.charAt(0) == '?' || buffer.charAt(0) == '&')) { + forward = search(); + } + // + // Command reading + // + else { + Operation obj = bindingReader.readBinding(keys, null, false); + if (obj == Operation.CHAR) { + char c = bindingReader.getLastBinding().charAt(0); + // Enter option mode or pattern edit mode + if (c == '-' || c == '/' || c == '?' || c == '&') { + buffer.setLength(0); + } + buffer.append(c); + } else if (obj == Operation.BACKSPACE) { + if (buffer.length() > 0) { + buffer.deleteCharAt(buffer.length() - 1); + } + } else { + op = obj; + } + } + if (op != null) { + message = null; + switch (op) { + case FORWARD_ONE_LINE: + moveForward(getStrictPositiveNumberInBuffer(1)); + break; + case BACKWARD_ONE_LINE: + moveBackward(getStrictPositiveNumberInBuffer(1)); + break; + case FORWARD_ONE_WINDOW_OR_LINES: + moveForward(getStrictPositiveNumberInBuffer(window)); + break; + case FORWARD_ONE_WINDOW_AND_SET: + window = getStrictPositiveNumberInBuffer(window); + moveForward(window); + break; + case FORWARD_ONE_WINDOW_NO_STOP: + moveForward(window); + // TODO: handle no stop + break; + case FORWARD_HALF_WINDOW_AND_SET: + halfWindow = getStrictPositiveNumberInBuffer(halfWindow); + moveForward(halfWindow); + break; + case BACKWARD_ONE_WINDOW_AND_SET: + window = getStrictPositiveNumberInBuffer(window); + moveBackward(window); + break; + case BACKWARD_ONE_WINDOW_OR_LINES: + moveBackward(getStrictPositiveNumberInBuffer(window)); + break; + case BACKWARD_HALF_WINDOW_AND_SET: + halfWindow = getStrictPositiveNumberInBuffer(halfWindow); + moveBackward(halfWindow); + break; + case GO_TO_FIRST_LINE_OR_N: + moveTo(getStrictPositiveNumberInBuffer(1) - 1); + break; + case GO_TO_LAST_LINE_OR_N: + int lineNum = getStrictPositiveNumberInBuffer(0) - 1; + if (lineNum < 0) { + moveForward(Integer.MAX_VALUE); + } else { + moveTo(lineNum); + } + break; + case HOME: + moveTo(0); + break; + case END: + moveForward(Integer.MAX_VALUE); + break; + case LEFT_ONE_HALF_SCREEN: + firstColumnToDisplay = Math.max(0, firstColumnToDisplay - size.getColumns() / 2); + break; + case RIGHT_ONE_HALF_SCREEN: + firstColumnToDisplay += size.getColumns() / 2; + break; + case REPEAT_SEARCH_BACKWARD_SPAN_FILES: + moveToMatch(!forward, true); + break; + case REPEAT_SEARCH_BACKWARD: + moveToMatch(!forward, false); + break; + case REPEAT_SEARCH_FORWARD_SPAN_FILES: + moveToMatch(forward, true); + break; + case REPEAT_SEARCH_FORWARD: + moveToMatch(forward, false); + break; + case UNDO_SEARCH: + pattern = null; + break; + case OPT_PRINT_LINES: + buffer.setLength(0); + printLineNumbers = !printLineNumbers; + message = + printLineNumbers ? "Constantly display line numbers" : "Don't use line numbers"; + break; + case OPT_QUIET: + buffer.setLength(0); + quiet = !quiet; + veryQuiet = false; + message = quiet + ? "Ring the bell for errors but not at eof/bof" + : "Ring the bell for errors AND at eof/bof"; + break; + case OPT_VERY_QUIET: + buffer.setLength(0); + veryQuiet = !veryQuiet; + quiet = false; + message = veryQuiet ? "Never ring the bell" : "Ring the bell for errors AND at eof/bof"; + break; + case OPT_CHOP_LONG_LINES: + buffer.setLength(0); + offsetInLine = 0; + chopLongLines = !chopLongLines; + message = chopLongLines ? "Chop long lines" : "Fold long lines"; + display.clear(); + break; + case OPT_IGNORE_CASE_COND: + ignoreCaseCond = !ignoreCaseCond; + ignoreCaseAlways = false; + message = + ignoreCaseCond ? "Ignore case in searches" : "Case is significant in searches"; + break; + case OPT_IGNORE_CASE_ALWAYS: + ignoreCaseAlways = !ignoreCaseAlways; + ignoreCaseCond = false; + message = ignoreCaseAlways + ? "Ignore case in searches and in patterns" + : "Case is significant in searches"; + break; + case OPT_SYNTAX_HIGHLIGHT: + highlight = !highlight; + message = "Highlight " + (highlight ? "enabled" : "disabled"); + break; + case ADD_FILE: + addFile(); + break; + case NEXT_FILE: + int next = getStrictPositiveNumberInBuffer(1); + if (sourceIdx < sources.size() - next) { + SavedSourcePositions ssp = new SavedSourcePositions(); + sourceIdx += next; + String newSource = sources.get(sourceIdx).getName(); + try { + openSource(); + } catch (FileNotFoundException exp) { + ssp.restore(newSource); + } + } else { + message = "No next file"; + } + break; + case PREV_FILE: + int prev = getStrictPositiveNumberInBuffer(1); + if (sourceIdx > prev) { + SavedSourcePositions ssp = new SavedSourcePositions(-1); + sourceIdx -= prev; + String newSource = sources.get(sourceIdx).getName(); + try { + openSource(); + } catch (FileNotFoundException exp) { + ssp.restore(newSource); + } + } else { + message = "No previous file"; + } + break; + case GOTO_FILE: + int tofile = getStrictPositiveNumberInBuffer(1); + if (tofile < sources.size()) { + SavedSourcePositions ssp = new SavedSourcePositions(tofile < sourceIdx ? -1 : 0); + sourceIdx = tofile; + String newSource = sources.get(sourceIdx).getName(); + try { + openSource(); + } catch (FileNotFoundException exp) { + ssp.restore(newSource); + } + } else { + message = "No such file"; + } + break; + case INFO_FILE: + message = MESSAGE_FILE_INFO; + break; + case DELETE_FILE: + if (sources.size() > 2) { + sources.remove(sourceIdx); + if (sourceIdx >= sources.size()) { + sourceIdx = sources.size() - 1; + } + openSource(); + } + break; + case REPAINT: + size.copy(terminal.getSize()); + display.clear(); + break; + case REPAINT_AND_DISCARD: + message = null; + size.copy(terminal.getSize()); + display.clear(); + break; + case HELP: + help(); + break; + } + buffer.setLength(0); + } + if (quitAtFirstEof && nbEof > 0 || quitAtSecondEof && nbEof > 1) { + if (sourceIdx < sources.size() - 1) { + sourceIdx++; + openSource(); + } else { + op = Operation.EXIT; + } + } + display(false); + } while (op != Operation.EXIT); + } catch (InterruptedException ie) { + // Do nothing + } finally { + terminal.setAttributes(attr); + if (prevHandler != null) { + terminal.handle(Terminal.Signal.WINCH, prevHandler); + } + // Use main buffer + if (!noInit) { + terminal.puts(Capability.exit_ca_mode); + } + if (!noKeypad) { + terminal.puts(Capability.keypad_local); + } + terminal.writer().flush(); + } + } finally { + if (reader != null) { + reader.close(); + } + if (status != null) { + status.restore(); + } + } + } + + private void moveToMatch(boolean forward, boolean spanFiles) throws IOException { + if (forward) { + moveToNextMatch(spanFiles); + } else { + moveToPreviousMatch(spanFiles); + } + } + + private void addSource(String file) throws IOException { + if (file.contains("*") || file.contains("?")) { + for (Path p : Commands.findFiles(currentDir, file)) { + sources.add(new URLSource(p.toUri().toURL(), p.toString())); + } + } else { + sources.add(new URLSource(currentDir.resolve(file).toUri().toURL(), file)); + } + sourceIdx = sources.size() - 1; + } + + private void addFile() throws IOException, InterruptedException { + KeyMap fileKeyMap = new KeyMap<>(); + fileKeyMap.setUnicode(Operation.INSERT); + for (char i = 32; i < 256; i++) { + fileKeyMap.bind(Operation.INSERT, Character.toString(i)); + } + fileKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right), alt('l')); + fileKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left), alt('h')); + fileKeyMap.bind(Operation.HOME, key(terminal, Capability.key_home), alt('0')); + fileKeyMap.bind(Operation.END, key(terminal, Capability.key_end), alt('$')); + fileKeyMap.bind(Operation.BACKSPACE, del()); + fileKeyMap.bind(Operation.DELETE, alt('x')); + fileKeyMap.bind(Operation.DELETE_WORD, alt('X')); + fileKeyMap.bind(Operation.DELETE_LINE, ctrl('U')); + fileKeyMap.bind(Operation.ACCEPT, "\r"); + + SavedSourcePositions ssp = new SavedSourcePositions(); + message = null; + buffer.append("Examine: "); + int curPos = buffer.length(); + final int begPos = curPos; + display(false, curPos); + LineEditor lineEditor = new LineEditor(begPos); + while (true) { + checkInterrupted(); + Operation op; + switch (op = bindingReader.readBinding(fileKeyMap)) { + case ACCEPT: + String name = buffer.substring(begPos); + addSource(name); + try { + openSource(); + } catch (Exception exp) { + ssp.restore(name); + } + return; + default: + curPos = lineEditor.editBuffer(op, curPos); + break; + } + if (curPos > begPos) { + display(false, curPos); + } else { + buffer.setLength(0); + return; + } + } + } + + private boolean search() throws IOException, InterruptedException { + KeyMap searchKeyMap = new KeyMap<>(); + searchKeyMap.setUnicode(Operation.INSERT); + for (char i = 32; i < 256; i++) { + searchKeyMap.bind(Operation.INSERT, Character.toString(i)); + } + searchKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right), alt('l')); + searchKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left), alt('h')); + searchKeyMap.bind(Operation.NEXT_WORD, alt('w')); + searchKeyMap.bind(Operation.PREV_WORD, alt('b')); + searchKeyMap.bind(Operation.HOME, key(terminal, Capability.key_home), alt('0')); + searchKeyMap.bind(Operation.END, key(terminal, Capability.key_end), alt('$')); + searchKeyMap.bind(Operation.BACKSPACE, del()); + searchKeyMap.bind(Operation.DELETE, alt('x')); + searchKeyMap.bind(Operation.DELETE_WORD, alt('X')); + searchKeyMap.bind(Operation.DELETE_LINE, ctrl('U')); + searchKeyMap.bind(Operation.UP, key(terminal, Capability.key_up), alt('k')); + searchKeyMap.bind(Operation.DOWN, key(terminal, Capability.key_down), alt('j')); + searchKeyMap.bind(Operation.ACCEPT, "\r"); + + boolean forward = true; + message = null; + int curPos = buffer.length(); + final int begPos = curPos; + final char type = buffer.charAt(0); + String currentBuffer = buffer.toString(); + LineEditor lineEditor = new LineEditor(begPos); + while (true) { + checkInterrupted(); + Operation op; + switch (op = bindingReader.readBinding(searchKeyMap)) { + case UP, DOWN: + buffer.setLength(0); + buffer.append(type); + curPos = buffer.length(); + break; + case ACCEPT: + try { + String _pattern = buffer.substring(1); + if (type == '&') { + displayPattern = _pattern.length() > 0 ? _pattern : null; + getPattern(true); + } else { + pattern = _pattern; + getPattern(); + if (type == '/') { + moveToNextMatch(); + } else { + if (lines.size() - firstLineToDisplay <= size.getRows()) { + firstLineToDisplay = lines.size(); + } else { + moveForward(size.getRows() - 1); + } + moveToPreviousMatch(); + forward = false; + } + } + buffer.setLength(0); + } catch (PatternSyntaxException e) { + String str = e.getMessage(); + if (str.indexOf('\n') > 0) { + str = str.substring(0, str.indexOf('\n')); + } + if (type == '&') { + displayPattern = null; + } else { + pattern = null; + } + buffer.setLength(0); + message = "Invalid pattern: " + str + " (Press a key)"; + display(false); + terminal.reader().read(); + message = null; + } + return forward; + default: + curPos = lineEditor.editBuffer(op, curPos); + currentBuffer = buffer.toString(); + break; + } + if (curPos < begPos) { + buffer.setLength(0); + return forward; + } else { + display(false, curPos); + } + } + } + + private void help() throws IOException { + SavedSourcePositions ssp = new SavedSourcePositions(); + printLineNumbers = false; + sourceIdx = 0; + try { + openSource(); + display(false); + Operation op; + do { + checkInterrupted(); + op = bindingReader.readBinding(keys, null, false); + if (op != null) { + switch (op) { + case FORWARD_ONE_WINDOW_OR_LINES: + moveForward(getStrictPositiveNumberInBuffer(window)); + break; + case BACKWARD_ONE_WINDOW_OR_LINES: + moveBackward(getStrictPositiveNumberInBuffer(window)); + break; + } + } + display(false); + } while (op != Operation.EXIT); + } catch (IOException | InterruptedException exp) { + // Do nothing + } finally { + ssp.restore(null); + } + } + + protected void openSource() throws IOException { + boolean wasOpen = false; + if (reader != null) { + reader.close(); + wasOpen = true; + } + boolean open; + boolean displayMessage = false; + do { + Source source = sources.get(sourceIdx); + try { + InputStream in = source.read(); + if (sources.size() == 2 || sourceIdx == 0) { + message = source.getName(); + } else { + message = source.getName() + " (file " + sourceIdx + " of " + (sources.size() - 1) + ")"; + } + reader = new BufferedReader(new InputStreamReader(new InterruptibleInputStream(in))); + firstLineInMemory = 0; + lines = new ArrayList<>(); + firstLineToDisplay = 0; + firstColumnToDisplay = 0; + offsetInLine = 0; + display.clear(); + if (sourceIdx == 0) { + syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, null, "none"); + } else { + syntaxHighlighter = + SyntaxHighlighter.build(syntaxFiles, source.getName(), syntaxName, nanorcIgnoreErrors); + } + open = true; + if (displayMessage) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.style(AttributedStyle.INVERSE); + asb.append(source.getName() + " (press RETURN)"); + asb.toAttributedString().println(terminal); + terminal.writer().flush(); + terminal.reader().read(); + } + } catch (FileNotFoundException exp) { + sources.remove(sourceIdx); + if (sourceIdx > sources.size() - 1) { + sourceIdx = sources.size() - 1; + } + if (wasOpen) { + throw exp; + } else { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(source.getName() + " not found!"); + asb.toAttributedString().println(terminal); + terminal.writer().flush(); + open = false; + displayMessage = true; + } + } + } while (!open && sourceIdx > 0); + if (!open) { + throw new FileNotFoundException(); + } + } + + void moveTo(int lineNum) throws IOException { + AttributedString line = getLine(lineNum); + if (line != null) { + display.clear(); + if (firstLineInMemory > lineNum) { + openSource(); + } + firstLineToDisplay = lineNum; + offsetInLine = 0; + } else { + message = "Cannot seek to line number " + (lineNum + 1); + } + } + + private void moveToNextMatch() throws IOException { + moveToNextMatch(false); + } + + private void moveToNextMatch(boolean spanFiles) throws IOException { + Pattern compiled = getPattern(); + Pattern dpCompiled = getPattern(true); + if (compiled != null) { + for (int lineNumber = firstLineToDisplay + 1; ; lineNumber++) { + AttributedString line = getLine(lineNumber); + if (line == null) { + break; + } else if (!toBeDisplayed(line, dpCompiled)) { + continue; + } else if (compiled.matcher(line).find()) { + display.clear(); + firstLineToDisplay = lineNumber; + offsetInLine = 0; + return; + } + } + } + if (spanFiles) { + if (sourceIdx < sources.size() - 1) { + SavedSourcePositions ssp = new SavedSourcePositions(); + String newSource = sources.get(++sourceIdx).getName(); + try { + openSource(); + moveToNextMatch(true); + } catch (FileNotFoundException exp) { + ssp.restore(newSource); + } + } else { + message = "Pattern not found"; + } + } else { + message = "Pattern not found"; + } + } + + private void moveToPreviousMatch() throws IOException { + moveToPreviousMatch(false); + } + + private void moveToPreviousMatch(boolean spanFiles) throws IOException { + Pattern compiled = getPattern(); + Pattern dpCompiled = getPattern(true); + if (compiled != null) { + for (int lineNumber = firstLineToDisplay - 1; lineNumber >= firstLineInMemory; lineNumber--) { + AttributedString line = getLine(lineNumber); + if (line == null) { + break; + } else if (!toBeDisplayed(line, dpCompiled)) { + continue; + } else if (compiled.matcher(line).find()) { + display.clear(); + firstLineToDisplay = lineNumber; + offsetInLine = 0; + return; + } + } + } + if (spanFiles) { + if (sourceIdx > 1) { + SavedSourcePositions ssp = new SavedSourcePositions(-1); + String newSource = sources.get(--sourceIdx).getName(); + try { + openSource(); + moveTo(Integer.MAX_VALUE); + moveToPreviousMatch(true); + } catch (FileNotFoundException exp) { + ssp.restore(newSource); + } + } else { + message = "Pattern not found"; + } + } else { + message = "Pattern not found"; + } + } + + private String printable(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == ESCAPE) { + sb.append("ESC"); + } else if (c < 32) { + sb.append('^').append((char) (c + '@')); + } else if (c < 128) { + sb.append(c); + } else { + sb.append('\\').append(String.format("%03o", (int) c)); + } + } + return sb.toString(); + } + + void moveForward(int lines) throws IOException { + Pattern dpCompiled = getPattern(true); + int width = size.getColumns() - (printLineNumbers ? 8 : 0); + int height = size.getRows(); + boolean doOffsets = firstColumnToDisplay == 0 && !chopLongLines; + if (lines >= size.getRows() - 1) { + display.clear(); + } + if (lines == Integer.MAX_VALUE) { + moveTo(Integer.MAX_VALUE); + firstLineToDisplay = height - 1; + for (int l = 0; l < height - 1; l++) { + firstLineToDisplay = + prevLine2display(firstLineToDisplay, dpCompiled).getU(); + } + } + while (--lines >= 0) { + int lastLineToDisplay = firstLineToDisplay; + if (!doOffsets) { + for (int l = 0; l < height - 1; l++) { + lastLineToDisplay = + nextLine2display(lastLineToDisplay, dpCompiled).getU(); + } + } else { + int off = offsetInLine; + for (int l = 0; l < height - 1; l++) { + Pair nextLine = nextLine2display(lastLineToDisplay, dpCompiled); + AttributedString line = nextLine.getV(); + if (line == null) { + lastLineToDisplay = nextLine.getU(); + break; + } + if (line.columnLength() > off + width) { + off += width; + } else { + off = 0; + lastLineToDisplay = nextLine.getU(); + } + } + } + if (getLine(lastLineToDisplay) == null) { + eof(); + return; + } + Pair nextLine = nextLine2display(firstLineToDisplay, dpCompiled); + AttributedString line = nextLine.getV(); + if (doOffsets && line.columnLength() > width + offsetInLine) { + offsetInLine += width; + } else { + offsetInLine = 0; + firstLineToDisplay = nextLine.getU(); + } + } + } + + void moveBackward(int lines) throws IOException { + Pattern dpCompiled = getPattern(true); + int width = size.getColumns() - (printLineNumbers ? 8 : 0); + if (lines >= size.getRows() - 1) { + display.clear(); + } + while (--lines >= 0) { + if (offsetInLine > 0) { + offsetInLine = Math.max(0, offsetInLine - width); + } else if (firstLineInMemory < firstLineToDisplay) { + Pair prevLine = prevLine2display(firstLineToDisplay, dpCompiled); + firstLineToDisplay = prevLine.getU(); + AttributedString line = prevLine.getV(); + if (line != null && firstColumnToDisplay == 0 && !chopLongLines) { + int length = line.columnLength(); + offsetInLine = length - length % width; + } + } else { + bof(); + return; + } + } + } + + private void eof() { + nbEof++; + if (sourceIdx > 0 && sourceIdx < sources.size() - 1) { + message = "(END) - Next: " + sources.get(sourceIdx + 1).getName(); + } else { + message = "(END)"; + } + if (!quiet && !veryQuiet && !quitAtFirstEof && !quitAtSecondEof) { + terminal.puts(Capability.bell); + terminal.writer().flush(); + } + } + + private void bof() { + if (!quiet && !veryQuiet) { + terminal.puts(Capability.bell); + terminal.writer().flush(); + } + } + + int getStrictPositiveNumberInBuffer(int def) { + try { + int n = Integer.parseInt(buffer.toString()); + return (n > 0) ? n : def; + } catch (NumberFormatException e) { + return def; + } finally { + buffer.setLength(0); + } + } + + private Pair nextLine2display(int line, Pattern dpCompiled) throws IOException { + AttributedString curLine; + do { + curLine = getLine(line++); + } while (!toBeDisplayed(curLine, dpCompiled)); + return new Pair<>(line, curLine); + } + + private Pair prevLine2display(int line, Pattern dpCompiled) throws IOException { + AttributedString curLine; + do { + curLine = getLine(line--); + } while (line > 0 && !toBeDisplayed(curLine, dpCompiled)); + if (line == 0 && !toBeDisplayed(curLine, dpCompiled)) { + curLine = null; + } + return new Pair<>(line, curLine); + } + + private boolean toBeDisplayed(AttributedString curLine, Pattern dpCompiled) { + return curLine == null + || dpCompiled == null + || sourceIdx == 0 + || dpCompiled.matcher(curLine).find(); + } + + synchronized boolean display(boolean oneScreen) throws IOException { + return display(oneScreen, null); + } + + synchronized boolean display(boolean oneScreen, Integer curPos) throws IOException { + List newLines = new ArrayList<>(); + int width = size.getColumns() - (printLineNumbers ? 8 : 0); + int height = size.getRows(); + int inputLine = firstLineToDisplay; + AttributedString curLine = null; + Pattern compiled = getPattern(); + Pattern dpCompiled = getPattern(true); + boolean fitOnOneScreen = false; + boolean eof = false; + if (highlight) { + syntaxHighlighter.reset(); + for (int i = Math.max(0, inputLine - height); i < inputLine; i++) { + syntaxHighlighter.highlight(getLine(i)); + } + } + for (int terminalLine = 0; terminalLine < height - 1; terminalLine++) { + if (curLine == null) { + Pair nextLine = nextLine2display(inputLine, dpCompiled); + inputLine = nextLine.getU(); + curLine = nextLine.getV(); + if (curLine == null) { + if (oneScreen) { + fitOnOneScreen = true; + break; + } + eof = true; + curLine = new AttributedString("~"); + } else if (highlight) { + curLine = syntaxHighlighter.highlight(curLine); + } + if (compiled != null) { + curLine = curLine.styleMatches(compiled, AttributedStyle.DEFAULT.inverse()); + } + } + AttributedString toDisplay; + if (firstColumnToDisplay > 0 || chopLongLines) { + int off = firstColumnToDisplay; + if (terminalLine == 0 && offsetInLine > 0) { + off = Math.max(offsetInLine, off); + } + toDisplay = curLine.columnSubSequence(off, off + width); + curLine = null; + } else { + if (terminalLine == 0 && offsetInLine > 0) { + curLine = curLine.columnSubSequence(offsetInLine, Integer.MAX_VALUE); + } + toDisplay = curLine.columnSubSequence(0, width); + curLine = curLine.columnSubSequence(width, Integer.MAX_VALUE); + if (curLine.length() == 0) { + curLine = null; + } + } + if (printLineNumbers && !eof) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.append(String.format("%7d ", inputLine)); + sb.append(toDisplay); + newLines.add(sb.toAttributedString()); + } else { + newLines.add(toDisplay); + } + } + if (oneScreen) { + if (fitOnOneScreen) { + newLines.forEach(l -> l.println(terminal)); + } + return fitOnOneScreen; + } + AttributedStringBuilder msg = new AttributedStringBuilder(); + if (MESSAGE_FILE_INFO.equals(message)) { + Source source = sources.get(sourceIdx); + Long allLines = source.lines(); + message = source.getName() + + (sources.size() > 2 ? " (file " + sourceIdx + " of " + (sources.size() - 1) + ")" : "") + + " lines " + (firstLineToDisplay + 1) + "-" + inputLine + "/" + + (allLines != null ? allLines : lines.size()) + + (eof ? " (END)" : ""); + } + if (buffer.length() > 0) { + msg.append(" ").append(buffer); + } else if (bindingReader.getCurrentBuffer().length() > 0 + && terminal.reader().peek(1) == NonBlockingReader.READ_EXPIRED) { + msg.append(" ").append(printable(bindingReader.getCurrentBuffer())); + } else if (message != null) { + msg.style(AttributedStyle.INVERSE); + msg.append(message); + msg.style(AttributedStyle.INVERSE.inverseOff()); + } else if (displayPattern != null) { + msg.append("&"); + } else { + msg.append(":"); + } + newLines.add(msg.toAttributedString()); + + display.resize(size.getRows(), size.getColumns()); + if (curPos == null) { + display.update(newLines, -1); + } else { + display.update(newLines, size.cursorPos(size.getRows() - 1, curPos + 1)); + } + return false; + } + + private Pattern getPattern() { + return getPattern(false); + } + + private Pattern getPattern(boolean doDisplayPattern) { + Pattern compiled = null; + String _pattern = doDisplayPattern ? displayPattern : pattern; + if (_pattern != null) { + boolean insensitive = + ignoreCaseAlways || ignoreCaseCond && _pattern.toLowerCase().equals(_pattern); + compiled = Pattern.compile( + "(" + _pattern + ")", insensitive ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE : 0); + } + return compiled; + } + + AttributedString getLine(int line) throws IOException { + while (line >= lines.size()) { + String str = reader.readLine(); + if (str != null) { + lines.add(AttributedString.fromAnsi(str, tabs)); + } else { + break; + } + } + if (line < lines.size()) { + return lines.get(line); + } + return null; + } + + private void bindKeys(KeyMap map) { + map.bind(Operation.HELP, "h", "H"); + map.bind(Operation.EXIT, "q", ":q", "Q", ":Q", "ZZ"); + map.bind(Operation.FORWARD_ONE_LINE, "e", ctrl('E'), "j", ctrl('N'), "\r", key(terminal, Capability.key_down)); + map.bind( + Operation.BACKWARD_ONE_LINE, + "y", + ctrl('Y'), + "k", + ctrl('K'), + ctrl('P'), + key(terminal, Capability.key_up)); + map.bind( + Operation.FORWARD_ONE_WINDOW_OR_LINES, + "f", + ctrl('F'), + ctrl('V'), + " ", + key(terminal, Capability.key_npage)); + map.bind(Operation.BACKWARD_ONE_WINDOW_OR_LINES, "b", ctrl('B'), alt('v'), key(terminal, Capability.key_ppage)); + map.bind(Operation.FORWARD_ONE_WINDOW_AND_SET, "z"); + map.bind(Operation.BACKWARD_ONE_WINDOW_AND_SET, "w"); + map.bind(Operation.FORWARD_ONE_WINDOW_NO_STOP, alt(' ')); + map.bind(Operation.FORWARD_HALF_WINDOW_AND_SET, "d", ctrl('D')); + map.bind(Operation.BACKWARD_HALF_WINDOW_AND_SET, "u", ctrl('U')); + map.bind(Operation.RIGHT_ONE_HALF_SCREEN, alt(')'), key(terminal, Capability.key_right)); + map.bind(Operation.LEFT_ONE_HALF_SCREEN, alt('('), key(terminal, Capability.key_left)); + map.bind(Operation.FORWARD_FOREVER, "F"); + map.bind(Operation.REPAINT, "r", ctrl('R'), ctrl('L')); + map.bind(Operation.REPAINT_AND_DISCARD, "R"); + map.bind(Operation.REPEAT_SEARCH_FORWARD, "n"); + map.bind(Operation.REPEAT_SEARCH_BACKWARD, "N"); + map.bind(Operation.REPEAT_SEARCH_FORWARD_SPAN_FILES, alt('n')); + map.bind(Operation.REPEAT_SEARCH_BACKWARD_SPAN_FILES, alt('N')); + map.bind(Operation.UNDO_SEARCH, alt('u')); + map.bind(Operation.GO_TO_FIRST_LINE_OR_N, "g", "<", alt('<')); + map.bind(Operation.GO_TO_LAST_LINE_OR_N, "G", ">", alt('>')); + map.bind(Operation.HOME, key(terminal, Capability.key_home)); + map.bind(Operation.END, key(terminal, Capability.key_end)); + map.bind(Operation.ADD_FILE, ":e", ctrl('X') + ctrl('V')); + map.bind(Operation.NEXT_FILE, ":n"); + map.bind(Operation.PREV_FILE, ":p"); + map.bind(Operation.GOTO_FILE, ":x"); + map.bind(Operation.INFO_FILE, "=", ":f", ctrl('G')); + map.bind(Operation.DELETE_FILE, ":d"); + map.bind(Operation.BACKSPACE, del()); + "-/0123456789?&".chars().forEach(c -> map.bind(Operation.CHAR, Character.toString((char) c))); + } + + protected enum Operation { + + // General + HELP, + EXIT, + + // Moving + FORWARD_ONE_LINE, + BACKWARD_ONE_LINE, + FORWARD_ONE_WINDOW_OR_LINES, + BACKWARD_ONE_WINDOW_OR_LINES, + FORWARD_ONE_WINDOW_AND_SET, + BACKWARD_ONE_WINDOW_AND_SET, + FORWARD_ONE_WINDOW_NO_STOP, + FORWARD_HALF_WINDOW_AND_SET, + BACKWARD_HALF_WINDOW_AND_SET, + LEFT_ONE_HALF_SCREEN, + RIGHT_ONE_HALF_SCREEN, + FORWARD_FOREVER, + REPAINT, + REPAINT_AND_DISCARD, + + // Searching + REPEAT_SEARCH_FORWARD, + REPEAT_SEARCH_BACKWARD, + REPEAT_SEARCH_FORWARD_SPAN_FILES, + REPEAT_SEARCH_BACKWARD_SPAN_FILES, + UNDO_SEARCH, + + // Jumping + GO_TO_FIRST_LINE_OR_N, + GO_TO_LAST_LINE_OR_N, + GO_TO_PERCENT_OR_N, + GO_TO_NEXT_TAG, + GO_TO_PREVIOUS_TAG, + FIND_CLOSE_BRACKET, + FIND_OPEN_BRACKET, + + // Options + OPT_PRINT_LINES, + OPT_CHOP_LONG_LINES, + OPT_QUIT_AT_FIRST_EOF, + OPT_QUIT_AT_SECOND_EOF, + OPT_QUIET, + OPT_VERY_QUIET, + OPT_IGNORE_CASE_COND, + OPT_IGNORE_CASE_ALWAYS, + OPT_SYNTAX_HIGHLIGHT, + + // Files + ADD_FILE, + NEXT_FILE, + PREV_FILE, + GOTO_FILE, + INFO_FILE, + DELETE_FILE, + + // + CHAR, + + // Edit pattern + INSERT, + RIGHT, + LEFT, + NEXT_WORD, + PREV_WORD, + HOME, + END, + BACKSPACE, + DELETE, + DELETE_WORD, + DELETE_LINE, + ACCEPT, + UP, + DOWN + } + + static class InterruptibleInputStream extends FilterInputStream { + InterruptibleInputStream(InputStream in) { + super(in); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedIOException(); + } + return super.read(b, off, len); + } + } + + static class Pair { + final U u; + final V v; + + public Pair(U u, V v) { + this.u = u; + this.v = v; + } + + public U getU() { + return u; + } + + public V getV() { + return v; + } + } + + private class LineEditor { + private final int begPos; + + public LineEditor(int begPos) { + this.begPos = begPos; + } + + public int editBuffer(Operation op, int curPos) { + switch (op) { + case INSERT: + buffer.insert(curPos++, bindingReader.getLastBinding()); + break; + case BACKSPACE: + if (curPos > begPos - 1) { + buffer.deleteCharAt(--curPos); + } + break; + case NEXT_WORD: + int newPos = buffer.length(); + for (int i = curPos; i < buffer.length(); i++) { + if (buffer.charAt(i) == ' ') { + newPos = i + 1; + break; + } + } + curPos = newPos; + break; + case PREV_WORD: + newPos = begPos; + for (int i = curPos - 2; i > begPos; i--) { + if (buffer.charAt(i) == ' ') { + newPos = i + 1; + break; + } + } + curPos = newPos; + break; + case HOME: + curPos = begPos; + break; + case END: + curPos = buffer.length(); + break; + case DELETE: + if (curPos >= begPos && curPos < buffer.length()) { + buffer.deleteCharAt(curPos); + } + break; + case DELETE_WORD: + while (true) { + if (curPos < buffer.length() && buffer.charAt(curPos) != ' ') { + buffer.deleteCharAt(curPos); + } else { + break; + } + } + while (true) { + if (curPos - 1 >= begPos) { + if (buffer.charAt(curPos - 1) != ' ') { + buffer.deleteCharAt(--curPos); + } else { + buffer.deleteCharAt(--curPos); + break; + } + } else { + break; + } + } + break; + case DELETE_LINE: + buffer.setLength(begPos); + curPos = 1; + break; + case LEFT: + if (curPos > begPos) { + curPos--; + } + break; + case RIGHT: + if (curPos < buffer.length()) { + curPos++; + } + break; + } + return curPos; + } + } + + private class SavedSourcePositions { + int saveSourceIdx; + int saveFirstLineToDisplay; + int saveFirstColumnToDisplay; + int saveOffsetInLine; + boolean savePrintLineNumbers; + + public SavedSourcePositions() { + this(0); + } + + public SavedSourcePositions(int dec) { + saveSourceIdx = sourceIdx + dec; + saveFirstLineToDisplay = firstLineToDisplay; + saveFirstColumnToDisplay = firstColumnToDisplay; + saveOffsetInLine = offsetInLine; + savePrintLineNumbers = printLineNumbers; + } + + public void restore(String failingSource) throws IOException { + sourceIdx = saveSourceIdx; + openSource(); + firstLineToDisplay = saveFirstLineToDisplay; + firstColumnToDisplay = saveFirstColumnToDisplay; + offsetInLine = saveOffsetInLine; + printLineNumbers = savePrintLineNumbers; + if (failingSource != null) { + message = failingSource + " not found!"; + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/NfaMatcher.java b/net-cli/src/main/java/org/jline/builtins/NfaMatcher.java new file mode 100644 index 0000000..52d7fb2 --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/NfaMatcher.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * NFA implementation. + * See https://swtch.com/~rsc/regexp/regexp1.html + */ +public class NfaMatcher { + + private final String regexp; + private final BiFunction matcher; + private volatile State start; + + public NfaMatcher(String regexp, BiFunction matcher) { + this.regexp = regexp; + this.matcher = matcher; + } + + static State toNfa(List postfix) { + Deque stack = new ArrayDeque<>(); + Frag e1, e2, e; + State s; + for (String p : postfix) { + switch (p) { + case ".": + e2 = stack.pollLast(); + e1 = stack.pollLast(); + e1.patch(e2.start); + stack.offerLast(new Frag(e1.start, e2.out)); + break; + case "|": + e2 = stack.pollLast(); + e1 = stack.pollLast(); + s = new State(State.Split, e1.start, e2.start); + stack.offerLast(new Frag(s, e1.out, e2.out)); + break; + case "?": + e = stack.pollLast(); + s = new State(State.Split, e.start, null); + stack.offerLast(new Frag(s, e.out, s::setOut1)); + break; + case "*": + e = stack.pollLast(); + s = new State(State.Split, e.start, null); + e.patch(s); + stack.offerLast(new Frag(s, s::setOut1)); + break; + case "+": + e = stack.pollLast(); + s = new State(State.Split, e.start, null); + e.patch(s); + stack.offerLast(new Frag(e.start, s::setOut1)); + break; + default: + s = new State(p, null, null); + stack.offerLast(new Frag(s, s::setOut)); + break; + } + } + e = stack.pollLast(); + if (!stack.isEmpty()) { + throw new IllegalStateException("Wrong postfix expression, " + stack.size() + " elements remaining"); + } + e.patch(new State(State.Match, null, null)); + return e.start; + } + + static List toPostFix(String regexp) { + List postfix = new ArrayList<>(); + int s = -1; + int natom = 0; + int nalt = 0; + Deque natoms = new ArrayDeque<>(); + Deque nalts = new ArrayDeque<>(); + for (int i = 0; i < regexp.length(); i++) { + char c = regexp.charAt(i); + // Scan identifiers + if (Character.isJavaIdentifierPart(c)) { + if (s < 0) { + s = i; + } + continue; + } + // End of identifier + if (s >= 0) { + if (natom > 1) { + --natom; + postfix.add("."); + } + postfix.add(regexp.substring(s, i)); + natom++; + s = -1; + } + // Ignore space + if (Character.isWhitespace(c)) { + continue; + } + // Special characters + switch (c) { + case '(': + if (natom > 1) { + --natom; + postfix.add("."); + } + nalts.offerLast(nalt); + natoms.offerLast(natom); + nalt = 0; + natom = 0; + break; + case '|': + if (natom == 0) { + throw new IllegalStateException("unexpected '" + c + "' at pos " + i); + } + while (--natom > 0) { + postfix.add("."); + } + nalt++; + break; + case ')': + if (nalts.isEmpty() || natom == 0) { + throw new IllegalStateException("unexpected '" + c + "' at pos " + i); + } + while (--natom > 0) { + postfix.add("."); + } + for (; nalt > 0; nalt--) { + postfix.add("|"); + } + nalt = nalts.pollLast(); + natom = natoms.pollLast(); + natom++; + break; + case '*': + case '+': + case '?': + if (natom == 0) { + throw new IllegalStateException("unexpected '" + c + "' at pos " + i); + } + postfix.add(String.valueOf(c)); + break; + default: + throw new IllegalStateException("unexpected '" + c + "' at pos " + i); + } + } + // End of identifier + if (s >= 0) { + if (natom > 1) { + --natom; + postfix.add("."); + } + postfix.add(regexp.substring(s)); + natom++; + } + // Append + while (--natom > 0) { + postfix.add("."); + } + // Alternatives + for (; nalt > 0; nalt--) { + postfix.add("|"); + } + return postfix; + } + + public void compile() { + if (start == null) { + start = toNfa(toPostFix(regexp)); + } + } + + public boolean match(List args) { + Set clist = new HashSet<>(); + compile(); + addState(clist, start); + for (T arg : args) { + Set nlist = new HashSet<>(); + clist.stream() + .filter(s -> !Objects.equals(State.Match, s.c) && !Objects.equals(State.Split, s.c)) + .filter(s -> matcher.apply(arg, s.c)) + .forEach(s -> addState(nlist, s.out)); + clist = nlist; + } + return clist.stream().anyMatch(s -> Objects.equals(State.Match, s.c)); + } + + /** + * Returns the list of possible matcher names for the next object + * + * @param args input list + * @return the list of possible matcher names for the next object + */ + public Set matchPartial(List args) { + Set clist = new HashSet<>(); + compile(); + addState(clist, start); + for (T arg : args) { + Set nlist = new HashSet<>(); + clist.stream() + .filter(s -> !Objects.equals(State.Match, s.c) && !Objects.equals(State.Split, s.c)) + .filter(s -> matcher.apply(arg, s.c)) + .forEach(s -> addState(nlist, s.out)); + clist = nlist; + } + return clist.stream() + .filter(s -> !Objects.equals(State.Match, s.c) && !Objects.equals(State.Split, s.c)) + .map(s -> s.c) + .collect(Collectors.toSet()); + } + + void addState(Set l, State s) { + if (s != null && l.add(s)) { + if (Objects.equals(State.Split, s.c)) { + addState(l, s.out); + addState(l, s.out1); + } + } + } + + static class State { + + static final String Match = "++MATCH++"; + static final String Split = "++SPLIT++"; + + final String c; + State out; + State out1; + + public State(String c, State out, State out1) { + this.c = c; + this.out = out; + this.out1 = out1; + } + + public void setOut(State out) { + this.out = out; + } + + public void setOut1(State out1) { + this.out1 = out1; + } + } + + private static class Frag { + final State start; + final List> out = new ArrayList<>(); + + public Frag(State start, Collection> l) { + this.start = start; + this.out.addAll(l); + } + + public Frag(State start, Collection> l1, Collection> l2) { + this.start = start; + this.out.addAll(l1); + this.out.addAll(l2); + } + + public Frag(State start, Consumer c) { + this.start = start; + this.out.add(c); + } + + public Frag(State start, Collection> l, Consumer c) { + this.start = start; + this.out.addAll(l); + this.out.add(c); + } + + public void patch(State s) { + out.forEach(c -> c.accept(s)); + } + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/Options.java b/net-cli/src/main/java/org/jline/builtins/Options.java new file mode 100644 index 0000000..4ba2721 --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/Options.java @@ -0,0 +1,588 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.StyleResolver; + +/** + * Yet another GNU long options parser. This one is configured by parsing its Usage string. + *

+ * Code coming from Apache Felix Gogo Shell + */ +public class Options { + + public static final String NL = System.getProperty("line.separator", "\n"); + + // Note: need to double \ within "" + private static final String regex = "(?x)\\s*" + "(?:-([^-]))?" + // 1: short-opt-1 + "(?:,?\\s*-(\\w))?" + + // 2: short-opt-2 + "(?:,?\\s*--(\\w[\\w-]*)(=\\w+)?)?" + + // 3: long-opt-1 and 4:arg-1 + "(?:,?\\s*--(\\w[\\w-]*))?" + + // 5: long-opt-2 + ".*?(?:\\(default=(.*)\\))?\\s*"; // 6: default + + private static final int GROUP_SHORT_OPT_1 = 1; + private static final int GROUP_SHORT_OPT_2 = 2; + private static final int GROUP_LONG_OPT_1 = 3; + private static final int GROUP_ARG_1 = 4; + private static final int GROUP_LONG_OPT_2 = 5; + private static final int GROUP_DEFAULT = 6; + + private static final Pattern parser = Pattern.compile(regex); + private static final Pattern uname = Pattern.compile("^Usage:\\s+(\\w+)"); + private static final String UNKNOWN = "unknown"; + private final Map unmodifiableOptSet; + private final Map unmodifiableOptArg; + private final Map optSet = new HashMap<>(); + private final Map optArg = new HashMap<>(); + private final Map optName = new HashMap<>(); + private final Map optAlias = new HashMap<>(); + private final List xargs = new ArrayList<>(); + private final String[] spec; + private final String[] gspec; + private final String defOpts; + private final String[] defArgs; + private List args = null; + private String usageName = UNKNOWN; + private int usageIndex = 0; + private String error = null; + + private boolean optionsFirst = false; + private boolean stopOnBadOption = false; + + // internal constructor + private Options(String[] spec, String[] gspec, Options opt, Function env) { + this.gspec = gspec; + + if (gspec == null && opt == null) { + this.spec = spec; + } else { + ArrayList list = new ArrayList<>(); + list.addAll(Arrays.asList(spec)); + list.addAll(Arrays.asList(gspec != null ? gspec : opt.gspec)); + this.spec = list.toArray(new String[list.size()]); + } + + Map myOptSet = new HashMap<>(); + Map myOptArg = new HashMap<>(); + + parseSpec(myOptSet, myOptArg); + + if (opt != null) { + for (Entry e : opt.optSet.entrySet()) { + if (e.getValue()) myOptSet.put(e.getKey(), true); + } + + for (Entry e : opt.optArg.entrySet()) { + if (!e.getValue().equals("")) myOptArg.put(e.getKey(), e.getValue()); + } + + opt.reset(); + } + + unmodifiableOptSet = Collections.unmodifiableMap(myOptSet); + unmodifiableOptArg = Collections.unmodifiableMap(myOptArg); + + defOpts = env != null ? env.apply(usageName.toUpperCase() + "_OPTS") : null; + defArgs = (defOpts != null) ? defOpts.split("\\s+") : new String[0]; + } + + public static Options compile(String[] optSpec) { + return new Options(optSpec, null, null, System::getenv); + } + + public static Options compile(String[] optSpec, Function env) { + return new Options(optSpec, null, null, env); + } + + public static Options compile(String optSpec) { + return compile(optSpec.split("\\n"), System::getenv); + } + + public static Options compile(String optSpec, Function env) { + return compile(optSpec.split("\\n"), env); + } + + public static Options compile(String[] optSpec, Options gopt) { + return new Options(optSpec, null, gopt, System::getenv); + } + + public static Options compile(String[] optSpec, String[] gspec) { + return new Options(optSpec, gspec, null, System::getenv); + } + + public Options setStopOnBadOption(boolean stopOnBadOption) { + this.stopOnBadOption = stopOnBadOption; + return this; + } + + public Options setOptionsFirst(boolean optionsFirst) { + this.optionsFirst = optionsFirst; + return this; + } + + public boolean isSet(String name) { + Boolean isSet = optSet.get(name); + if (isSet == null) { + throw new IllegalArgumentException("option not defined in spec: " + name); + } + return isSet; + } + + public Object getObject(String name) { + if (!optArg.containsKey(name)) throw new IllegalArgumentException("option not defined with argument: " + name); + + List list = getObjectList(name); + + return list.isEmpty() ? "" : list.get(list.size() - 1); + } + + @SuppressWarnings("unchecked") + public List getObjectList(String name) { + List list; + Object arg = optArg.get(name); + + if (arg == null) { + throw new IllegalArgumentException("option not defined with argument: " + name); + } + + if (arg instanceof String) { // default value + list = new ArrayList<>(); + if (!"".equals(arg)) list.add(arg); + } else { + list = (List) arg; + } + + return list; + } + + public List getList(String name) { + ArrayList list = new ArrayList<>(); + for (Object o : getObjectList(name)) { + try { + list.add((String) o); + } catch (ClassCastException e) { + throw new IllegalArgumentException("option not String: " + name); + } + } + return list; + } + + @SuppressWarnings("unchecked") + private void addArg(String name, Object value) { + List list; + Object arg = optArg.get(name); + + if (arg instanceof String) { // default value + list = new ArrayList<>(); + optArg.put(name, list); + } else { + list = (List) arg; + } + + list.add(value); + } + + public String get(String name) { + try { + return (String) getObject(name); + } catch (ClassCastException e) { + throw new IllegalArgumentException("option not String: " + name); + } + } + + public int getNumber(String name) { + String number = get(name); + try { + if (number != null) return Integer.parseInt(number); + return 0; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("option '" + name + "' not Number: " + number); + } + } + + public List argObjects() { + return xargs; + } + + public List args() { + if (args == null) { + args = new ArrayList<>(); + for (Object arg : xargs) { + args.add(arg == null ? "null" : arg.toString()); + } + } + return args; + } + + // Added for backword compability + public void usage(PrintStream err) { + err.print(usage()); + } + + public String usage() { + StringBuilder buf = new StringBuilder(); + int index = 0; + + if (error != null) { + buf.append(error); + buf.append(NL); + index = usageIndex; + } + + for (int i = index; i < spec.length; ++i) { + buf.append(spec[i]); + buf.append(NL); + } + + return buf.toString(); + } + + /** + * Prints usage message and returns IllegalArgumentException, for you to throw. + * + * @param s the message to display + * @return an exception with the generated message + */ + public IllegalArgumentException usageError(String s) { + error = usageName + ": " + s; + return new IllegalArgumentException(error); + } + + /** + * parse option spec. + */ + private void parseSpec(Map myOptSet, Map myOptArg) { + int index = 0; + for (String line : spec) { + Matcher m = parser.matcher(line); + + if (m.matches()) { + final String opt = m.group(GROUP_LONG_OPT_1); + final String name = (opt != null) ? opt : m.group(GROUP_SHORT_OPT_1); + + if (name != null) { + if (myOptSet.putIfAbsent(name, false) != null) + throw new IllegalArgumentException("duplicate option in spec: --" + name); + } + + String dflt = (m.group(GROUP_DEFAULT) != null) ? m.group(GROUP_DEFAULT) : ""; + if (m.group(GROUP_ARG_1) != null) myOptArg.put(opt, dflt); + + String opt2 = m.group(GROUP_LONG_OPT_2); + if (opt2 != null) { + optAlias.put(opt2, opt); + myOptSet.put(opt2, false); + if (m.group(GROUP_ARG_1) != null) myOptArg.put(opt2, ""); + } + + for (int i = 0; i < 2; ++i) { + String sopt = m.group(i == 0 ? GROUP_SHORT_OPT_1 : GROUP_SHORT_OPT_2); + if (sopt != null) { + if (optName.putIfAbsent(sopt, name) != null) + throw new IllegalArgumentException("duplicate option in spec: -" + sopt); + } + } + } + + if (Objects.equals(usageName, UNKNOWN)) { + Matcher u = uname.matcher(line); + if (u.find()) { + usageName = u.group(1); + usageIndex = index; + } + } + + index++; + } + } + + private void reset() { + optSet.clear(); + optSet.putAll(unmodifiableOptSet); + optArg.clear(); + optArg.putAll(unmodifiableOptArg); + xargs.clear(); + args = null; + error = null; + } + + public Options parse(Object[] argv) { + return parse(argv, false); + } + + public Options parse(List argv) { + return parse(argv, false); + } + + public Options parse(Object[] argv, boolean skipArg0) { + if (null == argv) throw new IllegalArgumentException("argv is null"); + + return parse(Arrays.asList(argv), skipArg0); + } + + public Options parse(List argv, boolean skipArg0) { + reset(); + List args = new ArrayList<>(); + args.addAll(Arrays.asList(defArgs)); + + for (Object arg : argv) { + if (skipArg0) { + skipArg0 = false; + usageName = arg.toString(); + } else { + args.add(arg); + } + } + + String needArg = null; + String needOpt = null; + boolean endOpt = false; + + for (Object oarg : args) { + String arg = oarg == null ? "null" : oarg.toString(); + + if (endOpt) { + xargs.add(oarg); + } else if (needArg != null) { + addArg(needArg, oarg); + needArg = null; + needOpt = null; + } else if (!arg.startsWith("-") + || (arg.length() > 1 && Character.isDigit(arg.charAt(1))) + || "-".equals(oarg)) { + if (optionsFirst) endOpt = true; + xargs.add(oarg); + } else { + if (arg.equals("--")) endOpt = true; + else if (arg.startsWith("--")) { + int eq = arg.indexOf("="); + String value = (eq == -1) ? null : arg.substring(eq + 1); + String name = arg.substring(2, ((eq == -1) ? arg.length() : eq)); + List names = new ArrayList<>(); + + if (optSet.containsKey(name)) { + names.add(name); + } else { + for (String k : optSet.keySet()) { + if (k.startsWith(name)) names.add(k); + } + } + + switch (names.size()) { + case 1: + name = names.get(0); + optSet.put(name, true); + if (optArg.containsKey(name)) { + if (value != null) addArg(name, value); + else needArg = name; + } else if (value != null) { + throw usageError("option '--" + name + "' doesn't allow an argument"); + } + break; + + case 0: + if (stopOnBadOption) { + endOpt = true; + xargs.add(oarg); + break; + } else throw usageError("invalid option '--" + name + "'"); + + default: + throw usageError("option '--" + name + "' is ambiguous: " + names); + } + } else { + for (int i = 1; i < arg.length(); i++) { + String c = String.valueOf(arg.charAt(i)); + if (optName.containsKey(c)) { + String name = optName.get(c); + optSet.put(name, true); + if (optArg.containsKey(name)) { + int k = i + 1; + if (k < arg.length()) { + addArg(name, arg.substring(k)); + } else { + needOpt = c; + needArg = name; + } + break; + } + } else { + if (stopOnBadOption) { + xargs.add("-" + c); + endOpt = true; + } else throw usageError("invalid option '" + c + "'"); + } + } + } + } + } + + if (needArg != null) { + String name = (needOpt != null) ? needOpt : "--" + needArg; + throw usageError("option '" + name + "' requires an argument"); + } + + // remove long option aliases + for (Entry alias : optAlias.entrySet()) { + if (optSet.get(alias.getKey())) { + optSet.put(alias.getValue(), true); + if (optArg.containsKey(alias.getKey())) optArg.put(alias.getValue(), optArg.get(alias.getKey())); + } + optSet.remove(alias.getKey()); + optArg.remove(alias.getKey()); + } + + return this; + } + + @Override + public String toString() { + return "isSet" + optSet + "\nArg" + optArg + "\nargs" + xargs; + } + + /** + * Exception thrown when using the --help option on a built-in command. + * It can be highlighted using the {@link #highlight(String, StyleResolver)} method and then printed + * to the {@link org.jline.terminal.Terminal}. + */ + @SuppressWarnings("serial") + public static class HelpException extends Exception { + + public HelpException(String message) { + super(message); + } + + public static StyleResolver defaultStyle() { + return Styles.helpStyle(); + } + + public static AttributedString highlight(String msg, StyleResolver resolver) { + Matcher tm = Pattern.compile("(^|\\n)(Usage|Summary)(:)").matcher(msg); + if (tm.find()) { + boolean subcommand = tm.group(2).equals("Summary"); + AttributedStringBuilder asb = new AttributedStringBuilder(msg.length()); + // Command + AttributedStringBuilder acommand = new AttributedStringBuilder() + .append(msg.substring(0, tm.start(2))) + .styleMatches( + Pattern.compile("(?:^\\s*)([a-z]+[a-zA-Z0-9-]*)\\b"), + Collections.singletonList(resolver.resolve(".co"))); + asb.append(acommand); + // Title + asb.styled(resolver.resolve(".ti"), tm.group(2)).append(":"); + // Syntax + for (String line : msg.substring(tm.end(3)).split("\n")) { + int ind = line.lastIndexOf(" "); + String syntax, comment; + if (ind > 20) { + syntax = line.substring(0, ind); + comment = line.substring(ind + 1); + } else { + syntax = line; + comment = ""; + } + asb.append(_highlightSyntax(syntax, resolver, subcommand)); + asb.append(_highlightComment(comment, resolver)); + asb.append("\n"); + } + return asb.toAttributedString(); + } else { + return AttributedString.fromAnsi(msg); + } + } + + public static AttributedString highlightSyntax(String syntax, StyleResolver resolver, boolean subcommands) { + return _highlightSyntax(syntax, resolver, subcommands).toAttributedString(); + } + + public static AttributedString highlightSyntax(String syntax, StyleResolver resolver) { + return _highlightSyntax(syntax, resolver, false).toAttributedString(); + } + + public static AttributedString highlightComment(String comment, StyleResolver resolver) { + return _highlightComment(comment, resolver).toAttributedString(); + } + + private static AttributedStringBuilder _highlightSyntax( + String syntax, StyleResolver resolver, boolean subcommand) { + StringBuilder indent = new StringBuilder(); + for (char c : syntax.toCharArray()) { + if (c != ' ') { + break; + } + indent.append(c); + } + AttributedStringBuilder asyntax = new AttributedStringBuilder().append(syntax.substring(indent.length())); + // command + asyntax.styleMatches( + Pattern.compile("(?:^)([a-z]+[a-zA-Z0-9-]*)\\b"), + Collections.singletonList(resolver.resolve(".co"))); + if (!subcommand) { + // argument + asyntax.styleMatches( + Pattern.compile("(?:<|\\[|\\s|=)([A-Za-z]+[A-Za-z_-]*)\\b"), + Collections.singletonList(resolver.resolve(".ar"))); + // option + asyntax.styleMatches( + Pattern.compile("(?:^|\\s|\\[)(-\\$|-\\?|[-]{1,2}[A-Za-z-]+\\b)"), + Collections.singletonList(resolver.resolve(".op"))); + } + return new AttributedStringBuilder().append(indent).append(asyntax); + } + + private static AttributedStringBuilder _highlightComment(String comment, StyleResolver resolver) { + AttributedStringBuilder acomment = new AttributedStringBuilder().append(comment); + // option + acomment.styleMatches( + Pattern.compile("(?:\\s|\\[)(-\\$|-\\?|[-]{1,2}[A-Za-z-]+\\b)"), + Collections.singletonList(resolver.resolve(".op"))); + // argument in comment + acomment.styleMatches( + Pattern.compile("(?:\\s)([a-z]+[-]+[a-z]+|[A-Z_]{2,})(?:\\s)"), + Collections.singletonList(resolver.resolve(".ar"))); + return acomment; + } + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/ScreenTerminal.java b/net-cli/src/main/java/org/jline/builtins/ScreenTerminal.java new file mode 100644 index 0000000..217ce19 --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/ScreenTerminal.java @@ -0,0 +1,1976 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Based on http://antony.lesuisse.org/software/ajaxterm/ + * Public Domain License + */ + +/** + * See http://www.ecma-international.org/publications/standards/Ecma-048.htm + * and http://vt100.net/docs/vt510-rm/ + */ + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import org.jline.utils.Colors; +import org.jline.utils.WCWidth; + +/** + * Screen terminal implementation. + * This class is copied from Apache Karaf WebConsole Gogo plugin + * and slightly adapted to support alternate screen / resizing / 256 colors. + */ +public class ScreenTerminal { + + private final List history = new ArrayList<>(); + private int width; + private int height; + private long attr; + private boolean eol; + private int cx; + private int cy; + private long[][] screen; + private long[][] screen2; + private State vt100_parse_state = State.None; + private int vt100_parse_len; + private int vt100_lastchar; + private int vt100_parse_func; + private String vt100_parse_param; + private boolean vt100_mode_autowrap; + private boolean vt100_mode_insert; + private boolean vt100_charset_is_single_shift; + private boolean vt100_charset_is_graphical; + private boolean vt100_mode_lfnewline; + private boolean vt100_mode_origin; + private boolean vt100_mode_inverse; + private boolean vt100_mode_cursorkey; + private boolean vt100_mode_cursor; + private boolean vt100_mode_alt_screen; + private boolean vt100_mode_backspace; + private boolean vt100_mode_column_switch; + private boolean vt100_keyfilter_escape; + private final int[] vt100_charset_graph = new int[]{ + 0x25ca, 0x2026, 0x2022, 0x3f, + 0xb6, 0x3f, 0xb0, 0xb1, + 0x3f, 0x3f, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0xaf, + 0x2014, 0x2014, 0x2014, 0x5f, + 0x2b, 0x2b, 0x2b, 0x2b, + 0x7c, 0x2264, 0x2265, 0xb6, + 0x2260, 0xa3, 0xb7, 0x7f + }; + private int vt100_charset_g_sel; + private int[] vt100_charset_g = {0, 0}; + private Map vt100_saved; + private Map vt100_saved2; + private int vt100_alternate_saved_cx; + private int vt100_alternate_saved_cy; + private int vt100_saved_cx; + private int vt100_saved_cy; + private String vt100_out; + + private int scroll_area_y0; + private int scroll_area_y1; + + private List tab_stops; + private final AtomicBoolean dirty = new AtomicBoolean(true); + + public ScreenTerminal() { + this(80, 24); + } + + public ScreenTerminal(int width, int height) { + this.width = width; + this.height = height; + reset_hard(); + } + + private void reset_hard() { + // Attribute mask: 0xYXFFFBBB00000000L + // X: Bit 0 - Underlined + // Bit 1 - Negative + // Bit 2 - Concealed + // Bit 3 - Bold + // Y: Bit 0 - Foreground set + // Bit 1 - Background set + // F: Foreground r-g-b + // B: Background r-g-b + attr = 0x0000000000000000L; + // Key filter + vt100_keyfilter_escape = false; + // Last char + vt100_lastchar = 0; + // Control sequences + vt100_parse_len = 0; + vt100_parse_state = State.None; + vt100_parse_func = 0; + vt100_parse_param = ""; + // Buffers + vt100_out = ""; + // Invoke other resets + reset_screen(); + reset_soft(); + } + + private void reset_soft() { + // Attribute mask: 0xYXFFFBBB00000000L + // X: Bit 0 - Underlined + // Bit 1 - Negative + // Bit 2 - Concealed + // Bit 3 - Bold + // Y: Bit 0 - Foreground set + // Bit 1 - Background set + // F: Foreground r-g-b + // B: Background r-g-b + attr = 0x0000000000000000L; + // Scroll parameters + scroll_area_y0 = 0; + scroll_area_y1 = height; + // Character sets + vt100_charset_is_single_shift = false; + vt100_charset_is_graphical = false; + vt100_charset_g_sel = 0; + vt100_charset_g = new int[]{0, 0}; + // Modes + vt100_mode_insert = false; + vt100_mode_lfnewline = false; + vt100_mode_cursorkey = false; + vt100_mode_column_switch = false; + vt100_mode_inverse = false; + vt100_mode_origin = false; + vt100_mode_autowrap = true; + vt100_mode_cursor = true; + vt100_mode_alt_screen = false; + vt100_mode_backspace = false; + // Init DECSC state + esc_DECSC(); + vt100_saved2 = vt100_saved; + esc_DECSC(); + } + + private void reset_screen() { + // Screen + screen = (long[][]) Array.newInstance(long.class, height, width); + screen2 = (long[][]) Array.newInstance(long.class, height, width); + for (int i = 0; i < height; i++) { + Arrays.fill(screen[i], attr | 0x00000020); + Arrays.fill(screen2[i], attr | 0x00000020); + } + // Scroll parameters + scroll_area_y0 = 0; + scroll_area_y1 = height; + // Cursor position + cx = 0; + cy = 0; + // Tab stops + tab_stops = new ArrayList<>(); + for (int i = 7; i < width; i += 8) { + tab_stops.add(i); + } + } + + private int utf8_charwidth(int c) { + return WCWidth.wcwidth(c); + } + + // + // UTF-8 functions + // + + private long[] peek(int y0, int x0, int y1, int x1) { + int from = width * y0 + x0; + int to = width * (y1 - 1) + x1; + int newLength = to - from; + if (newLength < 0) throw new IllegalArgumentException(from + " > " + to); + long[] copy = new long[newLength]; + int cur = from; + while (cur < to) { + int y = cur / width; + int x = cur % width; + int nb = Math.min(width - x, to - cur); + System.arraycopy(screen[y], x, copy, cur - from, nb); + cur += nb; + } + return copy; + } + + // + // Low-level terminal functions + // + + private void poke(int y, int x, long[] s) { + int cur = 0; + int max = s.length; + while (cur < max) { + int nb = Math.min(width - x, max - cur); + System.arraycopy(s, cur, screen[y++], x, nb); + x = 0; + cur += nb; + } + setDirty(); + } + + private void fill(int y0, int x0, int y1, int x1, long c) { + if (y0 == y1 - 1) { + if (x0 < x1 - 1) { + Arrays.fill(screen[y0], x0, x1, c); + setDirty(); + } + } else if (y0 < y1 - 1) { + Arrays.fill(screen[y0], x0, width, c); + for (int i = y0; i < y1 - 1; i++) { + Arrays.fill(screen[i], c); + } + Arrays.fill(screen[y1 - 1], 0, x1, c); + setDirty(); + } + } + + private void clear(int y0, int x0, int y1, int x1) { + fill(y0, x0, y1, x1, attr | 0x00000020); + } + + private void scroll_area_up(int y0, int y1) { + scroll_area_up(y0, y1, 1); + } + + // + // Scrolling functions + // + + private void scroll_area_up(int y0, int y1, int n) { + n = Math.min(y1 - y0, n); + if (y0 == 0 && y1 == height) { + for (int i = 0; i < n; i++) { + history.add(screen[i]); + } + System.arraycopy(screen, n, screen, 0, height - n); + for (int i = 1; i <= n; i++) { + screen[y1 - i] = new long[width]; + Arrays.fill(screen[y1 - 1], attr | 0x0020); + } + } else { + poke(y0, 0, peek(y0 + n, 0, y1, width)); + clear(y1 - n, 0, y1, width); + } + } + + private void scroll_area_down(int y0, int y1) { + scroll_area_down(y0, y1, 1); + } + + private void scroll_area_down(int y0, int y1, int n) { + n = Math.min(y1 - y0, n); + poke(y0 + n, 0, peek(y0, 0, y1 - n, width)); + clear(y0, 0, y0 + n, width); + } + + private void scroll_area_set(int y0, int y1) { + y0 = Math.max(0, Math.min(height - 1, y0)); + y1 = Math.max(1, Math.min(height, y1)); + if (y1 > y0) { + scroll_area_y0 = y0; + scroll_area_y1 = y1; + } + } + + private void scroll_line_right(int y, int x) { + scroll_line_right(y, x, 1); + } + + private void scroll_line_right(int y, int x, int n) { + if (x < width) { + n = Math.min(width - cx, n); + poke(y, x + n, peek(y, x, y + 1, width - n)); + clear(y, x, y + 1, x + n); + } + } + + private void scroll_line_left(int y, int x) { + scroll_line_left(y, x, 1); + } + + private void scroll_line_left(int y, int x, int n) { + if (x < width) { + n = Math.min(width - cx, n); + poke(y, x, peek(y, x + n, y + 1, width)); + clear(y, width - n, y + 1, width); + } + } + + private int[] cursor_line_width(int next_char) { + int wx = utf8_charwidth(next_char); + int lx = 0; + for (int x = 0; x < Math.min(cx, width); x++) { + int c = (int) (peek(cy, x, cy + 1, x + 1)[0] & 0x00000000ffffffffL); + wx += utf8_charwidth(c); + lx += 1; + } + return new int[]{wx, lx}; + } + + // + // Cursor functions + // + + private void cursor_up() { + cursor_up(1); + } + + private void cursor_up(int n) { + cy = Math.max(scroll_area_y0, cy - n); + setDirty(); + } + + private void cursor_down() { + cursor_down(1); + } + + private void cursor_down(int n) { + cy = Math.min(scroll_area_y1 - 1, cy + n); + setDirty(); + } + + private void cursor_left() { + cursor_left(1); + } + + private void cursor_left(int n) { + eol = false; + cx = Math.max(0, cx - n); + setDirty(); + } + + private void cursor_right() { + cursor_right(1); + } + + private void cursor_right(int n) { + eol = cx + n >= width; + cx = Math.min(width - 1, cx + n); + setDirty(); + } + + private void cursor_set_x(int x) { + eol = false; + cx = Math.max(0, x); + setDirty(); + } + + private void cursor_set_y(int y) { + cy = Math.max(0, Math.min(height - 1, y)); + setDirty(); + } + + private void cursor_set(int y, int x) { + cursor_set_x(x); + cursor_set_y(y); + } + + private void ctrl_BS() { + int dy = (cx - 1) / width; + cursor_set(Math.max(scroll_area_y0, cy + dy), (cx - 1) % width); + } + + // + // Dumb terminal + // + + private void ctrl_HT() { + ctrl_HT(1); + } + + private void ctrl_HT(int n) { + if (n > 0 && cx >= width) { + return; + } + if (n <= 0 && cx == 0) { + return; + } + int ts = -1; + for (int i = 0; i < tab_stops.size(); i++) { + if (cx >= tab_stops.get(i)) { + ts = i; + } + } + ts += n; + if (ts < tab_stops.size() && ts >= 0) { + cursor_set_x(tab_stops.get(ts)); + } else { + cursor_set_x(width - 1); + } + } + + private void ctrl_LF() { + if (vt100_mode_lfnewline) { + ctrl_CR(); + } + if (cy == scroll_area_y1 - 1) { + scroll_area_up(scroll_area_y0, scroll_area_y1); + } else { + cursor_down(); + } + } + + private void ctrl_CR() { + cursor_set_x(0); + } + + private boolean dumb_write(int c) { + if (c < 32) { + if (c == 8) { + ctrl_BS(); + } else if (c == 9) { + ctrl_HT(); + } else if (c >= 10 && c <= 12) { + ctrl_LF(); + } else if (c == 13) { + ctrl_CR(); + } + return true; + } + return false; + } + + private void dumb_echo(int c) { + if (eol) { + if (vt100_mode_autowrap) { + ctrl_CR(); + ctrl_LF(); + } else { + cx = cursor_line_width(c)[1] - 1; + } + } + if (vt100_mode_insert) { + scroll_line_right(cy, cx); + } + if (vt100_charset_is_single_shift) { + vt100_charset_is_single_shift = false; + } else if (vt100_charset_is_graphical && ((c & 0xffe0) == 0x0060)) { + c = vt100_charset_graph[c - 0x60]; + } + poke(cy, cx, new long[]{attr | c}); + cursor_right(); + } + + private void vt100_charset_update() { + vt100_charset_is_graphical = (vt100_charset_g[vt100_charset_g_sel] == 2); + } + + // + // VT100 + // + + private void vt100_charset_set(int g) { + // Invoke active character set + vt100_charset_g_sel = g; + vt100_charset_update(); + } + + private void vt100_charset_select(int g, int charset) { + // Select charset + vt100_charset_g[g] = charset; + vt100_charset_update(); + } + + private void vt100_setmode(String p, boolean state) { + // Set VT100 mode + String[] ps = vt100_parse_params(p, new String[0]); + for (String m : ps) { + // 1 : GATM: Guarded area transfer + // 2 : KAM: Keyboard action + // 3 : CRM: Control representation + switch (m) { + case "4": + // Insertion replacement mode + vt100_mode_insert = state; + break; + // 5 : SRTM: Status reporting transfer + // 7 : VEM: Vertical editing + // 10 : HEM: Horizontal editing + // 11 : PUM: Positioning nit + // 12 : SRM: Send/receive + // 13 : FEAM: Format effector action + // 14 : FETM: Format effector transfer + // 15 : MATM: Multiple area transfer + // 16 : TTM: Transfer termination + // 17 : SATM: Selected area transfer + // 18 : TSM: Tabulation stop + // 19 : EBM: Editing boundary + case "20": + // LNM: Line feed/new line + vt100_mode_lfnewline = state; + break; + case "?1": + // DECCKM: Cursor keys + vt100_mode_cursorkey = state; + break; + // ?2 : DECANM: ANSI + case "?3": + // DECCOLM: Column + if (vt100_mode_column_switch) { + if (state) { + width = 132; + } else { + width = 80; + } + reset_screen(); + } + break; + // ?4 : DECSCLM: Scrolling + case "?5": + // DECSCNM: Screen + vt100_mode_inverse = state; + break; + case "?6": + // DECOM: Origin + vt100_mode_origin = state; + if (state) { + cursor_set(scroll_area_y0, 0); + } else { + cursor_set(0, 0); + } + break; + case "?7": + // DECAWM: Autowrap + vt100_mode_autowrap = state; + break; + // ?8 : DECARM: Autorepeat + // ?9 : Interlacing + // ?18 : DECPFF: Print form feed + // ?19 : DECPEX: Printer extent + case "?25": + // DECTCEM: Text cursor enable + vt100_mode_cursor = state; + break; + // ?34 : DECRLM: Cursor direction, right to left + // ?35 : DECHEBM: Hebrew keyboard mapping + // ?36 : DECHEM: Hebrew encoding mode + case "?40": + // Column switch control + vt100_mode_column_switch = state; + break; + // ?42 : DECNRCM: National replacement character set + case "?1049": + // Alternate screen mode + if ((state && !vt100_mode_alt_screen) || (!state && vt100_mode_alt_screen)) { + long[][] s = screen; + screen = screen2; + screen2 = s; + Map map = vt100_saved; + vt100_saved = vt100_saved2; + vt100_saved2 = map; + int c; + c = vt100_alternate_saved_cx; + vt100_alternate_saved_cx = cx; + cx = Math.min(c, width - 1); + c = vt100_alternate_saved_cy; + vt100_alternate_saved_cy = cy; + cy = Math.min(c, height - 1); + } + vt100_mode_alt_screen = state; + break; + // ?57 : DECNAKB: Greek keyboard mapping + case "?67": + // DECBKM: Backarrow key + vt100_mode_backspace = state; + break; + // ?98 : DECARSM: auto-resize + // ?101 : DECCANSM: Conceal answerback message + // ?109 : DECCAPSLK: caps lock + } + } + } + + private void ctrl_SO() { + vt100_charset_set(1); + } + + private void ctrl_SI() { + vt100_charset_set(0); + } + + private void esc_CSI() { + vt100_parse_reset(State.Csi); + } + + private void esc_DECALN() { + fill(0, 0, height, width, 0x00ff0045); + } + + private void esc_G0_0() { + vt100_charset_select(0, 0); + } + + private void esc_G0_1() { + vt100_charset_select(0, 1); + } + + private void esc_G0_2() { + vt100_charset_select(0, 2); + } + + private void esc_G0_3() { + vt100_charset_select(0, 3); + } + + private void esc_G0_4() { + vt100_charset_select(0, 4); + } + + private void esc_G1_0() { + vt100_charset_select(1, 0); + } + + private void esc_G1_1() { + vt100_charset_select(1, 1); + } + + private void esc_G1_2() { + vt100_charset_select(1, 2); + } + + private void esc_G1_3() { + vt100_charset_select(1, 3); + } + + private void esc_G1_4() { + vt100_charset_select(1, 4); + } + + private void esc_DECSC() { + vt100_saved = new HashMap<>(); + vt100_saved.put("cx", cx); + vt100_saved.put("cy", cy); + vt100_saved.put("attr", attr); + vt100_saved.put("vt100_charset_g_sel", vt100_charset_g_sel); + vt100_saved.put("vt100_charset_g", vt100_charset_g); + vt100_saved.put("vt100_mode_autowrap", vt100_mode_autowrap); + vt100_saved.put("vt100_mode_origin", vt100_mode_origin); + } + + private void esc_DECRC() { + cx = (Integer) vt100_saved.get("cx"); + cy = (Integer) vt100_saved.get("cy"); + attr = (Long) vt100_saved.get("attr"); + vt100_charset_g_sel = (Integer) vt100_saved.get("vt100_charset_g_sel"); + vt100_charset_g = (int[]) vt100_saved.get("vt100_charset_g"); + vt100_charset_update(); + vt100_mode_autowrap = (Boolean) vt100_saved.get("vt100_mode_autowrap"); + vt100_mode_origin = (Boolean) vt100_saved.get("vt100_mode_origin"); + } + + private void esc_IND() { + ctrl_LF(); + } + + private void esc_NEL() { + ctrl_CR(); + ctrl_LF(); + } + + private void esc_HTS() { + csi_CTC("0"); + } + + private void esc_RI() { + if (cy == scroll_area_y0) { + scroll_area_down(scroll_area_y0, scroll_area_y1); + } else { + cursor_up(); + } + } + + private void esc_SS2() { + vt100_charset_is_single_shift = true; + } + + private void esc_SS3() { + vt100_charset_is_single_shift = true; + } + + private void esc_DCS() { + vt100_parse_reset(State.Str); + } + + private void esc_SOS() { + vt100_parse_reset(State.Str); + } + + private void esc_DECID() { + csi_DA("0"); + } + + private void esc_ST() { + } + + private void esc_OSC() { + vt100_parse_reset(State.Str); + } + + private void esc_PM() { + vt100_parse_reset(State.Str); + } + + private void esc_APC() { + vt100_parse_reset(State.Str); + } + + private void esc_RIS() { + reset_hard(); + } + + private void csi_ICH(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + scroll_line_right(cy, cx, ps[0]); + } + + private void csi_CUU(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + cursor_up(Math.max(1, ps[0])); + } + + private void csi_CUD(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + cursor_down(Math.max(1, ps[0])); + } + + private void csi_CUF(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + cursor_right(Math.max(1, ps[0])); + } + + private void csi_CUB(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + cursor_left(Math.max(1, ps[0])); + } + + private void csi_CNL(String p) { + csi_CUD(p); + ctrl_CR(); + } + + private void csi_CPL(String p) { + csi_CUU(p); + ctrl_CR(); + } + + private void csi_CHA(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + cursor_set_x(ps[0] - 1); + } + + private void csi_CUP(String p) { + int[] ps = vt100_parse_params(p, new int[]{1, 1}); + if (vt100_mode_origin) { + cursor_set(scroll_area_y0 + ps[0] - 1, ps[1] - 1); + } else { + cursor_set(ps[0] - 1, ps[1] - 1); + } + } + + private void csi_CHT(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + ctrl_HT(Math.max(1, ps[0])); + } + + private void csi_ED(String p) { + String[] ps = vt100_parse_params(p, new String[]{"0"}); + if ("0".equals(ps[0])) { + clear(cy, cx, height, width); + } else if ("1".equals(ps[0])) { + clear(0, 0, cy + 1, cx + 1); + } else if ("2".equals(ps[0])) { + clear(0, 0, height, width); + } + } + + private void csi_EL(String p) { + String[] ps = vt100_parse_params(p, new String[]{"0"}); + if ("0".equals(ps[0])) { + clear(cy, cx, cy + 1, width); + } else if ("1".equals(ps[0])) { + clear(cy, 0, cy + 1, cx + 1); + } else if ("2".equals(ps[0])) { + clear(cy, 0, cy + 1, width); + } + } + + private void csi_IL(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + if (cy >= scroll_area_y0 && cy < scroll_area_y1) { + scroll_area_down(cy, scroll_area_y1, Math.max(1, ps[0])); + } + } + + private void csi_DL(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + if (cy >= scroll_area_y0 && cy < scroll_area_y1) { + scroll_area_up(cy, scroll_area_y1, Math.max(1, ps[0])); + } + } + + private void csi_DCH(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + scroll_line_left(cy, cx, Math.max(1, ps[0])); + } + + private void csi_SU(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + scroll_area_up(scroll_area_y0, scroll_area_y1, Math.max(1, ps[0])); + } + + private void csi_SD(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + scroll_area_down(scroll_area_y0, scroll_area_y1, Math.max(1, ps[0])); + } + + private void csi_CTC(String p) { + String[] ps = vt100_parse_params(p, new String[]{"0"}); + for (String m : ps) { + if ("0".equals(m)) { + if (!tab_stops.contains(cx)) { + tab_stops.add(cx); + Collections.sort(tab_stops); + } + } else if ("2".equals(m)) { + tab_stops.remove(Integer.valueOf(cx)); + } else if ("5".equals(m)) { + tab_stops = new ArrayList<>(); + } + } + } + + private void csi_ECH(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + int n = Math.min(width - cx, Math.max(1, ps[0])); + clear(cy, cx, cy + 1, cx + n); + } + + private void csi_CBT(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + ctrl_HT(1 - Math.max(1, ps[0])); + } + + private void csi_HPA(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + cursor_set_x(ps[0] - 1); + } + + private void csi_HPR(String p) { + csi_CUF(p); + } + + private void csi_REP(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + if (vt100_lastchar < 32) { + return; + } + int n = Math.min(2000, Math.max(1, ps[0])); + while (n-- > 0) { + dumb_echo(vt100_lastchar); + } + vt100_lastchar = 0; + } + + private void csi_DA(String p) { + String[] ps = vt100_parse_params(p, new String[]{"0"}); + if ("0".equals(ps[0])) { + vt100_out = "\u001b[?1;2c"; + } else if (">0".equals(ps[0]) || ">".equals(ps[0])) { + vt100_out = "\u001b[>0;184;0c"; + } + } + + private void csi_VPA(String p) { + int[] ps = vt100_parse_params(p, new int[]{1}); + cursor_set_y(ps[0] - 1); + } + + private void csi_VPR(String p) { + csi_CUD(p); + } + + private void csi_HVP(String p) { + csi_CUP(p); + } + + private void csi_TBC(String p) { + String[] ps = vt100_parse_params(p, new String[]{"0"}); + if ("0".equals(ps[0])) { + csi_CTC("2"); + } else if ("3".equals(ps[0])) { + csi_CTC("5"); + } + } + + private void csi_SM(String p) { + vt100_setmode(p, true); + } + + private void csi_RM(String p) { + vt100_setmode(p, false); + } + + private void csi_SGR(String p) { + // Attribute mask: 0xYXFFFBBB00000000L + // X: Bit 0 - Underlined + // Bit 1 - Negative + // Bit 2 - Concealed + // Bit 3 - Bold + // Y: Bit 0 - Foreground set + // Bit 1 - Background set + // F: Foreground r-g-b + // B: Background r-g-b + int[] ps = vt100_parse_params(p, new int[]{0}); + for (int i = 0; i < ps.length; i++) { + int m = ps[i]; + if (m == 0) { + attr = 0x00000000L << 32; + } else if (m == 1) { + attr |= 0x08000000L << 32; // bold + } else if (m == 4) { + attr |= 0x01000000L << 32; // underline + } else if (m == 7) { + attr |= 0x02000000L << 32; // negative + } else if (m == 8) { + attr |= 0x04000000L << 32; // conceal + } else if (m == 21) { + attr &= 0xf7ffffffL << 32; // bold off + } else if (m == 24) { + attr &= 0xfeffffffL << 32; // underline off + } else if (m == 27) { + attr &= 0xfdffffffL << 32; // negative off + } else if (m == 28) { + attr &= 0xfbffffffL << 32; // conceal off + } else if (m >= 30 && m <= 37) { + attr = (attr & (0xef000fffL << 32)) | (0x10000000L << 32) | (col24(m - 30) << 44); // foreground + } else if (m == 38) { + m = ++i < ps.length ? ps[i] : 0; + if (m == 5) { + m = ++i < ps.length ? ps[i] : 0; + attr = (attr & (0xef000fffL << 32)) | (0x10000000L << 32) | (col24(m) << 44); // foreground + } + } else if (m == 39) { + attr &= 0xef000fffL << 32; + } else if (m >= 40 && m <= 47) { + attr = (attr & (0xdffff000L << 32)) | (0x20000000L << 32) | (col24(m - 40) << 32); // background + } else if (m == 48) { + m = ++i < ps.length ? ps[i] : 0; + if (m == 5) { + m = ++i < ps.length ? ps[i] : 0; + attr = (attr & (0xdffff000L << 32)) | (0x20000000L << 32) | (col24(m) << 32); // background + } + } else if (m == 49) { + attr &= 0xdf000fffL << 32; + } else if (m >= 90 && m <= 97) { + attr = (attr & (0xef000fffL << 32)) | (0x10000000L << 32) | (col24(m - 90 + 8) << 44); // foreground + } else if (m >= 100 && m <= 107) { + attr = (attr & (0xdffff000L << 32)) | (0x20000000L << 32) | (col24(m - 100 + 8) << 32); // background + } + } + } + + private long col24(int col) { + int c = Colors.rgbColor(col); + int r = (c >> 16) & 0xFF; + int g = (c >> 8) & 0xFF; + int b = (c >> 0) & 0xFF; + return ((r >> 4) << 8) | ((g >> 4) << 4) | ((b >> 4) << 0); + } + + private void csi_DSR(String p) { + String[] ps = vt100_parse_params(p, new String[]{"0"}); + if ("5".equals(ps[0])) { + vt100_out = "\u001b[0n"; + } else if ("6".equals(ps[0])) { + vt100_out = "\u001b[" + (cy + 1) + ";" + (cx + 1) + "R"; + } else if ("7".equals(ps[0])) { + vt100_out = "gogo-term"; + } else if ("8".equals(ps[0])) { + vt100_out = "1.0-SNAPSHOT"; + } else if ("?6".equals(ps[0])) { + vt100_out = "\u001b[" + (cy + 1) + ";" + (cx + 1) + ";0R"; + } else if ("?15".equals(ps[0])) { + vt100_out = "\u001b[?13n"; + } else if ("?25".equals(ps[0])) { + vt100_out = "\u001b[?20n"; + } else if ("?26".equals(ps[0])) { + vt100_out = "\u001b[?27;1n"; + } else if ("?53".equals(ps[0])) { + vt100_out = "\u001b[?53n"; + } + // ?75 : Data Integrity report + // ?62 : Macro Space report + // ?63 : Memory Checksum report + } + + private void csi_DECSTBM(String p) { + int[] ps = vt100_parse_params(p, new int[]{1, height}); + scroll_area_set(ps[0] - 1, ps[1]); + if (vt100_mode_origin) { + cursor_set(scroll_area_y0, 0); + } else { + cursor_set(0, 0); + } + } + + private void csi_SCP(String p) { + vt100_saved_cx = cx; + vt100_saved_cy = cy; + } + + private void csi_RCP(String p) { + cx = vt100_saved_cx; + cy = vt100_saved_cy; + } + + private void csi_DECREQTPARM(String p) { + String[] ps = vt100_parse_params(p, new String[0]); + if ("0".equals(ps[0])) { + vt100_out = "\u001b[2;1;1;112;112;1;0x"; + } else if ("1".equals(ps[0])) { + vt100_out = "\u001b[3;1;1;112;112;1;0x"; + } + } + + private void csi_DECSTR(String p) { + reset_soft(); + } + + private String[] vt100_parse_params(String p, String[] defaults) { + String prefix = ""; + if (p.length() > 0) { + if (p.charAt(0) >= '<' && p.charAt(0) <= '?') { + prefix = "" + p.charAt(0); + p = p.substring(1); + } + } + String[] ps = p.split(";"); + int n = Math.max(ps.length, defaults.length); + String[] values = new String[n]; + for (int i = 0; i < n; i++) { + String value = null; + if (i < ps.length && ps[i].length() > 0) { + value = prefix + ps[i]; + } + if (value == null && i < defaults.length) { + value = defaults[i]; + } + if (value == null) { + value = ""; + } + values[i] = value; + } + return values; + } + + // + // VT100 parser + // + + private int[] vt100_parse_params(String p, int[] defaults) { + String prefix = ""; + p = p == null ? "" : p; + if (p.length() > 0) { + if (p.charAt(0) >= '<' && p.charAt(0) <= '?') { + prefix = p.substring(0, 1); + p = p.substring(1); + } + } + String[] ps = p.split(";"); + int n = Math.max(ps.length, defaults.length); + int[] values = new int[n]; + for (int i = 0; i < n; i++) { + Integer value = null; + if (i < ps.length) { + String v = prefix + ps[i]; + try { + value = Integer.parseInt(v); + } catch (NumberFormatException e) { + } + } + if (value == null && i < defaults.length) { + value = defaults[i]; + } + if (value == null) { + value = 0; + } + values[i] = value; + } + return values; + } + + private void vt100_parse_reset() { + vt100_parse_reset(State.None); + } + + private void vt100_parse_reset(State state) { + vt100_parse_state = state; + vt100_parse_len = 0; + vt100_parse_func = 0; + vt100_parse_param = ""; + } + + private void vt100_parse_process() { + if (vt100_parse_state == State.Esc) { + switch (vt100_parse_func) { + case 0x0036: /* DECBI */ + break; + case 0x0037: + esc_DECSC(); + break; + case 0x0038: + esc_DECRC(); + break; + case 0x0042: /* BPH */ + break; + case 0x0043: /* NBH */ + break; + case 0x0044: + esc_IND(); + break; + case 0x0045: + esc_NEL(); + break; + case 0x0046: /* SSA */ + esc_NEL(); + break; + case 0x0048: + esc_HTS(); + break; + case 0x0049: /* HTJ */ + break; + case 0x004A: /* VTS */ + break; + case 0x004B: /* PLD */ + break; + case 0x004C: /* PLU */ + break; + case 0x004D: + esc_RI(); + break; + case 0x004E: + esc_SS2(); + break; + case 0x004F: + esc_SS3(); + break; + case 0x0050: + esc_DCS(); + break; + case 0x0051: /* PU1 */ + break; + case 0x0052: /* PU2 */ + break; + case 0x0053: /* STS */ + break; + case 0x0054: /* CCH */ + break; + case 0x0055: /* MW */ + break; + case 0x0056: /* SPA */ + break; + case 0x0057: /* ESA */ + break; + case 0x0058: + esc_SOS(); + break; + case 0x005A: /* SCI */ + break; + case 0x005B: + esc_CSI(); + break; + case 0x005C: + esc_ST(); + break; + case 0x005D: + esc_OSC(); + break; + case 0x005E: + esc_PM(); + break; + case 0x005F: + esc_APC(); + break; + case 0x0060: /* DMI */ + break; + case 0x0061: /* INT */ + break; + case 0x0062: /* EMI */ + break; + case 0x0063: + esc_RIS(); + break; + case 0x0064: /* CMD */ + break; + case 0x006C: /* RM */ + break; + case 0x006E: /* LS2 */ + break; + case 0x006F: /* LS3 */ + break; + case 0x007C: /* LS3R */ + break; + case 0x007D: /* LS2R */ + break; + case 0x007E: /* LS1R */ + break; + case 0x2338: + esc_DECALN(); + break; + case 0x2841: + esc_G0_0(); + break; + case 0x2842: + esc_G0_1(); + break; + case 0x2830: + esc_G0_2(); + break; + case 0x2831: + esc_G0_3(); + break; + case 0x2832: + esc_G0_4(); + break; + case 0x2930: + esc_G1_2(); + break; + case 0x2931: + esc_G1_3(); + break; + case 0x2932: + esc_G1_4(); + break; + case 0x2941: + esc_G1_0(); + break; + case 0x2942: + esc_G1_1(); + break; + } + if (vt100_parse_state == State.Esc) { + vt100_parse_reset(); + } + } else { + switch (vt100_parse_func) { + case 0x0040: + csi_ICH(vt100_parse_param); + break; + case 0x0041: + csi_CUU(vt100_parse_param); + break; + case 0x0042: + csi_CUD(vt100_parse_param); + break; + case 0x0043: + csi_CUF(vt100_parse_param); + break; + case 0x0044: + csi_CUB(vt100_parse_param); + break; + case 0x0045: + csi_CNL(vt100_parse_param); + break; + case 0x0046: + csi_CPL(vt100_parse_param); + break; + case 0x0047: + csi_CHA(vt100_parse_param); + break; + case 0x0048: + csi_CUP(vt100_parse_param); + break; + case 0x0049: + csi_CHT(vt100_parse_param); + break; + case 0x004A: + csi_ED(vt100_parse_param); + break; + case 0x004B: + csi_EL(vt100_parse_param); + break; + case 0x004C: + csi_IL(vt100_parse_param); + break; + case 0x004D: + csi_DL(vt100_parse_param); + break; + case 0x004E: /* EF */ + break; + case 0x004F: /* EA */ + break; + case 0x0050: + csi_DCH(vt100_parse_param); + break; + case 0x0051: /* SEE */ + break; + case 0x0052: /* CPR */ + break; + case 0x0053: + csi_SU(vt100_parse_param); + break; + case 0x0054: + csi_SD(vt100_parse_param); + break; + case 0x0055: /* NP */ + break; + case 0x0056: /* PP */ + break; + case 0x0057: + csi_CTC(vt100_parse_param); + break; + case 0x0058: + csi_ECH(vt100_parse_param); + break; + case 0x0059: /* CVT */ + break; + case 0x005A: + csi_CBT(vt100_parse_param); + break; + case 0x005B: /* SRS */ + break; + case 0x005C: /* PTX */ + break; + case 0x005D: /* SDS */ + break; + case 0x005E: /* SIMD */ + break; + case 0x0060: + csi_HPA(vt100_parse_param); + break; + case 0x0061: + csi_HPR(vt100_parse_param); + break; + case 0x0062: + csi_REP(vt100_parse_param); + break; + case 0x0063: + csi_DA(vt100_parse_param); + break; + case 0x0064: + csi_VPA(vt100_parse_param); + break; + case 0x0065: + csi_VPR(vt100_parse_param); + break; + case 0x0066: + csi_HVP(vt100_parse_param); + break; + case 0x0067: + csi_TBC(vt100_parse_param); + break; + case 0x0068: + csi_SM(vt100_parse_param); + break; + case 0x0069: /* MC */ + break; + case 0x006A: /* HPB */ + break; + case 0x006B: /* VPB */ + break; + case 0x006C: + csi_RM(vt100_parse_param); + break; + case 0x006D: + csi_SGR(vt100_parse_param); + break; + case 0x006E: + csi_DSR(vt100_parse_param); + break; + case 0x006F: /* DAQ */ + break; + case 0x0072: + csi_DECSTBM(vt100_parse_param); + break; + case 0x0073: + csi_SCP(vt100_parse_param); + break; + case 0x0075: + csi_RCP(vt100_parse_param); + break; + case 0x0078: + csi_DECREQTPARM(vt100_parse_param); + break; + case 0x2040: /* SL */ + break; + case 0x2041: /* SR */ + break; + case 0x2042: /* GSM */ + break; + case 0x2043: /* GSS */ + break; + case 0x2044: /* FNT */ + break; + case 0x2045: /* TSS */ + break; + case 0x2046: /* JFY */ + break; + case 0x2047: /* SPI */ + break; + case 0x2048: /* QUAD */ + break; + case 0x2049: /* SSU */ + break; + case 0x204A: /* PFS */ + break; + case 0x204B: /* SHS */ + break; + case 0x204C: /* SVS */ + break; + case 0x204D: /* IGS */ + break; + case 0x204E: /* deprecated: HTSA */ + break; + case 0x204F: /* IDCS */ + break; + case 0x2050: /* PPA */ + break; + case 0x2051: /* PPR */ + break; + case 0x2052: /* PPB */ + break; + case 0x2053: /* SPD */ + break; + case 0x2054: /* DTA */ + break; + case 0x2055: /* SLH */ + break; + case 0x2056: /* SLL */ + break; + case 0x2057: /* FNK */ + break; + case 0x2058: /* SPQR */ + break; + case 0x2059: /* SEF */ + break; + case 0x205A: /* PEC */ + break; + case 0x205B: /* SSW */ + break; + case 0x205C: /* SACS */ + break; + case 0x205D: /* SAPV */ + break; + case 0x205E: /* STAB */ + break; + case 0x205F: /* GCC */ + break; + case 0x2060: /* TAPE */ + break; + case 0x2061: /* TALE */ + break; + case 0x2062: /* TAC */ + break; + case 0x2063: /* TCC */ + break; + case 0x2064: /* TSR */ + break; + case 0x2065: /* SCO */ + break; + case 0x2066: /* SRCS */ + break; + case 0x2067: /* SCS */ + break; + case 0x2068: /* SLS */ + break; + case 0x2069: /* SPH */ + break; + case 0x206A: /* SPL */ + break; + case 0x206B: /* SCP */ + break; + case 0x2170: + csi_DECSTR(vt100_parse_param); + break; + case 0x2472: /* DECCARA */ + break; + case 0x2477: /* DECRQPSR */ + break; + } + if (vt100_parse_state == State.Csi) { + vt100_parse_reset(); + } + } + } + + private boolean vt100_write(int c) { + if (c < 32) { + if (c == 27) { + vt100_parse_reset(State.Esc); + return true; + } else if (c == 14) { + ctrl_SO(); + } else if (c == 15) { + ctrl_SI(); + } + } else if ((c & 0xffe0) == 0x0080) { + vt100_parse_reset(State.Esc); + vt100_parse_func = (char) (c - 0x0040); + vt100_parse_process(); + return true; + } + if (vt100_parse_state != State.None) { + if (vt100_parse_state == State.Str) { + if (c >= 32) { + return true; + } + vt100_parse_reset(); + } else { + if (c < 32) { + if (c == 24 || c == 26) { + vt100_parse_reset(); + return true; + } + } else { + vt100_parse_len += 1; + if (vt100_parse_len > 32) { + vt100_parse_reset(); + } else { + int msb = c & 0xf0; + if (msb == 0x20) { + vt100_parse_func <<= 8; + vt100_parse_func += (char) c; + } else if (msb == 0x30 && vt100_parse_state == State.Csi) { + vt100_parse_param += String.valueOf((char) c); + } else { + vt100_parse_func <<= 8; + vt100_parse_func += (char) c; + vt100_parse_process(); + } + return true; + } + } + } + } + vt100_lastchar = c; + return false; + } + + public boolean isDirty() { + return dirty.compareAndSet(true, false); + } + + // + // Dirty + // + + public synchronized void waitDirty() throws InterruptedException { + while (!dirty.compareAndSet(true, false)) { + wait(); + } + } + + protected synchronized void setDirty() { + dirty.set(true); + notifyAll(); + } + + public synchronized boolean setSize(int w, int h) { + if (w < 2 || w > 256 || h < 2 || h > 256) { + return false; + } + + // Set width + for (int i = 0; i < height; i++) { + if (screen[i].length < w) { + screen[i] = Arrays.copyOf(screen[i], w); + } + if (screen2[i].length < w) { + screen2[i] = Arrays.copyOf(screen2[i], w); + } + } + if (cx >= w) { + cx = w - 1; + } + + // Set height + if (h < height) { + int needed = height - h; + // Delete as many lines as possible from the bottom + int avail = height - 1 - cy; + if (avail > 0) { + if (avail > needed) { + avail = needed; + } + screen = Arrays.copyOfRange(screen, 0, height - avail); + } + needed -= avail; + // Move lines to history + for (int i = 0; i < needed; i++) { + history.add(screen[i]); + } + screen = Arrays.copyOfRange(screen, needed, screen.length); + cy -= needed; + } else if (h > height) { + int needed = h - height; + // Pull lines from history + int avail = history.size(); + if (avail > needed) { + avail = needed; + } + long[][] sc = new long[h][]; + if (avail > 0) { + for (int i = 0; i < avail; i++) { + sc[i] = history.remove(history.size() - avail + i); + } + cy += avail; + } + System.arraycopy(screen, 0, sc, avail, screen.length); + for (int i = avail + screen.length; i < sc.length; i++) { + sc[i] = new long[w]; + Arrays.fill(sc[i], attr | 0x00000020); + } + screen = sc; + } + + screen2 = (long[][]) Array.newInstance(long.class, h, w); + for (int i = 0; i < h; i++) { + Arrays.fill(screen2[i], attr | 0x00000020); + } + + // Scroll parameters + scroll_area_y0 = Math.min(h, scroll_area_y0); + scroll_area_y1 = scroll_area_y1 == height ? h : Math.min(h, scroll_area_y1); + // Cursor position + cx = Math.min(w - 1, cx); + cy = Math.min(h - 1, cy); + + width = w; + height = h; + + setDirty(); + return true; + } + + // + // External interface + // + + public synchronized String read() { + String d = vt100_out; + vt100_out = ""; + return d; + } + + public synchronized String pipe(String d) { + String o = ""; + for (char c : d.toCharArray()) { + if (vt100_keyfilter_escape) { + vt100_keyfilter_escape = false; + if (vt100_mode_cursorkey) { + switch (c) { + case '~': + o += "~"; + break; + case 'A': + o += "\u001bOA"; + break; + case 'B': + o += "\u001bOB"; + break; + case 'C': + o += "\u001bOC"; + break; + case 'D': + o += "\u001bOD"; + break; + case 'F': + o += "\u001bOF"; + break; + case 'H': + o += "\u001bOH"; + break; + case '1': + o += "\u001b[5~"; + break; + case '2': + o += "\u001b[6~"; + break; + case '3': + o += "\u001b[2~"; + break; + case '4': + o += "\u001b[3~"; + break; + case 'a': + o += "\u001bOP"; + break; + case 'b': + o += "\u001bOQ"; + break; + case 'c': + o += "\u001bOR"; + break; + case 'd': + o += "\u001bOS"; + break; + case 'e': + o += "\u001b[15~"; + break; + case 'f': + o += "\u001b[17~"; + break; + case 'g': + o += "\u001b[18~"; + break; + case 'h': + o += "\u001b[19~"; + break; + case 'i': + o += "\u001b[20~"; + break; + case 'j': + o += "\u001b[21~"; + break; + case 'k': + o += "\u001b[23~"; + break; + case 'l': + o += "\u001b[24~"; + break; + } + } else { + switch (c) { + case '~': + o += "~"; + break; + case 'A': + o += "\u001b[A"; + break; + case 'B': + o += "\u001b[B"; + break; + case 'C': + o += "\u001b[C"; + break; + case 'D': + o += "\u001b[D"; + break; + case 'F': + o += "\u001b[F"; + break; + case 'H': + o += "\u001b[H"; + break; + case '1': + o += "\u001b[5~"; + break; + case '2': + o += "\u001b[6~"; + break; + case '3': + o += "\u001b[2~"; + break; + case '4': + o += "\u001b[3~"; + break; + case 'a': + o += "\u001bOP"; + break; + case 'b': + o += "\u001bOQ"; + break; + case 'c': + o += "\u001bOR"; + break; + case 'd': + o += "\u001bOS"; + break; + case 'e': + o += "\u001b[15~"; + break; + case 'f': + o += "\u001b[17~"; + break; + case 'g': + o += "\u001b[18~"; + break; + case 'h': + o += "\u001b[19~"; + break; + case 'i': + o += "\u001b[20~"; + break; + case 'j': + o += "\u001b[21~"; + break; + case 'k': + o += "\u001b[23~"; + break; + case 'l': + o += "\u001b[24~"; + break; + } + } + } else if (c == '~') { + vt100_keyfilter_escape = true; + } else if (c == 127) { + if (vt100_mode_backspace) { + o += (char) 8; + } else { + o += (char) 127; + } + } else { + o += c; + if (vt100_mode_lfnewline && c == 13) { + o += (char) 10; + } + } + } + return o; + } + + public synchronized boolean write(CharSequence d) { + d.codePoints().forEachOrdered(c -> { + if (!vt100_write(c) && !dumb_write(c) && c <= 0xffff) { + dumb_echo(c); + } + }); + return true; + } + + public synchronized void dump(long[] fullscreen, int ftop, int fleft, int fheight, int fwidth, int[] cursor) { + int cx = Math.min(this.cx, width - 1); + int cy = this.cy; + for (int y = 0; y < Math.min(height, fheight - ftop); y++) { + System.arraycopy(screen[y], 0, fullscreen, (y + ftop) * fwidth + fleft, width); + } + if (cursor != null) { + cursor[0] = cx + fleft; + cursor[1] = cy + ftop; + } + } + + public synchronized String dump(long timeout, boolean forceDump) throws InterruptedException { + if (!dirty.get() && timeout > 0) { + wait(timeout); + } + if (dirty.compareAndSet(true, false) || forceDump) { + StringBuilder sb = new StringBuilder(); + int prev_attr = -1; + int cx = Math.min(this.cx, width - 1); + int cy = this.cy; + sb.append("
");
+            for (int y = 0; y < height; y++) {
+                int wx = 0;
+                for (int x = 0; x < width; x++) {
+                    long d = screen[y][x];
+                    int c = (int) (d & 0xffffffff);
+                    int a = (int) (d >> 32);
+                    if (cy == y && cx == x && vt100_mode_cursor) {
+                        a = a & 0xfff0 | 0x000c;
+                    }
+                    if (a != prev_attr) {
+                        if (prev_attr != -1) {
+                            sb.append("");
+                        }
+                        int bg = a & 0x000000ff;
+                        int fg = (a & 0x0000ff00) >> 8;
+                        boolean inv = (a & 0x00020000) != 0;
+                        boolean inv2 = vt100_mode_inverse;
+                        if (inv && !inv2 || inv2 && !inv) {
+                            int i = fg;
+                            fg = bg;
+                            bg = i;
+                        }
+                        if ((a & 0x00040000) != 0) {
+                            fg = 0x0c;
+                        }
+                        String ul;
+                        if ((a & 0x00010000) != 0) {
+                            ul = " ul";
+                        } else {
+                            ul = "";
+                        }
+                        String b;
+                        if ((a & 0x00080000) != 0) {
+                            b = " b";
+                        } else {
+                            b = "";
+                        }
+                        sb.append("");
+                        prev_attr = a;
+                    }
+                    switch (c) {
+                        case '&':
+                            sb.append("&");
+                            break;
+                        case '<':
+                            sb.append("<");
+                            break;
+                        case '>':
+                            sb.append(">");
+                            break;
+                        default:
+                            wx += utf8_charwidth(c);
+                            if (wx <= width) {
+                                sb.append((char) c);
+                            }
+                            break;
+                    }
+                }
+                sb.append("\n");
+            }
+            sb.append("
"); + return sb.toString(); + } + return null; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + sb.appendCodePoint((int) (screen[y][x] & 0xffffffffL)); + } + sb.append("\n"); + } + return sb.toString(); + } + + enum State { + None, + Esc, + Str, + Csi, + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/Source.java b/net-cli/src/main/java/org/jline/builtins/Source.java new file mode 100644 index 0000000..805aa2c --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/Source.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2002-2022, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.io.File; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.stream.Stream; + +public interface Source { + + String getName(); + + InputStream read() throws IOException; + + Long lines(); + + class URLSource implements Source { + final URL url; + final String name; + + public URLSource(URL url, String name) { + this.url = Objects.requireNonNull(url); + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public InputStream read() throws IOException { + return url.openStream(); + } + + @Override + public Long lines() { + Long out = null; + try (Stream lines = Files.lines(new File(url.toURI()).toPath())) { + out = lines.count(); + } catch (Exception ignore) { + } + return out; + } + } + + class PathSource implements Source { + final Path path; + final String name; + + public PathSource(File file, String name) { + this(Objects.requireNonNull(file).toPath(), name); + } + + public PathSource(Path path, String name) { + this.path = Objects.requireNonNull(path); + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public InputStream read() throws IOException { + return Files.newInputStream(path); + } + + @Override + public Long lines() { + Long out = null; + try (Stream lines = Files.lines(path)) { + out = lines.count(); + } catch (Exception ignore) { + } + return out; + } + } + + class InputStreamSource implements Source { + final InputStream in; + final String name; + + public InputStreamSource(InputStream in, boolean close, String name) { + Objects.requireNonNull(in); + if (close) { + this.in = in; + } else { + this.in = new FilterInputStream(in) { + @Override + public void close() throws IOException { + } + }; + } + if (this.in.markSupported()) { + this.in.mark(Integer.MAX_VALUE); + } + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public InputStream read() throws IOException { + if (in.markSupported()) { + in.reset(); + } + return in; + } + + @Override + public Long lines() { + return null; + } + } + + class StdInSource extends InputStreamSource { + + public StdInSource() { + this(System.in); + } + + public StdInSource(InputStream in) { + super(in, false, null); + } + } + + class ResourceSource implements Source { + final String resource; + final String name; + + public ResourceSource(String resource) { + this(resource, resource); + } + + public ResourceSource(String resource, String name) { + this.resource = Objects.requireNonNull(resource); + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public InputStream read() throws IOException { + return getClass().getResourceAsStream(resource); + } + + @Override + public Long lines() { + return null; + } + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/Styles.java b/net-cli/src/main/java/org/jline/builtins/Styles.java new file mode 100644 index 0000000..d281d2c --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/Styles.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.jline.utils.StyleResolver; +import static org.jline.builtins.SyntaxHighlighter.REGEX_TOKEN_NAME; + +public class Styles { + public static final String NANORC_THEME = "NANORC_THEME"; + protected static final List ANSI_STYLES = Arrays.asList( + "blink", + "bold", + "conceal", + "crossed-out", + "crossedout", + "faint", + "hidden", + "inverse", + "inverse-neg", + "inverseneg", + "italic", + "underline"); + private static final String DEFAULT_LS_COLORS = "di=1;91:ex=1;92:ln=1;96:fi="; + private static final String DEFAULT_HELP_COLORS = "ti=1;34:co=1:ar=3:op=33"; + private static final String DEFAULT_PRNT_COLORS = "th=1;34:rn=1;34:rs=,~grey15:mk=1;34:em=31:vs=32"; + private static final String LS_COLORS = "LS_COLORS"; + private static final String HELP_COLORS = "HELP_COLORS"; + private static final String PRNT_COLORS = "PRNT_COLORS"; + + private static final String KEY = "([a-z]{2}|\\*\\.[a-zA-Z0-9]+)"; + private static final String VALUE = "(([!~#]?[a-zA-Z0-9]+[a-z0-9-;]*)?|" + REGEX_TOKEN_NAME + ")"; + private static final String VALUES = VALUE + "(," + VALUE + ")*"; + private static final Pattern STYLE_ELEMENT_PATTERN = Pattern.compile(KEY + "=" + VALUES); + private static final Pattern STYLE_ELEMENT_SEPARATOR = Pattern.compile(":"); + + public static StyleResolver lsStyle() { + return style(LS_COLORS, DEFAULT_LS_COLORS); + } + + public static StyleResolver helpStyle() { + return style(HELP_COLORS, DEFAULT_HELP_COLORS); + } + + public static StyleResolver prntStyle() { + return style(PRNT_COLORS, DEFAULT_PRNT_COLORS); + } + + public static boolean isStylePattern(String style) { + final String[] styleElements = STYLE_ELEMENT_SEPARATOR.split(style); + return Arrays.stream(styleElements) + .allMatch(element -> element.isEmpty() + || STYLE_ELEMENT_PATTERN.matcher(element).matches()); + } + + public static StyleResolver style(String name, String defStyle) { + String style = consoleOption(name); + if (style == null) { + style = defStyle; + } + return style(style); + } + + private static ConsoleOptionGetter optionGetter() { + try { + return (ConsoleOptionGetter) Class.forName("org.jline.console.SystemRegistry") + .getDeclaredMethod("get") + .invoke(null); + } catch (Exception ignore) { + } + return null; + } + + private static T consoleOption(String name, T defVal) { + T out = defVal; + ConsoleOptionGetter cog = optionGetter(); + if (cog != null) { + out = cog.consoleOption(name, defVal); + } + return out; + } + + private static String consoleOption(String name) { + String out = null; + ConsoleOptionGetter cog = optionGetter(); + if (cog != null) { + out = (String) cog.consoleOption(name); + if (out != null && !isStylePattern(out)) { + out = null; + } + } + if (out == null) { + out = System.getenv(name); + if (out != null && !isStylePattern(out)) { + out = null; + } + } + return out; + } + + public static StyleResolver style(String style) { + Map colors = Arrays.stream(style.split(":")) + .collect(Collectors.toMap(s -> s.substring(0, s.indexOf('=')), s -> s.substring(s.indexOf('=') + 1))); + return new StyleResolver(new StyleCompiler(colors)::getStyle); + } + + public static class StyleCompiler { + private static final String ANSI_VALUE = "[0-9]*(;[0-9]+){0,2}"; + private static final String COLORS_24BIT = "[#x][0-9a-fA-F]{6}"; + private static final List COLORS_8 = + Arrays.asList("white", "black", "red", "blue", "green", "yellow", "magenta", "cyan"); + // https://github.com/lhmouse/nano-win/commit/a7aab18dfeef8a0e8073d5fa420677dc8fe548da + private static final Map COLORS_NANO = new HashMap<>(); + + static { + COLORS_NANO.put("pink", 204); + COLORS_NANO.put("purple", 163); + COLORS_NANO.put("mauve", 134); + COLORS_NANO.put("lagoon", 38); + COLORS_NANO.put("mint", 48); + COLORS_NANO.put("lime", 148); + COLORS_NANO.put("peach", 215); + COLORS_NANO.put("orange", 208); + COLORS_NANO.put("latte", 137); + } + + private final Map colors; + private final Map tokenColors; + private final boolean nanoStyle; + + public StyleCompiler(Map colors) { + this(colors, false); + } + + public StyleCompiler(Map colors, boolean nanoStyle) { + this.colors = colors; + this.nanoStyle = nanoStyle; + this.tokenColors = consoleOption(NANORC_THEME, new HashMap<>()); + } + + public String getStyle(String reference) { + String rawStyle = colors.get(reference); + if (rawStyle == null) { + return null; + } else if (rawStyle.matches(REGEX_TOKEN_NAME)) { + rawStyle = tokenColors.getOrDefault(rawStyle, "normal"); + } else if (!nanoStyle && rawStyle.matches(ANSI_VALUE)) { + return rawStyle; + } + StringBuilder out = new StringBuilder(); + boolean first = true; + boolean fg = true; + for (String s : rawStyle.split(",")) { + if (s.trim().isEmpty()) { + fg = false; + continue; + } + if (!first) { + out.append(","); + } + if (ANSI_STYLES.contains(s)) { + out.append(s); + } else if (COLORS_8.contains(s) + || COLORS_NANO.containsKey(s) + || s.startsWith("light") + || s.startsWith("bright") + || s.startsWith("~") + || s.startsWith("!") + || s.matches("\\d+") + || s.matches(COLORS_24BIT) + || s.equals("normal") + || s.equals("default")) { + if (s.matches(COLORS_24BIT)) { + if (fg) { + out.append("fg-rgb:"); + } else { + out.append("bg-rgb:"); + } + out.append(s); + } else if (s.matches("\\d+") || COLORS_NANO.containsKey(s)) { + if (fg) { + out.append("38;5;"); + } else { + out.append("48;5;"); + } + out.append(s.matches("\\d+") ? s : COLORS_NANO.get(s).toString()); + } else { + if (fg) { + out.append("fg:"); + } else { + out.append("bg:"); + } + if (COLORS_8.contains(s) || s.startsWith("~") || s.startsWith("!") || s.startsWith("bright-")) { + out.append(s); + } else if (s.startsWith("light")) { + out.append("!").append(s.substring(5)); + } else if (s.startsWith("bright")) { + out.append("!").append(s.substring(6)); + } else { + out.append("default"); + } + } + fg = false; + } + first = false; + } + return out.toString(); + } + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/SyntaxHighlighter.java b/net-cli/src/main/java/org/jline/builtins/SyntaxHighlighter.java new file mode 100644 index 0000000..21f8208 --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/SyntaxHighlighter.java @@ -0,0 +1,995 @@ +/* + * Copyright (c) 2002-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Stream; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.Log; +import org.jline.utils.StyleResolver; + +/** + * Java implementation of nanorc highlighter + * + * @author Matti Rinta-Nikkola + */ +public class SyntaxHighlighter { + public static final String REGEX_TOKEN_NAME = "[A-Z_]+"; + public static final String TYPE_NANORCTHEME = ".nanorctheme"; + public static final String DEFAULT_NANORC_FILE = "jnanorc"; + protected static final String DEFAULT_LESSRC_FILE = "jlessrc"; + protected static final String COMMAND_INCLUDE = "include"; + protected static final String COMMAND_THEME = "theme"; + private static final String TOKEN_NANORC = "NANORC"; + private final Path nanorc; + private final String syntaxName; + private final String nanorcUrl; + private final Map> rules = new HashMap<>(); + private Path currentTheme; + private boolean startEndHighlight; + private int ruleStartId = 0; + + private Parser parser; + + private SyntaxHighlighter() { + this(null, null, null); + } + + private SyntaxHighlighter(String nanorcUrl) { + this(null, null, nanorcUrl); + } + + private SyntaxHighlighter(Path nanorc, String syntaxName) { + this(nanorc, syntaxName, null); + } + + private SyntaxHighlighter(Path nanorc, String syntaxName, String nanorcUrl) { + this.nanorc = nanorc; + this.syntaxName = syntaxName; + this.nanorcUrl = nanorcUrl; + Map> defaultRules = new HashMap<>(); + defaultRules.put(TOKEN_NANORC, new ArrayList<>()); + rules.putAll(defaultRules); + } + + protected static SyntaxHighlighter build(List syntaxFiles, String file, String syntaxName) { + return build(syntaxFiles, file, syntaxName, false); + } + + protected static SyntaxHighlighter build( + List syntaxFiles, String file, String syntaxName, boolean ignoreErrors) { + SyntaxHighlighter out = new SyntaxHighlighter(); + Map colorTheme = new HashMap<>(); + try { + if (syntaxName == null || !syntaxName.equals("none")) { + for (Path p : syntaxFiles) { + try { + if (colorTheme.isEmpty() && p.getFileName().toString().endsWith(TYPE_NANORCTHEME)) { + out.setCurrentTheme(p); + try (BufferedReader reader = new BufferedReader(new FileReader(p.toFile()))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.length() > 0 && !line.startsWith("#")) { + List parts = Arrays.asList(line.split("\\s+", 2)); + colorTheme.put(parts.get(0), parts.get(1)); + } + } + } + } else { + NanorcParser nanorcParser = new NanorcParser(p, syntaxName, file, colorTheme); + nanorcParser.parse(); + if (nanorcParser.matches()) { + out.addRules(nanorcParser.getHighlightRules()); + out.setParser(nanorcParser.getParser()); + return out; + } else if (nanorcParser.isDefault()) { + out.addRules(nanorcParser.getHighlightRules()); + } + } + } catch (IOException e) { + // ignore + } + } + } + } catch (PatternSyntaxException e) { + if (!ignoreErrors) { + throw e; + } + } + return out; + } + + /** + * Build SyntaxHighlighter + * + * @param nanorc Path of nano config file jnanorc + * @param syntaxName syntax name e.g 'Java' + * @return SyntaxHighlighter + */ + public static SyntaxHighlighter build(Path nanorc, String syntaxName) { + SyntaxHighlighter out = new SyntaxHighlighter(nanorc, syntaxName); + List syntaxFiles = new ArrayList<>(); + try { + try (BufferedReader reader = new BufferedReader(new FileReader(nanorc.toFile()))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.length() > 0 && !line.startsWith("#")) { + List parts = RuleSplitter.split(line); + if (parts.get(0).equals(COMMAND_INCLUDE)) { + nanorcInclude(parts.get(1), syntaxFiles); + } else if (parts.get(0).equals(COMMAND_THEME)) { + nanorcTheme(parts.get(1), syntaxFiles); + } + } + } + } + SyntaxHighlighter sh = build(syntaxFiles, null, syntaxName); + out.addRules(sh.rules); + out.setParser(sh.parser); + out.setCurrentTheme(sh.currentTheme); + } catch (Exception e) { + // ignore + } + return out; + } + + protected static void nanorcInclude(String parameter, List syntaxFiles) throws IOException { + addFiles(parameter, s -> s.forEach(syntaxFiles::add)); + } + + protected static void nanorcTheme(String parameter, List syntaxFiles) throws IOException { + addFiles(parameter, s -> s.findFirst().ifPresent(p -> syntaxFiles.add(0, p))); + } + + protected static void addFiles(String parameter, Consumer> consumer) throws IOException { + if (parameter.contains("*") || parameter.contains("?")) { + PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + parameter); + try (Stream pathStream = Files.walk(Paths.get(new File(parameter).getParent()))) { + consumer.accept(pathStream.filter(pathMatcher::matches)); + } + } else { + consumer.accept(Stream.of(Paths.get(parameter))); + } + } + + /** + * Build SyntaxHighlighter + * + * @param nanorcUrl Url of nanorc file + * @return SyntaxHighlighter + */ + public static SyntaxHighlighter build(String nanorcUrl) { + SyntaxHighlighter out = new SyntaxHighlighter(nanorcUrl); + InputStream inputStream; + try { + if (nanorcUrl.startsWith("classpath:")) { + inputStream = new Source.ResourceSource(nanorcUrl.substring(10), null).read(); + } else { + inputStream = new Source.URLSource(new URI(nanorcUrl).toURL(), null).read(); + } + NanorcParser parser = new NanorcParser(inputStream, null, null); + parser.parse(); + out.addRules(parser.getHighlightRules()); + } catch (IOException | URISyntaxException e) { + // ignore + } + return out; + } + + private void addRules(Map> rules) { + this.rules.putAll(rules); + } + + public Path getCurrentTheme() { + return currentTheme; + } + + public void setCurrentTheme(Path currentTheme) { + this.currentTheme = currentTheme; + } + + public void setParser(Parser parser) { + this.parser = parser; + } + + public SyntaxHighlighter reset() { + ruleStartId = 0; + startEndHighlight = false; + if (parser != null) { + parser.reset(); + } + return this; + } + + public void refresh() { + SyntaxHighlighter sh; + if (nanorc != null && syntaxName != null) { + sh = SyntaxHighlighter.build(nanorc, syntaxName); + } else if (nanorcUrl != null) { + sh = SyntaxHighlighter.build(nanorcUrl); + } else { + throw new IllegalStateException("Not possible to refresh highlighter!"); + } + rules.clear(); + addRules(sh.rules); + parser = sh.parser; + currentTheme = sh.currentTheme; + } + + public AttributedString highlight(String string) { + return splitAndHighlight(new AttributedString(string)); + } + + public AttributedString highlight(AttributedStringBuilder asb) { + return splitAndHighlight(asb.toAttributedString()); + } + + public AttributedString highlight(AttributedString attributedString) { + return splitAndHighlight(attributedString); + } + + private AttributedString splitAndHighlight(AttributedString attributedString) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + boolean first = true; + for (AttributedString line : attributedString.columnSplitLength(Integer.MAX_VALUE)) { + if (!first) { + asb.append("\n"); + } + List tokens = new ArrayList<>(); + if (parser != null) { + parser.parse(line); + tokens = parser.getTokens(); + } + if (tokens.isEmpty()) { + asb.append(_highlight(line, rules.get(TOKEN_NANORC))); + } else { + int pos = 0; + for (ParsedToken t : tokens) { + if (t.getStart() > pos) { + AttributedStringBuilder head = + _highlight(line.columnSubSequence(pos, t.getStart() + 1), rules.get(TOKEN_NANORC)); + asb.append(head.columnSubSequence(0, head.length() - 1)); + } + asb.append(_highlight( + line.columnSubSequence(t.getStart(), t.getEnd()), + rules.get(t.getName()), + t.getStartWith(), + line.columnSubSequence(t.getEnd(), line.length()))); + pos = t.getEnd(); + } + if (pos < line.length()) { + asb.append(_highlight(line.columnSubSequence(pos, line.length()), rules.get(TOKEN_NANORC))); + } + } + first = false; + } + return asb.toAttributedString(); + } + + private AttributedStringBuilder _highlight(AttributedString line, List rules) { + return _highlight(line, rules, null, null); + } + + private AttributedStringBuilder _highlight( + AttributedString line, List rules, CharSequence startWith, CharSequence continueAs) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(line); + if (rules.isEmpty()) { + return asb; + } + int startId = ruleStartId; + boolean endHighlight = startEndHighlight; + for (int i = startId; i < (endHighlight ? startId + 1 : rules.size()); i++) { + HighlightRule rule = rules.get(i); + switch (rule.getType()) { + case PATTERN: + asb.styleMatches(rule.getPattern(), rule.getStyle()); + break; + case START_END: + boolean done = false; + Matcher start = rule.getStart().matcher(asb.toAttributedString()); + Matcher end = rule.getEnd().matcher(asb.toAttributedString()); + while (!done) { + AttributedStringBuilder a = new AttributedStringBuilder(); + if (startEndHighlight && ruleStartId == i) { + if (end.find()) { + ruleStartId = 0; + startEndHighlight = false; + a.append(asb.columnSubSequence(0, end.end()), rule.getStyle()); + a.append(_highlight( + asb.columnSubSequence(end.end(), asb.length()) + .toAttributedString(), + rules)); + } else { + a.append(asb, rule.getStyle()); + done = true; + } + asb = a; + } else { + if (start.find()) { + a.append(asb.columnSubSequence(0, start.start())); + if (end.find()) { + a.append(asb.columnSubSequence(start.start(), end.end()), rule.getStyle()); + a.append(asb.columnSubSequence(end.end(), asb.length())); + } else { + ruleStartId = i; + startEndHighlight = true; + a.append(asb.columnSubSequence(start.start(), asb.length()), rule.getStyle()); + done = true; + } + asb = a; + } else { + done = true; + } + } + } + break; + case PARSER_START_WITH: + if (startWith != null && startWith.toString().startsWith(rule.getStartWith())) { + asb.styleMatches(rule.getPattern(), rule.getStyle()); + } + break; + case PARSER_CONTINUE_AS: + if (continueAs != null && continueAs.toString().matches(rule.getContinueAs() + ".*")) { + asb.styleMatches(rule.getPattern(), rule.getStyle()); + } + break; + } + } + return asb; + } + + private static class HighlightRule { + private final RuleType type; + private final AttributedStyle style; + private Pattern pattern; + private Pattern start; + private Pattern end; + private String startWith; + private String continueAs; + public HighlightRule(AttributedStyle style, Pattern pattern) { + this.type = RuleType.PATTERN; + this.pattern = pattern; + this.style = style; + } + + public HighlightRule(AttributedStyle style, Pattern start, Pattern end) { + this.type = RuleType.START_END; + this.style = style; + this.start = start; + this.end = end; + } + + public HighlightRule(RuleType parserRuleType, AttributedStyle style, String value) { + this.type = parserRuleType; + this.style = style; + this.pattern = Pattern.compile(".*"); + if (parserRuleType == RuleType.PARSER_START_WITH) { + this.startWith = value; + } else if (parserRuleType == RuleType.PARSER_CONTINUE_AS) { + this.continueAs = value; + } else { + throw new IllegalArgumentException("Bad RuleType: " + parserRuleType); + } + } + + public static RuleType evalRuleType(List colorCfg) { + RuleType out = null; + if (colorCfg.get(0).equals("color") || colorCfg.get(0).equals("icolor")) { + out = RuleType.PATTERN; + if (colorCfg.size() == 3) { + if (colorCfg.get(2).startsWith("startWith=")) { + out = RuleType.PARSER_START_WITH; + } else if (colorCfg.get(2).startsWith("continueAs=")) { + out = RuleType.PARSER_CONTINUE_AS; + } + } else if (colorCfg.size() == 4) { + if (colorCfg.get(2).startsWith("start=") && colorCfg.get(3).startsWith("end=")) { + out = RuleType.START_END; + } + } + } + return out; + } + + public RuleType getType() { + return type; + } + + public AttributedStyle getStyle() { + return style; + } + + public Pattern getPattern() { + if (type == RuleType.START_END) { + throw new IllegalAccessError(); + } + return pattern; + } + + public Pattern getStart() { + if (type == RuleType.PATTERN) { + throw new IllegalAccessError(); + } + return start; + } + + public Pattern getEnd() { + if (type == RuleType.PATTERN) { + throw new IllegalAccessError(); + } + return end; + } + + public String getStartWith() { + return startWith; + } + + public String getContinueAs() { + return continueAs; + } + + public String toString() { + return "{type:" + type + + ", startWith: " + startWith + + ", continueAs: " + continueAs + + ", start: " + start + + ", end: " + end + + ", pattern: " + pattern + + "}"; + } + + public enum RuleType { + PATTERN, + START_END, + PARSER_START_WITH, + PARSER_CONTINUE_AS + } + } + + private static class NanorcParser { + private static final String DEFAULT_SYNTAX = "default"; + private final String name; + private final String target; + private final Map> highlightRules = new HashMap<>(); + private final BufferedReader reader; + private Map colorTheme = new HashMap<>(); + private boolean matches = false; + private String syntaxName = "unknown"; + + private Parser parser; + + public NanorcParser(Path file, String name, String target, Map colorTheme) throws IOException { + this(new Source.PathSource(file, null).read(), name, target); + this.colorTheme = colorTheme; + } + + public NanorcParser(InputStream in, String name, String target) { + this.reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + this.name = name; + this.target = target; + highlightRules.put(TOKEN_NANORC, new ArrayList<>()); + } + + public void parse() throws IOException { + String line; + int idx = 0; + try { + while ((line = reader.readLine()) != null) { + idx++; + line = line.trim(); + if (line.length() > 0 && !line.startsWith("#")) { + List parts = RuleSplitter.split(fixRegexes(line)); + if (parts.get(0).equals("syntax")) { + syntaxName = parts.get(1); + List filePatterns = new ArrayList<>(); + if (name != null) { + if (name.equals(syntaxName)) { + matches = true; + } else { + break; + } + } else if (target != null) { + for (int i = 2; i < parts.size(); i++) { + filePatterns.add(Pattern.compile(parts.get(i))); + } + for (Pattern p : filePatterns) { + if (p.matcher(target).find()) { + matches = true; + break; + } + } + if (!matches && !syntaxName.equals(DEFAULT_SYNTAX)) { + break; + } + } else { + matches = true; + } + } else if (parts.get(0).startsWith("$")) { + String key = themeKey(parts.get(0)); + if (colorTheme.containsKey(key)) { + if (parser == null) { + parser = new Parser(); + } + String[] args = parts.get(1).split(",\\s*"); + boolean validKey = true; + if (key.startsWith("$BLOCK_COMMENT")) { + parser.setBlockCommentDelimiters(key, args); + } else if (key.startsWith("$LINE_COMMENT")) { + parser.setLineCommentDelimiters(key, args); + } else if (key.startsWith("$BALANCED_DELIMITERS")) { + parser.setBalancedDelimiters(key, args); + } else { + Log.warn("Unknown token type: ", key); + validKey = false; + } + if (validKey) { + if (!highlightRules.containsKey(key)) { + highlightRules.put(key, new ArrayList<>()); + } + for (String l : colorTheme.get(key).split("\\\\n")) { + idx++; + addHighlightRule(RuleSplitter.split(fixRegexes(l)), idx, key); + } + } + } else { + Log.warn("Unknown token type: ", key); + } + } else if (!addHighlightRule(parts, idx, TOKEN_NANORC) + && parts.get(0).matches("\\+" + REGEX_TOKEN_NAME)) { + String key = themeKey(parts.get(0)); + String theme = colorTheme.get(key); + if (theme != null) { + for (String l : theme.split("\\\\n")) { + idx++; + addHighlightRule(RuleSplitter.split(fixRegexes(l)), idx, TOKEN_NANORC); + } + } else { + Log.warn("Unknown token type: ", key); + } + } + } + } + } finally { + reader.close(); + } + } + + private String fixRegexes(String line) { + return line.replaceAll("\\\\<", "\\\\b") + .replaceAll("\\\\>", "\\\\b") + .replaceAll("\\[:alnum:]", "\\\\p{Alnum}") + .replaceAll("\\[:alpha:]", "\\\\p{Alpha}") + .replaceAll("\\[:blank:]", "\\\\p{Blank}") + .replaceAll("\\[:cntrl:]", "\\\\p{Cntrl}") + .replaceAll("\\[:digit:]", "\\\\p{Digit}") + .replaceAll("\\[:graph:]", "\\\\p{Graph}") + .replaceAll("\\[:lower:]", "\\\\p{Lower}") + .replaceAll("\\[:print:]", "\\\\p{Print}") + .replaceAll("\\[:punct:]", "\\\\p{Punct}") + .replaceAll("\\[:space:]", "\\\\s") + .replaceAll("\\[:upper:]", "\\\\p{Upper}") + .replaceAll("\\[:xdigit:]", "\\\\p{XDigit}"); + } + + private boolean addHighlightRule(List parts, int idx, String tokenName) { + boolean out = true; + if (parts.get(0).equals("color")) { + addHighlightRule(syntaxName + idx, parts, false, tokenName); + } else if (parts.get(0).equals("icolor")) { + addHighlightRule(syntaxName + idx, parts, true, tokenName); + } else if (parts.get(0).matches(REGEX_TOKEN_NAME + "[:]?")) { + String key = themeKey(parts.get(0)); + String theme = colorTheme.get(key); + if (theme != null) { + parts.set(0, "color"); + parts.add(1, theme); + addHighlightRule(syntaxName + idx, parts, false, tokenName); + } else { + Log.warn("Unknown token type: ", key); + } + } else if (parts.get(0).matches("~" + REGEX_TOKEN_NAME + "[:]?")) { + String key = themeKey(parts.get(0)); + String theme = colorTheme.get(key); + if (theme != null) { + parts.set(0, "icolor"); + parts.add(1, theme); + addHighlightRule(syntaxName + idx, parts, true, tokenName); + } else { + Log.warn("Unknown token type: ", key); + } + } else { + out = false; + } + return out; + } + + private String themeKey(String key) { + if (key.startsWith("+")) { + return key; + } else { + int keyEnd = key.endsWith(":") ? key.length() - 1 : key.length(); + if (key.startsWith("~")) { + return key.substring(1, keyEnd); + } + return key.substring(0, keyEnd); + } + } + + public boolean matches() { + return matches; + } + + public Parser getParser() { + return parser; + } + + public Map> getHighlightRules() { + return highlightRules; + } + + public boolean isDefault() { + return syntaxName.equals(DEFAULT_SYNTAX); + } + + private void addHighlightRule(String reference, List parts, boolean caseInsensitive, String tokenName) { + Map spec = new HashMap<>(); + spec.put(reference, parts.get(1)); + Styles.StyleCompiler sh = new Styles.StyleCompiler(spec, true); + AttributedStyle style = new StyleResolver(sh::getStyle).resolve("." + reference); + + if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PATTERN) { + if (parts.size() == 2) { + highlightRules.get(tokenName).add(new HighlightRule(style, doPattern(".*", caseInsensitive))); + } else { + for (int i = 2; i < parts.size(); i++) { + highlightRules + .get(tokenName) + .add(new HighlightRule(style, doPattern(parts.get(i), caseInsensitive))); + } + } + } else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.START_END) { + String s = parts.get(2); + String e = parts.get(3); + highlightRules + .get(tokenName) + .add(new HighlightRule( + style, + doPattern(s.substring(7, s.length() - 1), caseInsensitive), + doPattern(e.substring(5, e.length() - 1), caseInsensitive))); + } else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PARSER_START_WITH) { + highlightRules + .get(tokenName) + .add(new HighlightRule( + HighlightRule.RuleType.PARSER_START_WITH, + style, + parts.get(2).substring(10))); + } else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PARSER_CONTINUE_AS) { + highlightRules + .get(tokenName) + .add(new HighlightRule( + HighlightRule.RuleType.PARSER_CONTINUE_AS, + style, + parts.get(2).substring(11))); + } + } + + private Pattern doPattern(String regex, boolean caseInsensitive) { + return caseInsensitive ? Pattern.compile(regex, Pattern.CASE_INSENSITIVE) : Pattern.compile(regex); + } + } + + protected static class RuleSplitter { + protected static List split(String s) { + List out = new ArrayList<>(); + if (s.length() == 0) { + return out; + } + int depth = 0; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '"') { + if (depth == 0) { + depth = 1; + } else { + char nextChar = i < s.length() - 1 ? s.charAt(i + 1) : ' '; + if (nextChar == ' ') { + depth = 0; + } + } + } else if (c == ' ' && depth == 0 && sb.length() > 0) { + out.add(stripQuotes(sb.toString())); + sb = new StringBuilder(); + continue; + } + if (sb.length() > 0 || (c != ' ' && c != '\t')) { + sb.append(c); + } + } + if (sb.length() > 0) { + out.add(stripQuotes(sb.toString())); + } + return out; + } + + private static String stripQuotes(String s) { + String out = s.trim(); + if (s.startsWith("\"") && s.endsWith("\"")) { + out = s.substring(1, s.length() - 1); + } + return out; + } + } + + private static class BlockCommentDelimiters { + private final String start; + private final String end; + + public BlockCommentDelimiters(String[] args) { + if (args.length != 2 + || args[0] == null + || args[1] == null + || args[0].isEmpty() + || args[1].isEmpty() + || args[0].equals(args[1])) { + throw new IllegalArgumentException("Bad block comment delimiters!"); + } + start = args[0]; + end = args[1]; + } + + public String getStart() { + return start; + } + + public String getEnd() { + return end; + } + } + + private static class ParsedToken { + private final String name; + private final CharSequence startWith; + private final int start; + private final int end; + + public ParsedToken(String name, CharSequence startWith, int start, int end) { + this.name = name; + this.startWith = startWith; + this.start = start; + this.end = end; + } + + public String getName() { + return name; + } + + public CharSequence getStartWith() { + return startWith; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + } + + private static class Parser { + private static final char escapeChar = '\\'; + private String blockCommentTokenName; + private BlockCommentDelimiters blockCommentDelimiters; + private String lineCommentTokenName; + private String[] lineCommentDelimiters; + private String balancedDelimiterTokenName; + private String[] balancedDelimiters; + private String balancedDelimiter; + private List tokens; + private CharSequence startWith; + private int tokenStart = 0; + private boolean blockComment; + private boolean lineComment; + private boolean balancedQuoted; + + public Parser() { + } + + public void setBlockCommentDelimiters(String tokenName, String[] args) { + try { + blockCommentTokenName = tokenName; + blockCommentDelimiters = new BlockCommentDelimiters(args); + } catch (Exception e) { + Log.warn(e.getMessage()); + } + } + + public void setLineCommentDelimiters(String tokenName, String[] args) { + lineCommentTokenName = tokenName; + lineCommentDelimiters = args; + } + + public void setBalancedDelimiters(String tokenName, String[] args) { + balancedDelimiterTokenName = tokenName; + balancedDelimiters = args; + } + + public void reset() { + startWith = null; + blockComment = false; + lineComment = false; + balancedQuoted = false; + tokenStart = 0; + } + + public void parse(final CharSequence line) { + if (line == null) { + return; + } + tokens = new ArrayList<>(); + if (blockComment || balancedQuoted) { + tokenStart = 0; + } + for (int i = 0; i < line.length(); i++) { + if (isEscapeChar(line, i) || isEscaped(line, i)) { + continue; + } + if (!blockComment && !lineComment && !balancedQuoted) { + if (blockCommentDelimiters != null && isDelimiter(line, i, blockCommentDelimiters.getStart())) { + blockComment = true; + tokenStart = i; + startWith = startWithSubstring(line, i); + i = i + blockCommentDelimiters.getStart().length() - 1; + } else if (isLineCommentDelimiter(line, i)) { + lineComment = true; + tokenStart = i; + startWith = startWithSubstring(line, i); + break; + } else if ((balancedDelimiter = balancedDelimiter(line, i)) != null) { + balancedQuoted = true; + tokenStart = i; + startWith = startWithSubstring(line, i); + i = i + balancedDelimiter.length() - 1; + } + } else if (blockComment) { + if (isDelimiter(line, i, blockCommentDelimiters.getEnd())) { + blockComment = false; + i = i + blockCommentDelimiters.getEnd().length() - 1; + tokens.add(new ParsedToken(blockCommentTokenName, startWith, tokenStart, i + 1)); + } + } else if (balancedQuoted) { + if (isDelimiter(line, i, balancedDelimiter)) { + balancedQuoted = false; + i = i + balancedDelimiter.length() - 1; + if (i - tokenStart + 1 > 2 * balancedDelimiter.length()) { + tokens.add(new ParsedToken(balancedDelimiterTokenName, startWith, tokenStart, i + 1)); + } + } + } + } + if (blockComment) { + tokens.add(new ParsedToken(blockCommentTokenName, startWith, tokenStart, line.length())); + } else if (lineComment) { + lineComment = false; + tokens.add(new ParsedToken(lineCommentTokenName, startWith, tokenStart, line.length())); + } else if (balancedQuoted) { + tokens.add(new ParsedToken(balancedDelimiterTokenName, startWith, tokenStart, line.length())); + } + } + + private CharSequence startWithSubstring(CharSequence line, int pos) { + return line.subSequence(pos, Math.min(pos + 5, line.length())); + } + + public List getTokens() { + return tokens; + } + + private String balancedDelimiter(final CharSequence buffer, final int pos) { + if (balancedDelimiters != null) { + for (String delimiter : balancedDelimiters) { + if (isDelimiter(buffer, pos, delimiter)) { + return delimiter; + } + } + } + return null; + } + + private boolean isDelimiter(final CharSequence buffer, final int pos, final String delimiter) { + if (pos < 0 || delimiter == null) { + return false; + } + final int length = delimiter.length(); + if (length <= buffer.length() - pos) { + for (int i = 0; i < length; i++) { + if (delimiter.charAt(i) != buffer.charAt(pos + i)) { + return false; + } + } + return true; + } + return false; + } + + private boolean isLineCommentDelimiter(final CharSequence buffer, final int pos) { + if (lineCommentDelimiters != null) { + for (String delimiter : lineCommentDelimiters) { + if (isDelimiter(buffer, pos, delimiter)) { + return true; + } + } + } + return false; + } + + private boolean isEscapeChar(char ch) { + return escapeChar == ch; + } + + /** + * Check if this character is a valid escape char (i.e. one that has not been escaped) + * + * @param buffer the buffer to check in + * @param pos the position of the character to check + * @return true if the character at the specified position in the given buffer is an escape + * character and the character immediately preceding it is not an escape character. + */ + private boolean isEscapeChar(final CharSequence buffer, final int pos) { + if (pos < 0) { + return false; + } + char ch = buffer.charAt(pos); + return isEscapeChar(ch) && !isEscaped(buffer, pos); + } + + /** + * Check if a character is escaped (i.e. if the previous character is an escape) + * + * @param buffer the buffer to check in + * @param pos the position of the character to check + * @return true if the character at the specified position in the given buffer is an escape + * character and the character immediately preceding it is an escape character. + */ + private boolean isEscaped(final CharSequence buffer, final int pos) { + if (pos <= 0) { + return false; + } + return isEscapeChar(buffer, pos - 1); + } + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/TTop.java b/net-cli/src/main/java/org/jline/builtins/TTop.java new file mode 100644 index 0000000..c695d43 --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/TTop.java @@ -0,0 +1,644 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.io.IOException; +import java.io.PrintStream; +import java.lang.management.ClassLoadingMXBean; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.OperatingSystemMXBean; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.jline.builtins.Options.HelpException; +import org.jline.keymap.BindingReader; +import org.jline.keymap.KeyMap; +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.Display; +import org.jline.utils.InfoCmp; +import org.jline.utils.Log; +import org.jline.utils.NonBlockingReader; +import static org.jline.builtins.TTop.Align.Left; +import static org.jline.builtins.TTop.Align.Right; + +/** + * Thread Top implementation. + *

+ * TODO: option modification at runtime (such as implemented in less) is not currently supported + * TODO: one possible addition would be to detect deadlock threads and display them in a specific way + */ +public class TTop { + + public static final String STAT_UPTIME = "uptime"; + + public static final String STAT_TID = "tid"; + public static final String STAT_NAME = "name"; + public static final String STAT_STATE = "state"; + public static final String STAT_BLOCKED_TIME = "blocked_time"; + public static final String STAT_BLOCKED_COUNT = "blocked_count"; + public static final String STAT_WAITED_TIME = "waited_time"; + public static final String STAT_WAITED_COUNT = "waited_count"; + public static final String STAT_LOCK_NAME = "lock_name"; + public static final String STAT_LOCK_OWNER_ID = "lock_owner_id"; + public static final String STAT_LOCK_OWNER_NAME = "lock_owner_name"; + public static final String STAT_USER_TIME = "user_time"; + public static final String STAT_USER_TIME_PERC = "user_time_perc"; + public static final String STAT_CPU_TIME = "cpu_time"; + public static final String STAT_CPU_TIME_PERC = "cpu_time_perc"; + private final Map columns = new LinkedHashMap<>(); + private final Terminal terminal; + private final Display display; + private final BindingReader bindingReader; + private final KeyMap keys; + + private final Size size = new Size(); + public List sort; + public long delay; + public List stats; + public int nthreads; + private Comparator>> comparator; + // Internal cache data + private final Map> previous = new HashMap<>(); + private final Map> changes = new HashMap<>(); + private final Map widths = new HashMap<>(); + + public TTop(Terminal terminal) { + this.terminal = terminal; + this.display = new Display(terminal, true); + this.bindingReader = new BindingReader(terminal.reader()); + + DecimalFormatSymbols dfs = new DecimalFormatSymbols(); + dfs.setDecimalSeparator('.'); + DecimalFormat perc = new DecimalFormat("0.00%", dfs); + + register(STAT_TID, Right, "TID", o -> String.format("%3d", (Long) o)); + register(STAT_NAME, Left, "NAME", padcut(40)); + register(STAT_STATE, Left, "STATE", o -> o.toString().toLowerCase()); + register(STAT_BLOCKED_TIME, Right, "T-BLOCKED", o -> millis((Long) o)); + register(STAT_BLOCKED_COUNT, Right, "#-BLOCKED", Object::toString); + register(STAT_WAITED_TIME, Right, "T-WAITED", o -> millis((Long) o)); + register(STAT_WAITED_COUNT, Right, "#-WAITED", Object::toString); + register(STAT_LOCK_NAME, Left, "LOCK-NAME", Object::toString); + register(STAT_LOCK_OWNER_ID, Right, "LOCK-OWNER-ID", id -> ((Long) id) >= 0 ? id.toString() : ""); + register(STAT_LOCK_OWNER_NAME, Left, "LOCK-OWNER-NAME", name -> name != null ? name.toString() : ""); + register(STAT_USER_TIME, Right, "T-USR", o -> nanos((Long) o)); + register(STAT_CPU_TIME, Right, "T-CPU", o -> nanos((Long) o)); + register(STAT_USER_TIME_PERC, Right, "%-USR", perc::format); + register(STAT_CPU_TIME_PERC, Right, "%-CPU", perc::format); + + keys = new KeyMap<>(); + bindKeys(keys); + } + + public static void ttop(Terminal terminal, PrintStream out, PrintStream err, String[] argv) throws Exception { + final String[] usage = { + "ttop - display and update sorted information about threads", + "Usage: ttop [OPTIONS]", + " -? --help Show help", + " -o --order=ORDER Comma separated list of sorting keys", + " -t --stats=STATS Comma separated list of stats to display", + " -s --seconds=SECONDS Delay between updates in seconds", + " -m --millis=MILLIS Delay between updates in milliseconds", + " -n --nthreads=NTHREADS Only display up to NTHREADS threads", + }; + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + TTop ttop = new TTop(terminal); + ttop.sort = opt.isSet("order") ? Arrays.asList(opt.get("order").split(",")) : null; + ttop.delay = opt.isSet("seconds") ? opt.getNumber("seconds") * 1000L : ttop.delay; + ttop.delay = opt.isSet("millis") ? opt.getNumber("millis") : ttop.delay; + ttop.stats = opt.isSet("stats") ? Arrays.asList(opt.get("stats").split(",")) : null; + ttop.nthreads = opt.isSet("nthreads") ? opt.getNumber("nthreads") : ttop.nthreads; + ttop.run(); + } + + private static String nanos(long nanos) { + return millis(nanos / 1_000_000L); + } + + private static String millis(long millis) { + long secs = millis / 1_000; + millis = millis % 1000; + long mins = secs / 60; + secs = secs % 60; + long hours = mins / 60; + mins = mins % 60; + if (hours > 0) { + return String.format("%d:%02d:%02d.%03d", hours, mins, secs, millis); + } else if (mins > 0) { + return String.format("%d:%02d.%03d", mins, secs, millis); + } else { + return String.format("%d.%03d", secs, millis); + } + } + + private static Function padcut(int nb) { + return o -> padcut(o.toString(), nb); + } + + private static String padcut(String str, int nb) { + if (str.length() <= nb) { + StringBuilder sb = new StringBuilder(nb); + sb.append(str); + while (sb.length() < nb) { + sb.append(' '); + } + return sb.toString(); + } else { + return str.substring(0, nb - 3) + "..."; + } + } + + private static String memory(long cur, long max) { + if (max > 0) { + String smax = humanReadableByteCount(max, false); + String cmax = humanReadableByteCount(cur, false); + StringBuilder sb = new StringBuilder(smax.length() * 2 + 3); + for (int i = cmax.length(); i < smax.length(); i++) { + sb.append(' '); + } + sb.append(cmax).append(" / ").append(smax); + return sb.toString(); + } else { + return humanReadableByteCount(cur, false); + } + } + + private static String humanReadableByteCount(long bytes, boolean si) { + int unit = si ? 1000 : 1024; + if (bytes < 1024) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(1024)); + String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); + return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + public KeyMap getKeys() { + return keys; + } + + public void run() throws IOException, InterruptedException { + comparator = buildComparator(sort); + delay = delay > 0 ? Math.max(delay, 100) : 1000; + if (stats == null || stats.isEmpty()) { + stats = new ArrayList<>(Arrays.asList(STAT_TID, STAT_NAME, STAT_STATE, STAT_CPU_TIME, STAT_LOCK_OWNER_ID)); + } + + Boolean isThreadContentionMonitoringEnabled = null; + ThreadMXBean threadsBean = ManagementFactory.getThreadMXBean(); + if (stats.contains(STAT_BLOCKED_TIME) + || stats.contains(STAT_BLOCKED_COUNT) + || stats.contains(STAT_WAITED_TIME) + || stats.contains(STAT_WAITED_COUNT)) { + if (threadsBean.isThreadContentionMonitoringSupported()) { + isThreadContentionMonitoringEnabled = threadsBean.isThreadContentionMonitoringEnabled(); + if (!isThreadContentionMonitoringEnabled) { + threadsBean.setThreadContentionMonitoringEnabled(true); + } + } else { + stats.removeAll( + Arrays.asList(STAT_BLOCKED_TIME, STAT_BLOCKED_COUNT, STAT_WAITED_TIME, STAT_WAITED_COUNT)); + } + } + Boolean isThreadCpuTimeEnabled = null; + if (stats.contains(STAT_USER_TIME) || stats.contains(STAT_CPU_TIME)) { + if (threadsBean.isThreadCpuTimeSupported()) { + isThreadCpuTimeEnabled = threadsBean.isThreadCpuTimeEnabled(); + if (!isThreadCpuTimeEnabled) { + threadsBean.setThreadCpuTimeEnabled(true); + } + } else { + stats.removeAll(Arrays.asList(STAT_USER_TIME, STAT_CPU_TIME)); + } + } + + size.copy(terminal.getSize()); + Terminal.SignalHandler prevHandler = terminal.handle(Terminal.Signal.WINCH, this::handle); + Attributes attr = terminal.enterRawMode(); + try { + + // Use alternate buffer + if (!terminal.puts(InfoCmp.Capability.enter_ca_mode)) { + terminal.puts(InfoCmp.Capability.clear_screen); + } + terminal.puts(InfoCmp.Capability.keypad_xmit); + terminal.puts(InfoCmp.Capability.cursor_invisible); + terminal.writer().flush(); + + long t0 = System.currentTimeMillis(); + + Operation op; + do { + display(); + checkInterrupted(); + + op = null; + + long delta = ((System.currentTimeMillis() - t0) / delay + 1) * delay + t0 - System.currentTimeMillis(); + int ch = bindingReader.peekCharacter(delta); + if (ch == -1) { + op = Operation.EXIT; + } else if (ch != NonBlockingReader.READ_EXPIRED) { + op = bindingReader.readBinding(keys, null, false); + } + if (op == null) { + continue; + } + + switch (op) { + case INCREASE_DELAY: + delay = delay * 2; + t0 = System.currentTimeMillis(); + break; + case DECREASE_DELAY: + delay = Math.max(delay / 2, 16); + t0 = System.currentTimeMillis(); + break; + case CLEAR: + display.clear(); + break; + case REVERSE: + comparator = comparator.reversed(); + break; + } + } while (op != Operation.EXIT); + } catch (InterruptedException ie) { + // Do nothing + } catch (Error err) { + Log.info("Error: ", err); + } finally { + terminal.setAttributes(attr); + if (prevHandler != null) { + terminal.handle(Terminal.Signal.WINCH, prevHandler); + } + // Use main buffer + if (!terminal.puts(InfoCmp.Capability.exit_ca_mode)) { + terminal.puts(InfoCmp.Capability.clear_screen); + } + terminal.puts(InfoCmp.Capability.keypad_local); + terminal.puts(InfoCmp.Capability.cursor_visible); + terminal.writer().flush(); + + if (isThreadContentionMonitoringEnabled != null) { + threadsBean.setThreadContentionMonitoringEnabled(isThreadContentionMonitoringEnabled); + } + if (isThreadCpuTimeEnabled != null) { + threadsBean.setThreadCpuTimeEnabled(isThreadCpuTimeEnabled); + } + } + } + + private void handle(Terminal.Signal signal) { + int prevw = size.getColumns(); + size.copy(terminal.getSize()); + try { + if (size.getColumns() < prevw) { + display.clear(); + } + display(); + } catch (IOException e) { + // ignore + } + } + + private List>> infos() { + long ctime = ManagementFactory.getRuntimeMXBean().getUptime(); + Long ptime = (Long) previous.computeIfAbsent(-1L, id -> new HashMap<>()).put(STAT_UPTIME, ctime); + long delta = ptime != null ? ctime - ptime : 0L; + + ThreadMXBean threadsBean = ManagementFactory.getThreadMXBean(); + ThreadInfo[] infos = threadsBean.dumpAllThreads(false, false); + List>> threads = new ArrayList<>(); + for (ThreadInfo ti : infos) { + Map> t = new HashMap<>(); + t.put(STAT_TID, ti.getThreadId()); + t.put(STAT_NAME, ti.getThreadName()); + t.put(STAT_STATE, ti.getThreadState()); + if (threadsBean.isThreadContentionMonitoringEnabled()) { + t.put(STAT_BLOCKED_TIME, ti.getBlockedTime()); + t.put(STAT_BLOCKED_COUNT, ti.getBlockedCount()); + t.put(STAT_WAITED_TIME, ti.getWaitedTime()); + t.put(STAT_WAITED_COUNT, ti.getWaitedCount()); + } + t.put(STAT_LOCK_NAME, ti.getLockName()); + t.put(STAT_LOCK_OWNER_ID, ti.getLockOwnerId()); + t.put(STAT_LOCK_OWNER_NAME, ti.getLockOwnerName()); + if (threadsBean.isThreadCpuTimeSupported() && threadsBean.isThreadCpuTimeEnabled()) { + long tid = ti.getThreadId(), t0, t1; + // Cpu + t1 = threadsBean.getThreadCpuTime(tid); + t0 = (Long) previous.computeIfAbsent(tid, id -> new HashMap<>()).getOrDefault(STAT_CPU_TIME, t1); + t.put(STAT_CPU_TIME, t1); + t.put(STAT_CPU_TIME_PERC, (delta != 0) ? (t1 - t0) / ((double) delta * 1000000) : 0.0d); + // User + t1 = threadsBean.getThreadUserTime(tid); + t0 = (Long) previous.computeIfAbsent(tid, id -> new HashMap<>()).getOrDefault(STAT_USER_TIME, t1); + t.put(STAT_USER_TIME, t1); + t.put(STAT_USER_TIME_PERC, (delta != 0) ? (t1 - t0) / ((double) delta * 1000000) : 0.0d); + } + threads.add(t); + } + return threads; + } + + private void align(AttributedStringBuilder sb, String val, int width, Align align) { + if (align == Align.Left) { + sb.append(val); + for (int i = 0; i < width - val.length(); i++) { + sb.append(' '); + } + } else { + for (int i = 0; i < width - val.length(); i++) { + sb.append(' '); + } + sb.append(val); + } + } + + private synchronized void display() throws IOException { + long now = System.currentTimeMillis(); + + display.resize(size.getRows(), size.getColumns()); + + List lines = new ArrayList<>(); + AttributedStringBuilder sb = new AttributedStringBuilder(size.getColumns()); + + // Top headers + sb.style(sb.style().bold()); + sb.append("ttop"); + sb.style(sb.style().boldOff()); + sb.append(" - "); + sb.append(String.format("%8tT", new Date())); + sb.append("."); + + OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); + String osinfo = "OS: " + os.getName() + " " + os.getVersion() + ", " + os.getArch() + ", " + + os.getAvailableProcessors() + " cpus."; + if (sb.length() + 1 + osinfo.length() < size.getColumns()) { + sb.append(" "); + } else { + lines.add(sb.toAttributedString()); + sb.setLength(0); + } + sb.append(osinfo); + + ClassLoadingMXBean cl = ManagementFactory.getClassLoadingMXBean(); + String clsinfo = "Classes: " + cl.getLoadedClassCount() + " loaded, " + cl.getUnloadedClassCount() + + " unloaded, " + cl.getTotalLoadedClassCount() + " loaded total."; + if (sb.length() + 1 + clsinfo.length() < size.getColumns()) { + sb.append(" "); + } else { + lines.add(sb.toAttributedString()); + sb.setLength(0); + } + sb.append(clsinfo); + + ThreadMXBean th = ManagementFactory.getThreadMXBean(); + String thinfo = "Threads: " + th.getThreadCount() + ", peak: " + th.getPeakThreadCount() + ", started: " + + th.getTotalStartedThreadCount() + "."; + if (sb.length() + 1 + thinfo.length() < size.getColumns()) { + sb.append(" "); + } else { + lines.add(sb.toAttributedString()); + sb.setLength(0); + } + sb.append(thinfo); + + MemoryMXBean me = ManagementFactory.getMemoryMXBean(); + String meinfo = "Memory: " + "heap: " + + memory( + me.getHeapMemoryUsage().getUsed(), + me.getHeapMemoryUsage().getMax()) + ", non heap: " + + memory( + me.getNonHeapMemoryUsage().getUsed(), + me.getNonHeapMemoryUsage().getMax()) + "."; + if (sb.length() + 1 + meinfo.length() < size.getColumns()) { + sb.append(" "); + } else { + lines.add(sb.toAttributedString()); + sb.setLength(0); + } + sb.append(meinfo); + + StringBuilder sbc = new StringBuilder(); + sbc.append("GC: "); + boolean first = true; + for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { + if (first) { + first = false; + } else { + sbc.append(", "); + } + long count = gc.getCollectionCount(); + long time = gc.getCollectionTime(); + sbc.append(gc.getName()) + .append(": ") + .append(count) + .append(" col. / ") + .append(String.format("%d", time / 1000)) + .append(".") + .append(String.format("%03d", time % 1000)) + .append(" s"); + } + sbc.append("."); + if (sb.length() + 1 + sbc.length() < size.getColumns()) { + sb.append(" "); + } else { + lines.add(sb.toAttributedString()); + sb.setLength(0); + } + sb.append(sbc); + lines.add(sb.toAttributedString()); + sb.setLength(0); + + lines.add(sb.toAttributedString()); + + // Threads + List>> threads = infos(); + Collections.sort(threads, comparator); + int nb = Math.min(size.getRows() - lines.size() - 2, nthreads > 0 ? nthreads : threads.size()); + // Compute values + List> values = threads.subList(0, nb).stream() + .map(thread -> stats.stream().collect(Collectors.toMap(Function.identity(), key -> columns.get(key) + .format + .apply(thread.get(key))))) + .collect(Collectors.toList()); + for (String key : stats) { + int width = + values.stream().mapToInt(map -> map.get(key).length()).max().orElse(0); + widths.put(key, Math.max(columns.get(key).header.length(), Math.max(width, widths.getOrDefault(key, 0)))); + } + List cstats; + if (widths.values().stream().mapToInt(Integer::intValue).sum() + stats.size() - 1 < size.getColumns()) { + cstats = stats; + } else { + cstats = new ArrayList<>(); + int sz = 0; + for (String stat : stats) { + int nsz = sz; + if (nsz > 0) { + nsz++; + } + nsz += widths.get(stat); + if (nsz < size.getColumns()) { + sz = nsz; + cstats.add(stat); + } else { + break; + } + } + } + // Headers + for (String key : cstats) { + if (sb.length() > 0) { + sb.append(" "); + } + Column col = columns.get(key); + align(sb, col.header, widths.get(key), col.align); + } + lines.add(sb.toAttributedString()); + sb.setLength(0); + // Threads + for (int i = 0; i < nb; i++) { + Map> thread = threads.get(i); + long tid = (Long) thread.get(STAT_TID); + for (String key : cstats) { + if (sb.length() > 0) { + sb.append(" "); + } + long last; + Object cur = thread.get(key); + Object prv = + previous.computeIfAbsent(tid, id -> new HashMap<>()).put(key, cur); + if (prv != null && !prv.equals(cur)) { + changes.computeIfAbsent(tid, id -> new HashMap<>()).put(key, now); + last = now; + } else { + last = changes.computeIfAbsent(tid, id -> new HashMap<>()).getOrDefault(key, 0L); + } + long fade = delay * 24; + if (now - last < fade) { + int r = (int) ((now - last) / (fade / 24)); + sb.style(sb.style().foreground(255 - r).background(9)); + } + align(sb, values.get(i).get(key), widths.get(key), columns.get(key).align); + sb.style(sb.style().backgroundOff().foregroundOff()); + } + lines.add(sb.toAttributedString()); + sb.setLength(0); + } + + display.update(lines, 0); + } + + private Comparator>> buildComparator(List sort) { + if (sort == null || sort.isEmpty()) { + sort = Collections.singletonList(STAT_TID); + } + Comparator>> comparator = null; + for (String key : sort) { + String fkey; + boolean asc; + if (key.startsWith("+")) { + fkey = key.substring(1); + asc = true; + } else if (key.startsWith("-")) { + fkey = key.substring(1); + asc = false; + } else { + fkey = key; + asc = true; + } + if (!columns.containsKey(fkey)) { + throw new IllegalArgumentException("Unsupported sort key: " + fkey); + } + @SuppressWarnings("unchecked") + Comparator>> comp = Comparator.comparing(m -> (Comparable) m.get(fkey)); + if (asc) { + comp = comp.reversed(); + } + if (comparator != null) { + comparator = comparator.thenComparing(comp); + } else { + comparator = comp; + } + } + return comparator; + } + + private void register(String name, Align align, String header, Function format) { + columns.put(name, new Column(name, align, header, format)); + } + + /** + * This is for long running commands to be interrupted by ctrl-c + */ + private void checkInterrupted() throws InterruptedException { + Thread.yield(); + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + } + + private void bindKeys(KeyMap map) { + map.bind(Operation.HELP, "h", "?"); + map.bind(Operation.EXIT, "q", ":q", "Q", ":Q", "ZZ"); + map.bind(Operation.INCREASE_DELAY, "+"); + map.bind(Operation.DECREASE_DELAY, "-"); + map.bind(Operation.CLEAR, KeyMap.ctrl('L')); + map.bind(Operation.REVERSE, "r"); + } + + public enum Align { + Left, + Right + } + + public enum Operation { + INCREASE_DELAY, + DECREASE_DELAY, + HELP, + EXIT, + CLEAR, + REVERSE + } + + private static class Column { + final String name; + final Align align; + final String header; + final Function format; + + Column(String name, Align align, String header, Function format) { + this.name = name; + this.align = align; + this.header = header; + this.format = format; + } + } +} diff --git a/net-cli/src/main/java/org/jline/builtins/Tmux.java b/net-cli/src/main/java/org/jline/builtins/Tmux.java new file mode 100644 index 0000000..94b6af0 --- /dev/null +++ b/net-cli/src/main/java/org/jline/builtins/Tmux.java @@ -0,0 +1,2095 @@ +/* + * Copyright (c) 2002-2019, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.text.DateFormat; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; +import org.jline.builtins.Options.HelpException; +import org.jline.keymap.BindingReader; +import org.jline.keymap.KeyMap; +import org.jline.reader.ParsedLine; +import org.jline.reader.impl.DefaultParser; +import org.jline.terminal.Attributes; +import org.jline.terminal.MouseEvent; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.Terminal.Signal; +import org.jline.terminal.Terminal.SignalHandler; +import org.jline.terminal.impl.LineDisciplineTerminal; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.Colors; +import org.jline.utils.Display; +import org.jline.utils.InfoCmp.Capability; +import org.jline.utils.Log; +import static org.jline.builtins.Tmux.Layout.Type.LeftRight; +import static org.jline.builtins.Tmux.Layout.Type.TopBottom; +import static org.jline.builtins.Tmux.Layout.Type.WindowPane; +import static org.jline.keymap.KeyMap.alt; +import static org.jline.keymap.KeyMap.display; +import static org.jline.keymap.KeyMap.esc; +import static org.jline.keymap.KeyMap.key; +import static org.jline.keymap.KeyMap.translate; + +/** + * Terminal multiplexer + */ +public class Tmux { + + public static final String OPT_PREFIX = "prefix"; + + public static final String CMD_COMMANDS = "commands"; + public static final String CMD_SEND_PREFIX = "send-prefix"; + public static final String CMD_SPLIT_WINDOW = "split-window"; + public static final String CMD_SPLITW = "splitw"; + public static final String CMD_SELECT_PANE = "select-pane"; + public static final String CMD_SELECTP = "selectp"; + public static final String CMD_RESIZE_PANE = "resize-pane"; + public static final String CMD_RESIZEP = "resizep"; + public static final String CMD_DISPLAY_PANES = "display-panes"; + public static final String CMD_DISPLAYP = "displayp"; + public static final String CMD_CLOCK_MODE = "clock-mode"; + public static final String CMD_SET_OPTION = "set-option"; + public static final String CMD_SET = "set"; + public static final String CMD_LIST_KEYS = "list-keys"; + public static final String CMD_LSK = "lsk"; + public static final String CMD_SEND_KEYS = "send-keys"; + public static final String CMD_SEND = "send"; + public static final String CMD_BIND_KEY = "bind-key"; + public static final String CMD_BIND = "bind"; + public static final String CMD_UNBIND_KEY = "unbind-key"; + public static final String CMD_UNBIND = "unbind"; + public static final String CMD_NEW_WINDOW = "new-window"; + public static final String CMD_NEWW = "neww"; + public static final String CMD_NEXT_WINDOW = "next-window"; + public static final String CMD_NEXT = "next"; + public static final String CMD_PREVIOUS_WINDOW = "previous-window"; + public static final String CMD_PREV = "prev"; + public static final String CMD_LIST_WINDOWS = "list-windows"; + public static final String CMD_LSW = "lsw"; + + private static final int[][][] WINDOW_CLOCK_TABLE = { + { + {1, 1, 1, 1, 1}, /* 0 */ + {1, 0, 0, 0, 1}, + {1, 0, 0, 0, 1}, + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1} + }, + { + {0, 0, 0, 0, 1}, /* 1 */ + {0, 0, 0, 0, 1}, + {0, 0, 0, 0, 1}, + {0, 0, 0, 0, 1}, + {0, 0, 0, 0, 1} + }, + { + {1, 1, 1, 1, 1}, /* 2 */ + {0, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 1} + }, + { + {1, 1, 1, 1, 1}, /* 3 */ + {0, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + {0, 0, 0, 0, 1}, + {1, 1, 1, 1, 1} + }, + { + {1, 0, 0, 0, 1}, /* 4 */ + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + {0, 0, 0, 0, 1}, + {0, 0, 0, 0, 1} + }, + { + {1, 1, 1, 1, 1}, /* 5 */ + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 1}, + {0, 0, 0, 0, 1}, + {1, 1, 1, 1, 1} + }, + { + {1, 1, 1, 1, 1}, /* 6 */ + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 1}, + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1} + }, + { + {1, 1, 1, 1, 1}, /* 7 */ + {0, 0, 0, 0, 1}, + {0, 0, 0, 0, 1}, + {0, 0, 0, 0, 1}, + {0, 0, 0, 0, 1} + }, + { + {1, 1, 1, 1, 1}, /* 8 */ + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1} + }, + { + {1, 1, 1, 1, 1}, /* 9 */ + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + {0, 0, 0, 0, 1}, + {1, 1, 1, 1, 1} + }, + { + {0, 0, 0, 0, 0}, /* : */ + {0, 0, 1, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 0, 0, 0} + }, + { + {1, 1, 1, 1, 1}, /* A */ + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + {1, 0, 0, 0, 1}, + {1, 0, 0, 0, 1} + }, + { + {1, 1, 1, 1, 1}, /* P */ + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0} + }, + { + {1, 0, 0, 0, 1}, /* M */ + {1, 1, 0, 1, 1}, + {1, 0, 1, 0, 1}, + {1, 0, 0, 0, 1}, + {1, 0, 0, 0, 1} + }, + }; + + private final AtomicBoolean dirty = new AtomicBoolean(true); + private final AtomicBoolean resized = new AtomicBoolean(true); + private final Terminal terminal; + private final Display display; + private final PrintStream err; + private final String term; + private final Consumer runner; + private final AtomicBoolean running = new AtomicBoolean(true); + private final Size size = new Size(); + private final Map serverOptions = new HashMap<>(); + int ACTIVE_COLOR = 0xF44; + int INACTIVE_COLOR = 0x44F; + int CLOCK_COLOR = 0x44F; + private final List windows = new ArrayList<>(); + private Integer windowsId = 0; + private int activeWindow = 0; + private boolean identify; + private ScheduledExecutorService executor; + private ScheduledFuture clockFuture; + private KeyMap keyMap; + + @SuppressWarnings("this-escape") + public Tmux(Terminal terminal, PrintStream err, Consumer runner) throws IOException { + this.terminal = terminal; + this.err = err; + this.runner = runner; + display = new Display(terminal, true); + // Find terminal to use + Integer colors = terminal.getNumericCapability(Capability.max_colors); + term = (colors != null && colors >= 256) ? "screen-256color" : "screen"; + // Setup defaults bindings + serverOptions.put(OPT_PREFIX, "`"); + keyMap = createKeyMap(serverOptions.get(OPT_PREFIX)); + } + + private static int findMatch(String layout, char c0, char c1) { + if (layout.charAt(0) != c0) { + throw new IllegalArgumentException(); + } + int nb = 0; + int i = 0; + while (i < layout.length()) { + char c = layout.charAt(i); + if (c == c0) { + nb++; + } else if (c == c1) { + if (--nb == 0) { + return i; + } + } + i++; + } + if (nb > 0) { + throw new IllegalArgumentException("No matching '" + c1 + "'"); + } + return i; + } + + protected KeyMap createKeyMap(String prefix) { + KeyMap keyMap = createEmptyKeyMap(prefix); + keyMap.bind(CMD_SEND_PREFIX, prefix + prefix); + keyMap.bind(CMD_SPLIT_WINDOW + " -v", prefix + "\""); + keyMap.bind(CMD_SPLIT_WINDOW + " -h", prefix + "%"); + keyMap.bind(CMD_SELECT_PANE + " -U", prefix + key(terminal, Capability.key_up)); + keyMap.bind(CMD_SELECT_PANE + " -D", prefix + key(terminal, Capability.key_down)); + keyMap.bind(CMD_SELECT_PANE + " -L", prefix + key(terminal, Capability.key_left)); + keyMap.bind(CMD_SELECT_PANE + " -R", prefix + key(terminal, Capability.key_right)); + keyMap.bind(CMD_RESIZE_PANE + " -U 5", prefix + esc() + key(terminal, Capability.key_up)); + keyMap.bind(CMD_RESIZE_PANE + " -D 5", prefix + esc() + key(terminal, Capability.key_down)); + keyMap.bind(CMD_RESIZE_PANE + " -L 5", prefix + esc() + key(terminal, Capability.key_left)); + keyMap.bind(CMD_RESIZE_PANE + " -R 5", prefix + esc() + key(terminal, Capability.key_right)); + keyMap.bind(CMD_RESIZE_PANE + " -U", prefix + translate("^[[1;5A"), prefix + alt(translate("^[[A"))); // ctrl-up + keyMap.bind( + CMD_RESIZE_PANE + " -D", prefix + translate("^[[1;5B"), prefix + alt(translate("^[[B"))); // ctrl-down + keyMap.bind( + CMD_RESIZE_PANE + " -L", prefix + translate("^[[1;5C"), prefix + alt(translate("^[[C"))); // ctrl-left + keyMap.bind( + CMD_RESIZE_PANE + " -R", prefix + translate("^[[1;5D"), prefix + alt(translate("^[[D"))); // ctrl-right + keyMap.bind(CMD_DISPLAY_PANES, prefix + "q"); + keyMap.bind(CMD_CLOCK_MODE, prefix + "t"); + keyMap.bind(CMD_NEW_WINDOW, prefix + "c"); + keyMap.bind(CMD_NEXT_WINDOW, prefix + "n"); + keyMap.bind(CMD_PREVIOUS_WINDOW, prefix + "p"); + return keyMap; + } + + protected KeyMap createEmptyKeyMap(String prefix) { + KeyMap keyMap = new KeyMap<>(); + keyMap.setUnicode(Binding.SelfInsert); + keyMap.setNomatch(Binding.SelfInsert); + for (int i = 0; i < 255; i++) { + keyMap.bind(Binding.Discard, prefix + (char) (i)); + } + keyMap.bind(Binding.Mouse, key(terminal, Capability.key_mouse)); + return keyMap; + } + + public void run() throws IOException { + SignalHandler prevWinchHandler = terminal.handle(Signal.WINCH, this::resize); + SignalHandler prevIntHandler = terminal.handle(Signal.INT, this::interrupt); + SignalHandler prevSuspHandler = terminal.handle(Signal.TSTP, this::suspend); + Attributes attributes = terminal.enterRawMode(); + terminal.puts(Capability.enter_ca_mode); + terminal.puts(Capability.keypad_xmit); + terminal.trackMouse(Terminal.MouseTracking.Any); + terminal.flush(); + executor = Executors.newSingleThreadScheduledExecutor(); + try { + // Create first pane + size.copy(terminal.getSize()); + windows.add(new Window(this)); + activeWindow = 0; + runner.accept(active().getConsole()); + // Start input loop + new Thread(this::inputLoop, "Mux input loop").start(); + // Redraw loop + redrawLoop(); + } catch (RuntimeException e) { + throw e; + } finally { + executor.shutdown(); + terminal.trackMouse(Terminal.MouseTracking.Off); + terminal.puts(Capability.keypad_local); + terminal.puts(Capability.exit_ca_mode); + terminal.flush(); + terminal.setAttributes(attributes); + terminal.handle(Signal.WINCH, prevWinchHandler); + terminal.handle(Signal.INT, prevIntHandler); + terminal.handle(Signal.TSTP, prevSuspHandler); + } + } + + private VirtualConsole active() { + return windows.get(activeWindow).getActive(); + } + + private List panes() { + return windows.get(activeWindow).getPanes(); + } + + private Window window() { + return windows.get(activeWindow); + } + + private void redrawLoop() { + while (running.get()) { + try { + synchronized (dirty) { + while (running.get() && !dirty.compareAndSet(true, false)) { + dirty.wait(); + } + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + handleResize(); + redraw(); + } + } + + private void setDirty() { + synchronized (dirty) { + dirty.set(true); + dirty.notifyAll(); + } + } + + private void inputLoop() { + try { + BindingReader reader = new BindingReader(terminal.reader()); + boolean first = true; + while (running.get()) { + Object b; + if (first) { + b = reader.readBinding(keyMap); + } else if (reader.peekCharacter(100) >= 0) { + b = reader.readBinding(keyMap, null, false); + } else { + b = null; + } + if (b == Binding.SelfInsert) { + if (active().clock) { + active().clock = false; + if (clockFuture != null && panes().stream().noneMatch(vc -> vc.clock)) { + clockFuture.cancel(false); + clockFuture = null; + } + setDirty(); + } else { + active().getMasterInputOutput() + .write(reader.getLastBinding().getBytes()); + first = false; + } + } else { + if (first) { + first = false; + } else { + active().getMasterInputOutput().flush(); + first = true; + } + if (b == Binding.Mouse) { + MouseEvent event = terminal.readMouseEvent(); + // System.err.println(event.toString()); + } else if (b instanceof String || b instanceof String[]) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + try (PrintStream pout = new PrintStream(out); + PrintStream perr = new PrintStream(err)) { + if (b instanceof String) { + execute(pout, perr, (String) b); + } else { + execute(pout, perr, Arrays.asList((String[]) b)); + } + } catch (Exception e) { + // TODO: log + } + } + } + } + } catch (IOException e) { + if (running.get()) { + Log.info("Error in tmux input loop", e); + } + } finally { + running.set(false); + setDirty(); + } + } + + private synchronized void close(VirtualConsole terminal) { + int idx = -1; + Window window = null; + for (Window w : windows) { + idx = w.getPanes().indexOf(terminal); + if (idx >= 0) { + window = w; + break; + } + } + if (idx >= 0) { + window.remove(terminal); + if (window.getPanes().isEmpty()) { + if (windows.size() > 1) { + windows.remove(window); + if (activeWindow >= windows.size()) { + activeWindow--; + } + resize(Signal.WINCH); + } else { + running.set(false); + setDirty(); + } + } else { + resize(Signal.WINCH); + } + } + } + + private void resize(Signal signal) { + resized.set(true); + setDirty(); + } + + private void interrupt(Signal signal) { + active().getConsole().raise(signal); + } + + private void suspend(Signal signal) { + active().getConsole().raise(signal); + } + + private void handleResize() { + // Re-compute the layout + if (resized.compareAndSet(true, false)) { + size.copy(terminal.getSize()); + } + window().handleResize(); + } + + public void execute(PrintStream out, PrintStream err, String command) throws Exception { + ParsedLine line = new DefaultParser().parse(command.trim(), 0); + execute(out, err, line.words()); + } + + public synchronized void execute(PrintStream out, PrintStream err, List command) throws Exception { + String name = command.get(0); + List args = command.subList(1, command.size()); + switch (name) { + case CMD_SEND_PREFIX: + sendPrefix(out, err, args); + break; + case CMD_SPLIT_WINDOW: + case CMD_SPLITW: + splitWindow(out, err, args); + break; + case CMD_SELECT_PANE: + case CMD_SELECTP: + selectPane(out, err, args); + break; + case CMD_RESIZE_PANE: + case CMD_RESIZEP: + resizePane(out, err, args); + break; + case CMD_DISPLAY_PANES: + case CMD_DISPLAYP: + displayPanes(out, err, args); + break; + case CMD_CLOCK_MODE: + clockMode(out, err, args); + break; + case CMD_BIND_KEY: + case CMD_BIND: + bindKey(out, err, args); + break; + case CMD_UNBIND_KEY: + case CMD_UNBIND: + unbindKey(out, err, args); + break; + case CMD_LIST_KEYS: + case CMD_LSK: + listKeys(out, err, args); + break; + case CMD_SEND_KEYS: + case CMD_SEND: + sendKeys(out, err, args); + break; + case CMD_SET_OPTION: + case CMD_SET: + setOption(out, err, args); + break; + case CMD_NEW_WINDOW: + case CMD_NEWW: + newWindow(out, err, args); + break; + case CMD_NEXT_WINDOW: + case CMD_NEXT: + nextWindow(out, err, args); + break; + case CMD_PREVIOUS_WINDOW: + case CMD_PREV: + previousWindow(out, err, args); + break; + case CMD_LIST_WINDOWS: + case CMD_LSW: + listWindows(out, err, args); + break; + } + } + + protected void listWindows(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = {"list-windows - ", "Usage: list-windows", " -? --help Show help"}; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + IntStream.range(0, windows.size()) + .mapToObj(i -> { + StringBuilder sb = new StringBuilder(); + sb.append(i); + sb.append(": "); + sb.append(windows.get(i).getName()); + sb.append(i == activeWindow ? "* " : " "); + sb.append("("); + sb.append(windows.get(i).getPanes().size()); + sb.append(" panes)"); + if (i == activeWindow) { + sb.append(" (active)"); + } + return sb.toString(); + }) + .sorted() + .forEach(out::println); + } + + protected void previousWindow(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "previous-window - ", "Usage: previous-window", " -? --help Show help" + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + if (windows.size() > 1) { + activeWindow--; + if (activeWindow < 0) { + activeWindow = windows.size() - 1; + } + setDirty(); + } + } + + protected void nextWindow(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = {"next-window - ", "Usage: next-window", " -? --help Show help"}; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + if (windows.size() > 1) { + activeWindow++; + if (activeWindow >= windows.size()) { + activeWindow = 0; + } + setDirty(); + } + } + + protected void newWindow(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = {"new-window - ", "Usage: new-window", " -? --help Show help"}; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + windows.add(new Window(this)); + activeWindow = windows.size() - 1; + runner.accept(active().getConsole()); + setDirty(); + } + + protected void setOption(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "set-option - ", + "Usage: set-option [-agosquw] option [value]", + " -? --help Show help", + " -u --unset Unset the option" + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + int nbargs = opt.args().size(); + if (nbargs < 1 || nbargs > 2) { + throw new HelpException(opt.usage()); + } + String name = opt.args().get(0); + String value = nbargs > 1 ? opt.args().get(1) : null; + if (name.startsWith("@")) { + // set user option + } else { + // set server option + switch (name) { + case OPT_PREFIX: + if (value == null) { + throw new IllegalArgumentException("Missing argument"); + } + String prefix = translate(value); + String oldPrefix = serverOptions.put(OPT_PREFIX, prefix); + KeyMap newKeys = createEmptyKeyMap(prefix); + for (Map.Entry e : keyMap.getBoundKeys().entrySet()) { + if (e.getValue() instanceof String) { + if (e.getKey().equals(oldPrefix + oldPrefix)) { + newKeys.bind(e.getValue(), prefix + prefix); + } else if (e.getKey().startsWith(oldPrefix)) { + newKeys.bind(e.getValue(), prefix + e.getKey().substring(oldPrefix.length())); + } else { + newKeys.bind(e.getValue(), e.getKey()); + } + } + } + keyMap = newKeys; + break; + } + } + } + + protected void bindKey(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "bind-key - ", + "Usage: bind-key key command [arguments]", /* [-cnr] [-t mode-table] [-T key-table] */ + " -? --help Show help" + }; + Options opt = Options.compile(usage).setOptionsFirst(true).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + List vargs = opt.args(); + if (vargs.size() < 2) { + throw new HelpException(opt.usage()); + } + String prefix = serverOptions.get(OPT_PREFIX); + String key = prefix + KeyMap.translate(vargs.remove(0)); + keyMap.unbind(key.substring(0, 2)); + keyMap.bind(vargs.toArray(new String[vargs.size()]), key); + } + + protected void unbindKey(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "unbind-key - ", + "Usage: unbind-key key", /* [-an] [-t mode-table] [-T key-table] */ + " -? --help Show help" + }; + Options opt = Options.compile(usage).setOptionsFirst(true).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + List vargs = opt.args(); + if (vargs.size() != 1) { + throw new HelpException(opt.usage()); + } + String prefix = serverOptions.get(OPT_PREFIX); + String key = prefix + KeyMap.translate(vargs.remove(0)); + keyMap.unbind(key); + keyMap.bind(Binding.Discard, key); + } + + protected void listKeys(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "list-keys - ", + "Usage: list-keys ", /* [-t mode-table] [-T key-table] */ + " -? --help Show help", + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + String prefix = serverOptions.get(OPT_PREFIX); + keyMap.getBoundKeys().entrySet().stream() + .filter(e -> e.getValue() instanceof String) + .map(e -> { + String key = e.getKey(); + String val = (String) e.getValue(); + StringBuilder sb = new StringBuilder(); + sb.append("bind-key -T "); + if (key.startsWith(prefix)) { + sb.append("prefix "); + key = key.substring(prefix.length()); + } else { + sb.append("root "); + } + sb.append(display(key)); + while (sb.length() < 32) { + sb.append(" "); + } + sb.append(val); + return sb.toString(); + }) + .sorted() + .forEach(out::println); + } + + protected void sendKeys(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "send-keys - ", + "Usage: send-keys [-lXRM] [-N repeat-count] [-t target-pane] key...", + " -? --help Show help", + " -l --literal Send key literally", + " -N --number=repeat-count Specifies a repeat count" + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + for (int i = 0, n = opt.getNumber("number"); i < n; i++) { + for (String arg : opt.args()) { + String s = opt.isSet("literal") ? arg : KeyMap.translate(arg); + active().getMasterInputOutput().write(s.getBytes()); + } + } + } + + protected void clockMode(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = {"clock-mode - ", "Usage: clock-mode", " -? --help Show help"}; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + active().clock = true; + + if (clockFuture == null) { + long initial = Instant.now() + .until(Instant.now().truncatedTo(ChronoUnit.MINUTES).plusSeconds(60), ChronoUnit.MILLIS); + long delay = TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS); + clockFuture = executor.scheduleWithFixedDelay(this::setDirty, initial, delay, TimeUnit.MILLISECONDS); + } + setDirty(); + } + + protected void displayPanes(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = {"display-panes - ", "Usage: display-panes", " -? --help Show help"}; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + identify = true; + setDirty(); + executor.schedule( + () -> { + identify = false; + setDirty(); + }, + 1, + TimeUnit.SECONDS); + } + + protected void resizePane(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "resize-pane - ", + "Usage: resize-pane [-UDLR] [-x width] [-y height] [-t target-pane] [adjustment]", + " -? --help Show help", + " -U Resize pane upward", + " -D Select pane downward", + " -L Select pane to the left", + " -R Select pane to the right", + " -x --width=width Set the width of the pane", + " -y --height=height Set the height of the pane" + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + int adjust; + if (opt.args().size() == 0) { + adjust = 1; + } else if (opt.args().size() == 1) { + adjust = Integer.parseInt(opt.args().get(0)); + } else { + throw new HelpException(opt.usage()); + } + window().resizePane(opt, adjust); + setDirty(); + } + + protected void selectPane(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "select-pane - ", + "Usage: select-pane [-UDLR] [-t target-pane]", + " -? --help Show help", + " -U Select pane up", + " -D Select pane down", + " -L Select pane left", + " -R Select pane right", + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + if (window().selectPane(opt)) { + setDirty(); + } + } + + protected void sendPrefix(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "send-prefix - ", "Usage: send-prefix [-2] [-t target-pane]", " -? --help Show help", + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + active().getMasterInputOutput().write(serverOptions.get(OPT_PREFIX).getBytes()); + } + + protected void splitWindow(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "split-window - ", + "Usage: split-window [-bdfhvP] [-c start-directory] [-F format] [-p percentage|-l size] [-t target-pane] [command]", + " -? --help Show help", + " -h --horizontal Horizontal split", + " -v --vertical Vertical split", + " -l --size=size Size", + " -p --perc=percentage Percentage", + " -b --before Insert the new pane before the active one", + " -f Split the full window instead of the active pane", + " -d Do not make the new pane the active one" + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + VirtualConsole newConsole = window().splitPane(opt); + runner.accept(newConsole.getConsole()); + setDirty(); + } + + protected void layoutResize() { + // See layout_resize + } + + protected synchronized void redraw() { + long[] screen = new long[size.getRows() * size.getColumns()]; + // Fill + Arrays.fill(screen, 0x00000020L); + int[] cursor = new int[2]; + for (VirtualConsole terminal : panes()) { + if (terminal.clock) { + String str = DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date()); + print(screen, terminal, str, CLOCK_COLOR); + } else { + // Dump terminal + terminal.dump( + screen, + terminal.top(), + terminal.left(), + size.getRows(), + size.getColumns(), + terminal == active() ? cursor : null); + } + + if (identify) { + String id = Integer.toString(terminal.id); + print(screen, terminal, id, terminal == active() ? ACTIVE_COLOR : INACTIVE_COLOR); + } + // Draw border + drawBorder(screen, size, terminal, 0x0L); + } + drawBorder(screen, size, active(), 0x010080000L << 32); + // Draw status + Arrays.fill( + screen, + (size.getRows() - 1) * size.getColumns(), + size.getRows() * size.getColumns(), + 0x20000080L << 32 | 0x0020L); + + // Attribute mask: 0xYXFFFBBB00000000L + // X: Bit 0 - Underlined + // Bit 1 - Negative + // Bit 2 - Concealed + // Bit 3 - Bold + // Y: Bit 0 - Foreground set + // Bit 1 - Background set + // F: Foreground r-g-b + // B: Background r-g-b + + List lines = new ArrayList<>(); + int prevBg = 0; + int prevFg = 0; + boolean prevInv = false; + boolean prevUl = false; + boolean prevBold = false; + boolean prevConceal = false; + boolean prevHasFg = false; + boolean prevHasBg = false; + for (int y = 0; y < size.getRows(); y++) { + AttributedStringBuilder sb = new AttributedStringBuilder(size.getColumns()); + for (int x = 0; x < size.getColumns(); x++) { + long d = screen[y * size.getColumns() + x]; + int c = (int) (d & 0xffffffffL); + int a = (int) (d >> 32); + int bg = a & 0x000fff; + int fg = (a & 0xfff000) >> 12; + boolean ul = ((a & 0x01000000) != 0); + boolean inv = ((a & 0x02000000) != 0); + boolean conceal = ((a & 0x04000000) != 0); + boolean bold = ((a & 0x08000000) != 0); + boolean hasFg = ((a & 0x10000000) != 0); + boolean hasBg = ((a & 0x20000000) != 0); + + if ((hasBg && prevHasBg && bg != prevBg) || prevHasBg != hasBg) { + if (!hasBg) { + sb.style(sb.style().backgroundDefault()); + } else { + int col = bg; + col = Colors.roundRgbColor((col & 0xF00) >> 4, (col & 0x0F0), (col & 0x00F) << 4, 256); + sb.style(sb.style().background(col)); + } + prevBg = bg; + prevHasBg = hasBg; + } + if ((hasFg && prevHasFg && fg != prevFg) || prevHasFg != hasFg) { + if (!hasFg) { + sb.style(sb.style().foregroundDefault()); + } else { + int col = fg; + col = Colors.roundRgbColor((col & 0xF00) >> 4, (col & 0x0F0), (col & 0x00F) << 4, 256); + sb.style(sb.style().foreground(col)); + } + prevFg = fg; + prevHasFg = hasFg; + } + if (conceal != prevConceal) { + sb.style(conceal ? sb.style().conceal() : sb.style().concealOff()); + prevConceal = conceal; + } + if (inv != prevInv) { + sb.style(inv ? sb.style().inverse() : sb.style().inverseOff()); + prevInv = inv; + } + if (ul != prevUl) { + sb.style(ul ? sb.style().underline() : sb.style().underlineOff()); + prevUl = ul; + } + if (bold != prevBold) { + sb.style(bold ? sb.style().bold() : sb.style().boldOff()); + prevBold = bold; + } + sb.append((char) c); + } + lines.add(sb.toAttributedString()); + } + display.resize(size.getRows(), size.getColumns()); + display.update(lines, size.cursorPos(cursor[1], cursor[0])); + } + + private void print(long[] screen, VirtualConsole terminal, String id, int color) { + if (terminal.height() > 5) { + long attr = ((long) color << 32) | 0x02000000000000000L; + int yoff = (terminal.height() - 5) / 2; + int xoff = (terminal.width() - id.length() * 6) / 2; + for (int i = 0; i < id.length(); i++) { + char ch = id.charAt(i); + int idx; + switch (ch) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + idx = ch - '0'; + break; + case ':': + idx = 10; + break; + case 'A': + idx = 11; + break; + case 'P': + idx = 12; + break; + case 'M': + idx = 13; + break; + default: + idx = -1; + break; + } + if (idx >= 0) { + int[][] data = WINDOW_CLOCK_TABLE[idx]; + for (int y = 0; y < data.length; y++) { + for (int x = 0; x < data[y].length; x++) { + if (data[y][x] != 0) { + int off = (terminal.top + yoff + y) * size.getColumns() + + terminal.left() + + xoff + + x + + 6 * i; + screen[off] = attr | ' '; + } + } + } + } + } + } else { + long attr = ((long) color << 44) | 0x01000000000000000L; + int yoff = (terminal.height() + 1) / 2; + int xoff = (terminal.width() - id.length()) / 2; + int off = (terminal.top + yoff) * size.getColumns() + terminal.left() + xoff; + for (int i = 0; i < id.length(); i++) { + screen[off + i] = attr | id.charAt(i); + } + } + } + + private void drawBorder(long[] screen, Size size, VirtualConsole terminal, long attr) { + for (int i = terminal.left(); i < terminal.right(); i++) { + int y0 = terminal.top() - 1; + int y1 = terminal.bottom(); + drawBorderChar(screen, size, i, y0, attr, '─'); + drawBorderChar(screen, size, i, y1, attr, '─'); + } + for (int i = terminal.top(); i < terminal.bottom(); i++) { + int x0 = terminal.left() - 1; + int x1 = terminal.right(); + drawBorderChar(screen, size, x0, i, attr, '│'); + drawBorderChar(screen, size, x1, i, attr, '│'); + } + drawBorderChar(screen, size, terminal.left() - 1, terminal.top() - 1, attr, '┌'); + drawBorderChar(screen, size, terminal.right(), terminal.top() - 1, attr, '┐'); + drawBorderChar(screen, size, terminal.left() - 1, terminal.bottom(), attr, '└'); + drawBorderChar(screen, size, terminal.right(), terminal.bottom(), attr, '┘'); + } + + private void drawBorderChar(long[] screen, Size size, int x, int y, long attr, int c) { + if (x >= 0 && x < size.getColumns() && y >= 0 && y < size.getRows() - 1) { + int oldc = (int) (screen[y * size.getColumns() + x] & 0xFFFFFFFFL); + c = addBorder(c, oldc); + screen[y * size.getColumns() + x] = attr | c; + } + } + + private int addBorder(int c, int oldc) { + if (oldc == ' ') { + return c; + } + if (oldc == '┼') { + return '┼'; + } + switch (c) { + case '│': + return addBorder('╷', addBorder('╵', oldc)); + case '─': + return addBorder('╴', addBorder('╶', oldc)); + case '┌': + return addBorder('╶', addBorder('╷', oldc)); + case '┐': + return addBorder('╴', addBorder('╷', oldc)); + case '└': + return addBorder('╶', addBorder('╵', oldc)); + case '┘': + return addBorder('╴', addBorder('╵', oldc)); + case '├': + return addBorder('╶', addBorder('│', oldc)); + case '┤': + return addBorder('╴', addBorder('│', oldc)); + case '┬': + return addBorder('╷', addBorder('─', oldc)); + case '┴': + return addBorder('╵', addBorder('─', oldc)); + case '╴': + switch (oldc) { + case '│': + return '┤'; + case '─': + return '─'; + case '┌': + return '┬'; + case '┐': + return '┐'; + case '└': + return '┴'; + case '┘': + return '┘'; + case '├': + return '┼'; + case '┤': + return '┤'; + case '┬': + return '┬'; + case '┴': + return '┴'; + default: + throw new IllegalArgumentException(); + } + case '╵': + switch (oldc) { + case '│': + return '│'; + case '─': + return '┴'; + case '┌': + return '├'; + case '┐': + return '┤'; + case '└': + return '└'; + case '┘': + return '┘'; + case '├': + return '├'; + case '┤': + return '┤'; + case '┬': + return '┼'; + case '┴': + return '┴'; + default: + throw new IllegalArgumentException(); + } + case '╶': + switch (oldc) { + case '│': + return '├'; + case '─': + return '─'; + case '┌': + return '┌'; + case '┐': + return '┬'; + case '└': + return '└'; + case '┘': + return '┴'; + case '├': + return '├'; + case '┤': + return '┼'; + case '┬': + return '┬'; + case '┴': + return '┴'; + default: + throw new IllegalArgumentException(); + } + case '╷': + switch (oldc) { + case '│': + return '│'; + case '─': + return '┬'; + case '┌': + return '┌'; + case '┐': + return '┐'; + case '└': + return '├'; + case '┘': + return '┤'; + case '├': + return '├'; + case '┤': + return '┤'; + case '┬': + return '┬'; + case '┴': + return '┼'; + default: + throw new IllegalArgumentException(); + } + default: + throw new IllegalArgumentException(); + } + } + + enum Binding { + Discard, + SelfInsert, + Mouse + } + + static class Layout { + + static final Pattern PATTERN = Pattern.compile("([0-9]+)x([0-9]+),([0-9]+),([0-9]+)([^0-9]\\S*)?"); + private static final int PANE_MINIMUM = 3; + Type type; + Layout parent; + int sx; + int sy; + int xoff; + int yoff; + List cells = new CopyOnWriteArrayList<>(); + + public static Layout parse(String layout) { + if (layout.length() < 6) { + throw new IllegalArgumentException("Bad syntax"); + } + String chk = layout.substring(0, 4); + if (layout.charAt(4) != ',') { + throw new IllegalArgumentException("Bad syntax"); + } + layout = layout.substring(5); + if (Integer.parseInt(chk, 16) != checksum(layout)) { + throw new IllegalArgumentException("Bad checksum"); + } + return parseCell(null, layout); + } + + private static char toHexChar(int i) { + return (i < 10) ? (char) (i + '0') : (char) (i - 10 + 'a'); + } + + private static int checksum(CharSequence layout) { + return checksum(layout, 0); + } + + private static int checksum(CharSequence layout, int start) { + int csum = 0; + for (int i = start; i < layout.length(); i++) { + csum = (csum >> 1) + ((csum & 1) << 15); + csum += layout.charAt(i); + } + return csum; + } + + private static Layout parseCell(Layout parent, String layout) { + Matcher matcher = PATTERN.matcher(layout); + if (matcher.matches()) { + Layout cell = new Layout(); + cell.type = Type.WindowPane; + cell.parent = parent; + cell.sx = Integer.parseInt(matcher.group(1)); + cell.sy = Integer.parseInt(matcher.group(2)); + cell.xoff = Integer.parseInt(matcher.group(3)); + cell.yoff = Integer.parseInt(matcher.group(4)); + if (parent != null) { + parent.cells.add(cell); + } + layout = matcher.group(5); + if (layout == null || layout.isEmpty()) { + return cell; + } + if (layout.charAt(0) == ',') { + int i = 1; + while (i < layout.length() && Character.isDigit(layout.charAt(i))) { + i++; + } + if (i == layout.length()) { + return cell; + } + if (layout.charAt(i) == ',') { + layout = layout.substring(i); + } + } + int i; + switch (layout.charAt(0)) { + case '{': + cell.type = LeftRight; + i = findMatch(layout, '{', '}'); + parseCell(cell, layout.substring(1, i)); + layout = layout.substring(i + 1); + if (!layout.isEmpty() && layout.charAt(0) == ',') { + parseCell(parent, layout.substring(1)); + } + return cell; + case '[': + cell.type = Type.TopBottom; + i = findMatch(layout, '[', ']'); + parseCell(cell, layout.substring(1, i)); + layout = layout.substring(i + 1); + if (!layout.isEmpty() && layout.charAt(0) == ',') { + parseCell(parent, layout.substring(1)); + } + return cell; + case ',': + parseCell(parent, layout.substring(1)); + return cell; + default: + throw new IllegalArgumentException("Unexpected '" + layout.charAt(0) + "'"); + } + } else { + throw new IllegalArgumentException("Bad syntax"); + } + } + + public String dump() { + StringBuilder sb = new StringBuilder(64); + sb.append("0000,"); + doDump(sb); + int chk = checksum(sb, 5); + sb.setCharAt(0, toHexChar((chk >> 12) & 0x000F)); + sb.setCharAt(1, toHexChar((chk >> 8) & 0x000F)); + sb.setCharAt(2, toHexChar((chk >> 4) & 0x000F)); + sb.setCharAt(3, toHexChar(chk & 0x000F)); + return sb.toString(); + } + + private void doDump(StringBuilder sb) { + sb.append(sx) + .append('x') + .append(sy) + .append(',') + .append(xoff) + .append(',') + .append(yoff); + switch (type) { + case WindowPane: + sb.append(',').append('0'); + break; + case TopBottom: + case LeftRight: + sb.append(type == Type.TopBottom ? '[' : '{'); + boolean first = true; + for (Layout c : cells) { + if (first) { + first = false; + } else { + sb.append(','); + } + c.doDump(sb); + } + sb.append(type == Type.TopBottom ? ']' : '}'); + break; + } + } + + public void resize(Type type, int change, boolean opposite) { + /* Find next parent of the same type. */ + Layout lc = this; + Layout lcparent = lc.parent; + while (lcparent != null && lcparent.type != type) { + lc = lcparent; + lcparent = lc.parent; + } + if (lcparent == null) { + return; + } + /* If this is the last cell, move back one. */ + if (lc.nextSibling() == null) { + lc = lc.prevSibling(); + } + /* Grow or shrink the cell. */ + int size; + int needed = change; + while (needed != 0) { + if (change > 0) { + size = lc.resizePaneGrow(type, needed, opposite); + needed -= size; + } else { + size = lc.resizePaneShrink(type, needed); + needed += size; + } + if (size == 0) { + /* no more change possible */ + break; + } + } + fixOffsets(); + fixPanes(); + } + + int resizePaneGrow(Type type, int needed, boolean opposite) { + int size = 0; + /* Growing. Always add to the current cell. */ + Layout lcadd = this; + /* Look towards the tail for a suitable cell for reduction. */ + Layout lcremove = this.nextSibling(); + while (lcremove != null) { + size = lcremove.resizeCheck(type); + if (size > 0) { + break; + } + lcremove = lcremove.nextSibling(); + } + /* If none found, look towards the head. */ + if (opposite && lcremove == null) { + lcremove = this.prevSibling(); + while (lcremove != null) { + size = lcremove.resizeCheck(type); + if (size > 0) { + break; + } + lcremove = lcremove.prevSibling(); + } + } + if (lcremove == null) { + return 0; + } + /* Change the cells. */ + if (size > needed) { + size = needed; + } + lcadd.resizeAdjust(type, size); + lcremove.resizeAdjust(type, -size); + return size; + } + + int resizePaneShrink(Type type, int needed) { + int size = 0; + /* Shrinking. Find cell to remove from by walking towards head. */ + Layout lcremove = this; + do { + size = lcremove.resizeCheck(type); + if (size > 0) { + break; + } + lcremove = lcremove.prevSibling(); + } while (lcremove != null); + if (lcremove == null) { + return 0; + } + /* And add onto the next cell (from the original cell). */ + Layout lcadd = this.nextSibling(); + if (lcadd == null) { + return 0; + } + /* Change the cells. */ + if (size > -needed) { + size = -needed; + } + lcadd.resizeAdjust(type, size); + lcremove.resizeAdjust(type, -size); + return size; + } + + Layout prevSibling() { + int idx = parent.cells.indexOf(this); + if (idx > 0) { + return parent.cells.get(idx - 1); + } else { + return null; + } + } + + Layout nextSibling() { + int idx = parent.cells.indexOf(this); + if (idx < parent.cells.size() - 1) { + return parent.cells.get(idx + 1); + } else { + return null; + } + } + + public void resizeTo(Type type, int new_size) { + /* Find next parent of the same type. */ + Layout lc = this; + Layout lcparent = lc.parent; + while (lcparent != null && lcparent.type != type) { + lc = lcparent; + lcparent = lc.parent; + } + if (lcparent == null) { + return; + } + /* Work out the size adjustment. */ + int size = type == LeftRight ? lc.sx : lc.sy; + int change = lc.nextSibling() == null ? size - new_size : new_size - size; + /* Resize the pane. */ + lc.resize(type, change, true); + } + + public void resize(int sx, int sy) { + // Horizontal + int xchange = sx - this.sx; + int xlimit = resizeCheck(LeftRight); + if (xchange < 0 && xchange < -xlimit) { + xchange = -xlimit; + } + if (xlimit == 0) { + if (sx <= this.sx) { + xchange = 0; + } else { + xchange = sx - this.sx; + } + } + if (xchange != 0) { + resizeAdjust(LeftRight, xchange); + } + + // Horizontal + int ychange = sy - this.sy; + int ylimit = resizeCheck(Type.TopBottom); + if (ychange < 0 && ychange < -ylimit) { + ychange = -ylimit; + } + if (ylimit == 0) { + if (sy <= this.sy) { + ychange = 0; + } else { + ychange = sy - this.sy; + } + } + if (ychange != 0) { + resizeAdjust(Type.TopBottom, ychange); + } + + // Fix offsets + fixOffsets(); + fixPanes(sx, sy); + } + + public void remove() { + if (parent == null) { + throw new IllegalStateException(); + } + int idx = parent.cells.indexOf(this); + Layout other = parent.cells.get(idx == 0 ? 1 : idx - 1); + other.resizeAdjust(parent.type, parent.type == LeftRight ? (sx + 1) : (sy + 1)); + parent.cells.remove(this); + if (other.parent.cells.size() == 1) { + if (other.parent.parent == null) { + other.parent = null; + } else { + other.parent.parent.cells.set(other.parent.parent.cells.indexOf(other.parent), other); + other.parent = other.parent.parent; + } + } + } + + private int resizeCheck(Type type) { + if (this.type == Type.WindowPane) { + int min = PANE_MINIMUM; + int avail; + if (type == LeftRight) { + avail = this.sx; + } else { + avail = this.sy; + min += 1; // TODO: need status + } + if (avail > min) { + avail -= min; + } else { + avail = 0; + } + return avail; + } else if (this.type == type) { + return this.cells.stream() + .mapToInt(c -> c != null ? c.resizeCheck(type) : 0) + .sum(); + } else { + return this.cells.stream() + .mapToInt(c -> c != null ? c.resizeCheck(type) : Integer.MAX_VALUE) + .min() + .orElse(Integer.MAX_VALUE); + } + } + + private void resizeAdjust(Type type, int change) { + if (type == LeftRight) { + this.sx += change; + } else { + this.sy += change; + } + if (this.type == Type.WindowPane) { + return; + } + if (this.type != type) { + for (Layout c : cells) { + c.resizeAdjust(type, change); + } + return; + } + while (change != 0) { + for (Layout c : cells) { + if (change == 0) { + break; + } + if (change > 0) { + c.resizeAdjust(type, 1); + change--; + continue; + } + if (c.resizeCheck(type) > 0) { + c.resizeAdjust(type, -1); + change++; + } + } + } + } + + public void fixOffsets() { + if (type == LeftRight) { + int xoff = this.xoff; + for (Layout cell : cells) { + cell.xoff = xoff; + cell.yoff = this.yoff; + cell.fixOffsets(); + xoff += cell.sx + 1; + } + } else if (type == TopBottom) { + int yoff = this.yoff; + for (Layout cell : cells) { + cell.xoff = this.xoff; + cell.yoff = yoff; + cell.fixOffsets(); + yoff += cell.sy + 1; + } + } + } + + public void fixPanes() { + } + + public void fixPanes(int sx, int sy) { + } + + public int countCells() { + switch (type) { + case LeftRight: + case TopBottom: + return cells.stream().mapToInt(Layout::countCells).sum(); + default: + return 1; + } + } + + public Layout split(Type type, int size, boolean insertBefore) { + if (type == WindowPane) { + throw new IllegalStateException(); + } + if ((type == LeftRight ? sx : sy) < PANE_MINIMUM * 2 + 1) { + return null; + } + if (parent == null) { + throw new IllegalStateException(); + } + + int saved_size = type == LeftRight ? sx : sy; + int size2 = size < 0 ? ((saved_size + 1) / 2) - 1 : insertBefore ? saved_size - size - 1 : size; + if (size2 < PANE_MINIMUM) { + size2 = PANE_MINIMUM; + } else if (size2 > saved_size - 2) { + size2 = saved_size - 2; + } + int size1 = saved_size - 1 - size2; + + if (parent.type != type) { + Layout p = new Layout(); + p.type = type; + p.parent = parent; + p.sx = sx; + p.sy = sy; + p.xoff = xoff; + p.yoff = yoff; + parent.cells.set(parent.cells.indexOf(this), p); + p.cells.add(this); + parent = p; + } + Layout cell = new Layout(); + cell.type = WindowPane; + cell.parent = parent; + parent.cells.add(parent.cells.indexOf(this) + (insertBefore ? 0 : 1), cell); + + int sx = this.sx; + int sy = this.sy; + int xoff = this.xoff; + int yoff = this.yoff; + Layout cell1, cell2; + if (insertBefore) { + cell1 = cell; + cell2 = this; + } else { + cell1 = this; + cell2 = cell; + } + if (type == LeftRight) { + cell1.setSize(size1, sy, xoff, yoff); + cell2.setSize(size2, sy, xoff + size1 + 1, yoff); + } else { + cell1.setSize(sx, size1, xoff, yoff); + cell2.setSize(sx, size2, xoff, yoff + size1 + 1); + } + return cell; + } + + private void setSize(int sx, int sy, int xoff, int yoff) { + this.sx = sx; + this.sy = sy; + this.xoff = xoff; + this.yoff = yoff; + } + + enum Type { + LeftRight, + TopBottom, + WindowPane + } + } + + private static class VirtualConsole implements Closeable { + private final ScreenTerminal terminal; + private final Consumer closer; + private final int id; + private final Layout layout; + private final OutputStream masterOutput; + private final OutputStream masterInputOutput; + private final LineDisciplineTerminal console; + private int left; + private int top; + private int active; + private boolean clock; + + public VirtualConsole( + int id, + String type, + int left, + int top, + int columns, + int rows, + Runnable dirty, + Consumer closer, + Layout layout) + throws IOException { + String name = String.format("tmux%02d", id); + this.id = id; + this.left = left; + this.top = top; + this.closer = closer; + this.terminal = new ScreenTerminal(columns, rows) { + @Override + protected void setDirty() { + super.setDirty(); + dirty.run(); + } + }; + this.masterOutput = new MasterOutputStream(); + this.masterInputOutput = new OutputStream() { + @Override + public void write(int b) throws IOException { + console.processInputByte(b); + } + }; + this.console = new LineDisciplineTerminal(name, type, masterOutput, null) { + @Override + protected void doClose() throws IOException { + super.doClose(); + closer.accept(VirtualConsole.this); + } + }; + this.console.setSize(new Size(columns, rows)); + this.layout = layout; + } + + Layout layout() { + return layout; + } + + public int left() { + return left; + } + + public int top() { + return top; + } + + public int right() { + return left() + width(); + } + + public int bottom() { + return top() + height(); + } + + public int width() { + return console.getWidth(); + } + + public int height() { + return console.getHeight(); + } + + public LineDisciplineTerminal getConsole() { + return console; + } + + public OutputStream getMasterInputOutput() { + return masterInputOutput; + } + + public void resize(int left, int top, int width, int height) { + this.left = left; + this.top = top; + console.setSize(new Size(width, height)); + terminal.setSize(width, height); + console.raise(Signal.WINCH); + } + + public void dump(long[] fullscreen, int ftop, int fleft, int fheight, int fwidth, int[] cursor) { + terminal.dump(fullscreen, ftop, fleft, fheight, fwidth, cursor); + } + + @Override + public void close() throws IOException { + console.close(); + } + + private class MasterOutputStream extends OutputStream { + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final CharsetDecoder decoder = Charset.defaultCharset() + .newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + + @Override + public synchronized void write(int b) { + buffer.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + buffer.write(b, off, len); + } + + @Override + public synchronized void flush() throws IOException { + int size = buffer.size(); + if (size > 0) { + CharBuffer out; + for (; ; ) { + out = CharBuffer.allocate(size); + ByteBuffer in = ByteBuffer.wrap(buffer.toByteArray()); + CoderResult result = decoder.decode(in, out, false); + if (result.isOverflow()) { + size *= 2; + } else { + buffer.reset(); + buffer.write(in.array(), in.arrayOffset(), in.remaining()); + break; + } + } + if (out.position() > 0) { + out.flip(); + terminal.write(out); + masterInputOutput.write(terminal.read().getBytes()); + } + } + } + + @Override + public void close() throws IOException { + flush(); + } + } + } + + private class Window { + private final AtomicInteger paneId = new AtomicInteger(); + private final List panes = new CopyOnWriteArrayList<>(); + private VirtualConsole active; + private int lastActive; + private Layout layout; + private final Tmux tmux; + private final String name; + + public Window(Tmux tmux) throws IOException { + this.tmux = tmux; + layout = new Layout(); + layout.sx = size.getColumns(); + layout.sy = size.getRows(); + layout.type = WindowPane; + active = new VirtualConsole( + paneId.incrementAndGet(), + term, + 0, + 0, + size.getColumns(), + size.getRows() - 1, + tmux::setDirty, + tmux::close, + layout); + active.active = lastActive++; + active.getConsole().setAttributes(terminal.getAttributes()); + panes.add(active); + name = "win" + (windowsId < 10 ? "0" + windowsId : windowsId); + windowsId++; + } + + public String getName() { + return name; + } + + public List getPanes() { + return panes; + } + + public VirtualConsole getActive() { + return active; + } + + public void remove(VirtualConsole console) { + panes.remove(console); + if (!panes.isEmpty()) { + console.layout.remove(); + if (active == console) { + active = panes.stream() + .sorted(Comparator.comparingInt(p -> p.active) + .reversed()) + .findFirst() + .get(); + } + layout = active.layout; + while (layout.parent != null) { + layout = layout.parent; + } + layout.fixOffsets(); + layout.fixPanes(size.getColumns(), size.getRows()); + } + } + + public void handleResize() { + layout.resize(size.getColumns(), size.getRows() - 1); + panes.forEach(vc -> { + if (vc.width() != vc.layout.sx + || vc.height() != vc.layout.sy + || vc.left() != vc.layout.xoff + || vc.top() != vc.layout.yoff) { + vc.resize(vc.layout.xoff, vc.layout.yoff, vc.layout.sx, vc.layout.sy); + display.clear(); + } + }); + } + + public VirtualConsole splitPane(Options opt) throws IOException { + Layout.Type type = opt.isSet("horizontal") ? LeftRight : TopBottom; + // If we're splitting the main pane, create a parent + if (layout.type == WindowPane) { + Layout p = new Layout(); + p.sx = layout.sx; + p.sy = layout.sy; + p.type = type; + p.cells.add(layout); + layout.parent = p; + layout = p; + } + Layout cell = active.layout(); + if (opt.isSet("f")) { + while (cell.parent != layout) { + cell = cell.parent; + } + } + int size = -1; + if (opt.isSet("size")) { + size = opt.getNumber("size"); + } else if (opt.isSet("perc")) { + int p = opt.getNumber("perc"); + if (type == TopBottom) { + size = (cell.sy * p) / 100; + } else { + size = (cell.sx * p) / 100; + } + } + // Split now + Layout newCell = cell.split(type, size, opt.isSet("before")); + if (newCell == null) { + err.println("create pane failed: pane too small"); + return null; + } + + VirtualConsole newConsole = new VirtualConsole( + paneId.incrementAndGet(), + term, + newCell.xoff, + newCell.yoff, + newCell.sx, + newCell.sy, + tmux::setDirty, + tmux::close, + newCell); + panes.add(newConsole); + newConsole.getConsole().setAttributes(terminal.getAttributes()); + if (!opt.isSet("d")) { + active = newConsole; + active.active = lastActive++; + } + return newConsole; + } + + public boolean selectPane(Options opt) { + VirtualConsole prevActive = active; + if (opt.isSet("L")) { + active = panes.stream() + .filter(c -> c.bottom() > active.top() && c.top() < active.bottom()) + .filter(c -> c != active) + .sorted(Comparator.comparingInt( + c -> c.left() > active.left() ? c.left() : c.left() + size.getColumns()) + .reversed() + .thenComparingInt(c -> -c.active)) + .findFirst() + .orElse(active); + } else if (opt.isSet("R")) { + active = panes.stream() + .filter(c -> c.bottom() > active.top() && c.top() < active.bottom()) + .filter(c -> c != active) + .sorted(Comparator.comparingInt( + c -> c.left() > active.left() ? c.left() : c.left() + size.getColumns()) + .thenComparingInt(c -> -c.active)) + .findFirst() + .orElse(active); + } else if (opt.isSet("U")) { + active = panes.stream() + .filter(c -> c.right() > active.left() && c.left() < active.right()) + .filter(c -> c != active) + .sorted(Comparator.comparingInt( + c -> c.top() > active.top() ? c.top() : c.top() + size.getRows()) + .reversed() + .thenComparingInt(c -> -c.active)) + .findFirst() + .orElse(active); + } else if (opt.isSet("D")) { + active = panes.stream() + .filter(c -> c.right() > active.left() && c.left() < active.right()) + .filter(c -> c != active) + .sorted(Comparator.comparingInt( + c -> c.top() > active.top() ? c.top() : c.top() + size.getRows()) + .thenComparingInt(c -> -c.active)) + .findFirst() + .orElse(active); + } + boolean out = false; + if (prevActive != active) { + active.active = lastActive++; + out = true; + } + return out; + } + + public void resizePane(Options opt, int adjust) { + if (opt.isSet("width")) { + int x = opt.getNumber("width"); + active.layout().resizeTo(LeftRight, x); + } + if (opt.isSet("height")) { + int y = opt.getNumber("height"); + active.layout().resizeTo(TopBottom, y); + } + if (opt.isSet("L")) { + active.layout().resize(LeftRight, -adjust, true); + } else if (opt.isSet("R")) { + active.layout().resize(LeftRight, adjust, true); + } else if (opt.isSet("U")) { + active.layout().resize(TopBottom, -adjust, true); + } else if (opt.isSet("D")) { + active.layout().resize(TopBottom, adjust, true); + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/console/ArgDesc.java b/net-cli/src/main/java/org/jline/console/ArgDesc.java new file mode 100644 index 0000000..9c1ed00 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/ArgDesc.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.util.ArrayList; +import java.util.List; +import org.jline.utils.AttributedString; + +public class ArgDesc { + private final String name; + private final List description; + + public ArgDesc(String name) { + this(name, new ArrayList<>()); + } + + public ArgDesc(String name, List description) { + if (name.contains("\t") || name.contains(" ")) { + throw new IllegalArgumentException("Bad argument name: " + name); + } + this.name = name; + this.description = new ArrayList<>(description); + } + + public static List doArgNames(List names) { + List out = new ArrayList<>(); + for (String n : names) { + out.add(new ArgDesc(n)); + } + return out; + } + + public String getName() { + return name; + } + + public List getDescription() { + return description; + } +} diff --git a/net-cli/src/main/java/org/jline/console/CmdDesc.java b/net-cli/src/main/java/org/jline/console/CmdDesc.java new file mode 100644 index 0000000..3ce8127 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/CmdDesc.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; +import org.jline.utils.AttributedString; + +public class CmdDesc { + private List mainDesc; + private List argsDesc; + private TreeMap> optsDesc; + private Pattern errorPattern; + private int errorIndex = -1; + private boolean valid = true; + private boolean command = false; + private boolean subcommand = false; + private boolean highlighted = true; + + public CmdDesc() { + command = false; + } + + public CmdDesc(boolean valid) { + this.valid = valid; + } + + public CmdDesc(List argsDesc) { + this(new ArrayList<>(), argsDesc, new HashMap<>()); + } + + public CmdDesc(List argsDesc, Map> optsDesc) { + this(new ArrayList<>(), argsDesc, optsDesc); + } + + public CmdDesc( + List mainDesc, List argsDesc, Map> optsDesc) { + this.argsDesc = new ArrayList<>(argsDesc); + this.optsDesc = new TreeMap<>(optsDesc); + if (mainDesc.isEmpty() && optsDesc.containsKey("main")) { + this.mainDesc = new ArrayList<>(optsDesc.get("main")); + this.optsDesc.remove("main"); + } else { + this.mainDesc = new ArrayList<>(mainDesc); + } + this.command = true; + } + + public boolean isValid() { + return valid; + } + + public boolean isCommand() { + return command; + } + + public boolean isSubcommand() { + return subcommand; + } + + public void setSubcommand(boolean subcommand) { + this.subcommand = subcommand; + } + + public boolean isHighlighted() { + return highlighted; + } + + public void setHighlighted(boolean highlighted) { + this.highlighted = highlighted; + } + + public CmdDesc mainDesc(List mainDesc) { + this.mainDesc = new ArrayList<>(mainDesc); + return this; + } + + public List getMainDesc() { + return mainDesc; + } + + public void setMainDesc(List mainDesc) { + this.mainDesc = new ArrayList<>(mainDesc); + } + + public TreeMap> getOptsDesc() { + return optsDesc; + } + + public Pattern getErrorPattern() { + return errorPattern; + } + + public void setErrorPattern(Pattern errorPattern) { + this.errorPattern = errorPattern; + } + + public int getErrorIndex() { + return errorIndex; + } + + public void setErrorIndex(int errorIndex) { + this.errorIndex = errorIndex; + } + + public List getArgsDesc() { + return argsDesc; + } + + public boolean optionWithValue(String option) { + for (String key : optsDesc.keySet()) { + if (key.matches("(^|.*\\s)" + option + "($|=.*|\\s.*)")) { + return key.contains("="); + } + } + return false; + } + + public AttributedString optionDescription(String key) { + return optsDesc.get(key).size() > 0 ? optsDesc.get(key).get(0) : new AttributedString(""); + } +} diff --git a/net-cli/src/main/java/org/jline/console/CmdLine.java b/net-cli/src/main/java/org/jline/console/CmdLine.java new file mode 100644 index 0000000..08dd1e1 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/CmdLine.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.util.ArrayList; +import java.util.List; + +public class CmdLine { + private final String line; + private final String head; + private final String tail; + private final List args; + private final DescriptionType descType; + /** + * CmdLine class constructor. + * + * @param line Command line + * @param head Command line till cursor, method parameters and opening parenthesis before the cursor are removed. + * @param tail Command line after cursor, method parameters and closing parenthesis after the cursor are removed. + * @param args Parsed command line arguments. + * @param descType Request COMMAND, METHOD or SYNTAX description + */ + public CmdLine(String line, String head, String tail, List args, DescriptionType descType) { + this.line = line; + this.head = head; + this.tail = tail; + this.args = new ArrayList<>(args); + this.descType = descType; + } + + public String getLine() { + return line; + } + + public String getHead() { + return head; + } + + public String getTail() { + return tail; + } + + public List getArgs() { + return args; + } + + public DescriptionType getDescriptionType() { + return descType; + } + + public enum DescriptionType { + /** + * Cursor is at the end of line. The args[0] is completed, the line does not have unclosed opening parenthesis + * and does not end to the closing parenthesis. + */ + COMMAND, + /** + * The part of the line from beginning till cursor has unclosed opening parenthesis. + */ + METHOD, + /** + * The part of the line from beginning till cursor ends to the closing parenthesis. + */ + SYNTAX + } +} diff --git a/net-cli/src/main/java/org/jline/console/CommandInput.java b/net-cli/src/main/java/org/jline/console/CommandInput.java new file mode 100644 index 0000000..36e33ba --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/CommandInput.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.io.InputStream; +import java.io.PrintStream; +import org.jline.terminal.Terminal; + +public class CommandInput { + String command; + String[] args; + Object[] xargs; + Terminal terminal; + InputStream in; + PrintStream out; + PrintStream err; + + public CommandInput(String command, Object[] xargs, CommandRegistry.CommandSession session) { + if (xargs != null) { + this.xargs = xargs; + this.args = new String[xargs.length]; + for (int i = 0; i < xargs.length; i++) { + this.args[i] = xargs[i] != null ? xargs[i].toString() : null; + } + } + this.command = command; + this.terminal = session.terminal(); + this.in = session.in(); + this.out = session.out(); + this.err = session.err(); + } + + public CommandInput( + String command, Object[] args, Terminal terminal, InputStream in, PrintStream out, PrintStream err) { + this(command, args, new CommandRegistry.CommandSession(terminal, in, out, err)); + } + + public String command() { + return command; + } + + public String[] args() { + return args; + } + + public Object[] xargs() { + return xargs; + } + + public Terminal terminal() { + return terminal; + } + + public InputStream in() { + return in; + } + + public PrintStream out() { + return out; + } + + public PrintStream err() { + return err; + } + + public CommandRegistry.CommandSession session() { + return new CommandRegistry.CommandSession(terminal, in, out, err); + } +} diff --git a/net-cli/src/main/java/org/jline/console/CommandMethods.java b/net-cli/src/main/java/org/jline/console/CommandMethods.java new file mode 100644 index 0000000..9f7e2ed --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/CommandMethods.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import org.jline.reader.Completer; + +public class CommandMethods { + Function execute; + Function> compileCompleter; + + public CommandMethods(Function execute, Function> compileCompleter) { + this.execute = execute; + this.compileCompleter = compileCompleter; + } + + public CommandMethods(Consumer execute, Function> compileCompleter) { + this.execute = (CommandInput i) -> { + execute.accept(i); + return null; + }; + this.compileCompleter = compileCompleter; + } + + public Function execute() { + return execute; + } + + public Function> compileCompleter() { + return compileCompleter; + } +} diff --git a/net-cli/src/main/java/org/jline/console/CommandRegistry.java b/net-cli/src/main/java/org/jline/console/CommandRegistry.java new file mode 100644 index 0000000..5dff0c2 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/CommandRegistry.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.io.InputStream; +import java.io.PrintStream; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.jline.reader.impl.completer.SystemCompleter; +import org.jline.terminal.Terminal; + +/** + * Store command information, compile tab completers and execute registered commands. + * + * @author Matti Rinta-Nikkola + */ +public interface CommandRegistry { + + /** + * Aggregate SystemCompleters of commandRegisteries + * + * @param commandRegistries command registeries which completers is to be aggregated + * @return uncompiled SystemCompleter + */ + static SystemCompleter aggregateCompleters(CommandRegistry... commandRegistries) { + SystemCompleter out = new SystemCompleter(); + for (CommandRegistry r : commandRegistries) { + out.add(r.compileCompleters()); + } + return out; + } + + /** + * Aggregate and compile SystemCompleters of commandRegisteries + * + * @param commandRegistries command registeries which completers is to be aggregated and compile + * @return compiled SystemCompleter + */ + static SystemCompleter compileCompleters(CommandRegistry... commandRegistries) { + SystemCompleter out = aggregateCompleters(commandRegistries); + out.compile(); + return out; + } + + /** + * Returns the name of this registry. + * + * @return the name of the registry + */ + default String name() { + return this.getClass().getSimpleName(); + } + + /** + * Returns the command names known by this registry. + * + * @return the set of known command names, excluding aliases + */ + Set commandNames(); + + /** + * Returns a map of alias-to-command names known by this registry. + * + * @return a map with alias keys and command name values + */ + Map commandAliases(); + + /** + * Returns a short info about command known by this registry. + * + * @param command the command name + * @return a short info about command + */ + List commandInfo(String command); + + /** + * Returns whether a command with the specified name is known to this registry. + * + * @param command the command name to test + * @return true if the specified command is registered + */ + boolean hasCommand(String command); + + /** + * Returns a {@code SystemCompleter} that can provide detailed completion + * information for all registered commands. + * + * @return a SystemCompleter that can provide command completion for all registered commands + */ + SystemCompleter compileCompleters(); + + /** + * Returns a command description for use in the JLine Widgets framework. + * Default method must be overridden to return sub command descriptions. + * + * @param args command (args[0]) and its arguments + * @return command description for JLine TailTipWidgets to be displayed + * in the terminal status bar. + */ + CmdDesc commandDescription(List args); + + /** + * Execute a command. + * + * @param session the data of the current command session + * @param command the name of the command + * @param args arguments of the command + * @return result of the command execution + * @throws Exception in case of error + */ + default Object invoke(CommandSession session, String command, Object... args) throws Exception { + throw new IllegalStateException( + "CommandRegistry method invoke(session, command, ... args) is not implemented!"); + } + + class CommandSession { + private final Terminal terminal; + private final InputStream in; + private final PrintStream out; + private final PrintStream err; + + public CommandSession() { + this.in = System.in; + this.out = System.out; + this.err = System.err; + this.terminal = null; + } + + public CommandSession(Terminal terminal) { + this(terminal, terminal.input(), new PrintStream(terminal.output()), new PrintStream(terminal.output())); + } + + public CommandSession(Terminal terminal, InputStream in, PrintStream out, PrintStream err) { + this.terminal = terminal; + this.in = in; + this.out = out; + this.err = err; + } + + public Terminal terminal() { + return terminal; + } + + public InputStream in() { + return in; + } + + public PrintStream out() { + return out; + } + + public PrintStream err() { + return err; + } + } +} diff --git a/net-cli/src/main/java/org/jline/console/ConsoleEngine.java b/net-cli/src/main/java/org/jline/console/ConsoleEngine.java new file mode 100644 index 0000000..21347ec --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/ConsoleEngine.java @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2002-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.Widget; + +/** + * Manage console variables, commands and script executions. + * + * @author Matti Rinta-Nikkola + */ +public interface ConsoleEngine extends CommandRegistry { + + /** + * Console string variable of nanorc file full path + */ + String VAR_NANORC = "NANORC"; + + /** + * Removes the command name first character if it is colon + * + * @param command the name of the command to complete + * @return command name without starting colon + */ + static String plainCommand(String command) { + return command.startsWith(":") ? command.substring(1) : command; + } + + /** + * Sets lineReader + * + * @param reader LineReader + */ + void setLineReader(LineReader reader); + + /** + * Sets systemRegistry + * + * @param systemRegistry SystemRegistry + */ + void setSystemRegistry(SystemRegistry systemRegistry); + + /** + * Substituting args references with their values. + * + * @param args the arguments to be expanded + * @return expanded arguments + * @throws Exception in case of error + */ + Object[] expandParameters(String[] args) throws Exception; + + /** + * Substitutes command line with system registry invoke method call. + * + * @param line command line to be expanded + * @return expanded command line + */ + String expandCommandLine(String line); + + /** + * Expands parameter list to string + * + * @param params list of script parameters + * @return expanded parameters list + */ + String expandToList(List params); + + /** + * Returns all scripts found from PATH + * + * @return map keys have script file names and value is true if it is console script + */ + Map scripts(); + + /** + * Sets file name extension used by console scripts + * + * @param extension console script file extension + */ + void setScriptExtension(String extension); + + /** + * Returns true if alias 'name' exists + * + * @param name alias name + * @return true if alias exists + */ + boolean hasAlias(String name); + + /** + * Returns alias 'name' value + * + * @param name alias name + * @return value of alias + */ + String getAlias(String name); + + /** + * Returns defined pipes + * + * @return map of defined pipes + */ + Map> getPipes(); + + /** + * Returns named pipe names + * + * @return list of named pipe names + */ + List getNamedPipes(); + + /** + * Returns script and variable completers + * + * @return script and variable completers + */ + List scriptCompleters(); + + /** + * Persist object to file + * + * @param file file where object should be written + * @param object object to persist + */ + void persist(Path file, Object object); + + /** + * Read object from file + * + * @param file file from where object should be read + * @return object + * @throws IOException in case of error + */ + Object slurp(Path file) throws IOException; + + /** + * Read console option value + * + * @param option type + * @param option option name + * @param defval default value + * @return option value + */ + T consoleOption(String option, T defval); + + /** + * Set console option value + * + * @param name the option name + * @param value value to assign console option + */ + void setConsoleOption(String name, Object value); + + /** + * Executes command line that does not contain known command by the system registry. + * If the line is neither JLine or ScriptEngine script it will be evaluated + * as ScriptEngine statement. + * + * @param name parsed command/script name + * @param rawLine raw command line + * @param args parsed arguments of the command + * @return command line execution result + * @throws Exception in case of error + */ + Object execute(String name, String rawLine, String[] args) throws Exception; + + /** + * Executes either JLine or ScriptEngine script. + * + * @param script script file + * @return script execution result + * @throws Exception in case of error + */ + default Object execute(File script) throws Exception { + return execute(script, "", new String[0]); + } + + /** + * Executes either JLine or ScriptEngine script. + * + * @param script script file + * @param rawLine raw command line + * @param args script arguments + * @return script execution result + * @throws Exception in case of error + */ + Object execute(File script, String rawLine, String[] args) throws Exception; + + /** + * Post processes execution result. If result is to be assigned to the console variable + * then method will return null. + * + * @param line command line + * @param result command result to process + * @param output command redirected output + * @return processed result + */ + ExecutionResult postProcess(String line, Object result, String output); + + /** + * Post processes execution result. + * + * @param result command result to process + * @return processed result + */ + ExecutionResult postProcess(Object result); + + /** + * Print object if trace is enabled + * + * @param object object to print + */ + void trace(Object object); + + /** + * Print object. + * + * @param object object to print + */ + void println(Object object); + + /** + * Create console variable + * + * @param name name of the variable + * @param value value of the variable + */ + void putVariable(String name, Object value); + + /** + * Get variable value + * + * @param name name of the variable + * @return variable value + */ + Object getVariable(String name); + + /** + * Test if variable with name exists + * + * @param name name of the variable + * @return true if variable with name exists + */ + boolean hasVariable(String name); + + /** + * Delete temporary console variables + */ + void purge(); + + /** + * Execute widget function + * + * @param function to execute + * @return true on success + */ + boolean executeWidget(Object function); + + /** + * Checks if consoleEngine is executing script + * + * @return true when executing script + */ + boolean isExecuting(); + + class ExecutionResult { + final int status; + final Object result; + + public ExecutionResult(int status, Object result) { + this.status = status; + this.result = result; + } + + public int status() { + return status; + } + + public Object result() { + return result; + } + } + + class WidgetCreator implements Widget { + private final ConsoleEngine consoleEngine; + private final Object function; + private final String name; + + public WidgetCreator(ConsoleEngine consoleEngine, String function) { + this.consoleEngine = consoleEngine; + this.name = function; + this.function = consoleEngine.getVariable(function); + } + + @Override + public boolean apply() { + return consoleEngine.executeWidget(function); + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/net-cli/src/main/java/org/jline/console/Printer.java b/net-cli/src/main/java/org/jline/console/Printer.java new file mode 100644 index 0000000..c2559c3 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/Printer.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2002-2022, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Print object to the console. + * + * @author Matti Rinta-Nikkola + */ +public interface Printer { + /** + * Value: Boolean
+ * Applies: TABLE
+ * Ignore columnsOut configuration. + */ + String ALL = "all"; + // + // option names + // + // 1) command options + // + /** + * Value: {@code List}
+ * Applies: MAP and TABLE
+ * Display given keys/columns on map/table. + */ + String COLUMNS = "columns"; + /** + * Value: {@code List}
+ * Applies: TABLE
+ * Exclude given columns on table. + */ + String EXCLUDE = "exclude"; + /** + * Value: {@code List}
+ * Applies: TABLE
+ * Include given columns on table. + */ + String INCLUDE = "include"; + /** + * Value: Integer
+ * Applies: MAP
+ * Indention size. + */ + String INDENTION = "indention"; + /** + * Value: Integer
+ * Applies: MAP and TABLE
+ * Maximum column width. + */ + String MAX_COLUMN_WIDTH = "maxColumnWidth"; + /** + * Value: Integer
+ * Applies: MAP
+ * Maximum depth objects are resolved. + */ + String MAX_DEPTH = "maxDepth"; + /** + * Value: Integer
+ * Applies: MAP and TABLE
+ * Maximum number of lines to display. + */ + String MAXROWS = "maxrows"; + /** + * Value: Boolean
+ * Applies: TABLE
+ * Display one row data on table. + */ + String ONE_ROW_TABLE = "oneRowTable"; + /** + * Value: Boolean
+ * Applies: TABLE
+ * Display table row numbers. + */ + String ROWNUM = "rownum"; + /** + * Value: Boolean
+ * Applies: TABLE
+ * Truncate table column names: property.field to field. + */ + String SHORT_NAMES = "shortNames"; + /** + * Value: Boolean
+ * Applies: MAP and TABLE
+ * Ignore all options defined in PRNT_OPTIONS. + */ + String SKIP_DEFAULT_OPTIONS = "skipDefaultOptions"; + /** + * Value: Boolean
+ * Applies: TABLE
+ * Display object structures and lists on table. + */ + String STRUCT_ON_TABLE = "structsOnTable"; + /** + * Value: String
+ * Use nanorc STYLE
+ */ + String STYLE = "style"; + /** + * Value: Boolean
+ * Applies: MAP and TABLE
+ * Use object's toString() method to get print value + * DEFAULT: object's fields are put to property map before printing + */ + String TO_STRING = "toString"; + /** + * Value: String
+ * Applies: MAP and TABLE
+ * Nanorc syntax style used to highlight values. + */ + String VALUE_STYLE = "valueStyle"; + /** + * Value: Integer
+ * Applies: MAP and TABLE
+ * Display width (default terminal width). + */ + String WIDTH = "width"; + /** + * Value: String
+ * Applies: TABLE
+ * Table cell vertical border character. + */ + String BORDER = "border"; + /** + * Value: TableRows
+ * Applies: TABLE
+ * Highlight table rows. + */ + String ROW_HIGHLIGHT = "rowHighlight"; + /** + * Value: {@code List}
+ * Applies: TABLE
+ * These map values will be added to the table before all the other keys. + */ + String COLUMNS_IN = "columnsIn"; + // + // 2) additional PRNT_OPTIONS + // + /** + * Value: {@code List}
+ * Applies: TABLE
+ * These map values will not be inserted to the table. + */ + String COLUMNS_OUT = "columnsOut"; + /** + * Value: {@code Map}.
+ * Applies: TABLE
+ * If command result map key matches with regex the highlight function is applied + * to the corresponding map value. The regex = * is processed after all the other regexes and the highlight + * function will be applied to all map values that have not been already highlighted. + */ + String HIGHLIGHT_VALUE = "highlightValue"; + /** + * Value: Double
+ * Applies: MAP and TABLE
+ * default value 0.8 i.e. if at least of 4 of the 5 results map keys match with reference key set the + * result will be printed out as a table. + */ + String MAP_SIMILARITY = "mapSimilarity"; + /** + * Value: {@code Map}
+ * Applies: MAP and TABLE
+ * Overrides the ScriptEngine toMap() method. + */ + String OBJECT_TO_MAP = "objectToMap"; + /** + * Value: {@code Map}
+ * Applies: MAP and TABLE
+ * Overrides the ScriptEngine toString() method. + */ + String OBJECT_TO_STRING = "objectToString"; + /** + * Value: Boolean
+ * Applies: MAP and TABLE
+ * Highlight everything also strings with spaces + * DEFAULT: highlight only strings without spaces or enclosed by quotes or brackets + */ + String VALUE_STYLE_ALL = "valueStyleAll"; + /** + * Value: Boolean
+ * Applies: TABLE
+ * List the collection of simple values in multiple columns + * DEFAULT: list values in one column + */ + String MULTI_COLUMNS = "multiColumns"; + List BOOLEAN_KEYS = Arrays.asList( + ALL, + ONE_ROW_TABLE, + ROWNUM, + SHORT_NAMES, + SKIP_DEFAULT_OPTIONS, + STRUCT_ON_TABLE, + TO_STRING, + VALUE_STYLE_ALL, + MULTI_COLUMNS); + + default void println(Object object) { + println(new HashMap<>(), object); + } + + void println(Map options, Object object); + + default Exception prntCommand(CommandInput input) { + return null; + } + + /** + * Clear printer syntax highlighter cache + */ + boolean refresh(); + + enum TableRows { + EVEN, + ODD, + ALL + } +} diff --git a/net-cli/src/main/java/org/jline/console/ScriptEngine.java b/net-cli/src/main/java/org/jline/console/ScriptEngine.java new file mode 100644 index 0000000..e9b941d --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/ScriptEngine.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.jline.reader.Completer; + +/** + * Manage scriptEngine variables, statements and script execution. + * + * @author Matti Rinta-Nikkola + */ +public interface ScriptEngine { + + /** + * @return scriptEngine name + */ + String getEngineName(); + + /** + * @return script file name extensions + */ + Collection getExtensions(); + + /** + * @return script tab completer + */ + Completer getScriptCompleter(); + + /** + * Tests if console variable exists + * + * @param name variable name + * @return true if variable exists + */ + boolean hasVariable(String name); + + /** + * Creates variable + * + * @param name variable name + * @param value value + */ + void put(String name, Object value); + + /** + * Gets variable value + * + * @param name variable name + * @return value of the variable + */ + Object get(String name); + + /** + * Gets all variables with values + * + * @return map of the variables + */ + default Map find() { + return find(null); + } + + /** + * Gets all the variables that match the name. Name can contain * wild cards. + * + * @param name variable name + * @return map the variables + */ + Map find(String name); + + /** + * Deletes variables. Variable name can contain * wild cards. + * + * @param vars variables to be deleted + */ + void del(String... vars); + + /** + * Serialize object to JSON string. + * + * @param object object to serialize to JSON + * @return formatted JSON string + */ + String toJson(Object object); + + /** + * Converts object to string. + * + * @param object the object + * @return object string value + */ + String toString(Object object); + + /** + * Converts object fields to map. + * + * @param object the object + * @return object fields map + */ + Map toMap(Object object); + + /** + * Deserialize value + * + * @param value the value + * @return deserialized value + */ + default Object deserialize(String value) { + return deserialize(value, null); + } + + /** + * Deserialize value + * + * @param value the value + * @param format serialization format + * @return deserialized value + */ + Object deserialize(String value, String format); + + /** + * @return Supported serialization formats + */ + List getSerializationFormats(); + + /** + * @return Supported deserialization formats + */ + List getDeserializationFormats(); + + /** + * Persists object value to file. + * + * @param file file + * @param object object + */ + void persist(Path file, Object object); + + /** + * Persists object value to file. + * + * @param file the file + * @param object the object + * @param format serialization format + */ + void persist(Path file, Object object, String format); + + /** + * Executes scriptEngine statement + * + * @param statement the statement + * @return result + * @throws Exception in case of error + */ + Object execute(String statement) throws Exception; + + /** + * Executes scriptEngine script + * + * @param script the script + * @return result + * @throws Exception in case of error + */ + default Object execute(File script) throws Exception { + return execute(script, null); + } + + /** + * Executes scriptEngine script + * + * @param script the script + * @param args arguments + * @return result + * @throws Exception in case of error + */ + Object execute(File script, Object[] args) throws Exception; + + /** + * Executes scriptEngine closure + * + * @param closure closure + * @param args arguments + * @return result + */ + Object execute(Object closure, Object... args); +} diff --git a/net-cli/src/main/java/org/jline/console/SystemRegistry.java b/net-cli/src/main/java/org/jline/console/SystemRegistry.java new file mode 100644 index 0000000..8214bc5 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/SystemRegistry.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console; + +import java.io.File; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.jline.builtins.ConsoleOptionGetter; +import org.jline.reader.Completer; +import org.jline.reader.ParsedLine; +import org.jline.terminal.Terminal; + +/** + * Aggregate command registries and dispatch command executions. + * + * @author Matti Rinta-Nikkola + */ +public interface SystemRegistry extends CommandRegistry, ConsoleOptionGetter { + + /** + * @return systemRegistry of the current thread + */ + static SystemRegistry get() { + return Registeries.getInstance().getSystemRegistry(); + } + + /** + * Add systemRegistry to the thread map + * + * @param systemRegistry the systemRegistry + */ + static void add(SystemRegistry systemRegistry) { + Registeries.getInstance().addRegistry(systemRegistry); + } + + /** + * Remove systemRegistry of the current thread from the thread map + */ + static void remove() { + Registeries.getInstance().removeRegistry(); + } + + /** + * Set command registries + * + * @param commandRegistries command registries used by the application + */ + void setCommandRegistries(CommandRegistry... commandRegistries); + + /** + * Register subcommand registry + * + * @param command main command + * @param subcommandRegistry subcommand registry + */ + void register(String command, CommandRegistry subcommandRegistry); + + /** + * Initialize consoleEngine environment by executing console script + * + * @param script initialization script + */ + void initialize(File script); + + /** + * @return pipe names defined in systemRegistry + */ + Collection getPipeNames(); + + /** + * Returns command completer that includes also console variable and script completion. + * + * @return command completer + */ + Completer completer(); + + /** + * Returns a command, method or syntax description for use in the JLine Widgets framework. + * + * @param line command line whose description to return + * @return command description for JLine TailTipWidgets to be displayed + * in the terminal status bar. + */ + CmdDesc commandDescription(CmdLine line); + + /** + * Execute a command, script or evaluate scriptEngine statement + * + * @param line command line to be executed + * @return execution result + * @throws Exception in case of error + */ + Object execute(String line) throws Exception; + + /** + * Delete temporary console variables and reset output streams + */ + void cleanUp(); + + /** + * Print exception on terminal + * + * @param exception exception to print on terminal + */ + void trace(Throwable exception); + + /** + * Print exception on terminal + * + * @param stack print stack trace if stack true otherwise message + * @param exception exception to be printed + */ + void trace(boolean stack, Throwable exception); + + /** + * Return console option value + * + * @param name the option name + * @return option value + */ + Object consoleOption(String name); + + /** + * Return console option value + * + * @param name the option name + * @param defVal value to return if console option does not exists + * @return option value + */ + T consoleOption(String name, T defVal); + + /** + * Set console option value + * + * @param name the option name + * @param value value to assign console option + */ + void setConsoleOption(String name, Object value); + + /** + * @return terminal + */ + Terminal terminal(); + + /** + * Execute command with arguments + * + * @param command command to be executed + * @param args arguments of the command + * @return command execution result + * @throws Exception in case of error + */ + Object invoke(String command, Object... args) throws Exception; + + /** + * Returns whether a line contains command/script that is known to this registry. + * + * @param line the parsed command line to test + * @return true if the specified line has a command registered + */ + boolean isCommandOrScript(ParsedLine line); + + /** + * Returns whether command is known to this registry. + * + * @param command the command to test + * @return true if the specified command is known + */ + boolean isCommandOrScript(String command); + + /** + * Returns whether alias is known command alias. + * + * @param alias the alias to test + * @return true if the alias is known command alias + */ + boolean isCommandAlias(String alias); + + /** + * Orderly close SystemRegistry. + */ + void close(); + + /** + * Manage systemRegistry store + */ + class Registeries { + private static final Registeries instance = new Registeries(); + private final Map systemRegisteries = new HashMap<>(); + + private Registeries() { + } + + protected static Registeries getInstance() { + return instance; + } + + // TODO: Thread.getId() should be replaced with Thread.threadId() when minimum is JDK >= 19 + @SuppressWarnings("deprecation") + private static long getThreadId() { + return Thread.currentThread().getId(); + } + + protected void addRegistry(SystemRegistry systemRegistry) { + systemRegisteries.put(getThreadId(), systemRegistry); + } + + protected SystemRegistry getSystemRegistry() { + return systemRegisteries.getOrDefault(getThreadId(), null); + } + + protected void removeRegistry() { + systemRegisteries.remove(getThreadId()); + } + } +} diff --git a/net-cli/src/main/java/org/jline/console/impl/AbstractCommandRegistry.java b/net-cli/src/main/java/org/jline/console/impl/AbstractCommandRegistry.java new file mode 100644 index 0000000..e315f71 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/impl/AbstractCommandRegistry.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.jline.console.CmdDesc; +import org.jline.console.CommandInput; +import org.jline.console.CommandMethods; +import org.jline.console.CommandRegistry; +import org.jline.reader.impl.completer.SystemCompleter; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; + +/** + * CommandRegistry common methods. + * + * @author Matti Rinta-Nikkola + */ +public abstract class AbstractCommandRegistry implements CommandRegistry { + private CmdRegistry cmdRegistry; + private Exception exception; + + public AbstractCommandRegistry() { + } + + public CmdDesc doHelpDesc(String command, List info, CmdDesc cmdDesc) { + List mainDesc = new ArrayList<>(); + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(command.toLowerCase()).append(" - "); + for (String s : info) { + if (asb.length() == 0) { + asb.append("\t"); + } + asb.append(s); + mainDesc.add(asb.toAttributedString()); + asb = new AttributedStringBuilder(); + asb.tabs(2); + } + asb = new AttributedStringBuilder(); + asb.tabs(7); + asb.append("Usage:"); + for (AttributedString as : cmdDesc.getMainDesc()) { + asb.append("\t"); + asb.append(as); + mainDesc.add(asb.toAttributedString()); + asb = new AttributedStringBuilder(); + asb.tabs(7); + } + return new CmdDesc(mainDesc, new ArrayList<>(), cmdDesc.getOptsDesc()); + } + + public > void registerCommands( + Map commandName, Map commandExecute) { + cmdRegistry = new EnumCmdRegistry<>(commandName, commandExecute); + } + + public void registerCommands(Map commandExecute) { + cmdRegistry = new NameCmdRegistry(commandExecute); + } + + @Override + public Object invoke(CommandSession session, String command, Object... args) throws Exception { + exception = null; + CommandMethods methods = getCommandMethods(command); + Object out = methods.execute().apply(new CommandInput(command, args, session)); + if (exception != null) { + throw exception; + } + return out; + } + + public void saveException(Exception exception) { + this.exception = exception; + } + + @Override + public boolean hasCommand(String command) { + return cmdRegistry.hasCommand(command); + } + + @Override + public Set commandNames() { + return cmdRegistry.commandNames(); + } + + @Override + public Map commandAliases() { + return cmdRegistry.commandAliases(); + } + + public > void rename(V command, String newName) { + cmdRegistry.rename(command, newName); + } + + public void alias(String alias, String command) { + cmdRegistry.alias(alias, command); + } + + @Override + public SystemCompleter compileCompleters() { + return cmdRegistry.compileCompleters(); + } + + public CommandMethods getCommandMethods(String command) { + return cmdRegistry.getCommandMethods(command); + } + + public Object registeredCommand(String command) { + return cmdRegistry.command(command); + } + + private interface CmdRegistry { + boolean hasCommand(String command); + + Set commandNames(); + + Map commandAliases(); + + Object command(String command); + + > void rename(V command, String newName); + + void alias(String alias, String command); + + SystemCompleter compileCompleters(); + + CommandMethods getCommandMethods(String command); + } + + private static class EnumCmdRegistry> implements CmdRegistry { + private final Map commandName; + private final Map commandExecute; + private final Map aliasCommand = new HashMap<>(); + private Map nameCommand = new HashMap<>(); + + public EnumCmdRegistry(Map commandName, Map commandExecute) { + this.commandName = commandName; + this.commandExecute = commandExecute; + doNameCommand(); + } + + private void doNameCommand() { + nameCommand = + commandName.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); + } + + public Set commandNames() { + return nameCommand.keySet(); + } + + public Map commandAliases() { + return aliasCommand; + } + + @SuppressWarnings("unchecked") + public > void rename(V command, String newName) { + if (nameCommand.containsKey(newName)) { + throw new IllegalArgumentException("Duplicate command name!"); + } else if (!commandName.containsKey(command)) { + throw new IllegalArgumentException("Command does not exists!"); + } + commandName.put((T) command, newName); + doNameCommand(); + } + + public void alias(String alias, String command) { + if (!nameCommand.containsKey(command)) { + throw new IllegalArgumentException("Command does not exists!"); + } + aliasCommand.put(alias, command); + } + + public boolean hasCommand(String name) { + return nameCommand.containsKey(name) || aliasCommand.containsKey(name); + } + + public SystemCompleter compileCompleters() { + SystemCompleter out = new SystemCompleter(); + for (Map.Entry entry : commandName.entrySet()) { + out.add( + entry.getValue(), + commandExecute.get(entry.getKey()).compileCompleter().apply(entry.getValue())); + } + out.addAliases(aliasCommand); + return out; + } + + public T command(String name) { + T out; + name = aliasCommand.getOrDefault(name, name); + if (nameCommand.containsKey(name)) { + out = nameCommand.get(name); + } else { + throw new IllegalArgumentException("Command does not exists!"); + } + return out; + } + + public CommandMethods getCommandMethods(String command) { + return commandExecute.get(command(command)); + } + } + + private static class NameCmdRegistry implements CmdRegistry { + private final Map commandExecute; + private final Map aliasCommand = new HashMap<>(); + + public NameCmdRegistry(Map commandExecute) { + this.commandExecute = commandExecute; + } + + public Set commandNames() { + return commandExecute.keySet(); + } + + public Map commandAliases() { + return aliasCommand; + } + + public > void rename(V command, String newName) { + throw new IllegalArgumentException(); + } + + public void alias(String alias, String command) { + if (!commandExecute.containsKey(command)) { + throw new IllegalArgumentException("Command does not exists!"); + } + aliasCommand.put(alias, command); + } + + public boolean hasCommand(String name) { + return commandExecute.containsKey(name) || aliasCommand.containsKey(name); + } + + public SystemCompleter compileCompleters() { + SystemCompleter out = new SystemCompleter(); + for (String c : commandExecute.keySet()) { + out.add(c, commandExecute.get(c).compileCompleter().apply(c)); + } + out.addAliases(aliasCommand); + return out; + } + + public String command(String name) { + if (commandExecute.containsKey(name)) { + return name; + } + return aliasCommand.get(name); + } + + public CommandMethods getCommandMethods(String command) { + return commandExecute.get(command(command)); + } + } +} diff --git a/net-cli/src/main/java/org/jline/console/impl/Builtins.java b/net-cli/src/main/java/org/jline/console/impl/Builtins.java new file mode 100644 index 0000000..c356cbb --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/impl/Builtins.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2002-2022, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console.impl; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import org.jline.builtins.Commands; +import org.jline.builtins.Completers.FilesCompleter; +import org.jline.builtins.Completers.OptDesc; +import org.jline.builtins.Completers.OptionCompleter; +import org.jline.builtins.ConfigurationPath; +import org.jline.builtins.TTop; +import org.jline.console.CommandInput; +import org.jline.console.CommandMethods; +import org.jline.console.CommandRegistry; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.LineReader.Option; +import org.jline.reader.Widget; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.reader.impl.completer.StringsCompleter; + +/** + * Builtins: create tab completers, execute and create descriptions for builtins commands. + * + * @author Matti Rinta-Nikkola + */ +public class Builtins extends JlineCommandRegistry implements CommandRegistry { + private final ConfigurationPath configPath; + private final Function widgetCreator; + private final Supplier workDir; + private LineReader reader; + public Builtins(Path workDir, ConfigurationPath configPath, Function widgetCreator) { + this(null, () -> workDir, configPath, widgetCreator); + } + + public Builtins( + Set commands, Path workDir, ConfigurationPath configpath, Function widgetCreator) { + this(commands, () -> workDir, configpath, widgetCreator); + } + + public Builtins(Supplier workDir, ConfigurationPath configPath, Function widgetCreator) { + this(null, workDir, configPath, widgetCreator); + } + + @SuppressWarnings("this-escape") + public Builtins( + Set commands, + Supplier workDir, + ConfigurationPath configpath, + Function widgetCreator) { + super(); + Objects.requireNonNull(configpath); + this.configPath = configpath; + this.widgetCreator = widgetCreator; + this.workDir = workDir; + Set cmds; + Map commandName = new HashMap<>(); + Map commandExecute = new HashMap<>(); + if (commands == null) { + cmds = new HashSet<>(EnumSet.allOf(Command.class)); + } else { + cmds = new HashSet<>(commands); + } + for (Command c : cmds) { + commandName.put(c, c.name().toLowerCase()); + } + commandExecute.put(Command.LESS, new CommandMethods(this::less, this::lessCompleter)); + commandExecute.put(Command.HISTORY, new CommandMethods(this::history, this::historyCompleter)); + commandExecute.put(Command.WIDGET, new CommandMethods(this::widget, this::widgetCompleter)); + commandExecute.put(Command.KEYMAP, new CommandMethods(this::keymap, this::defaultCompleter)); + commandExecute.put(Command.SETOPT, new CommandMethods(this::setopt, this::setoptCompleter)); + commandExecute.put(Command.SETVAR, new CommandMethods(this::setvar, this::setvarCompleter)); + commandExecute.put(Command.UNSETOPT, new CommandMethods(this::unsetopt, this::unsetoptCompleter)); + commandExecute.put(Command.TTOP, new CommandMethods(this::ttop, this::defaultCompleter)); + commandExecute.put(Command.COLORS, new CommandMethods(this::colors, this::defaultCompleter)); + registerCommands(commandName, commandExecute); + } + + public void setLineReader(LineReader reader) { + this.reader = reader; + } + + private void less(CommandInput input) { + try { + Commands.less( + input.terminal(), input.in(), input.out(), input.err(), workDir.get(), input.xargs(), configPath); + } catch (Exception e) { + saveException(e); + } + } + + private void history(CommandInput input) { + try { + Commands.history(reader, input.out(), input.err(), workDir.get(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void widget(CommandInput input) { + try { + Commands.widget(reader, input.out(), input.err(), widgetCreator, input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void keymap(CommandInput input) { + try { + Commands.keymap(reader, input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void setopt(CommandInput input) { + try { + Commands.setopt(reader, input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void setvar(CommandInput input) { + try { + Commands.setvar(reader, input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void unsetopt(CommandInput input) { + try { + Commands.unsetopt(reader, input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void ttop(CommandInput input) { + try { + TTop.ttop(input.terminal(), input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void colors(CommandInput input) { + try { + Commands.colors(input.terminal(), input.out(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private List unsetOptions(boolean set) { + List out = new ArrayList<>(); + for (Option option : Option.values()) { + if (set == (reader.isSet(option) == option.isDef())) { + out.add((option.isDef() ? "no-" : "") + + option.toString().toLowerCase().replace('_', '-')); + } + } + return out; + } + + private List highlighterCompleter(String name) { + List completers = new ArrayList<>(); + List optDescs = commandOptions(name); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, new OptionCompleter(NullCompleter.INSTANCE, optDescs, 1))); + return completers; + } + + private Set allWidgets() { + Set out = new HashSet<>(); + for (String s : reader.getWidgets().keySet()) { + out.add(s); + out.add(reader.getWidgets().get(s).toString()); + } + return out; + } + + private List lessCompleter(String name) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, new OptionCompleter(new FilesCompleter(workDir), this::commandOptions, 1))); + return completers; + } + + private List historyCompleter(String name) { + List completers = new ArrayList<>(); + List optDescs = commandOptions(name); + for (OptDesc o : optDescs) { + if (o.shortOption() != null + && (o.shortOption().equals("-A") + || o.shortOption().equals("-W") + || o.shortOption().equals("-R"))) { + o.setValueCompleter(new FilesCompleter(workDir)); + } + } + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, new OptionCompleter(NullCompleter.INSTANCE, optDescs, 1))); + return completers; + } + + private List widgetCompleter(String name) { + List completers = new ArrayList<>(); + List optDescs = commandOptions(name); + Candidate aliasOption = new Candidate("-A", "-A", null, null, null, null, true); + Iterator i = optDescs.iterator(); + while (i.hasNext()) { + OptDesc o = i.next(); + if (o.shortOption() != null) { + if (o.shortOption().equals("-D")) { + o.setValueCompleter( + new StringsCompleter(() -> reader.getWidgets().keySet())); + } else if (o.shortOption().equals("-A")) { + aliasOption = + new Candidate(o.shortOption(), o.shortOption(), null, o.description(), null, null, true); + i.remove(); + } + } + } + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, new OptionCompleter(NullCompleter.INSTANCE, optDescs, 1))); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, + new StringsCompleter(aliasOption), + new StringsCompleter(this::allWidgets), + new StringsCompleter(() -> reader.getWidgets().keySet()), + NullCompleter.INSTANCE)); + return completers; + } + + private List setvarCompleter(String name) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, + new StringsCompleter(() -> reader.getVariables().keySet()), + NullCompleter.INSTANCE)); + return completers; + } + + private List setoptCompleter(String name) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE, new StringsCompleter(() -> unsetOptions(true)))); + return completers; + } + + private List unsetoptCompleter(String name) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE, new StringsCompleter(() -> unsetOptions(false)))); + return completers; + } + + public enum Command { + LESS, + HISTORY, + WIDGET, + KEYMAP, + SETOPT, + SETVAR, + UNSETOPT, + TTOP, + COLORS + } +} diff --git a/net-cli/src/main/java/org/jline/console/impl/ConsoleEngineImpl.java b/net-cli/src/main/java/org/jline/console/impl/ConsoleEngineImpl.java new file mode 100644 index 0000000..531de1e --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/impl/ConsoleEngineImpl.java @@ -0,0 +1,1389 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console.impl; + +import java.awt.Desktop; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jline.builtins.Completers.FilesCompleter; +import org.jline.builtins.Completers.OptDesc; +import org.jline.builtins.Completers.OptionCompleter; +import org.jline.builtins.ConfigurationPath; +import org.jline.builtins.Options; +import org.jline.builtins.Options.HelpException; +import org.jline.builtins.Styles; +import org.jline.console.CommandInput; +import org.jline.console.CommandMethods; +import org.jline.console.CommandRegistry; +import org.jline.console.ConsoleEngine; +import org.jline.console.Printer; +import org.jline.console.ScriptEngine; +import org.jline.console.SystemRegistry; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.EOFError; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser; +import org.jline.reader.Parser.ParseContext; +import org.jline.reader.SyntaxError; +import org.jline.reader.impl.completer.AggregateCompleter; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.Log; +import org.jline.utils.OSUtils; + +/** + * Manage console variables, commands and script execution. + * + * @author Matti Rinta-Nikkola + */ +public class ConsoleEngineImpl extends JlineCommandRegistry implements ConsoleEngine { + private static final String VAR_CONSOLE_OPTIONS = "CONSOLE_OPTIONS"; + private static final String VAR_PATH = "PATH"; + private static final String[] OPTION_HELP = {"-?", "--help"}; + private static final String OPTION_VERBOSE = "-v"; + private static final String SLURP_FORMAT_TEXT = "TEXT"; + private static final String END_HELP = "END_HELP"; + private static final int HELP_MAX_SIZE = 30; + private final ScriptEngine engine; + private final Supplier workDir; + private final Map aliases = new HashMap<>(); + private final Map> pipes = new HashMap<>(); + private final Printer printer; + private Exception exception; + private SystemRegistry systemRegistry; + private String scriptExtension = "jline"; + private Path aliasFile; + private LineReader reader; + private boolean executing = false; + public ConsoleEngineImpl(ScriptEngine engine, Printer printer, Supplier workDir, ConfigurationPath configPath) + throws IOException { + this(null, engine, printer, workDir, configPath); + } + + @SuppressWarnings({"unchecked", "this-escape"}) + public ConsoleEngineImpl( + Set commands, + ScriptEngine engine, + Printer printer, + Supplier workDir, + ConfigurationPath configPath) + throws IOException { + super(); + this.engine = engine; + this.workDir = workDir; + this.printer = printer; + Map commandName = new HashMap<>(); + Map commandExecute = new HashMap<>(); + Set cmds; + if (commands == null) { + cmds = new HashSet<>(EnumSet.allOf(Command.class)); + } else { + cmds = new HashSet<>(commands); + } + for (Command c : cmds) { + commandName.put(c, c.name().toLowerCase()); + } + commandExecute.put(Command.DEL, new CommandMethods(this::del, this::variableCompleter)); + commandExecute.put(Command.SHOW, new CommandMethods(this::show, this::variableCompleter)); + commandExecute.put(Command.PRNT, new CommandMethods(this::prnt, this::prntCompleter)); + commandExecute.put(Command.SLURP, new CommandMethods(this::slurpcmd, this::slurpCompleter)); + commandExecute.put(Command.ALIAS, new CommandMethods(this::aliascmd, this::aliasCompleter)); + commandExecute.put(Command.UNALIAS, new CommandMethods(this::unalias, this::unaliasCompleter)); + commandExecute.put(Command.DOC, new CommandMethods(this::doc, this::docCompleter)); + commandExecute.put(Command.PIPE, new CommandMethods(this::pipe, this::defaultCompleter)); + aliasFile = configPath.getUserConfig("aliases.json"); + if (aliasFile == null) { + aliasFile = configPath.getUserConfig("aliases.json", true); + if (aliasFile == null) { + Log.warn("Failed to write in user config path!"); + aliasFile = OSUtils.IS_WINDOWS ? Paths.get("NUL") : Paths.get("/dev/null"); + } + persist(aliasFile, aliases); + } else { + aliases.putAll((Map) slurp(aliasFile)); + } + registerCommands(commandName, commandExecute); + } + + @Override + public void setLineReader(LineReader reader) { + this.reader = reader; + } + + private Parser parser() { + return reader.getParser(); + } + + private Terminal terminal() { + return systemRegistry.terminal(); + } + + public boolean isExecuting() { + return executing; + } + + @Override + public void setSystemRegistry(SystemRegistry systemRegistry) { + this.systemRegistry = systemRegistry; + } + + @Override + public void setScriptExtension(String extension) { + this.scriptExtension = extension; + } + + @Override + public boolean hasAlias(String name) { + return aliases.containsKey(name); + } + + @Override + public String getAlias(String name) { + return aliases.getOrDefault(name, null); + } + + @Override + public Map> getPipes() { + return pipes; + } + + @Override + public List getNamedPipes() { + List out = new ArrayList<>(); + List opers = new ArrayList<>(); + for (String p : pipes.keySet()) { + if (p.matches("[a-zA-Z0-9]+")) { + out.add(p); + } else { + opers.add(p); + } + } + opers.addAll(systemRegistry.getPipeNames()); + for (Map.Entry entry : aliases.entrySet()) { + if (opers.contains(entry.getValue().split(" ")[0])) { + out.add(entry.getKey()); + } + } + return out; + } + + @Override + public List scriptCompleters() { + List out = new ArrayList<>(); + out.add(new ArgumentCompleter( + new StringsCompleter(this::scriptNames), + new OptionCompleter(NullCompleter.INSTANCE, this::commandOptions, 1))); + out.add(new ArgumentCompleter(new StringsCompleter(this::commandAliasNames), NullCompleter.INSTANCE)); + return out; + } + + private Set commandAliasNames() { + Set opers = + pipes.keySet().stream().filter(p -> !p.matches("\\w+")).collect(Collectors.toSet()); + opers.addAll(systemRegistry.getPipeNames()); + return aliases.entrySet().stream() + .filter(e -> !opers.contains(e.getValue().split(" ")[0])) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + private Set scriptNames() { + return scripts().keySet(); + } + + @SuppressWarnings("unchecked") + @Override + public Map scripts() { + Map out = new HashMap<>(); + try { + List scripts = new ArrayList<>(); + if (engine.hasVariable(VAR_PATH)) { + List dirs = new ArrayList<>(); + for (String file : (List) engine.get(VAR_PATH)) { + file = file.startsWith("~") ? file.replace("~", System.getProperty("user.home")) : file; + File dir = new File(file); + if (dir.exists() && dir.isDirectory()) { + dirs.add(file); + } + } + for (String pp : dirs) { + for (String e : scriptExtensions()) { + String regex = pp + "/*." + e; + PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + regex); + try (Stream pathStream = + Files.walk(new File(regex).getParentFile().toPath())) { + pathStream.filter(pathMatcher::matches).forEach(scripts::add); + } + } + } + } + for (Path p : scripts) { + String name = p.toFile().getName(); + int idx = name.lastIndexOf("."); + out.put(name.substring(0, idx), name.substring(idx + 1).equals(scriptExtension)); + } + } catch (NoSuchFileException e) { + error("Failed reading PATH. No file found: " + e.getMessage()); + } catch (InvalidPathException e) { + error("Failed reading PATH. Invalid path:"); + error(e.toString()); + } catch (Exception e) { + error("Failed reading PATH:"); + trace(e); + engine.put("exception", e); + } + return out; + } + + @Override + public Object[] expandParameters(String[] args) throws Exception { + Object[] out = new Object[args.length]; + String regexPath = "(.*)\\$\\{(.*?)}(/.*)"; + for (int i = 0; i < args.length; i++) { + if (args[i].matches(regexPath)) { + Matcher matcher = Pattern.compile(regexPath).matcher(args[i]); + if (matcher.find()) { + out[i] = matcher.group(1) + engine.get(matcher.group(2)) + matcher.group(3); + } else { + throw new IllegalArgumentException(); + } + } else if (args[i].startsWith("${")) { + out[i] = engine.execute(expandName(args[i])); + } else if (args[i].startsWith("$")) { + out[i] = engine.get(expandName(args[i])); + } else { + out[i] = engine.deserialize(args[i]); + } + } + return out; + } + + private String expandToList(String[] args) { + return expandToList(Arrays.asList(args)); + } + + @Override + public String expandToList(List params) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + boolean first = true; + for (String param : params) { + if (!first) { + sb.append(","); + } + if (param.equalsIgnoreCase("true") || param.equalsIgnoreCase("false") || param.equalsIgnoreCase("null")) { + sb.append(param.toLowerCase()); + } else if (isNumber(param)) { + sb.append(param); + } else { + sb.append(param.startsWith("$") ? param.substring(1) : quote(param)); + } + first = false; + } + sb.append("]"); + return sb.toString(); + } + + private String expandName(String name) { + String regexVar = "[a-zA-Z_]+[a-zA-Z0-9_-]*"; + String out = name; + if (name.matches("^\\$" + regexVar)) { + out = name.substring(1); + } else if (name.matches("^\\$\\{" + regexVar + "}.*")) { + Matcher matcher = Pattern.compile("^\\$\\{(" + regexVar + ")}(.*)").matcher(name); + if (matcher.find()) { + out = matcher.group(1) + matcher.group(2); + } else { + throw new IllegalArgumentException(); + } + } + return out; + } + + private boolean isNumber(String str) { + return str.matches("-?\\d+(\\.\\d+)?"); + } + + private boolean isCodeBlock(String line) { + return line.contains("\n") && line.trim().endsWith("}"); + } + + private boolean isCommandLine(String line) { + String command = parser().getCommand(line); + boolean out = false; + if (command != null && command.startsWith(":")) { + command = command.substring(1); + if (hasAlias(command)) { + command = getAlias(command); + } + if (systemRegistry.hasCommand(command)) { + out = true; + } else { + ScriptFile sf = new ScriptFile(command, "", new String[0]); + if (sf.isScript()) { + out = true; + } + } + } + return out; + } + + private String quote(String var) { + if ((var.startsWith("\"") && var.endsWith("\"")) || (var.startsWith("'") && var.endsWith("'"))) { + return var; + } + if (var.contains("\\\"")) { + return "'" + var + "'"; + } + return "\"" + var + "\""; + } + + private List scriptExtensions() { + List extensions = new ArrayList<>(engine.getExtensions()); + extensions.add(scriptExtension); + return extensions; + } + + @Override + public Object execute(File script, String cmdLine, String[] args) throws Exception { + ScriptFile file = new ScriptFile(script, cmdLine, args); + file.execute(); + return file.getResult(); + } + + @Override + public String expandCommandLine(String line) { + String out; + if (isCommandLine(line)) { + StringBuilder sb = new StringBuilder(); + List ws = parser().parse(line, 0, ParseContext.COMPLETE).words(); + int idx = ws.get(0).lastIndexOf(":"); + if (idx > 0) { + sb.append(ws.get(0), 0, idx); + } + String[] argv = new String[ws.size()]; + for (int i = 1; i < ws.size(); i++) { + argv[i] = ws.get(i); + if (argv[i].startsWith("${")) { + Matcher argvMatcher = Pattern.compile("\\$\\{(.*)}").matcher(argv[i]); + if (argvMatcher.find()) { + argv[i] = argv[i].replace(argv[i], argvMatcher.group(1)); + } + } else if (argv[i].startsWith("$")) { + argv[i] = argv[i].substring(1); + } else { + argv[i] = quote(argv[i]); + } + } + String cmd = hasAlias(ws.get(0).substring(idx + 1)) + ? getAlias(ws.get(0).substring(idx + 1)) + : ws.get(0).substring(idx + 1); + sb.append(SystemRegistry.class.getCanonicalName()) + .append(".get().invoke('") + .append(cmd) + .append("'"); + for (int i = 1; i < argv.length; i++) { + sb.append(", "); + sb.append(argv[i]); + } + sb.append(")"); + out = sb.toString(); + } else { + out = line; + } + return out; + } + + @Override + public Object execute(String cmd, String line, String[] args) throws Exception { + if (line.trim().startsWith("#")) { + return null; + } + Object out = null; + ScriptFile file = null; + if (parser().validCommandName(cmd)) { + file = new ScriptFile(cmd, line, args); + } else { + File f = new File(line.split("\\s+")[0]); + if (f.exists()) { + file = new ScriptFile(f, line, args); + } + } + if (file != null && file.execute()) { + out = file.getResult(); + } else { + line = line.trim(); + if (isCodeBlock(line)) { + StringBuilder sb = new StringBuilder(); + for (String s : line.split("\\r?\\n")) { + sb.append(expandCommandLine(s)); + sb.append("\n"); + } + line = sb.toString(); + } + if (engine.hasVariable(line)) { + out = engine.get(line); + } else if (parser().getVariable(line) == null) { + out = engine.execute(line); + engine.put("_", out); + } else { + engine.execute(line); + } + } + return out; + } + + @Override + public void purge() { + engine.del("_*"); + } + + @Override + public void putVariable(String name, Object value) { + engine.put(name, value); + } + + @Override + public Object getVariable(String name) { + if (!engine.hasVariable(name)) { + throw new IllegalArgumentException("Variable " + name + " does not exists!"); + } + return engine.get(name); + } + + @Override + public boolean hasVariable(String name) { + return engine.hasVariable(name); + } + + @Override + public boolean executeWidget(Object function) { + engine.put("_reader", reader); + engine.put("_widgetFunction", function); + try { + if (engine.getEngineName().equals("GroovyEngine")) { + engine.execute("def _buffer() {_reader.getBuffer()}"); + engine.execute("def _widget(w) {_reader.callWidget(w)}"); + } + engine.execute("_widgetFunction()"); + } catch (Exception e) { + trace(e); + return false; + } finally { + purge(); + } + return true; + } + + @SuppressWarnings("unchecked") + private Map consoleOptions() { + return engine.hasVariable(VAR_CONSOLE_OPTIONS) + ? (Map) engine.get(VAR_CONSOLE_OPTIONS) + : new HashMap<>(); + } + + @SuppressWarnings("unchecked") + @Override + public T consoleOption(String option, T defval) { + T out = defval; + try { + out = (T) consoleOptions().getOrDefault(option, defval); + } catch (Exception e) { + trace(new Exception("Bad CONSOLE_OPTION value: " + e.getMessage())); + } + return out; + } + + @Override + public void setConsoleOption(String name, Object value) { + consoleOptions().put(name, value); + } + + private boolean consoleOption(String option) { + boolean out = false; + try { + out = consoleOptions().containsKey(option); + } catch (Exception e) { + trace(new Exception("Bad CONSOLE_OPTION value: " + e.getMessage())); + } + return out; + } + + @Override + public ExecutionResult postProcess(String line, Object result, String output) { + ExecutionResult out; + Object _output = output != null && !output.trim().isEmpty() && !consoleOption("no-splittedOutput") + ? output.split("\\r?\\n") + : output; + String consoleVar = parser().getVariable(line); + if (consoleVar != null && result != null) { + engine.put("output", _output); + } + if (systemRegistry.hasCommand(parser().getCommand(line))) { + out = postProcess(line, consoleVar != null && result == null ? _output : result); + } else { + Object _result = result == null ? _output : result; + int status = saveResult(consoleVar, _result); + out = new ExecutionResult(status, consoleVar != null && !consoleVar.startsWith("_") ? null : _result); + } + return out; + } + + private ExecutionResult postProcess(String line, Object result) { + int status = 0; + Object out = result instanceof String && ((String) result).trim().isEmpty() ? null : result; + String consoleVar = parser().getVariable(line); + if (consoleVar != null) { + status = saveResult(consoleVar, result); + out = null; + } else if (!parser().getCommand(line).equals("show")) { + if (result != null) { + status = saveResult("_", result); + } else { + status = 1; + } + } + return new ExecutionResult(status, out); + } + + @Override + public ExecutionResult postProcess(Object result) { + return new ExecutionResult(saveResult(null, result), result); + } + + private int saveResult(String var, Object result) { + int out; + try { + engine.put("_executionResult", result); + if (var != null) { + if (var.contains(".") || var.contains("[")) { + engine.execute(var + " = _executionResult"); + } else { + engine.put(var, result); + } + } + out = (int) engine.execute("_executionResult ? 0 : 1"); + } catch (Exception e) { + trace(e); + out = 1; + } + return out; + } + + @Override + public Object invoke(CommandRegistry.CommandSession session, String command, Object... args) throws Exception { + exception = null; + Object out = null; + if (hasCommand(command)) { + out = getCommandMethods(command).execute().apply(new CommandInput(command, args, session)); + } else { + String[] _args = new String[args.length]; + for (int i = 0; i < args.length; i++) { + if (!(args[i] instanceof String)) { + throw new IllegalArgumentException(); + } + _args[i] = args[i].toString(); + } + ScriptFile sf = new ScriptFile(command, "", _args); + if (sf.execute()) { + out = sf.getResult(); + } + } + if (exception != null) { + throw exception; + } + return out; + } + + public void trace(final Object object) { + Object toPrint = object; + int level = consoleOption("trace", 0); + Map options = new HashMap<>(); + if (level < 2) { + options.put("exception", "message"); + } + if (level == 0) { + if (!(object instanceof Throwable)) { + toPrint = null; + } + } else if (level == 1) { + if (object instanceof SystemRegistryImpl.CommandData) { + toPrint = ((SystemRegistryImpl.CommandData) object).rawLine(); + } + } else if (level > 1) { + if (object instanceof SystemRegistryImpl.CommandData) { + toPrint = object.toString(); + } + } + printer.println(options, toPrint); + } + + private void error(String message) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.styled(Styles.prntStyle().resolve(".em"), message); + asb.println(terminal()); + } + + @Override + public void println(Object object) { + printer.println(object); + } + + private Object show(CommandInput input) { + final String[] usage = { + "show - list console variables", + "Usage: show [VARIABLE]", + " -? --help Displays command help", + }; + try { + parseOptions(usage, input.args()); + Map options = new HashMap<>(); + options.put(Printer.MAX_DEPTH, 0); + printer.println(options, engine.find(input.args().length > 0 ? input.args()[0] : null)); + } catch (Exception e) { + exception = e; + } + return null; + } + + private Object del(CommandInput input) { + final String[] usage = { + "del - delete console variables, methods, classes and imports", + "Usage: del [var1] ...", + " -? --help Displays command help", + }; + try { + parseOptions(usage, input.args()); + engine.del(input.args()); + } catch (Exception e) { + exception = e; + } + return null; + } + + private Object prnt(CommandInput input) { + Exception result = printer.prntCommand(input); + if (result != null) { + exception = result; + } + return null; + } + + private Object slurpcmd(CommandInput input) { + final String[] usage = { + "slurp - slurp file or string variable context to object", + "Usage: slurp [OPTIONS] file|variable", + " -? --help Displays command help", + " -e --encoding=ENCODING Encoding (default UTF-8)", + " -f --format=FORMAT Serialization format" + }; + Object out = null; + try { + Options opt = parseOptions(usage, input.xargs()); + if (!opt.args().isEmpty()) { + Object _arg = opt.argObjects().get(0); + if (!(_arg instanceof String arg)) { + throw new IllegalArgumentException( + "Invalid parameter type: " + _arg.getClass().getSimpleName()); + } + Charset encoding = + opt.isSet("encoding") ? Charset.forName(opt.get("encoding")) : StandardCharsets.UTF_8; + String format = opt.isSet("format") + ? opt.get("format") + : engine.getSerializationFormats().get(0); + try { + Path path = Paths.get(arg); + if (path.toFile().exists()) { + if (!format.equals(SLURP_FORMAT_TEXT)) { + out = slurp(path, encoding, format); + } else { + out = Files.readAllLines(Paths.get(arg), encoding); + } + } else { + if (!format.equals(SLURP_FORMAT_TEXT)) { + out = engine.deserialize(arg, format); + } else { + out = arg.split("\n"); + } + } + } catch (Exception e) { + out = engine.deserialize(arg, format); + } + } + } catch (Exception e) { + exception = e; + } + return out; + } + + @Override + public void persist(Path file, Object object) { + engine.persist(file, object); + } + + @Override + public Object slurp(Path file) throws IOException { + return slurp( + file, StandardCharsets.UTF_8, engine.getSerializationFormats().get(0)); + } + + private Object slurp(Path file, Charset encoding, String format) throws IOException { + byte[] encoded = Files.readAllBytes(file); + return engine.deserialize(new String(encoded, encoding), format); + } + + private Object aliascmd(CommandInput input) { + final String[] usage = { + "alias - create command alias", + "Usage: alias [ALIAS] [COMMANDLINE]", + " -? --help Displays command help" + }; + Object out = null; + try { + Options opt = parseOptions(usage, input.args()); + List args = opt.args(); + if (args.isEmpty()) { + out = aliases; + } else if (args.size() == 1) { + out = aliases.getOrDefault(args.get(0), null); + } else { + String alias = String.join(" ", args.subList(1, args.size())); + for (int j = 0; j < 10; j++) { + alias = alias.replaceAll("%" + j, "\\$" + j); + alias = alias.replaceAll("%\\{" + j + "}", "\\$\\{" + j + "\\}"); + alias = alias.replaceAll("%\\{" + j + ":-", "\\$\\{" + j + ":-"); + } + alias = alias.replaceAll("%@", "\\$@"); + alias = alias.replaceAll("%\\{@}", "\\$\\{@\\}"); + aliases.put(args.get(0), alias); + persist(aliasFile, aliases); + } + } catch (Exception e) { + exception = e; + } + return out; + } + + private Object unalias(CommandInput input) { + final String[] usage = { + "unalias - remove command alias", + "Usage: unalias [ALIAS...]", + " -? --help Displays command help" + }; + try { + Options opt = parseOptions(usage, input.args()); + for (String a : opt.args()) { + aliases.remove(a); + } + persist(aliasFile, aliases); + } catch (Exception e) { + exception = e; + } + return null; + } + + private Object pipe(CommandInput input) { + final String[] usage = { + "pipe - create/delete pipe operator", + "Usage: pipe [OPERATOR] [PREFIX] [POSTFIX]", + " pipe --list", + " pipe --delete [OPERATOR...]", + " -? --help Displays command help", + " -d --delete Delete pipe operators", + " -l --list List pipe operators", + }; + try { + Options opt = parseOptions(usage, input.args()); + Map options = new HashMap<>(); + if (opt.isSet("delete")) { + if (opt.args().size() == 1 && opt.args().get(0).equals("*")) { + pipes.clear(); + } else { + for (String p : opt.args()) { + pipes.remove(p.trim()); + } + } + } else if (opt.isSet("list") || opt.args().size() == 0) { + options.put(Printer.MAX_DEPTH, 0); + printer.println(options, pipes); + } else if (opt.args().size() != 3) { + exception = new IllegalArgumentException("Bad number of arguments!"); + } else if (systemRegistry.getPipeNames().contains(opt.args().get(0))) { + exception = new IllegalArgumentException("Reserved pipe operator"); + } else { + List fixes = new ArrayList<>(); + fixes.add(opt.args().get(1)); + fixes.add(opt.args().get(2)); + pipes.put(opt.args().get(0), fixes); + } + } catch (Exception e) { + exception = e; + } + return null; + } + + private Object doc(CommandInput input) { + final String[] usage = { + "doc - open document on browser", + "Usage: doc [OBJECT]", + " -? --help Displays command help" + }; + try { + parseOptions(usage, input.xargs()); + if (input.xargs().length == 0) { + return null; + } + if (!Desktop.isDesktopSupported()) { + throw new IllegalStateException("Desktop is not supported!"); + } + Map docs; + try { + docs = consoleOption("docs", null); + } catch (Exception e) { + Exception exception = new IllegalStateException("Bad documents configuration!"); + exception.addSuppressed(e); + throw exception; + } + if (docs == null) { + throw new IllegalStateException("No documents configuration!"); + } + boolean done = false; + Object arg = input.xargs()[0]; + if (arg instanceof String) { + String address = (String) docs.get(input.args()[0]); + if (address != null) { + done = true; + if (urlExists(address)) { + Desktop.getDesktop().browse(new URI(address)); + } else { + throw new IllegalArgumentException("Document not found: " + address); + } + } + } + if (!done) { + String name; + if (arg instanceof String && ((String) arg).matches("([a-z]+\\.)+[A-Z][a-zA-Z]+")) { + name = (String) arg; + } else { + name = arg.getClass().getCanonicalName(); + } + name = name.replaceAll("\\.", "/") + ".html"; + Object doc = null; + for (Map.Entry entry : docs.entrySet()) { + if (name.matches(entry.getKey())) { + doc = entry.getValue(); + break; + } + } + if (doc == null) { + throw new IllegalArgumentException("No document configuration for " + name); + } + String url = name; + if (doc instanceof Collection) { + for (Object o : (Collection) doc) { + url = o + name; + if (urlExists(url)) { + Desktop.getDesktop().browse(new URI(url)); + done = true; + } + } + } else { + url = doc + name; + if (urlExists(url)) { + Desktop.getDesktop().browse(new URI(url)); + done = true; + } + } + if (!done) { + throw new IllegalArgumentException("Document not found: " + url); + } + } + } catch (Exception e) { + exception = e; + } + return null; + } + + private boolean urlExists(String weburl) { + try { + URL url = URI.create(weburl).toURL(); + HttpURLConnection huc = (HttpURLConnection) url.openConnection(); + huc.setRequestMethod("HEAD"); + return huc.getResponseCode() == HttpURLConnection.HTTP_OK; + } catch (Exception e) { + return false; + } + } + + private List slurpCompleter(String command) { + List completers = new ArrayList<>(); + List optDescs = commandOptions("slurp"); + for (OptDesc o : optDescs) { + if (o.shortOption() != null && o.shortOption().equals("-f")) { + List formats = new ArrayList<>(engine.getDeserializationFormats()); + formats.add(SLURP_FORMAT_TEXT); + o.setValueCompleter(new StringsCompleter(formats)); + break; + } + } + AggregateCompleter argCompleter = + new AggregateCompleter(new FilesCompleter(workDir), new VariableReferenceCompleter(engine)); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, + new OptionCompleter(Arrays.asList(argCompleter, NullCompleter.INSTANCE), optDescs, 1))); + return completers; + } + + private List variableCompleter(String command) { + List completers = new ArrayList<>(); + completers.add(new StringsCompleter(() -> engine.find().keySet())); + return completers; + } + + private List prntCompleter(String command) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, + new OptionCompleter( + Arrays.asList(new VariableReferenceCompleter(engine), NullCompleter.INSTANCE), + this::commandOptions, + 1))); + return completers; + } + + private List aliasCompleter(String command) { + List completers = new ArrayList<>(); + List params = new ArrayList<>(); + params.add(new StringsCompleter(aliases::keySet)); + params.add(new AliasValueCompleter(aliases)); + completers.add( + new ArgumentCompleter(NullCompleter.INSTANCE, new OptionCompleter(params, this::commandOptions, 1))); + return completers; + } + + private List unaliasCompleter(String command) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, + new OptionCompleter(new StringsCompleter(aliases::keySet), this::commandOptions, 1))); + return completers; + } + + private List docs() { + List out = new ArrayList<>(); + Map docs = consoleOption("docs", null); + if (docs == null) { + return out; + } + for (String v : engine.find().keySet()) { + out.add("$" + v); + } + if (!docs.isEmpty()) { + for (String d : docs.keySet()) { + if (d.matches("\\w+")) { + out.add(d); + } + } + } + return out; + } + + private List docCompleter(String command) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, + new OptionCompleter( + Arrays.asList(new StringsCompleter(this::docs), NullCompleter.INSTANCE), + this::commandOptions, + 1))); + return completers; + } + + public enum Command { + SHOW, + DEL, + PRNT, + ALIAS, + PIPE, + UNALIAS, + DOC, + SLURP + } + + private static class VariableReferenceCompleter implements Completer { + private final ScriptEngine engine; + + public VariableReferenceCompleter(ScriptEngine engine) { + this.engine = engine; + } + + @Override + @SuppressWarnings("unchecked") + public void complete(LineReader reader, ParsedLine commandLine, List candidates) { + assert commandLine != null; + assert candidates != null; + String word = commandLine.word(); + try { + if (!word.contains(".") && !word.contains("}")) { + for (String v : engine.find().keySet()) { + String c = "${" + v + "}"; + candidates.add(new Candidate(AttributedString.stripAnsi(c), c, null, null, null, null, false)); + } + } else if (word.startsWith("${") && word.contains("}") && word.contains(".")) { + String var = word.substring(2, word.indexOf('}')); + if (engine.hasVariable(var)) { + String curBuf = word.substring(0, word.lastIndexOf(".")); + String objStatement = curBuf.replace("${", "").replace("}", ""); + Object obj = curBuf.contains(".") ? engine.execute(objStatement) : engine.get(var); + Map map = obj instanceof Map ? (Map) obj : null; + Set identifiers = new HashSet<>(); + if (map != null + && !map.isEmpty() + && map.keySet().iterator().next() instanceof String) { + identifiers = (Set) map.keySet(); + } else if (map == null && obj != null) { + identifiers = getClassMethodIdentifiers(obj.getClass()); + } + for (String key : identifiers) { + candidates.add(new Candidate( + AttributedString.stripAnsi(curBuf + "." + key), + key, + null, + null, + null, + null, + false)); + } + } + } + } catch (Exception ignore) { + } + } + + private Set getClassMethodIdentifiers(Class clazz) { + Set out = new HashSet<>(); + do { + for (Method m : clazz.getMethods()) { + if (!m.isSynthetic() && m.getParameterCount() == 0) { + String name = m.getName(); + if (name.matches("get[A-Z].*")) { + out.add(convertGetMethod2identifier(name)); + } + } + } + clazz = clazz.getSuperclass(); + } while (clazz != null); + return out; + } + + private String convertGetMethod2identifier(String name) { + char[] c = name.substring(3).toCharArray(); + c[0] = Character.toLowerCase(c[0]); + return new String(c); + } + } + + private static class AliasValueCompleter implements Completer { + private final Map aliases; + + public AliasValueCompleter(Map aliases) { + this.aliases = aliases; + } + + @Override + public void complete(LineReader reader, ParsedLine commandLine, List candidates) { + assert commandLine != null; + assert candidates != null; + List words = commandLine.words(); + if (words.size() > 1) { + String h = words.get(words.size() - 2); + if (h != null && h.length() > 0) { + String v = aliases.get(h); + if (v != null) { + candidates.add(new Candidate(AttributedString.stripAnsi(v), v, null, null, null, null, true)); + } + } + } + } + } + + private class ScriptFile { + private File script; + private String extension = ""; + private String cmdLine; + private String[] args; + private boolean verbose; + private Object result; + + @SuppressWarnings("unchecked") + public ScriptFile(String command, String cmdLine, String[] args) { + if (!parser().validCommandName(command)) { + return; + } + try { + this.script = new File(command); + this.cmdLine = cmdLine; + if (script.exists()) { + scriptExtension(command); + } else if (engine.hasVariable(VAR_PATH)) { + boolean found = false; + for (String p : (List) engine.get(VAR_PATH)) { + for (String e : scriptExtensions()) { + String file = command + "." + e; + Path path = Paths.get(p, file); + if (path.toFile().exists()) { + script = path.toFile(); + scriptExtension(command); + found = true; + break; + } + } + if (found) { + break; + } + } + } + doArgs(args); + } catch (Exception e) { + // ignore + } + } + + public ScriptFile(File script, String cmdLine, String[] args) { + if (!script.exists()) { + throw new IllegalArgumentException("Script file not found!"); + } + this.script = script; + this.cmdLine = cmdLine; + scriptExtension(script.getName()); + doArgs(args); + } + + private void scriptExtension(String command) { + String name = script.getName(); + this.extension = name.contains(".") ? name.substring(name.lastIndexOf(".") + 1) : ""; + if (!isEngineScript() && !isConsoleScript()) { + throw new IllegalArgumentException("Command not found: " + command); + } + } + + private void doArgs(String[] args) { + List _args = new ArrayList<>(); + if (isConsoleScript()) { + _args.add(script.getAbsolutePath()); + } + for (String a : args) { + if (isConsoleScript()) { + if (!a.equals(OPTION_VERBOSE)) { + _args.add(a); + } else { + this.verbose = true; + } + } else { + _args.add(a); + } + } + this.args = _args.toArray(new String[0]); + } + + private boolean isEngineScript() { + return engine.getExtensions().contains(extension); + } + + private boolean isConsoleScript() { + return scriptExtension.equals(extension); + } + + private boolean isScript() { + return engine.getExtensions().contains(extension) || scriptExtension.equals(extension); + } + + public boolean execute() throws Exception { + if (!isScript()) { + return false; + } + result = null; + if (Arrays.asList(args).contains(OPTION_HELP[0]) + || Arrays.asList(args).contains(OPTION_HELP[1])) { + try (BufferedReader br = new BufferedReader(new FileReader(script))) { + int size = 0; + StringBuilder usage = new StringBuilder(); + boolean helpEnd = false; + boolean headComment = false; + for (String l; (l = br.readLine()) != null; ) { + size++; + l = l.replaceAll("\\s+$", ""); + String line = l; + if (size > HELP_MAX_SIZE || line.endsWith(END_HELP)) { + helpEnd = line.endsWith(END_HELP); + break; + } + if (headComment || size < 3) { + String ltr = l.trim(); + if (ltr.startsWith("*") || ltr.startsWith("#")) { + headComment = true; + line = ltr.length() > 1 ? ltr.substring(2) : ""; + } else if (ltr.startsWith("/*") || ltr.startsWith("//")) { + headComment = true; + line = ltr.length() > 2 ? ltr.substring(3) : ""; + } + } + usage.append(line).append('\n'); + } + if (usage.length() > 0) { + usage.append("\n"); + if (!helpEnd) { + usage.insert(0, "\n"); + } + throw new HelpException(usage.toString()); + } else { + internalExecute(); + } + } + } else { + internalExecute(); + } + return true; + } + + private String expandParameterName(String parameter) { + if (parameter.startsWith("$")) { + return expandName(parameter); + } else if (isNumber(parameter)) { + return parameter; + } + return quote(parameter); + } + + private void internalExecute() throws Exception { + if (isEngineScript()) { + result = engine.execute(script, expandParameters(args)); + } else if (isConsoleScript()) { + executing = true; + boolean done = true; + String line = ""; + try (BufferedReader br = new BufferedReader(new FileReader(script))) { + for (String l; (l = br.readLine()) != null; ) { + if (l.trim().isEmpty() || l.trim().startsWith("#")) { + done = true; + continue; + } + try { + line += l; + parser().parse(line, line.length() + 1, ParseContext.ACCEPT_LINE); + done = true; + for (int i = 1; i < args.length; i++) { + line = line.replaceAll( + "\\s\\$" + i + "\\b", (" " + expandParameterName(args[i]) + " ")); + line = line.replaceAll("\\$\\{" + i + "(|:-.*)}", expandParameterName(args[i])); + } + line = line.replaceAll("\\$\\{@}", expandToList(args)); + line = line.replaceAll("\\$@", expandToList(args)); + line = line.replaceAll("\\s\\$\\d\\b", ""); + line = line.replaceAll("\\$\\{\\d+}", ""); + Matcher matcher = + Pattern.compile("\\$\\{\\d+:-(.*?)}").matcher(line); + if (matcher.find()) { + line = matcher.replaceAll(expandParameterName(matcher.group(1))); + } + if (verbose) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.styled(Styles.prntStyle().resolve(".vs"), line); + asb.toAttributedString().println(terminal()); + terminal().flush(); + } + println(systemRegistry.execute(line)); + line = ""; + } catch (EOFError e) { + done = false; + line += "\n"; + } catch (SyntaxError e) { + throw e; + } catch (EndOfFileException e) { + done = true; + result = engine.get("_return"); + postProcess(cmdLine, result); + break; + } catch (Exception e) { + executing = false; + throw new IllegalArgumentException(line + "\n" + e.getMessage()); + } + } + if (!done) { + executing = false; + throw new IllegalArgumentException("Incompleted command: \n" + line); + } + executing = false; + } + } + } + + public Object getResult() { + return result; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + try { + sb.append("script:").append(script.getCanonicalPath()); + } catch (Exception e) { + sb.append(e.getMessage()); + } + sb.append(", "); + sb.append("extension:").append(extension); + sb.append(", "); + sb.append("cmdLine:").append(cmdLine); + sb.append(", "); + sb.append("args:").append(Arrays.asList(args)); + sb.append(", "); + sb.append("verbose:").append(verbose); + sb.append(", "); + sb.append("result:").append(result); + sb.append("]"); + return sb.toString(); + } + } +} diff --git a/net-cli/src/main/java/org/jline/console/impl/DefaultPrinter.java b/net-cli/src/main/java/org/jline/console/impl/DefaultPrinter.java new file mode 100644 index 0000000..327d728 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/impl/DefaultPrinter.java @@ -0,0 +1,1239 @@ +/* + * Copyright (c) 2002-2024, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console.impl; + +import java.io.File; +import java.math.BigDecimal; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.jline.builtins.ConfigurationPath; +import org.jline.builtins.Options; +import org.jline.builtins.Styles; +import org.jline.builtins.SyntaxHighlighter; +import org.jline.console.CmdDesc; +import org.jline.console.CommandInput; +import org.jline.console.Printer; +import org.jline.console.ScriptEngine; +import org.jline.console.SystemRegistry; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.Log; +import org.jline.utils.StyleResolver; +import static org.jline.builtins.SyntaxHighlighter.DEFAULT_NANORC_FILE; +import static org.jline.console.ConsoleEngine.VAR_NANORC; + +/** + * Print highlighted objects to console. + * + * @author Matti Rinta-Nikkola + */ +public class DefaultPrinter extends JlineCommandRegistry implements Printer { + protected static final String VAR_PRNT_OPTIONS = "PRNT_OPTIONS"; + protected static final int PRNT_MAX_ROWS = 100000; + protected static final int PRNT_MAX_DEPTH = 1; + protected static final int PRNT_INDENTION = 4; + private static final int NANORC_MAX_STRING_LENGTH = 400; + private static final int HIGHLIGHTER_CACHE_SIZE = 5; + private final ScriptEngine engine; + private final ConfigurationPath configPath; + @SuppressWarnings("serial") + private final LinkedHashMap highlighters = + new LinkedHashMap(HIGHLIGHTER_CACHE_SIZE + 1, .75F, false) { + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > HIGHLIGHTER_CACHE_SIZE; + } + }; + private Map, Function>> objectToMap = new HashMap<>(); + private Map, Function> objectToString = new HashMap<>(); + private Map> highlightValue = new HashMap<>(); + private int totLines; + private StyleResolver prntStyle; + + public DefaultPrinter(ConfigurationPath configPath) { + this(null, configPath); + } + + public DefaultPrinter(ScriptEngine engine, ConfigurationPath configPath) { + this.engine = engine; + this.configPath = configPath; + } + + @Override + public void println(Object object) { + internalPrintln(defaultPrntOptions(false), object); + } + + @Override + public void println(Map optionsIn, Object object) { + Map options = new HashMap<>(optionsIn); + for (Map.Entry entry : defaultPrntOptions(options.containsKey(Printer.SKIP_DEFAULT_OPTIONS)) + .entrySet()) { + options.putIfAbsent(entry.getKey(), entry.getValue()); + } + manageBooleanOptions(options); + internalPrintln(options, object); + } + + @Override + public boolean refresh() { + highlighters.clear(); + return true; + } + + public String[] appendUsage(String[] customUsage) { + final String[] usage = { + "prnt - print object", + "Usage: prnt [OPTIONS] object", + " -? --help Displays command help", + " -a --all Ignore columnsOut configuration", + " -b --border=CHAR Table cell vertical border character", + " -c --columns=COLUMNS,... Display given columns on map/table", + " -e --exclude=COLUMNS,... Exclude given columns on table", + " -i --include=COLUMNS,... Include given columns on table", + " --indention=INDENTION Indention size", + " --maxColumnWidth=WIDTH Maximum column width", + " -d --maxDepth=DEPTH Maximum depth objects are resolved", + " -n --maxrows=ROWS Maximum number of lines to display", + " -m --multiColumns Display the collection of simple data in multiple columns", + " --oneRowTable Display one row data on table", + " -h --rowHighlight=ROW Highlight table rows. ROW = EVEN, ODD, ALL", + " -r --rownum Display table row numbers", + " --shortNames Truncate table column names (property.field -> field)", + " --skipDefaultOptions Ignore all options defined in PRNT_OPTIONS", + " --structsOnTable Display structs and lists on table", + " -s --style=STYLE Use nanorc STYLE to highlight Object.", + " STYLE = JSON serialize object to JSON string before printing", + " --toString Use object's toString() method to get print value", + " DEFAULT: object's fields are put to property map before printing", + " --valueStyle=STYLE Use nanorc style to highlight string and column/map values", + " -w --width=WIDTH Display width (default terminal width)" + }; + String[] out; + if (customUsage == null || customUsage.length == 0) { + out = usage; + } else { + out = new String[usage.length + customUsage.length]; + System.arraycopy(usage, 0, out, 0, usage.length); + System.arraycopy(customUsage, 0, out, usage.length, customUsage.length); + } + return out; + } + + public Map compileOptions(Options opt) { + Map options = new HashMap<>(); + if (opt.isSet(Printer.SKIP_DEFAULT_OPTIONS)) { + options.put(Printer.SKIP_DEFAULT_OPTIONS, true); + } else if (opt.isSet(Printer.STYLE)) { + options.put(Printer.STYLE, opt.get(Printer.STYLE)); + } + if (opt.isSet(Printer.TO_STRING)) { + options.put(Printer.TO_STRING, true); + } + if (opt.isSet(Printer.WIDTH)) { + options.put(Printer.WIDTH, opt.getNumber(Printer.WIDTH)); + } + if (opt.isSet(Printer.ROWNUM)) { + options.put(Printer.ROWNUM, true); + } + if (opt.isSet(Printer.ONE_ROW_TABLE)) { + options.put(Printer.ONE_ROW_TABLE, true); + } + if (opt.isSet(Printer.SHORT_NAMES)) { + options.put(Printer.SHORT_NAMES, true); + } + if (opt.isSet(Printer.STRUCT_ON_TABLE)) { + options.put(Printer.STRUCT_ON_TABLE, true); + } + if (opt.isSet(Printer.COLUMNS)) { + options.put(Printer.COLUMNS, Arrays.asList(opt.get(Printer.COLUMNS).split(","))); + } + if (opt.isSet(Printer.EXCLUDE)) { + options.put(Printer.EXCLUDE, Arrays.asList(opt.get(Printer.EXCLUDE).split(","))); + } + if (opt.isSet(Printer.INCLUDE)) { + options.put(Printer.INCLUDE, Arrays.asList(opt.get(Printer.INCLUDE).split(","))); + } + if (opt.isSet(Printer.ALL)) { + options.put(Printer.ALL, true); + } + if (opt.isSet(Printer.MAXROWS)) { + options.put(Printer.MAXROWS, opt.getNumber(Printer.MAXROWS)); + } + if (opt.isSet(Printer.MAX_COLUMN_WIDTH)) { + options.put(Printer.MAX_COLUMN_WIDTH, opt.getNumber(Printer.MAX_COLUMN_WIDTH)); + } + if (opt.isSet(Printer.MAX_DEPTH)) { + options.put(Printer.MAX_DEPTH, opt.getNumber(Printer.MAX_DEPTH)); + } + if (opt.isSet(Printer.INDENTION)) { + options.put(Printer.INDENTION, opt.getNumber(Printer.INDENTION)); + } + if (opt.isSet(Printer.VALUE_STYLE)) { + options.put(Printer.VALUE_STYLE, opt.get(Printer.VALUE_STYLE)); + } + if (opt.isSet(Printer.BORDER)) { + options.put(Printer.BORDER, opt.get(Printer.BORDER)); + } + if (opt.isSet(Printer.ROW_HIGHLIGHT)) { + try { + options.put(Printer.ROW_HIGHLIGHT, optionRowHighlight(opt.get(Printer.ROW_HIGHLIGHT))); + } catch (Exception e) { + RuntimeException exception = new BadOptionValueException( + Printer.ROW_HIGHLIGHT + " has a bad value: " + opt.get(Printer.ROW_HIGHLIGHT)); + exception.addSuppressed(e); + throw exception; + } + } + if (opt.isSet(Printer.MULTI_COLUMNS)) { + options.put(Printer.MULTI_COLUMNS, true); + } + options.put("exception", "stack"); + return options; + } + + private TableRows optionRowHighlight(Object value) { + if (value instanceof TableRows || value == null) { + return (TableRows) value; + } else if (value instanceof String) { + String val = ((String) value).trim().toUpperCase(); + if (!val.isEmpty() && !val.equals("NULL")) { + return TableRows.valueOf(val); + } else { + return null; + } + } + throw new IllegalArgumentException("rowHighlight has a bad option value type: " + value.getClass()); + } + + @Override + public Exception prntCommand(CommandInput input) { + Exception out = null; + String[] usage = appendUsage(null); + try { + Options opt = parseOptions(usage, input.xargs()); + Map options = compileOptions(opt); + List args = opt.argObjects(); + if (args.size() > 0) { + println(options, args.get(0)); + } + } catch (Exception e) { + out = e; + } + return out; + } + + /** + * Override ScriptEngine toMap() method + * + * @param objectToMap key: object class, value: toMap function + */ + public void setObjectToMap(Map, Function>> objectToMap) { + this.objectToMap = objectToMap; + } + + /** + * Override ScriptEngine toString() method + * + * @param objectToString key: object class, value: toString function + */ + public void setObjectToString(Map, Function> objectToString) { + this.objectToString = objectToString; + } + + /** + * Highlight column value + * + * @param highlightValue key: regex for column name, value: highlight function + */ + public void setHighlightValue(Map> highlightValue) { + this.highlightValue = highlightValue; + } + + /** + * @return terminal to which will be printed + */ + protected Terminal terminal() { + return SystemRegistry.get().terminal(); + } + + /** + * Boolean printing options Printer checks only if key is present. + * Boolean options that have false value are removed from the options Map. + * + * @param options printing options + */ + protected void manageBooleanOptions(Map options) { + for (String key : Printer.BOOLEAN_KEYS) { + Object option = options.get(key); + boolean value = option instanceof Boolean && (boolean) option; + if (!value) { + options.remove(key); + } + } + } + + /** + * Set default and mandatory printing options. + * Also unsupported options will be removed when Printer is used without scriptEngine + * + * @param skipDefault when true does not set default options + * @return default, mandatory and supported options + */ + @SuppressWarnings("unchecked") + protected Map defaultPrntOptions(boolean skipDefault) { + Map out = new HashMap<>(); + if (engine != null && !skipDefault && engine.hasVariable(VAR_PRNT_OPTIONS)) { + out.putAll((Map) engine.get(VAR_PRNT_OPTIONS)); + out.remove(Printer.SKIP_DEFAULT_OPTIONS); + manageBooleanOptions(out); + } + out.putIfAbsent(Printer.MAXROWS, PRNT_MAX_ROWS); + out.putIfAbsent(Printer.MAX_DEPTH, PRNT_MAX_DEPTH); + out.putIfAbsent(Printer.INDENTION, PRNT_INDENTION); + out.putIfAbsent(Printer.COLUMNS_OUT, new ArrayList()); + out.putIfAbsent(Printer.COLUMNS_IN, new ArrayList()); + if (engine == null) { + out.remove(Printer.OBJECT_TO_MAP); + out.remove(Printer.OBJECT_TO_STRING); + out.remove(Printer.HIGHLIGHT_VALUE); + } + return out; + } + + @SuppressWarnings("unchecked") + private void internalPrintln(Map options, Object object) { + if (object == null) { + return; + } + long start = new Date().getTime(); + if (options.containsKey(Printer.EXCLUDE)) { + List colOut = optionList(Printer.EXCLUDE, options); + List colIn = optionList(Printer.COLUMNS_IN, options); + colIn.removeAll(colOut); + colOut.addAll((List) options.get(Printer.COLUMNS_OUT)); + options.put(Printer.COLUMNS_IN, colIn); + options.put(Printer.COLUMNS_OUT, colOut); + } + if (options.containsKey(Printer.INCLUDE)) { + List colIn = optionList(Printer.INCLUDE, options); + colIn.addAll((List) options.get(Printer.COLUMNS_IN)); + options.put(Printer.COLUMNS_IN, colIn); + } + options.put(Printer.VALUE_STYLE, valueHighlighter((String) options.getOrDefault(Printer.VALUE_STYLE, null))); + prntStyle = Styles.prntStyle(); + options.putIfAbsent(Printer.WIDTH, terminal().getSize().getColumns()); + String style = (String) options.getOrDefault(Printer.STYLE, ""); + options.put(Printer.STYLE, valueHighlighter(style)); + int width = (int) options.get(Printer.WIDTH); + int maxrows = (int) options.get(Printer.MAXROWS); + if (!style.isEmpty() && object instanceof String) { + highlightAndPrint(width, (SyntaxHighlighter) options.get(Printer.STYLE), (String) object, true, maxrows); + } else if (style.equalsIgnoreCase("JSON")) { + if (engine == null) { + throw new IllegalArgumentException("JSON style not supported!"); + } + String json = engine.toJson(object); + highlightAndPrint(width, (SyntaxHighlighter) options.get(Printer.STYLE), json, true, maxrows); + } else if (options.containsKey(Printer.SKIP_DEFAULT_OPTIONS)) { + highlightAndPrint(options, object); + } else if (object instanceof Exception) { + highlightAndPrint(options, (Exception) object); + } else if (object instanceof CmdDesc) { + highlight((CmdDesc) object).println(terminal()); + } else if (object instanceof String || object instanceof Number) { + String str = object.toString(); + SyntaxHighlighter highlighter = (SyntaxHighlighter) options.getOrDefault(Printer.VALUE_STYLE, null); + highlightAndPrint(width, highlighter, str, doValueHighlight(options, str), maxrows); + } else { + highlightAndPrint(options, object); + } + terminal().flush(); + Log.debug("println: ", new Date().getTime() - start, " msec"); + } + + /** + * Highlight and print an exception + * + * @param options Printing options + * @param exception Exception to be printed + */ + protected void highlightAndPrint(Map options, Throwable exception) { + SystemRegistry.get().trace(options.getOrDefault("exception", "stack").equals("stack"), exception); + } + + private AttributedString highlight(CmdDesc cmdDesc) { + StringBuilder sb = new StringBuilder(); + for (AttributedString as : cmdDesc.getMainDesc()) { + sb.append(as.toString()); + sb.append("\n"); + } + List tabs = Arrays.asList(0, 2, 33); + for (Map.Entry> entry : + cmdDesc.getOptsDesc().entrySet()) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.tabs(tabs); + asb.append("\t"); + asb.append(entry.getKey()); + asb.append("\t"); + boolean first = true; + for (AttributedString as : entry.getValue()) { + if (!first) { + asb.append("\t"); + asb.append("\t"); + } + asb.append(as); + asb.append("\n"); + first = false; + } + sb.append(asb); + } + return Options.HelpException.highlight(sb.toString(), Styles.helpStyle()); + } + + private SyntaxHighlighter valueHighlighter(String style) { + SyntaxHighlighter out; + if (style == null || style.isEmpty()) { + out = null; + } else if (highlighters.containsKey(style)) { + out = highlighters.get(style); + } else if (style.matches("[a-z]+:.*")) { + out = SyntaxHighlighter.build(style); + highlighters.put(style, out); + } else { + Path nanorc = configPath != null ? configPath.getConfig(DEFAULT_NANORC_FILE) : null; + if (engine != null && engine.hasVariable(VAR_NANORC)) { + nanorc = Paths.get((String) engine.get(VAR_NANORC)); + } + if (nanorc == null) { + nanorc = Paths.get("/etc/nanorc"); + } + out = SyntaxHighlighter.build(nanorc, style); + highlighters.put(style, out); + } + return out; + } + + private String truncate4nanorc(String obj) { + String val = obj; + if (val.length() > NANORC_MAX_STRING_LENGTH && !val.contains("\n")) { + val = val.substring(0, NANORC_MAX_STRING_LENGTH - 1); + } + return val; + } + + private AttributedString highlight( + Integer width, SyntaxHighlighter highlighter, String object, boolean doValueHighlight) { + AttributedString out; + AttributedStringBuilder asb = new AttributedStringBuilder(); + String val = object; + if (highlighter != null && doValueHighlight) { + val = truncate4nanorc(object); + } + asb.append(val); + if (highlighter != null && val.length() < NANORC_MAX_STRING_LENGTH && doValueHighlight) { + out = highlighter.highlight(asb); + } else { + out = asb.toAttributedString(); + } + if (width != null) { + out = out.columnSubSequence(0, width); + } + return out; + } + + private boolean doValueHighlight(Map options, String value) { + if (options.containsKey(Printer.VALUE_STYLE_ALL) + || value.matches("\"(\\.|[^\"])*\"|'(\\.|[^'])*'") + || (value.startsWith("[") && value.endsWith("]")) + || (value.startsWith("(") && value.endsWith(")")) + || (value.startsWith("{") && value.endsWith("}")) + || (value.startsWith("<") && value.endsWith(">"))) { + return true; + } else { + return !value.contains(" ") && !value.contains("\t"); + } + } + + private void highlightAndPrint( + int width, SyntaxHighlighter highlighter, String object, boolean doValueHighlight, int maxRows) { + String lineBreak = null; + if (object.indexOf("\r\n") >= 0) { + lineBreak = "\r\n"; + } else if (object.indexOf("\n") >= 0) { + lineBreak = "\n"; + } else if (object.indexOf("\r") >= 0) { + lineBreak = "\r"; + } + if (lineBreak == null) { + highlightAndPrint(width, highlighter, object, doValueHighlight); + } else { + int rows = 0; + int i0 = 0; + while (rows < maxRows) { + rows++; + int i1 = object.indexOf(lineBreak, i0); + String line = i1 >= 0 ? object.substring(i0, i1) : object.substring(i0); + highlightAndPrint(width, highlighter, line, doValueHighlight); + if (i1 < 0) { + break; + } + i0 = i1 + lineBreak.length(); + } + if (rows == maxRows) { + throw new TruncatedOutputException("Truncated output: " + maxRows); + } + } + } + + private void highlightAndPrint(int width, SyntaxHighlighter highlighter, String object, boolean doValueHighlight) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + List sas = asb.append(object).columnSplitLength(width); + for (AttributedString as : sas) { + highlight(width, highlighter, as.toString(), doValueHighlight).println(terminal()); + } + } + + private Map keysToString(Map map) { + Map out = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() instanceof String) { + out.put((String) entry.getKey(), entry.getValue()); + } else if (entry.getKey() != null) { + out.put(entry.getKey().toString(), entry.getValue()); + } else { + out.put("null", entry.getValue()); + } + } + return out; + } + + @SuppressWarnings("unchecked") + private Object mapValue(Map options, String key, Map map) { + Object out = null; + if (map.containsKey(key)) { + out = map.get(key); + } else if (key.contains(".")) { + String[] keys = key.split("\\."); + out = map.get(keys[0]); + for (int i = 1; i < keys.length; i++) { + if (out instanceof Map) { + Map m = keysToString((Map) out); + out = m.get(keys[i]); + } else if (canConvert(out)) { + out = engine.toMap(out).get(keys[i]); + } else { + break; + } + } + } + if (!(out instanceof Map) && canConvert(out)) { + out = objectToMap(options, out); + } + return out; + } + + @SuppressWarnings("unchecked") + private List optionList(String key, Map options) { + List out = new ArrayList<>(); + Object option = options.get(key); + if (option instanceof String) { + out.addAll(Arrays.asList(((String) option).split(","))); + } else if (option instanceof Collection) { + out.addAll((Collection) option); + } else if (option != null) { + throw new IllegalArgumentException( + "Unsupported option list: {key: " + key + ", type: " + option.getClass() + "}"); + } + return out; + } + + private boolean hasMatch(List regexes, String value) { + for (String r : regexes) { + if (value.matches(r)) { + return true; + } + } + return false; + } + + private AttributedString addPadding(AttributedString str, int width) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + for (int i = str.columnLength(); i < width; i++) { + sb.append(" "); + } + sb.append(str); + return sb.toAttributedString(); + } + + private String addPadding(String str, int width) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + for (int i = str.length(); i < width; i++) { + sb.append(" "); + } + sb.append(str); + return sb.toString(); + } + + private String columnValue(String value) { + return value.replaceAll("\r", "CR").replaceAll("\n", "LF"); + } + + @SuppressWarnings("unchecked") + private Map objectToMap(Map options, Object obj) { + if (obj != null) { + Map, Object> toMap = + (Map, Object>) options.getOrDefault(Printer.OBJECT_TO_MAP, Collections.emptyMap()); + if (toMap.containsKey(obj.getClass())) { + return (Map) engine.execute(toMap.get(obj.getClass()), obj); + } else if (objectToMap.containsKey(obj.getClass())) { + return objectToMap.get(obj.getClass()).apply(obj); + } + } + return engine.toMap(obj); + } + + @SuppressWarnings("unchecked") + private String objectToString(Map options, Object obj) { + String out = "null"; + if (obj != null) { + Map, Object> toString = options.containsKey(Printer.OBJECT_TO_STRING) + ? (Map, Object>) options.get(Printer.OBJECT_TO_STRING) + : new HashMap<>(); + if (toString.containsKey(obj.getClass())) { + out = (String) engine.execute(toString.get(obj.getClass()), obj); + } else if (objectToString.containsKey(obj.getClass())) { + out = objectToString.get(obj.getClass()).apply(obj); + } else if (obj instanceof Class) { + out = ((Class) obj).getName(); + } else if (engine != null) { + out = engine.toString(obj); + } else { + out = obj.toString(); + } + } + return out; + } + + private AttributedString highlightMapValue(Map options, String key, Map map) { + return highlightValue(options, key, mapValue(options, key, map)); + } + + private boolean isHighlighted(AttributedString value) { + for (int i = 0; i < value.length(); i++) { + if (value.styleAt(i).getStyle() != AttributedStyle.DEFAULT.getStyle()) { + return true; + } + } + return false; + } + + @SuppressWarnings("unchecked") + private AttributedString highlightValue(Map options, String column, Object obj) { + AttributedString out = null; + Object raw = options.containsKey(Printer.TO_STRING) && obj != null ? objectToString(options, obj) : obj; + Map hv = options.containsKey(Printer.HIGHLIGHT_VALUE) + ? (Map) options.get(Printer.HIGHLIGHT_VALUE) + : new HashMap<>(); + if (column != null && simpleObject(raw)) { + for (Map.Entry entry : hv.entrySet()) { + if (!entry.getKey().equals("*") && column.matches(entry.getKey())) { + out = (AttributedString) engine.execute(hv.get(entry.getKey()), raw); + break; + } + } + if (out == null) { + for (Map.Entry> entry : highlightValue.entrySet()) { + if (!entry.getKey().equals("*") && column.matches(entry.getKey())) { + out = highlightValue.get(entry.getKey()).apply(raw); + break; + } + } + } + } + if (out == null) { + if (raw instanceof String) { + out = new AttributedString(columnValue((String) raw)); + } else { + out = new AttributedString(columnValue(objectToString(options, raw))); + } + } + if ((simpleObject(raw) || raw == null) + && (hv.containsKey("*") || highlightValue.containsKey("*")) + && !isHighlighted(out)) { + if (hv.containsKey("*")) { + out = (AttributedString) engine.execute(hv.get("*"), out); + } + Function func = highlightValue.get("*"); + if (func != null) { + out = func.apply(out); + } + } + if (options.containsKey(Printer.VALUE_STYLE) && !isHighlighted(out)) { + out = highlight( + null, + (SyntaxHighlighter) options.get(Printer.VALUE_STYLE), + out.toString(), + doValueHighlight(options, out.toString())); + } + return truncateValue(options, out); + } + + private AttributedString truncateValue(Map options, AttributedString value) { + if (value.columnLength() > (int) options.getOrDefault(Printer.MAX_COLUMN_WIDTH, Integer.MAX_VALUE)) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(value.columnSubSequence(0, (int) options.get(Printer.MAX_COLUMN_WIDTH) - 3)); + asb.append("..."); + return asb.toAttributedString(); + } + return value; + } + + private String truncateValue(int maxWidth, String value) { + if (value.length() > maxWidth) { + return value.subSequence(0, maxWidth - 3) + "..."; + } + return value; + } + + @SuppressWarnings("unchecked") + private List objectToList(Object obj) { + List out = new ArrayList<>(); + if (obj instanceof List) { + out = (List) obj; + } else if (obj instanceof Collection) { + out.addAll((Collection) obj); + } else if (obj instanceof Object[]) { + out.addAll(Arrays.asList((Object[]) obj)); + } else if (obj instanceof Iterator) { + ((Iterator) obj).forEachRemaining(out::add); + } else if (obj instanceof Iterable) { + ((Iterable) obj).forEach(out::add); + } else { + out.add(obj); + } + return out; + } + + private boolean similarSets(final List ref, final Set c2, final int matchLimit) { + boolean out = false; + int limit = matchLimit; + for (String s : ref) { + if (c2.contains(s)) { + limit--; + if (limit == 0) { + out = true; + break; + } + } + } + return out; + } + + private void println(AttributedString line, int maxrows) { + line.println(terminal()); + totLines++; + if (totLines > maxrows) { + totLines = 0; + throw new TruncatedOutputException("Truncated output: " + maxrows); + } + } + + private String columnName(String name, boolean shortName) { + String out = name; + if (shortName) { + String[] p = name.split("\\."); + out = p[p.length - 1]; + } + return out; + } + + private boolean isNumber(String str) { + return str.matches("-?\\d+(\\.\\d+)?"); + } + + @SuppressWarnings("unchecked") + private void highlightAndPrint(Map options, Object obj) { + int width = (int) options.get(Printer.WIDTH); + int maxrows = (int) options.get(Printer.MAXROWS); + totLines = 0; + String message = null; + RuntimeException runtimeException = null; + if (obj == null) { + // do nothing + } else if (obj instanceof Map) { + highlightMap(options, keysToString((Map) obj), width); + } else if (collectionObject(obj)) { + List collection = objectToList(obj); + if (collection.size() > maxrows) { + message = "Truncated output: " + maxrows + "/" + collection.size(); + collection = collection.subList(collection.size() - maxrows, collection.size()); + } + if (!collection.isEmpty()) { + if (collection.size() == 1 && !options.containsKey(Printer.ONE_ROW_TABLE)) { + Object elem = collection.iterator().next(); + if (elem instanceof Map) { + highlightMap(options, keysToString((Map) elem), width); + } else if (canConvert(elem) && !options.containsKey(Printer.TO_STRING)) { + highlightMap(options, objectToMap(options, elem), width); + } else if (elem instanceof String && options.get(Printer.STYLE) != null) { + highlightAndPrint( + width, (SyntaxHighlighter) options.get(Printer.STYLE), (String) elem, true, maxrows); + } else { + highlightValue(options, null, objectToString(options, obj)) + .println(terminal()); + } + } else { + String columnSep = ""; + TableRows tableRows = null; + boolean rownum = options.containsKey(Printer.ROWNUM); + try { + columnSep = (String) options.getOrDefault(Printer.BORDER, ""); + tableRows = optionRowHighlight(options.getOrDefault(Printer.ROW_HIGHLIGHT, null)); + } catch (Exception e) { + runtimeException = new BadOptionValueException( + "Option " + Printer.BORDER + " or " + Printer.ROW_HIGHLIGHT + " has a bad value!"); + runtimeException.addSuppressed(e); + } + try { + Object elem = collection.iterator().next(); + boolean convert = canConvert(elem); + if ((elem instanceof Map || convert) && !options.containsKey(Printer.TO_STRING)) { + List> convertedCollection = new ArrayList<>(); + Set keys = new HashSet<>(); + for (Object o : collection) { + Map m = + convert ? objectToMap(options, o) : keysToString((Map) o); + convertedCollection.add(m); + keys.addAll(m.keySet()); + } + List _header; + List columnsIn = optionList(Printer.COLUMNS_IN, options); + List columnsOut = !options.containsKey("all") + ? optionList(Printer.COLUMNS_OUT, options) + : new ArrayList<>(); + if (options.containsKey(Printer.COLUMNS)) { + _header = (List) options.get(Printer.COLUMNS); + } else { + _header = columnsIn; + _header.addAll(keys.stream() + .filter(k -> !columnsIn.contains(k) && !hasMatch(columnsOut, k)) + .collect(Collectors.toList())); + } + List header = new ArrayList<>(); + List columns = new ArrayList<>(); + int headerWidth = 0; + List refKeys = new ArrayList<>(); + for (String v : _header) { + String value = v.split("\\.")[0]; + if (!keys.contains(value) && !keys.contains(v)) { + continue; + } + boolean addKey = false; + for (Map m : convertedCollection) { + Object val = mapValue(options, v, m); + if (val != null) { + addKey = simpleObject(val) + || options.containsKey(Printer.COLUMNS) + || options.containsKey(Printer.STRUCT_ON_TABLE); + break; + } + } + if (!addKey) { + continue; + } + refKeys.add(value); + header.add(v); + String cn = columnName(v, options.containsKey(Printer.SHORT_NAMES)); + columns.add(cn.length() + 1); + headerWidth += cn.length() + 1; + if (headerWidth > width) { + break; + } + } + if (header.size() == 0) { + throw new Exception("No columns for table!"); + } + double mapSimilarity = ((BigDecimal) + options.getOrDefault(Printer.MAP_SIMILARITY, new BigDecimal("0.8"))) + .doubleValue(); + int matchLimit = (int) Math.ceil(header.size() * mapSimilarity); + for (Map m : convertedCollection) { + if (!similarSets(refKeys, m.keySet(), matchLimit)) { + throw new Exception("Not homogenous list!"); + } + for (int i = 0; i < header.size(); i++) { + int cw = highlightMapValue(options, header.get(i), m) + .columnLength(); + if (cw > columns.get(i) - 1) { + columns.set(i, cw + 1); + } + } + } + toTabStops(columns, collection.size(), rownum, columnSep); + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(columns); + asb.style(prntStyle.resolve(".th")); + int firstColumn = 0; + if (rownum) { + asb.append(addPadding("", columns.get(0) - columnSep.length() - 1)); + asb.append(columnSep); + asb.append("\t"); + firstColumn = 1; + } + boolean first = true; + for (String s : header) { + if (!first) { + asb.append(columnSep); + } + asb.append(columnName(s, options.containsKey(Printer.SHORT_NAMES))); + asb.append("\t"); + first = false; + } + asb.columnSubSequence(0, width).println(terminal()); + int row = 0; + for (Map m : convertedCollection) { + AttributedStringBuilder asb2 = new AttributedStringBuilder().tabs(columns); + if (doRowHighlight(row, tableRows)) { + asb2.style(prntStyle.resolve(".rs")); + } + if (rownum) { + asb2.styled( + prntStyle.resolve(".rn"), + addPadding(Integer.toString(row), columns.get(0) - columnSep.length() - 1)); + asb2.append(columnSep); + asb2.append("\t"); + } + row++; + for (int i = 0; i < header.size(); i++) { + if (i > 0) { + asb2.append(columnSep); + } + AttributedString v = highlightMapValue(options, header.get(i), m); + if (isNumber(v.toString())) { + v = addPadding(v, cellWidth(firstColumn + i, columns, rownum, columnSep) - 1); + } + asb2.append(v); + asb2.append("\t"); + } + asb2.columnSubSequence(0, width).println(terminal()); + } + } else if (collectionObject(elem) && !options.containsKey(Printer.TO_STRING)) { + List columns = new ArrayList<>(); + for (Object o : collection) { + List inner = objectToList(o); + for (int i = 0; i < inner.size(); i++) { + int len1 = objectToString(options, inner.get(i)) + .length() + + 1; + if (columns.size() <= i) { + columns.add(len1); + } else if (len1 > columns.get(i)) { + columns.set(i, len1); + } + } + } + toTabStops(columns, collection.size(), rownum, columnSep); + int row = 0; + int firstColumn = rownum ? 1 : 0; + for (Object o : collection) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(columns); + if (doRowHighlight(row, tableRows)) { + asb.style(prntStyle.resolve(".rs")); + } + if (rownum) { + asb.styled( + prntStyle.resolve(".rn"), + addPadding(Integer.toString(row), columns.get(0) - columnSep.length() - 1)); + asb.append(columnSep); + asb.append("\t"); + } + row++; + List inner = objectToList(o); + for (int i = 0; i < inner.size(); i++) { + if (i > 0) { + asb.append(columnSep); + } + AttributedString v = highlightValue(options, null, inner.get(i)); + if (isNumber(v.toString())) { + v = addPadding(v, cellWidth(firstColumn + i, columns, rownum, columnSep) - 1); + } + asb.append(v); + asb.append("\t"); + } + asb.columnSubSequence(0, width).println(terminal()); + } + } else { + highlightList(options, collection, width); + } + } catch (Exception e) { + Log.debug("Stack: ", e); + highlightList(options, collection, width); + } + } + } else { + highlightValue(options, null, objectToString(options, obj)).println(terminal()); + } + } else if (canConvert(obj) && !options.containsKey(Printer.TO_STRING)) { + highlightMap(options, objectToMap(options, obj), width); + } else { + highlightValue(options, null, objectToString(options, obj)).println(terminal()); + } + if (message != null) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.styled(prntStyle.resolve(".em"), message); + asb.println(terminal()); + } + if (runtimeException != null) { + throw runtimeException; + } + } + + private boolean doRowHighlight(int row, TableRows tableRows) { + if (tableRows == null) { + return false; + } + switch (tableRows) { + case EVEN: + return row % 2 == 0; + case ODD: + return row % 2 == 1; + case ALL: + return true; + } + return false; + } + + private void highlightList(Map options, List collection, int width) { + highlightList(options, collection, width, 0); + } + + private void highlightList(Map options, List collection, int width, int depth) { + int row = 0; + int maxrows = (int) options.get(Printer.MAXROWS); + int indent = (int) options.get(Printer.INDENTION); + List tabs = new ArrayList<>(); + SyntaxHighlighter highlighter = depth == 0 ? (SyntaxHighlighter) options.get(Printer.STYLE) : null; + if (!(boolean) options.getOrDefault(Printer.MULTI_COLUMNS, false)) { + tabs.add(indent * depth); + if (options.containsKey(Printer.ROWNUM)) { + tabs.add(indent * depth + digits(collection.size()) + 2); + } + options.remove(Printer.MAX_COLUMN_WIDTH); + for (Object o : collection) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); + if (depth > 0) { + asb.append("\t"); + } + if (options.containsKey(Printer.ROWNUM)) { + asb.styled(prntStyle.resolve(".rn"), Integer.toString(row)).append(":"); + asb.append("\t"); + row++; + } + if (highlighter != null && o instanceof String) { + asb.append(highlighter.highlight((String) o)); + } else { + asb.append(highlightValue(options, null, o)); + } + println(asb.columnSubSequence(0, width), maxrows); + } + } else { + int maxWidth = 0; + for (Object o : collection) { + AttributedString as; + if (highlighter != null && o instanceof String) { + as = highlighter.highlight((String) o); + } else { + as = highlightValue(options, null, o); + } + if (as.length() > maxWidth) { + maxWidth = as.length(); + } + } + int mcw = (int) options.getOrDefault(Printer.MAX_COLUMN_WIDTH, Integer.MAX_VALUE); + maxWidth = mcw < maxWidth ? mcw : maxWidth; + tabs.add(maxWidth + 1); + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); + for (Object o : collection) { + if (asb.length() + maxWidth > width) { + println(asb.columnSubSequence(0, width), maxrows); + asb = new AttributedStringBuilder().tabs(tabs); + } + if (highlighter != null && o instanceof String) { + asb.append(highlighter.highlight((String) o)); + } else { + asb.append(highlightValue(options, null, o)); + } + asb.append("\t"); + } + println(asb.columnSubSequence(0, width), maxrows); + } + } + + private boolean collectionObject(Object obj) { + return obj instanceof Iterator || obj instanceof Iterable || obj instanceof Object[]; + } + + private boolean simpleObject(Object obj) { + return obj instanceof Number + || obj instanceof String + || obj instanceof Date + || obj instanceof File + || obj instanceof Boolean + || obj instanceof Enum; + } + + private boolean canConvert(Object obj) { + return engine != null + && obj != null + && !(obj instanceof Class) + && !(obj instanceof Map) + && !simpleObject(obj) + && !collectionObject(obj); + } + + private int digits(int number) { + if (number < 100) { + return number < 10 ? 1 : 2; + } else if (number < 1000) { + return 3; + } else { + return number < 10000 ? 4 : 5; + } + } + + private int cellWidth(int pos, List columns, boolean rownum, String columnSep) { + if (pos == 0) { + return columns.get(0); + } + return columns.get(pos) - columns.get(pos - 1) - (rownum && pos == 1 ? 0 : columnSep.length()); + } + + private void toTabStops(List columns, int rows, boolean rownum, String columnSep) { + if (rownum) { + columns.add(0, digits(rows) + 2 + columnSep.length()); + } + for (int i = 1; i < columns.size(); i++) { + columns.set(i, columns.get(i - 1) + columns.get(i) + (i > 1 || !rownum ? columnSep.length() : 0)); + } + } + + private void highlightMap(Map options, Map map, int width) { + if (!map.isEmpty()) { + highlightMap(options, map, width, 0); + } else { + highlightValue(options, null, objectToString(options, map)).println(terminal()); + } + } + + @SuppressWarnings("unchecked") + private void highlightMap(Map options, Map map, int width, int depth) { + int maxrows = (int) options.get(Printer.MAXROWS); + int max = map.keySet().stream() + .map(String::length) + .max(Integer::compareTo) + .get(); + if (max > (int) options.getOrDefault(Printer.MAX_COLUMN_WIDTH, Integer.MAX_VALUE)) { + max = (int) options.get(Printer.MAX_COLUMN_WIDTH); + } + Map mapOptions = new HashMap<>(options); + mapOptions.remove(Printer.MAX_COLUMN_WIDTH); + int indent = (int) options.get(Printer.INDENTION); + int maxDepth = (int) options.get(Printer.MAX_DEPTH); + for (Map.Entry entry : map.entrySet()) { + if (depth == 0 + && options.containsKey(Printer.COLUMNS) + && !((List) options.get(Printer.COLUMNS)).contains(entry.getKey())) { + continue; + } + AttributedStringBuilder asb = + new AttributedStringBuilder().tabs(Arrays.asList(0, depth * indent, depth * indent + max + 1)); + if (depth != 0) { + asb.append("\t"); + } + asb.styled(prntStyle.resolve(".mk"), truncateValue(max, entry.getKey())); + Object elem = entry.getValue(); + boolean convert = canConvert(elem); + boolean highlightValue = true; + if (depth < maxDepth && !options.containsKey(Printer.TO_STRING)) { + if (elem instanceof Map || convert) { + Map childMap = + convert ? objectToMap(options, elem) : keysToString((Map) elem); + if (!childMap.isEmpty()) { + println(asb.columnSubSequence(0, width), maxrows); + highlightMap(options, childMap, width, depth + 1); + highlightValue = false; + } + } else if (collectionObject(elem)) { + List collection = objectToList(elem); + if (!collection.isEmpty()) { + println(asb.columnSubSequence(0, width), maxrows); + Map listOptions = new HashMap<>(options); + listOptions.put(Printer.TO_STRING, true); + highlightList(listOptions, collection, width, depth + 1); + highlightValue = false; + } + } + } + if (highlightValue) { + AttributedString val = highlightMapValue(mapOptions, entry.getKey(), map); + asb.append("\t"); + if (map.size() == 1) { + if (val.contains('\n')) { + for (String v : val.toString().split("\\r?\\n")) { + asb.append(highlightValue(options, entry.getKey(), v)); + println(asb.columnSubSequence(0, width), maxrows); + asb = new AttributedStringBuilder().tabs(Arrays.asList(0, max + 1)); + } + } else { + asb.append(val); + println(asb.columnSubSequence(0, width), maxrows); + } + } else { + if (val.contains('\n')) { + val = new AttributedString( + Arrays.asList(val.toString().split("\\r?\\n")).toString()); + asb.append(highlightValue(options, entry.getKey(), val.toString())); + } else { + asb.append(val); + } + println(asb.columnSubSequence(0, width), maxrows); + } + } + } + } + + @SuppressWarnings("serial") + private static class BadOptionValueException extends RuntimeException { + public BadOptionValueException(String message) { + super(message); + } + } + + @SuppressWarnings("serial") + private static class TruncatedOutputException extends RuntimeException { + public TruncatedOutputException(String message) { + super(message); + } + } +} diff --git a/net-cli/src/main/java/org/jline/console/impl/JlineCommandRegistry.java b/net-cli/src/main/java/org/jline/console/impl/JlineCommandRegistry.java new file mode 100644 index 0000000..1c29ab8 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/impl/JlineCommandRegistry.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jline.builtins.Completers.AnyCompleter; +import org.jline.builtins.Completers.OptDesc; +import org.jline.builtins.Completers.OptionCompleter; +import org.jline.builtins.Options; +import org.jline.builtins.Options.HelpException; +import org.jline.console.ArgDesc; +import org.jline.console.CmdDesc; +import org.jline.reader.Completer; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.utils.AttributedString; +import org.jline.utils.Log; + +/** + * CommandRegistry common methods for JLine commands that are using HelpException. + * + * @author Matti Rinta-Nikkola + */ +public abstract class JlineCommandRegistry extends AbstractCommandRegistry { + + public JlineCommandRegistry() { + super(); + } + + // + // Utils for helpMessage parsing + // + private static AttributedString highlightComment(String comment) { + return HelpException.highlightComment(comment, HelpException.defaultStyle()); + } + + private static String[] helpLines(String helpMessage, boolean body) { + return new HelpLines(helpMessage, body).lines(); + } + + public static CmdDesc compileCommandDescription(String helpMessage) { + List main = new ArrayList<>(); + Map> options = new HashMap<>(); + String prevOpt = null; + boolean mainDone = false; + HelpLines hl = new HelpLines(helpMessage, true); + for (String s : hl.lines()) { + if (s.matches("^\\s+-.*$")) { + mainDone = true; + int ind = s.lastIndexOf(" "); + if (ind > 0) { + String o = s.substring(0, ind); + String d = s.substring(ind); + if (o.trim().length() > 0) { + prevOpt = o.trim(); + options.put(prevOpt, new ArrayList<>(Collections.singletonList(highlightComment(d.trim())))); + } + } + } else if (s.matches("^[\\s]{20}.*$") && prevOpt != null && options.containsKey(prevOpt)) { + int ind = s.lastIndexOf(" "); + if (ind > 0) { + options.get(prevOpt).add(highlightComment(s.substring(ind).trim())); + } + } else { + prevOpt = null; + } + if (!mainDone) { + main.add(HelpException.highlightSyntax(s.trim(), HelpException.defaultStyle(), hl.subcommands())); + } + } + return new CmdDesc(main, ArgDesc.doArgNames(Collections.singletonList("")), options); + } + + public static List compileCommandOptions(String helpMessage) { + List out = new ArrayList<>(); + for (String s : helpLines(helpMessage, true)) { + if (s.matches("^\\s+-.*$")) { + int ind = s.lastIndexOf(" "); + if (ind > 0) { + String[] op = s.substring(0, ind).trim().split("\\s+"); + String d = s.substring(ind).trim(); + String so = null; + String lo = null; + if (op.length == 1) { + if (op[0].startsWith("--")) { + lo = op[0]; + } else { + so = op[0]; + } + } else { + so = op[0]; + lo = op[1]; + } + boolean hasValue = false; + if (lo != null && lo.contains("=")) { + hasValue = true; + lo = lo.split("=")[0]; + } + out.add(new OptDesc(so, lo, d, hasValue ? AnyCompleter.INSTANCE : null)); + } + } + } + return out; + } + + public static List compileCommandInfo(String helpMessage) { + List out = new ArrayList<>(); + boolean first = true; + for (String s : helpLines(helpMessage, false)) { + if (first && s.contains(" - ")) { + out.add(s.substring(s.indexOf(" - ") + 3).trim()); + } else { + out.add(s.trim()); + } + first = false; + } + return out; + } + + public List commandInfo(String command) { + try { + Object[] args = {"--help"}; + if (command.equals("help")) { + args = new Object[]{}; + } + invoke(new CommandSession(), command, args); + } catch (HelpException e) { + return compileCommandInfo(e.getMessage()); + } catch (Exception e) { + Log.info("Error while getting command info", e); + if (Log.isDebugEnabled()) { + e.printStackTrace(); + } + return new ArrayList<>(); + } + throw new IllegalArgumentException("JlineCommandRegistry.commandInfo() method must be overridden in class " + + this.getClass().getCanonicalName()); + } + + public CmdDesc commandDescription(List args) { + String command = args != null && !args.isEmpty() ? args.get(0) : ""; + try { + invoke(new CommandSession(), command, "--help"); + } catch (HelpException e) { + return compileCommandDescription(e.getMessage()); + } catch (Exception e) { + // ignore + } + throw new IllegalArgumentException( + "JlineCommandRegistry.commandDescription() method must be overridden in class " + + this.getClass().getCanonicalName()); + } + + public List commandOptions(String command) { + try { + invoke(new CommandSession(), command, "--help"); + } catch (HelpException e) { + return compileCommandOptions(e.getMessage()); + } catch (Exception e) { + // ignore + } + return null; + } + + public List defaultCompleter(String command) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, new OptionCompleter(NullCompleter.INSTANCE, this::commandOptions, 1))); + return completers; + } + + public Options parseOptions(String[] usage, Object[] args) throws HelpException { + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + return opt; + } + + private static class HelpLines { + private final String helpMessage; + private final boolean body; + private boolean subcommands; + + public HelpLines(String helpMessage, boolean body) { + this.helpMessage = helpMessage; + this.body = body; + } + + public String[] lines() { + String out = ""; + Matcher tm = Pattern.compile("(^|\\n)(Usage|Summary)(:)").matcher(helpMessage); + if (tm.find()) { + subcommands = tm.group(2).matches("Summary"); + if (body) { + out = helpMessage.substring(tm.end(3)); + } else { + out = helpMessage.substring(0, tm.start(1)); + } + } else if (!body) { + out = helpMessage; + } + return out.split("\\r?\\n"); + } + + public boolean subcommands() { + return subcommands; + } + } +} diff --git a/net-cli/src/main/java/org/jline/console/impl/SystemHighlighter.java b/net-cli/src/main/java/org/jline/console/impl/SystemHighlighter.java new file mode 100644 index 0000000..247cf9c --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/impl/SystemHighlighter.java @@ -0,0 +1,397 @@ +/* + * Copyright (c) 2002-2022, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console.impl; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import org.jline.builtins.Styles; +import org.jline.builtins.SyntaxHighlighter; +import org.jline.console.SystemRegistry; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser; +import org.jline.reader.impl.DefaultHighlighter; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.Log; +import org.jline.utils.OSUtils; +import org.jline.utils.StyleResolver; +import static org.jline.builtins.Styles.NANORC_THEME; +import static org.jline.builtins.SyntaxHighlighter.REGEX_TOKEN_NAME; + +/** + * Highlight command and language syntax using nanorc highlighter. + * + * @author Matti Rinta-Nikkola + */ +public class SystemHighlighter extends DefaultHighlighter { + private static final String REGEX_COMMENT_LINE = "\\s*#.*"; + private static final String READER_COLORS = "READER_COLORS"; + protected final SyntaxHighlighter commandHighlighter; + protected final SyntaxHighlighter argsHighlighter; + protected final SyntaxHighlighter langHighlighter; + protected final SystemRegistry systemRegistry; + protected final Map fileHighlight = new HashMap<>(); + protected final Map specificHighlighter = new HashMap<>(); + private final List> externalHighlightersRefresh = new ArrayList<>(); + protected int commandIndex; + private StyleResolver resolver = Styles.lsStyle(); + + public SystemHighlighter( + SyntaxHighlighter commandHighlighter, + SyntaxHighlighter argsHighlighter, + SyntaxHighlighter langHighlighter) { + this.commandHighlighter = commandHighlighter; + this.argsHighlighter = argsHighlighter; + this.langHighlighter = langHighlighter; + this.systemRegistry = SystemRegistry.get(); + } + + public void setSpecificHighlighter(String command, SyntaxHighlighter highlighter) { + this.specificHighlighter.put(command, highlighter); + } + + @Override + public void refresh(LineReader lineReader) { + Path currentTheme = null; + if (commandHighlighter != null) { + commandHighlighter.refresh(); + currentTheme = compareThemes(commandHighlighter, currentTheme); + } + if (argsHighlighter != null) { + argsHighlighter.refresh(); + currentTheme = compareThemes(argsHighlighter, currentTheme); + } + if (langHighlighter != null) { + langHighlighter.refresh(); + currentTheme = compareThemes(langHighlighter, currentTheme); + } + for (SyntaxHighlighter sh : specificHighlighter.values()) { + sh.refresh(); + currentTheme = compareThemes(sh, currentTheme); + } + if (currentTheme != null) { + try (BufferedReader reader = new BufferedReader(new FileReader(currentTheme.toFile()))) { + String line; + Map tokens = new HashMap<>(); + while ((line = reader.readLine()) != null) { + String[] parts = line.trim().split("\\s+", 2); + if (parts[0].matches(REGEX_TOKEN_NAME) && parts.length == 2) { + tokens.put(parts[0], parts[1]); + } + } + SystemRegistry registry = SystemRegistry.get(); + registry.setConsoleOption(NANORC_THEME, tokens); + Map readerColors = registry.consoleOption(READER_COLORS, new HashMap<>()); + Styles.StyleCompiler styleCompiler = new Styles.StyleCompiler(readerColors); + for (String key : readerColors.keySet()) { + lineReader.setVariable(key, styleCompiler.getStyle(key)); + } + for (Supplier refresh : externalHighlightersRefresh) { + refresh.get(); + } + resolver = Styles.lsStyle(); + } catch (IOException e) { + Log.warn(e.getMessage()); + } + } + } + + public void addExternalHighlighterRefresh(Supplier refresh) { + externalHighlightersRefresh.add(refresh); + } + + private Path compareThemes(SyntaxHighlighter highlighter, Path currentTheme) { + Path out; + if (currentTheme != null) { + Path theme = highlighter.getCurrentTheme(); + try { + if (theme != null && !Files.isSameFile(theme, currentTheme)) { + Log.warn("Multiple nanorc themes are in use!"); + } + } catch (Exception e) { + Log.warn(e.getMessage()); + } + out = currentTheme; + } else { + out = highlighter.getCurrentTheme(); + } + return out; + } + + @Override + public AttributedString highlight(LineReader reader, String buffer) { + return doDefaultHighlight(reader) ? super.highlight(reader, buffer) : systemHighlight(reader, buffer); + } + + public void addFileHighlight(String... commands) { + for (String c : commands) { + fileHighlight.put(c, new FileHighlightCommand()); + } + } + + public void addFileHighlight(String command, String subcommand, Collection fileOptions) { + fileHighlight.put(command, new FileHighlightCommand(subcommand, fileOptions)); + } + + private boolean doDefaultHighlight(LineReader reader) { + String search = reader.getSearchTerm(); + return ((search != null && search.length() > 0) + || reader.getRegionActive() != LineReader.RegionType.NONE + || errorIndex > -1 + || errorPattern != null); + } + + protected AttributedString systemHighlight(LineReader reader, String buffer) { + AttributedString out; + Parser parser = reader.getParser(); + ParsedLine pl = parser.parse(buffer, 0, Parser.ParseContext.SPLIT_LINE); + String command = pl.words().size() > 0 ? parser.getCommand(pl.words().get(0)) : ""; + command = command.startsWith("!") ? "!" : command; + commandIndex = buffer.indexOf(command) + command.length(); + if (buffer.trim().isEmpty()) { + out = new AttributedStringBuilder().append(buffer).toAttributedString(); + } else if (specificHighlighter.containsKey(command)) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + if (commandHighlighter == null) { + asb.append(specificHighlighter.get(command).reset().highlight(buffer)); + } else { + highlightCommand(buffer.substring(0, commandIndex), asb); + asb.append(specificHighlighter.get(command).reset().highlight(buffer.substring(commandIndex))); + } + out = asb.toAttributedString(); + } else if (fileHighlight.containsKey(command)) { + FileHighlightCommand fhc = fileHighlight.get(command); + if (!fhc.hasFileOptions()) { + out = doFileArgsHighlight(reader, buffer, pl.words(), fhc); + } else { + out = doFileOptsHighlight(reader, buffer, pl.words(), fhc); + } + } else if (systemRegistry.isCommandOrScript(command) + || systemRegistry.isCommandAlias(command) + || command.isEmpty() + || buffer.matches(REGEX_COMMENT_LINE)) { + out = doCommandHighlight(buffer); + } else if (langHighlighter != null) { + out = langHighlighter.reset().highlight(buffer); + } else { + out = new AttributedStringBuilder().append(buffer).toAttributedString(); + } + return out; + } + + protected AttributedString doFileOptsHighlight( + LineReader reader, String buffer, List words, FileHighlightCommand fhc) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + if (commandIndex < 0) { + highlightCommand(buffer, asb); + } else { + highlightCommand(buffer.substring(0, commandIndex), asb); + if (!fhc.isSubcommand() || (words.size() > 2 && fhc.getSubcommand().equals(words.get(1)))) { + boolean subCommand = fhc.isSubcommand(); + int idx = buffer.indexOf(words.get(0)) + words.get(0).length(); + boolean fileOption = false; + for (int i = 1; i < words.size(); i++) { + int nextIdx = buffer.substring(idx).indexOf(words.get(i)) + idx; + for (int j = idx; j < nextIdx; j++) { + asb.append(buffer.charAt(j)); + } + String word = words.get(i); + if (subCommand) { + subCommand = false; + highlightArgs(word, asb); + } else if (word.contains("=") + && fhc.getFileOptions().contains(word.substring(0, word.indexOf("=")))) { + highlightArgs(word.substring(0, word.indexOf("=") + 1), asb); + highlightFileArg(reader, word.substring(word.indexOf("=") + 1), asb); + } else if (fhc.getFileOptions().contains(word)) { + highlightArgs(word, asb); + fileOption = true; + } else if (fileOption) { + highlightFileArg(reader, word, asb); + } else { + highlightArgs(word, asb); + fileOption = false; + } + idx = nextIdx + word.length(); + } + } else { + highlightArgs(buffer.substring(commandIndex), asb); + } + } + return asb.toAttributedString(); + } + + protected AttributedString doFileArgsHighlight( + LineReader reader, String buffer, List words, FileHighlightCommand fhc) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + if (commandIndex < 0) { + highlightCommand(buffer, asb); + } else { + highlightCommand(buffer.substring(0, commandIndex), asb); + if (!fhc.isSubcommand() || (words.size() > 2 && fhc.getSubcommand().equals(words.get(1)))) { + boolean subCommand = fhc.isSubcommand(); + int idx = buffer.indexOf(words.get(0)) + words.get(0).length(); + for (int i = 1; i < words.size(); i++) { + int nextIdx = buffer.substring(idx).indexOf(words.get(i)) + idx; + for (int j = idx; j < nextIdx; j++) { + asb.append(buffer.charAt(j)); + } + if (subCommand) { + subCommand = false; + highlightArgs(words.get(i), asb); + } else { + highlightFileArg(reader, words.get(i), asb); + idx = nextIdx + words.get(i).length(); + } + } + } else { + highlightArgs(buffer.substring(commandIndex), asb); + } + } + return asb.toAttributedString(); + } + + protected AttributedString doCommandHighlight(String buffer) { + AttributedString out; + if (commandHighlighter != null || argsHighlighter != null) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + if (commandIndex < 0 || buffer.matches(REGEX_COMMENT_LINE)) { + highlightCommand(buffer, asb); + } else { + highlightCommand(buffer.substring(0, commandIndex), asb); + highlightArgs(buffer.substring(commandIndex), asb); + } + out = asb.toAttributedString(); + } else { + out = new AttributedStringBuilder().append(buffer).toAttributedString(); + } + return out; + } + + private void highlightFileArg(LineReader reader, String arg, AttributedStringBuilder asb) { + if (arg.startsWith("-")) { + highlightArgs(arg, asb); + } else { + String separator = reader.isSet(LineReader.Option.USE_FORWARD_SLASH) + ? "/" + : Paths.get(System.getProperty("user.dir")).getFileSystem().getSeparator(); + StringBuilder sb = new StringBuilder(); + try { + Path path = new File(arg).toPath(); + Iterator iterator = path.iterator(); + if (OSUtils.IS_WINDOWS && arg.matches("^[A-Za-z]:.*$")) { + if (arg.length() == 2) { + sb.append(arg); + asb.append(arg); + } else if (arg.charAt(2) == separator.charAt(0)) { + sb.append(arg, 0, 3); + asb.append(arg.substring(0, 3)); + } + } + if (arg.startsWith(separator)) { + sb.append(separator); + asb.append(separator); + } + while (iterator.hasNext()) { + sb.append(iterator.next()); + highlightFile(new File(sb.toString()).toPath(), asb); + if (iterator.hasNext()) { + sb.append(separator); + asb.append(separator); + } + } + if (arg.length() > 2 && !arg.matches("^[A-Za-z]:" + separator) && arg.endsWith(separator)) { + asb.append(separator); + } + } catch (Exception e) { + asb.append(arg); + } + } + } + + private void highlightFile(Path path, AttributedStringBuilder asb) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + String name = path.getFileName().toString(); + int idx = name.lastIndexOf("."); + String type = idx != -1 ? ".*" + name.substring(idx) : null; + if (Files.isSymbolicLink(path)) { + sb.styled(resolver.resolve(".ln"), name); + } else if (Files.isDirectory(path)) { + sb.styled(resolver.resolve(".di"), name); + } else if (Files.isExecutable(path) && !OSUtils.IS_WINDOWS) { + sb.styled(resolver.resolve(".ex"), name); + } else if (type != null && resolver.resolve(type).getStyle() != 0) { + sb.styled(resolver.resolve(type), name); + } else if (Files.isRegularFile(path)) { + sb.styled(resolver.resolve(".fi"), name); + } else { + sb.append(name); + } + asb.append(sb); + } + + private void highlightArgs(String args, AttributedStringBuilder asb) { + if (argsHighlighter != null) { + asb.append(argsHighlighter.reset().highlight(args)); + } else { + asb.append(args); + } + } + + private void highlightCommand(String command, AttributedStringBuilder asb) { + if (commandHighlighter != null) { + asb.append(commandHighlighter.reset().highlight(command)); + } else { + asb.append(command); + } + } + + protected static class FileHighlightCommand { + private final String subcommand; + private final List fileOptions = new ArrayList<>(); + + public FileHighlightCommand() { + this(null, new ArrayList<>()); + } + + public FileHighlightCommand(String subcommand, Collection fileOptions) { + this.subcommand = subcommand; + this.fileOptions.addAll(fileOptions); + } + + public boolean isSubcommand() { + return subcommand != null; + } + + public boolean hasFileOptions() { + return !fileOptions.isEmpty(); + } + + public String getSubcommand() { + return subcommand; + } + + public List getFileOptions() { + return fileOptions; + } + } +} diff --git a/net-cli/src/main/java/org/jline/console/impl/SystemRegistryImpl.java b/net-cli/src/main/java/org/jline/console/impl/SystemRegistryImpl.java new file mode 100644 index 0000000..b9dffe8 --- /dev/null +++ b/net-cli/src/main/java/org/jline/console/impl/SystemRegistryImpl.java @@ -0,0 +1,2087 @@ +/* + * Copyright (c) 2002-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.console.impl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jline.builtins.Completers.FilesCompleter; +import org.jline.builtins.Completers.OptDesc; +import org.jline.builtins.Completers.OptionCompleter; +import org.jline.builtins.ConfigurationPath; +import org.jline.builtins.Options; +import org.jline.builtins.Options.HelpException; +import org.jline.builtins.Styles; +import org.jline.console.ArgDesc; +import org.jline.console.CmdDesc; +import org.jline.console.CmdLine; +import org.jline.console.CommandInput; +import org.jline.console.CommandMethods; +import org.jline.console.CommandRegistry; +import org.jline.console.ConsoleEngine; +import org.jline.console.ConsoleEngine.ExecutionResult; +import org.jline.console.SystemRegistry; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser; +import org.jline.reader.Parser.ParseContext; +import org.jline.reader.impl.completer.AggregateCompleter; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.reader.impl.completer.SystemCompleter; +import org.jline.terminal.Attributes; +import org.jline.terminal.Attributes.InputFlag; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.Log; +import org.jline.utils.OSUtils; +import org.jline.utils.StyleResolver; +import static org.jline.keymap.KeyMap.ctrl; + +/** + * Aggregate command registries. + * + * @author Matti Rinta-Nikkola + */ +public class SystemRegistryImpl implements SystemRegistry { + + private static final Class[] BUILTIN_REGISTRIES = {Builtins.class, ConsoleEngineImpl.class}; + protected final Parser parser; + protected final ConfigurationPath configPath; + protected final Supplier workDir; + private final Map subcommands = new HashMap<>(); + private final Map pipeName = new HashMap<>(); + private final Map commandExecute = new HashMap<>(); + private final Map> commandInfos = new HashMap<>(); + private final CommandOutputStream outputStream; + private final SystemCompleter customSystemCompleter = new SystemCompleter(); + private final AggregateCompleter customAggregateCompleter = new AggregateCompleter(new ArrayList<>()); + private CommandRegistry[] commandRegistries; + private Integer consoleId; + private Exception exception; + private ScriptStore scriptStore = new ScriptStore(); + private NamesAndValues names = new NamesAndValues(); + private boolean commandGroups = true; + private Function scriptDescription; + @SuppressWarnings("this-escape") + public SystemRegistryImpl(Parser parser, Terminal terminal, Supplier workDir, ConfigurationPath configPath) { + this.parser = parser; + this.workDir = workDir; + this.configPath = configPath; + outputStream = new CommandOutputStream(terminal); + pipeName.put(Pipe.FLIP, "|;"); + pipeName.put(Pipe.NAMED, "|"); + pipeName.put(Pipe.AND, "&&"); + pipeName.put(Pipe.OR, "||"); + commandExecute.put("exit", new CommandMethods(this::exit, this::exitCompleter)); + commandExecute.put("help", new CommandMethods(this::help, this::helpCompleter)); + } + + public void rename(Pipe pipe, String name) { + if (name.matches("/w+") || pipeName.containsValue(name)) { + throw new IllegalArgumentException(); + } + pipeName.put(pipe, name); + } + + @Override + public Collection getPipeNames() { + return pipeName.values(); + } + + @Override + public void setCommandRegistries(CommandRegistry... commandRegistries) { + this.commandRegistries = commandRegistries; + for (int i = 0; i < commandRegistries.length; i++) { + if (commandRegistries[i] instanceof ConsoleEngine) { + if (consoleId != null) { + throw new IllegalArgumentException(); + } else { + this.consoleId = i; + ((ConsoleEngine) commandRegistries[i]).setSystemRegistry(this); + this.scriptStore = new ScriptStore((ConsoleEngine) commandRegistries[i]); + this.names = new NamesAndValues(configPath); + } + } else if (commandRegistries[i] instanceof SystemRegistry) { + throw new IllegalArgumentException(); + } + } + SystemRegistry.add(this); + } + + @Override + public void initialize(File script) { + if (consoleId != null) { + try { + consoleEngine().execute(script); + } catch (Exception e) { + trace(e); + } + } + } + + @Override + public Set commandNames() { + Set out = new HashSet<>(); + for (CommandRegistry r : commandRegistries) { + out.addAll(r.commandNames()); + } + out.addAll(localCommandNames()); + return out; + } + + private Set localCommandNames() { + return commandExecute.keySet(); + } + + @Override + public Map commandAliases() { + Map out = new HashMap<>(); + for (CommandRegistry r : commandRegistries) { + out.putAll(r.commandAliases()); + } + return out; + } + + @Override + public Object consoleOption(String name) { + return consoleOption(name, null); + } + + @Override + public T consoleOption(String name, T defVal) { + T out = defVal; + if (consoleId != null) { + out = consoleEngine().consoleOption(name, defVal); + } + return out; + } + + @Override + public void setConsoleOption(String name, Object value) { + if (consoleId != null) { + consoleEngine().setConsoleOption(name, value); + } + } + + /** + * Register subcommand registry + * + * @param command main command + * @param subcommandRegistry subcommand registry + */ + @Override + public void register(String command, CommandRegistry subcommandRegistry) { + subcommands.put(command, subcommandRegistry); + commandExecute.put(command, new CommandMethods(this::subcommand, this::emptyCompleter)); + } + + private List localCommandInfo(String command) { + try { + CommandRegistry subCommand = subcommands.get(command); + if (subCommand != null) { + registryHelp(subCommand); + } else { + localExecute(command, new String[]{"--help"}); + } + } catch (HelpException e) { + exception = null; + return JlineCommandRegistry.compileCommandInfo(e.getMessage()); + } catch (Exception e) { + trace(e); + } + return new ArrayList<>(); + } + + @Override + public List commandInfo(String command) { + int id = registryId(command); + List out = new ArrayList<>(); + if (id > -1) { + if (!commandInfos.containsKey(command)) { + commandInfos.put(command, commandRegistries[id].commandInfo(command)); + } + out = commandInfos.get(command); + } else if (scriptStore.hasScript(command) && consoleEngine() != null) { + out = consoleEngine().commandInfo(command); + } else if (isLocalCommand(command)) { + out = localCommandInfo(command); + } + return out; + } + + @Override + public boolean hasCommand(String command) { + return registryId(command) > -1 || isLocalCommand(command); + } + + public void setGroupCommandsInHelp(boolean commandGroups) { + this.commandGroups = commandGroups; + } + + public SystemRegistryImpl groupCommandsInHelp(boolean commandGroups) { + this.commandGroups = commandGroups; + return this; + } + + private boolean isLocalCommand(String command) { + return commandExecute.containsKey(command); + } + + @Override + public boolean isCommandOrScript(ParsedLine line) { + return isCommandOrScript(parser.getCommand(line.words().get(0))); + } + + @Override + public boolean isCommandOrScript(String command) { + if (hasCommand(command)) { + return true; + } + return scriptStore.hasScript(command); + } + + public void addCompleter(Completer completer) { + if (completer instanceof SystemCompleter sc) { + if (sc.isCompiled()) { + customAggregateCompleter.getCompleters().add(sc); + } else { + customSystemCompleter.add(sc); + } + } else { + customAggregateCompleter.getCompleters().add(completer); + } + } + + @Override + public SystemCompleter compileCompleters() { + throw new IllegalStateException("Use method completer() to retrieve Completer!"); + } + + private SystemCompleter _compileCompleters() { + SystemCompleter out = CommandRegistry.aggregateCompleters(commandRegistries); + SystemCompleter local = new SystemCompleter(); + for (String command : commandExecute.keySet()) { + CommandRegistry subCommand = subcommands.get(command); + if (subCommand != null) { + for (Map.Entry> entry : + subCommand.compileCompleters().getCompleters().entrySet()) { + for (Completer cc : entry.getValue()) { + if (!(cc instanceof ArgumentCompleter)) { + throw new IllegalArgumentException(); + } + List cmps = ((ArgumentCompleter) cc).getCompleters(); + cmps.add(0, NullCompleter.INSTANCE); + cmps.set(1, new StringsCompleter(entry.getKey())); + Completer last = cmps.get(cmps.size() - 1); + if (last instanceof OptionCompleter) { + ((OptionCompleter) last).setStartPos(cmps.size() - 1); + cmps.set(cmps.size() - 1, last); + } + local.add(command, new ArgumentCompleter(cmps)); + } + } + } else { + local.add( + command, commandExecute.get(command).compileCompleter().apply(command)); + } + } + local.add(customSystemCompleter); + out.add(local); + out.compile(); + return out; + } + + @Override + public Completer completer() { + List completers = new ArrayList<>(); + completers.add(_compileCompleters()); + completers.add(customAggregateCompleter); + if (consoleId != null) { + completers.addAll(consoleEngine().scriptCompleters()); + completers.add(new PipelineCompleter(workDir, pipeName, names).doCompleter()); + } + return new AggregateCompleter(completers); + } + + private CmdDesc localCommandDescription(String command) { + if (!isLocalCommand(command)) { + throw new IllegalArgumentException(); + } + try { + localExecute(command, new String[]{"--help"}); + } catch (HelpException e) { + exception = null; + return JlineCommandRegistry.compileCommandDescription(e.getMessage()); + } catch (Exception e) { + trace(e); + } + return null; + } + + @Override + public CmdDesc commandDescription(List args) { + CmdDesc out = new CmdDesc(false); + String command = args.get(0); + int id = registryId(command); + if (id > -1) { + out = commandRegistries[id].commandDescription(args); + } else if (scriptStore.hasScript(command) && consoleEngine() != null) { + out = consoleEngine().commandDescription(args); + } else if (isLocalCommand(command)) { + out = localCommandDescription(command); + } + return out; + } + + private CmdDesc commandDescription(CommandRegistry subreg) { + List main = new ArrayList<>(); + Map> options = new HashMap<>(); + StyleResolver helpStyle = Styles.helpStyle(); + for (String sc : new TreeSet<>(subreg.commandNames())) { + for (String info : subreg.commandInfo(sc)) { + main.add(HelpException.highlightSyntax(sc + " - " + info, helpStyle, true)); + break; + } + } + return new CmdDesc(main, ArgDesc.doArgNames(Collections.singletonList("")), options); + } + + public void setScriptDescription(Function scriptDescription) { + this.scriptDescription = scriptDescription; + } + + @Override + public CmdDesc commandDescription(CmdLine line) { + CmdDesc out = null; + String cmd = parser.getCommand(line.getArgs().get(0)); + switch (line.getDescriptionType()) { + case COMMAND: + if (isCommandOrScript(cmd) && !names.hasPipes(line.getArgs())) { + List args = line.getArgs(); + CommandRegistry subCommand = subcommands.get(cmd); + if (subCommand != null) { + String c = args.size() > 1 ? args.get(1) : null; + if (c == null || subCommand.hasCommand(c)) { + if (c != null && c.equals("help")) { + out = null; + } else if (c != null) { + out = subCommand.commandDescription(Collections.singletonList(c)); + } else { + out = commandDescription(subCommand); + } + } else { + out = commandDescription(subCommand); + } + if (out != null) { + out.setSubcommand(true); + } + } else { + args.set(0, cmd); + out = commandDescription(args); + } + } + break; + case METHOD: + case SYNTAX: + if (!isCommandOrScript(cmd) && scriptDescription != null) { + out = scriptDescription.apply(line); + } + break; + } + return out; + } + + @Override + public Object invoke(String command, Object... args) throws Exception { + Object out = null; + command = ConsoleEngine.plainCommand(command); + args = args == null ? new Object[]{null} : args; + int id = registryId(command); + if (id > -1) { + out = commandRegistries[id].invoke(commandSession(), command, args); + } else if (isLocalCommand(command)) { + out = localExecute(command, args); + } else if (consoleId != null) { + out = consoleEngine().invoke(commandSession(), command, args); + } + return out; + } + + private Object localExecute(String command, Object[] args) throws Exception { + if (!isLocalCommand(command)) { + throw new IllegalArgumentException(); + } + Object out = commandExecute.get(command).execute().apply(new CommandInput(command, args, commandSession())); + if (exception != null) { + throw exception; + } + return out; + } + + public Terminal terminal() { + return commandSession().terminal(); + } + + private CommandSession commandSession() { + return outputStream.getCommandSession(); + } + + @Override + public boolean isCommandAlias(String command) { + if (consoleEngine() == null) { + return false; + } + ConsoleEngine consoleEngine = consoleEngine(); + if (!parser.validCommandName(command) || !consoleEngine.hasAlias(command)) { + return false; + } + String value = consoleEngine.getAlias(command).split("\\s+")[0]; + return !names.isPipe(value); + } + + private String replaceCommandAlias(String variable, String command, String rawLine) { + ConsoleEngine consoleEngine = consoleEngine(); + assert consoleEngine != null; + return variable == null + ? rawLine.replaceFirst(command + "(\\b|$)", consoleEngine.getAlias(command)) + : rawLine.replaceFirst("=" + command + "(\\b|$)", "=" + consoleEngine.getAlias(command)); + } + + private String replacePipeAlias( + ArgsParser ap, String pipeAlias, List args, Map> customPipes) { + ConsoleEngine consoleEngine = consoleEngine(); + assert consoleEngine != null; + String alias = pipeAlias; + for (int j = 0; j < args.size(); j++) { + alias = alias.replaceAll("\\s\\$" + j + "\\b", " " + args.get(j)); + alias = alias.replaceAll("\\$\\{" + j + "(|:-.*)}", args.get(j)); + } + alias = alias.replaceAll("\\$\\{@}", consoleEngine.expandToList(args)); + alias = alias.replaceAll("\\$@", consoleEngine.expandToList(args)); + alias = alias.replaceAll("\\s+\\$\\d\\b", ""); + alias = alias.replaceAll("\\s+\\$\\{\\d+}", ""); + alias = alias.replaceAll("\\$\\{\\d+}", ""); + Matcher matcher = Pattern.compile("\\$\\{\\d+:-(.*?)}").matcher(alias); + if (matcher.find()) { + alias = matcher.replaceAll("$1"); + } + ap.parse(alias); + List ws = ap.args(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ws.size(); i++) { + if (ws.get(i).equals(pipeName.get(Pipe.NAMED))) { + if (i + 1 < ws.size() && consoleEngine.hasAlias(ws.get(i + 1))) { + args.clear(); + String innerPipe = consoleEngine.getAlias(ws.get(++i)); + while (i < ws.size() - 1 && !names.isPipe(ws.get(i + 1), customPipes.keySet())) { + args.add(ws.get(++i)); + } + sb.append(replacePipeAlias(ap, innerPipe, args, customPipes)); + } else { + sb.append(ws.get(i)).append(' '); + } + } else { + sb.append(ws.get(i)).append(' '); + } + } + return sb.toString(); + } + + private void replacePipeAliases(ConsoleEngine consoleEngine, Map> customPipes, ArgsParser ap) { + List words = ap.args(); + if (consoleEngine != null && words.contains(pipeName.get(Pipe.NAMED))) { + StringBuilder sb = new StringBuilder(); + boolean trace = false; + for (int i = 0; i < words.size(); i++) { + if (words.get(i).equals(pipeName.get(Pipe.NAMED))) { + if (i + 1 < words.size() && consoleEngine.hasAlias(words.get(i + 1))) { + trace = true; + List args = new ArrayList<>(); + String pipeAlias = consoleEngine.getAlias(words.get(++i)); + while (i < words.size() - 1 && !names.isPipe(words.get(i + 1), customPipes.keySet())) { + args.add(words.get(++i)); + } + sb.append(replacePipeAlias(ap, pipeAlias, args, customPipes)); + } else { + sb.append(words.get(i)).append(' '); + } + } else { + sb.append(words.get(i)).append(' '); + } + } + ap.parse(sb.toString()); + if (trace) { + consoleEngine.trace(ap.line()); + } + } + } + + private List compileCommandLine(String commandLine) { + List out = new ArrayList<>(); + ArgsParser ap = new ArgsParser(parser); + ap.parse(commandLine); + ConsoleEngine consoleEngine = consoleEngine(); + Map> customPipes = consoleEngine != null ? consoleEngine.getPipes() : new HashMap<>(); + replacePipeAliases(consoleEngine, customPipes, ap); + List words = ap.args(); + String nextRawLine = ap.line(); + int first = 0; + int last; + List pipes = new ArrayList<>(); + String pipeSource = null; + String rawLine = null; + String pipeResult = null; + if (isCommandAlias(ap.command())) { + ap.parse(replaceCommandAlias(ap.variable(), ap.command(), nextRawLine)); + replacePipeAliases(consoleEngine, customPipes, ap); + nextRawLine = ap.line(); + words = ap.args(); + } + if (!names.hasPipes(words)) { + out.add(new CommandData(ap, false, nextRawLine, ap.variable(), null, false, "")); + } else { + // + // compile pipe line + // + do { + String rawCommand = parser.getCommand(words.get(first)); + String command = ConsoleEngine.plainCommand(rawCommand); + String variable = parser.getVariable(words.get(first)); + if (isCommandAlias(command)) { + ap.parse(replaceCommandAlias(variable, command, nextRawLine)); + replacePipeAliases(consoleEngine, customPipes, ap); + rawCommand = ap.rawCommand(); + command = ap.command(); + words = ap.args(); + first = 0; + } + if (scriptStore.isConsoleScript(command) && !rawCommand.startsWith(":")) { + throw new IllegalArgumentException("Commands must be used in pipes with colon prefix!"); + } + last = words.size(); + File file = null; + boolean append = false; + boolean pipeStart = false; + boolean skipPipe = false; + List _words = new ArrayList<>(); + // + // find next pipe + // + for (int i = first; i < last; i++) { + if (words.get(i).equals(">") || words.get(i).equals(">>")) { + pipes.add(words.get(i)); + append = words.get(i).equals(">>"); + if (i + 1 >= last) { + throw new IllegalArgumentException(); + } + file = redirectFile(words.get(i + 1)); + last = i + 1; + break; + } else if (consoleId == null) { + _words.add(words.get(i)); + } else if (words.get(i).equals(pipeName.get(Pipe.FLIP))) { + if (variable != null || file != null || pipeResult != null || consoleId == null) { + throw new IllegalArgumentException(); + } + pipes.add(words.get(i)); + last = i; + variable = "_pipe" + (pipes.size() - 1); + break; + } else if (words.get(i).equals(pipeName.get(Pipe.NAMED)) + || (words.get(i).matches("^.*[^a-zA-Z0-9 ].*$") && customPipes.containsKey(words.get(i)))) { + String pipe = words.get(i); + if (pipe.equals(pipeName.get(Pipe.NAMED))) { + if (i + 1 >= last) { + throw new IllegalArgumentException("Pipe is NULL!"); + } + pipe = words.get(i + 1); + if (!pipe.matches("\\w+") || !customPipes.containsKey(pipe)) { + throw new IllegalArgumentException("Unknown or illegal pipe name: " + pipe); + } + } + pipes.add(pipe); + last = i; + if (pipeSource == null) { + pipeSource = "_pipe" + (pipes.size() - 1); + pipeResult = variable; + variable = pipeSource; + pipeStart = true; + } + break; + } else if (words.get(i).equals(pipeName.get(Pipe.OR)) + || words.get(i).equals(pipeName.get(Pipe.AND))) { + if (variable != null || pipeSource != null) { + pipes.add(words.get(i)); + } else if (pipes.size() > 0 + && (pipes.get(pipes.size() - 1).equals(">") + || pipes.get(pipes.size() - 1).equals(">>"))) { + pipes.remove(pipes.size() - 1); + out.get(out.size() - 1).setPipe(words.get(i)); + skipPipe = true; + } else { + pipes.add(words.get(i)); + pipeSource = "_pipe" + (pipes.size() - 1); + pipeResult = variable; + variable = pipeSource; + pipeStart = true; + } + last = i; + break; + } else { + _words.add(words.get(i)); + } + } + if (last == words.size()) { + pipes.add("END_PIPE"); + } else if (skipPipe) { + first = last + 1; + continue; + } + // + // compose pipe command + // + String subLine = last < words.size() || first > 0 ? String.join(" ", _words) : ap.line(); + if (last + 1 < words.size()) { + nextRawLine = String.join(" ", words.subList(last + 1, words.size())); + } + boolean done = true; + boolean statement = false; + List arglist = new ArrayList<>(); + if (_words.size() > 0) { + arglist.addAll(_words.subList(1, _words.size())); + } + if (rawLine != null || (pipes.size() > 1 && customPipes.containsKey(pipes.get(pipes.size() - 2)))) { + done = false; + if (rawLine == null) { + rawLine = pipeSource; + } + if (customPipes.containsKey(pipes.get(pipes.size() - 2))) { + List fixes = customPipes.get(pipes.get(pipes.size() - 2)); + if (pipes.get(pipes.size() - 2).matches("\\w+")) { + int idx = subLine.indexOf(" "); + subLine = idx > 0 ? subLine.substring(idx + 1) : ""; + } + rawLine += fixes.get(0) + + (consoleId != null ? consoleEngine().expandCommandLine(subLine) : subLine) + + fixes.get(1); + statement = true; + } + if (pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.FLIP)) + || pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.AND)) + || pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.OR))) { + done = true; + pipeSource = null; + if (variable != null) { + rawLine = variable + " = " + rawLine; + } + } + if (last + 1 >= words.size() || file != null) { + done = true; + pipeSource = null; + if (pipeResult != null) { + rawLine = pipeResult + " = " + rawLine; + } + } + } else if (pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.FLIP)) || pipeStart) { + if (pipeStart && pipeResult != null) { + subLine = subLine.substring(subLine.indexOf("=") + 1); + } + rawLine = flipArgument(command, subLine, pipes, arglist); + rawLine = variable + "=" + rawLine; + } else { + rawLine = flipArgument(command, subLine, pipes, arglist); + } + if (done) { + // + // add composed command to return list + // + out.add(new CommandData( + ap, statement, rawLine, variable, file, append, pipes.get(pipes.size() - 1))); + if (pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.AND)) + || pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.OR))) { + pipeSource = null; + pipeResult = null; + } + rawLine = null; + } + first = last + 1; + } while (first < words.size()); + } + return out; + } + + private File redirectFile(String name) { + File out; + if (name.equals("null")) { + out = OSUtils.IS_WINDOWS ? new File("NUL") : new File("/dev/null"); + } else { + out = new File(name); + } + return out; + } + + private String flipArgument( + final String command, final String subLine, final List pipes, List arglist) { + String out; + if (pipes.size() > 1 && pipes.get(pipes.size() - 2).equals(pipeName.get(Pipe.FLIP))) { + String s = isCommandOrScript(command) ? "$" : ""; + out = subLine + " " + s + "_pipe" + (pipes.size() - 2); + if (!command.isEmpty()) { + arglist.add(s + "_pipe" + (pipes.size() - 2)); + } + } else { + out = subLine; + } + return out; + } + + private Object execute(String command, String rawLine, String[] args) throws Exception { + if (!parser.validCommandName(command)) { + throw new UnknownCommandException("Invalid command: " + rawLine); + } + Object out; + if (isLocalCommand(command)) { + out = localExecute(command, consoleId != null ? consoleEngine().expandParameters(args) : args); + } else { + int id = registryId(command); + if (id > -1) { + Object[] _args = consoleId != null ? consoleEngine().expandParameters(args) : args; + out = commandRegistries[id].invoke(outputStream.getCommandSession(), command, _args); + } else if (scriptStore.hasScript(command) && consoleEngine() != null) { + out = consoleEngine().execute(command, rawLine, args); + } else { + throw new UnknownCommandException("Unknown command: " + command); + } + } + return out; + } + + @Override + public Object execute(String line) throws Exception { + if (line.trim().isEmpty() || line.trim().startsWith("#")) { + return null; + } + long start = new Date().getTime(); + Object out = null; + boolean statement = false; + boolean postProcessed = false; + int errorCount = 0; + scriptStore.refresh(); + List cmds = compileCommandLine(line); + ConsoleEngine consoleEngine = consoleEngine(); + for (CommandData cmd : cmds) { + if (cmd.file() != null && scriptStore.isConsoleScript(cmd.command())) { + throw new IllegalArgumentException("Console script output cannot be redirected!"); + } + try { + outputStream.close(); + if (consoleEngine != null && !consoleEngine.isExecuting()) { + trace(cmd); + } + exception = null; + statement = false; + postProcessed = false; + if (cmd.variable() != null || cmd.file() != null) { + if (cmd.file() != null) { + outputStream.redirect(cmd.file(), cmd.append()); + } else if (consoleId != null) { + outputStream.redirect(); + } + outputStream.open(consoleOption("redirectColor", false)); + } + boolean consoleScript = false; + try { + out = execute(cmd.command(), cmd.rawLine(), cmd.args()); + } catch (UnknownCommandException e) { + if (consoleEngine == null) { + throw e; + } + consoleScript = true; + } + if (consoleEngine != null) { + if (consoleScript) { + statement = cmd.command().isEmpty() || !scriptStore.hasScript(cmd.command()); + if (statement && outputStream.isByteOutputStream()) { + outputStream.close(); + } + out = consoleEngine.execute(cmd.command(), cmd.rawLine(), cmd.args()); + } + if (cmd.pipe().equals(pipeName.get(Pipe.OR)) || cmd.pipe().equals(pipeName.get(Pipe.AND))) { + ExecutionResult er = postProcess(cmd, statement, consoleEngine, out); + postProcessed = true; + consoleEngine.println(er.result()); + out = null; + boolean success = er.status() == 0; + if ((cmd.pipe().equals(pipeName.get(Pipe.OR)) && success) + || (cmd.pipe().equals(pipeName.get(Pipe.AND)) && !success)) { + break; + } + } + } + } catch (HelpException e) { + trace(e); + } catch (Exception e) { + errorCount++; + if (cmd.pipe().equals(pipeName.get(Pipe.OR))) { + trace(e); + postProcessed = true; + } else { + throw e; + } + } finally { + if (!postProcessed && consoleEngine != null) { + out = postProcess(cmd, statement, consoleEngine, out).result(); + } + } + } + if (errorCount == 0) { + names.extractNames(line); + } + Log.debug("execute: ", new Date().getTime() - start, " msec"); + return out; + } + + private ExecutionResult postProcess( + CommandData cmd, boolean statement, ConsoleEngine consoleEngine, Object result) { + ExecutionResult out; + if (cmd.file() != null) { + int status = 1; + if (cmd.file().exists()) { + long delta = new Date().getTime() - cmd.file().lastModified(); + status = delta < 100 ? 0 : 1; + } + out = new ExecutionResult(status, result); + } else if (!statement) { + outputStream.close(); + out = consoleEngine.postProcess(cmd.rawLine(), result, outputStream.getOutput()); + } else if (cmd.variable() != null) { + if (consoleEngine.hasVariable(cmd.variable())) { + out = consoleEngine.postProcess(consoleEngine.getVariable(cmd.variable())); + } else { + out = consoleEngine.postProcess(result); + } + out = new ExecutionResult(out.status(), null); + } else { + out = consoleEngine.postProcess(result); + } + return out; + } + + public void cleanUp() { + outputStream.close(); + outputStream.resetOutput(); + if (consoleEngine() != null) { + consoleEngine().purge(); + } + } + + private void trace(CommandData commandData) { + if (consoleEngine() != null) { + consoleEngine().trace(commandData); + } else { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(commandData.rawLine(), AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)) + .println(terminal()); + } + } + + @Override + public void trace(Throwable exception) { + outputStream.close(); + ConsoleEngine consoleEngine = consoleEngine(); + if (consoleEngine != null) { + if (!(exception instanceof Options.HelpException)) { + consoleEngine.putVariable("exception", exception); + } + consoleEngine.trace(exception); + } else { + trace(false, exception); + } + } + + @Override + public void trace(boolean stack, Throwable exception) { + if (exception instanceof Options.HelpException) { + Options.HelpException.highlight((exception).getMessage(), Styles.helpStyle()) + .print(terminal()); + } else if (exception instanceof UnknownCommandException) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(exception.getMessage(), Styles.prntStyle().resolve(".em")); + asb.toAttributedString().println(terminal()); + } else if (stack) { + exception.printStackTrace(); + } else { + String message = exception.getMessage(); + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.style(Styles.prntStyle().resolve(".em")); + if (message != null) { + asb.append(exception.getClass().getSimpleName()).append(": ").append(message); + } else { + asb.append("Caught exception: "); + asb.append(exception.getClass().getCanonicalName()); + } + asb.toAttributedString().println(terminal()); + Log.debug("Stack: ", exception); + } + } + + @Override + public void close() { + names.save(); + } + + public ConsoleEngine consoleEngine() { + return consoleId != null ? (ConsoleEngine) commandRegistries[consoleId] : null; + } + + private boolean isBuiltinRegistry(CommandRegistry registry) { + for (Class c : BUILTIN_REGISTRIES) { + if (c == registry.getClass()) { + return true; + } + } + return false; + } + + private void printHeader(String header) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(2); + asb.append("\t"); + asb.append(header, HelpException.defaultStyle().resolve(".ti")); + asb.append(":"); + asb.toAttributedString().println(terminal()); + } + + private void printCommandInfo(String command, String info, int max) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(Arrays.asList(4, max + 4)); + asb.append("\t"); + asb.append(command, HelpException.defaultStyle().resolve(".co")); + asb.append("\t"); + asb.append(info); + asb.setLength(terminal().getWidth()); + asb.toAttributedString().println(terminal()); + } + + private void printCommands(Collection commands, int max) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(Arrays.asList(4, max + 4)); + int col = 0; + asb.append("\t"); + col += 4; + boolean done = false; + for (String c : commands) { + asb.append(c, HelpException.defaultStyle().resolve(".co")); + asb.append("\t"); + col += max; + if (col + max > terminal().getWidth()) { + asb.toAttributedString().println(terminal()); + asb = new AttributedStringBuilder().tabs(Arrays.asList(4, max + 4)); + col = 0; + asb.append("\t"); + col += 4; + done = true; + } else { + done = false; + } + } + if (!done) { + asb.toAttributedString().println(terminal()); + } + terminal().flush(); + } + + private String doCommandInfo(List info) { + return info != null && info.size() > 0 ? info.get(0) : " "; + } + + private boolean isInTopics(List args, String name) { + return args.isEmpty() || args.contains(name); + } + + private Options parseOptions(String[] usage, Object[] args) throws HelpException { + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + return opt; + } + + private Object help(CommandInput input) { + String groupsOption = commandGroups ? "nogroups" : "groups"; + String groupsHelp = commandGroups + ? " --nogroups Commands are not grouped by registries" + : " --groups Commands are grouped by registries"; + final String[] usage = { + "help - command help", + "Usage: help [TOPIC...]", + " -? --help Displays command help", + groupsHelp, + " -i --info List commands with a short command info" + }; + try { + Options opt = parseOptions(usage, input.args()); + boolean doTopic = false; + boolean cg = commandGroups; + boolean info = false; + if (!opt.args().isEmpty() && opt.args().size() == 1) { + try { + String[] args = {"--help"}; + String command = opt.args().get(0); + execute(command, command + " " + args[0], args); + } catch (UnknownCommandException e) { + doTopic = true; + } catch (Exception e) { + exception = e; + } + } else { + doTopic = true; + if (opt.isSet(groupsOption)) { + cg = !cg; + } + if (opt.isSet("info")) { + info = true; + } + } + if (doTopic) { + helpTopic(opt.args(), cg, info); + } + } catch (Exception e) { + exception = e; + } + return null; + } + + private void helpTopic(List topics, boolean commandGroups, boolean info) { + Set commands = commandNames(); + commands.addAll(scriptStore.getScripts()); + boolean withInfo = commands.size() < terminal().getHeight() || !topics.isEmpty() || info; + int max = + Collections.max(commands, Comparator.comparing(String::length)).length() + 1; + TreeMap builtinCommands = new TreeMap<>(); + TreeMap systemCommands = new TreeMap<>(); + if (!commandGroups && topics.isEmpty()) { + TreeSet ordered = new TreeSet<>(commands); + if (withInfo) { + for (String c : ordered) { + List infos = commandInfo(c); + String cmdInfo = infos.isEmpty() ? "" : infos.get(0); + printCommandInfo(c, cmdInfo, max); + } + } else { + printCommands(ordered, max); + } + } else { + for (CommandRegistry r : commandRegistries) { + if (isBuiltinRegistry(r)) { + for (String c : r.commandNames()) { + builtinCommands.put(c, doCommandInfo(commandInfo(c))); + } + } + } + for (String c : localCommandNames()) { + systemCommands.put(c, doCommandInfo(commandInfo(c))); + exception = null; + } + if (isInTopics(topics, "System")) { + printHeader("System"); + if (withInfo) { + for (Map.Entry entry : systemCommands.entrySet()) { + printCommandInfo(entry.getKey(), entry.getValue(), max); + } + } else { + printCommands(systemCommands.keySet(), max); + } + } + if (isInTopics(topics, "Builtins") && !builtinCommands.isEmpty()) { + printHeader("Builtins"); + if (withInfo) { + for (Map.Entry entry : builtinCommands.entrySet()) { + printCommandInfo(entry.getKey(), entry.getValue(), max); + } + } else { + printCommands(builtinCommands.keySet(), max); + } + } + for (CommandRegistry r : commandRegistries) { + if (isBuiltinRegistry(r) + || !isInTopics(topics, r.name()) + || r.commandNames().isEmpty()) { + continue; + } + TreeSet cmds = new TreeSet<>(r.commandNames()); + printHeader(r.name()); + if (withInfo) { + for (String c : cmds) { + printCommandInfo(c, doCommandInfo(commandInfo(c)), max); + } + } else { + printCommands(cmds, max); + } + } + if (consoleId != null + && isInTopics(topics, "Scripts") + && !scriptStore.getScripts().isEmpty()) { + printHeader("Scripts"); + if (withInfo) { + for (String c : scriptStore.getScripts()) { + printCommandInfo(c, doCommandInfo(commandInfo(c)), max); + } + } else { + printCommands(scriptStore.getScripts(), max); + } + } + } + terminal().flush(); + } + + private Object exit(CommandInput input) { + final String[] usage = { + "exit - exit from app/script", + "Usage: exit [OBJECT]", + " -? --help Displays command help" + }; + try { + Options opt = parseOptions(usage, input.xargs()); + ConsoleEngine consoleEngine = consoleEngine(); + if (!opt.argObjects().isEmpty() && consoleEngine != null) { + try { + consoleEngine.putVariable( + "_return", + opt.argObjects().size() == 1 ? opt.argObjects().get(0) : opt.argObjects()); + } catch (Exception e) { + trace(e); + } + } + exception = new EndOfFileException(); + } catch (Exception e) { + exception = e; + } + return null; + } + + private void registryHelp(CommandRegistry registry) throws Exception { + List tabs = new ArrayList<>(); + tabs.add(0); + tabs.add(9); + int max = registry.commandNames().stream() + .map(String::length) + .max(Integer::compareTo) + .get(); + tabs.add(10 + max); + AttributedStringBuilder sb = new AttributedStringBuilder().tabs(tabs); + sb.append(" - "); + sb.append(registry.name()); + sb.append(" registry"); + sb.append("\n"); + boolean first = true; + for (String c : new TreeSet<>(registry.commandNames())) { + if (first) { + sb.append("Summary:"); + first = false; + } + sb.append("\t"); + sb.append(c); + sb.append("\t"); + sb.append(registry.commandInfo(c).get(0)); + sb.append("\n"); + } + throw new HelpException(sb.toString()); + } + + private Object subcommand(CommandInput input) { + Object out = null; + try { + if (input.args().length > 0 && subcommands.get(input.command()).hasCommand(input.args()[0])) { + out = subcommands + .get(input.command()) + .invoke( + input.session(), + input.args()[0], + input.xargs().length > 1 + ? Arrays.copyOfRange(input.xargs(), 1, input.xargs().length) + : new Object[]{}); + } else { + registryHelp(subcommands.get(input.command())); + } + } catch (Exception e) { + exception = e; + } + return out; + } + + private List commandOptions(String command) { + try { + localExecute(command, new String[]{"--help"}); + } catch (HelpException e) { + exception = null; + return JlineCommandRegistry.compileCommandOptions(e.getMessage()); + } catch (Exception e) { + trace(e); + } + return null; + } + + private List registryNames() { + List out = new ArrayList<>(); + out.add("System"); + out.add("Builtins"); + if (consoleId != null) { + out.add("Scripts"); + } + for (CommandRegistry r : commandRegistries) { + if (!isBuiltinRegistry(r)) { + out.add(r.name()); + } + } + out.addAll(commandNames()); + out.addAll(scriptStore.getScripts()); + return out; + } + + private List emptyCompleter(String command) { + return new ArrayList<>(); + } + + private List helpCompleter(String command) { + List completers = new ArrayList<>(); + List params = new ArrayList<>(); + params.add(new StringsCompleter(this::registryNames)); + params.add(NullCompleter.INSTANCE); + completers.add( + new ArgumentCompleter(NullCompleter.INSTANCE, new OptionCompleter(params, this::commandOptions, 1))); + return completers; + } + + private List exitCompleter(String command) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter( + NullCompleter.INSTANCE, new OptionCompleter(NullCompleter.INSTANCE, this::commandOptions, 1))); + return completers; + } + + private int registryId(String command) { + for (int i = 0; i < commandRegistries.length; i++) { + if (commandRegistries[i].hasCommand(command)) { + return i; + } + } + return -1; + } + + public enum Pipe { + FLIP, + NAMED, + AND, + OR + } + + private static class CommandOutputStream { + private final PrintStream origOut; + private final PrintStream origErr; + private final Terminal origTerminal; + private OutputStream outputStream; + private Terminal terminal; + private String output; + private CommandRegistry.CommandSession commandSession; + private boolean redirecting = false; + + public CommandOutputStream(Terminal terminal) { + this.origOut = System.out; + this.origErr = System.err; + this.origTerminal = terminal; + this.terminal = terminal; + PrintStream ps = new PrintStream(terminal.output()); + this.commandSession = new CommandRegistry.CommandSession(terminal, terminal.input(), ps, ps); + } + + public void redirect() { + outputStream = new ByteArrayOutputStream(); + } + + public void redirect(File file, boolean append) throws IOException { + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + (new File(file.getParent())).mkdirs(); + file.createNewFile(); + } + } + outputStream = new FileOutputStream(file, append); + } + + public void open(boolean redirectColor) throws IOException { + if (redirecting || outputStream == null) { + return; + } + output = null; + PrintStream out = new PrintStream(outputStream); + System.setOut(out); + System.setErr(out); + String input = ctrl('X') + "q"; + InputStream in = new ByteArrayInputStream(input.getBytes()); + Attributes attrs = new Attributes(); + if (OSUtils.IS_WINDOWS) { + attrs.setInputFlag(InputFlag.IGNCR, true); + } + try { + terminal = TerminalBuilder.builder() + .streams(in, outputStream) + .attributes(attrs) + .type((redirectColor ? Terminal.TYPE_DUMB_COLOR : Terminal.TYPE_DUMB)) + .build(); + this.commandSession = new CommandRegistry.CommandSession(terminal, terminal.input(), out, out); + redirecting = true; + } catch (IOException e) { + reset(); + throw e; + } + } + + public void close() { + if (!redirecting) { + return; + } + try { + terminal.flush(); + if (outputStream instanceof ByteArrayOutputStream) { + output = outputStream.toString(); + } + terminal.close(); + } catch (Exception e) { + // ignore + } + reset(); + } + + public void resetOutput() { + output = null; + } + + private void reset() { + outputStream = null; + System.setOut(origOut); + System.setErr(origErr); + terminal = null; + terminal = origTerminal; + PrintStream ps = new PrintStream(terminal.output()); + this.commandSession = new CommandRegistry.CommandSession(terminal, terminal.input(), ps, ps); + redirecting = false; + } + + public CommandRegistry.CommandSession getCommandSession() { + return commandSession; + } + + public String getOutput() { + return output; + } + + public boolean isRedirecting() { + return redirecting; + } + + public boolean isByteOutputStream() { + return outputStream instanceof ByteArrayOutputStream; + } + } + + private static class ArgsParser { + private final Parser parser; + private int round = 0; + private int curly = 0; + private int square = 0; + private boolean quoted; + private boolean doubleQuoted; + private String line; + private String command = ""; + private String variable = ""; + private List args; + + public ArgsParser(Parser parser) { + this.parser = parser; + } + + private void reset() { + round = 0; + curly = 0; + square = 0; + quoted = false; + doubleQuoted = false; + } + + private void next(String arg) { + char prevChar = ' '; + for (int i = 0; i < arg.length(); i++) { + char c = arg.charAt(i); + if (!parser.isEscapeChar(prevChar)) { + if (!quoted && !doubleQuoted) { + if (c == '(') { + round++; + } else if (c == ')') { + round--; + } else if (c == '{') { + curly++; + } else if (c == '}') { + curly--; + } else if (c == '[') { + square++; + } else if (c == ']') { + square--; + } else if (c == '"') { + doubleQuoted = true; + } else if (c == '\'') { + quoted = true; + } + } else if (quoted && c == '\'') { + quoted = false; + } else if (doubleQuoted && c == '"') { + doubleQuoted = false; + } + } + prevChar = c; + } + } + + private boolean isEnclosed() { + return round == 0 && curly == 0 && square == 0 && !quoted && !doubleQuoted; + } + + public boolean isEnclosed(String arg) { + reset(); + next(arg); + return isEnclosed(); + } + + private void enclosedArgs(List words) { + args = new ArrayList<>(); + reset(); + boolean first = true; + StringBuilder sb = new StringBuilder(); + for (String a : words) { + next(a); + if (!first) { + sb.append(" "); + } + if (isEnclosed()) { + sb.append(a); + args.add(sb.toString()); + sb = new StringBuilder(); + first = true; + } else { + sb.append(a); + first = false; + } + } + if (!first) { + args.add(sb.toString()); + } + } + + public void parse(String line) { + this.line = line; + ParsedLine pl = parser.parse(line, 0, ParseContext.SPLIT_LINE); + enclosedArgs(pl.words()); + if (!args.isEmpty()) { + this.command = parser.getCommand(args.get(0)); + if (!parser.validCommandName(command)) { + this.command = ""; + } + this.variable = parser.getVariable(args.get(0)); + } else { + this.line = ""; + } + } + + public String line() { + return line; + } + + public String command() { + return ConsoleEngine.plainCommand(command); + } + + public String rawCommand() { + return command; + } + + public String variable() { + return variable; + } + + public List args() { + return args; + } + + private int closingQuote(String arg) { + int out = -1; + char prevChar = ' '; + for (int i = 1; i < arg.length(); i++) { + char c = arg.charAt(i); + if (!parser.isEscapeChar(prevChar)) { + if (c == arg.charAt(0)) { + out = i; + break; + } + } + prevChar = c; + } + return out; + } + + private String unquote(String arg) { + if (arg.length() > 1 && (arg.startsWith("\"") && arg.endsWith("\"")) + || (arg.startsWith("'") && arg.endsWith("'"))) { + if (closingQuote(arg) == arg.length() - 1) { + return arg.substring(1, arg.length() - 1); + } + } + return arg; + } + + /** + * Unescapes a string that contains standard Java escape sequences. + *
    + *
  • \b \f \n \r \t \" \' : + * BS, FF, NL, CR, TAB, double and single quote.
  • + *
  • \X \XX \XXX : Octal character + * specification (0 - 377, 0x00 - 0xFF).
  • + *
  • \uXXXX : Hexadecimal based Unicode character.
  • + *
+ * + * @param arg A string optionally containing standard java escape sequences. + * @return The translated string. + * @author Udo Klimaschewski, https://gist.github.com/uklimaschewski/6741769 + */ + private String unescape(String arg) { + if (arg == null || !parser.isEscapeChar('\\')) { + return arg; + } + StringBuilder sb = new StringBuilder(arg.length()); + for (int i = 0; i < arg.length(); i++) { + char ch = arg.charAt(i); + if (ch == '\\') { + char nextChar = (i == arg.length() - 1) ? '\\' : arg.charAt(i + 1); + // Octal escape? + if (nextChar >= '0' && nextChar <= '7') { + String code = "" + nextChar; + i++; + if ((i < arg.length() - 1) && arg.charAt(i + 1) >= '0' && arg.charAt(i + 1) <= '7') { + code += arg.charAt(i + 1); + i++; + if ((i < arg.length() - 1) && arg.charAt(i + 1) >= '0' && arg.charAt(i + 1) <= '7') { + code += arg.charAt(i + 1); + i++; + } + } + sb.append((char) Integer.parseInt(code, 8)); + continue; + } + switch (nextChar) { + case '\\': + ch = '\\'; + break; + case 'b': + ch = '\b'; + break; + case 'f': + ch = '\f'; + break; + case 'n': + ch = '\n'; + break; + case 'r': + ch = '\r'; + break; + case 't': + ch = '\t'; + break; + case '\"': + ch = '\"'; + break; + case '\'': + ch = '\''; + break; + case ' ': + ch = ' '; + break; + // Hex Unicode: u???? + case 'u': + if (i >= arg.length() - 5) { + ch = 'u'; + break; + } + int code = Integer.parseInt( + "" + arg.charAt(i + 2) + arg.charAt(i + 3) + arg.charAt(i + 4) + arg.charAt(i + 5), + 16); + sb.append(Character.toChars(code)); + i += 5; + continue; + } + i++; + } + sb.append(ch); + } + return sb.toString(); + } + } + + protected static class CommandData { + private final String rawLine; + private final File file; + private final boolean append; + private final String variable; + private String command; + private String[] args; + private String pipe; + + public CommandData( + ArgsParser parser, + boolean statement, + String rawLine, + String variable, + File file, + boolean append, + String pipe) { + this.rawLine = rawLine; + this.variable = variable; + this.file = file; + this.append = append; + this.pipe = pipe; + this.args = new String[]{}; + this.command = ""; + if (!statement) { + parser.parse(rawLine); + this.command = parser.command(); + if (parser.args().size() > 1) { + this.args = new String[parser.args().size() - 1]; + for (int i = 1; i < parser.args().size(); i++) { + args[i - 1] = + parser.unescape(parser.unquote(parser.args().get(i))); + } + } + } + } + + public void setPipe(String pipe) { + this.pipe = pipe; + } + + public File file() { + return file; + } + + public boolean append() { + return append; + } + + public String variable() { + return variable; + } + + public String command() { + return command; + } + + public String[] args() { + return args; + } + + public String rawLine() { + return rawLine; + } + + public String pipe() { + return pipe; + } + + @Override + public String toString() { + return "[" + "rawLine:" + + rawLine + ", " + "command:" + + command + ", " + "args:" + + Arrays.asList(args) + ", " + "variable:" + + variable + ", " + "file:" + + file + ", " + "append:" + + append + ", " + "pipe:" + + pipe + "]"; + } + } + + private static class ScriptStore { + ConsoleEngine engine; + Map scripts = new HashMap<>(); + + public ScriptStore() { + } + + public ScriptStore(ConsoleEngine engine) { + this.engine = engine; + } + + public void refresh() { + if (engine != null) { + scripts = engine.scripts(); + } + } + + public boolean hasScript(String name) { + return scripts.containsKey(name); + } + + public boolean isConsoleScript(String name) { + return scripts.getOrDefault(name, false); + } + + public Set getScripts() { + return scripts.keySet(); + } + } + + @SuppressWarnings("serial") + public static class UnknownCommandException extends Exception { + public UnknownCommandException(String message) { + super(message); + } + } + + private static class PipelineCompleter implements Completer { + private final NamesAndValues names; + private final Supplier workDir; + private final Map pipeName; + + public PipelineCompleter(Supplier workDir, Map pipeName, NamesAndValues names) { + this.workDir = workDir; + this.pipeName = pipeName; + this.names = names; + } + + public Completer doCompleter() { + ArgumentCompleter out = new ArgumentCompleter(this); + out.setStrict(false); + return out; + } + + @Override + public void complete(LineReader reader, ParsedLine commandLine, List candidates) { + assert commandLine != null; + assert candidates != null; + ArgsParser ap = new ArgsParser(reader.getParser()); + ap.parse(commandLine.line().substring(0, commandLine.cursor())); + List args = ap.args(); + if (args.size() < 2 || !names.hasPipes(args)) { + return; + } + boolean enclosed = ap.isEnclosed(args.get(args.size() - 1)); + String pWord = commandLine.words().get(commandLine.wordIndex() - 1); + if (enclosed && pWord.equals(pipeName.get(Pipe.NAMED))) { + for (String name : names.namedPipes()) { + candidates.add(new Candidate(name, name, null, null, null, null, true)); + } + } else if (enclosed && pWord.equals(">") || pWord.equals(">>")) { + Completer c = new FilesCompleter(workDir); + c.complete(reader, commandLine, candidates); + } else { + String buffer = commandLine.word().substring(0, commandLine.wordCursor()); + String param = buffer; + String curBuf = ""; + int lastDelim = names.indexOfLastDelim(buffer); + if (lastDelim > -1) { + param = buffer.substring(lastDelim + 1); + curBuf = buffer.substring(0, lastDelim + 1); + } + if (curBuf.startsWith("--") && !curBuf.contains("=")) { + doCandidates(candidates, names.options(), curBuf, "", param); + } else if (param.length() == 0) { + doCandidates(candidates, names.fieldsAndValues(), curBuf, "", ""); + } else if (param.contains(".")) { + int point = buffer.lastIndexOf("."); + param = buffer.substring(point + 1); + curBuf = buffer.substring(0, point + 1); + doCandidates(candidates, names.fields(), curBuf, "", param); + } else if (names.encloseBy(param).length() == 1) { + lastDelim++; + String postFix = names.encloseBy(param); + param = buffer.substring(lastDelim + 1); + curBuf = buffer.substring(0, lastDelim + 1); + doCandidates(candidates, names.quoted(), curBuf, postFix, param); + } else { + doCandidates(candidates, names.fieldsAndValues(), curBuf, "", param); + } + } + } + + private void doCandidates( + List candidates, Collection fields, String curBuf, String postFix, String hint) { + if (fields == null) { + return; + } + for (String s : fields) { + if (s != null && s.startsWith(hint)) { + candidates.add(new Candidate( + AttributedString.stripAnsi(curBuf + s + postFix), s, null, null, null, null, false)); + } + } + } + } + + private class NamesAndValues { + private final String[] delims = { + "&", "\\|", "\\{", "\\}", "\\[", "\\]", "\\(", "\\)", "\\+", "-", "\\*", "=", ">", "<", "~", "!", ":", ",", + ";" + }; + private final Map> names = new HashMap<>(); + private Path fileNames; + private List namedPipes; + + public NamesAndValues() { + this(null); + } + + @SuppressWarnings("unchecked") + public NamesAndValues(ConfigurationPath configPath) { + names.put("fields", new ArrayList<>()); + names.put("values", new ArrayList<>()); + names.put("quoted", new ArrayList<>()); + names.put("options", new ArrayList<>()); + ConsoleEngine consoleEngine = consoleEngine(); + if (configPath != null && consoleEngine != null) { + try { + fileNames = configPath.getUserConfig("pipeline-names.json", true); + Map> temp = (Map>) consoleEngine.slurp(fileNames); + for (Entry> entry : temp.entrySet()) { + names.get(entry.getKey()).addAll(entry.getValue()); + } + } catch (Exception e) { + // ignore + } + } + } + + public boolean isPipe(String arg) { + Map> customPipes = + consoleEngine() != null ? consoleEngine().getPipes() : new HashMap<>(); + return isPipe(arg, customPipes.keySet()); + } + + public boolean hasPipes(Collection args) { + Map> customPipes = + consoleEngine() != null ? consoleEngine().getPipes() : new HashMap<>(); + for (String a : args) { + if (isPipe(a, customPipes.keySet()) || a.contains(">") || a.contains(">>")) { + return true; + } + } + return false; + } + + private boolean isPipe(String arg, Set pipes) { + return pipeName.containsValue(arg) || pipes.contains(arg); + } + + public void extractNames(String line) { + if (parser.getCommand(line).equals("pipe")) { + return; + } + ArgsParser ap = new ArgsParser(parser); + ap.parse(line); + List args = ap.args(); + int pipeId = 0; + for (String a : args) { + if (isPipe(a)) { + break; + } + pipeId++; + } + if (pipeId < args.size()) { + StringBuilder sb = new StringBuilder(); + int redirectPipe = -1; + for (int i = pipeId + 1; i < args.size(); i++) { + String arg = args.get(i); + if (!isPipe(arg) && !namedPipes().contains(arg) && !arg.matches("\\d+") && redirectPipe != i - 1) { + if (arg.equals(">") || arg.equals(">>")) { + redirectPipe = i; + } else if (arg.matches("\\w+(\\(\\))?")) { + addValues(arg); + } else if (arg.matches("--\\w+(=.*|)$") && arg.length() > 4) { + int idx = arg.indexOf('='); + if (idx > 0) { + if (idx > 4) { + addOptions(arg.substring(2, idx)); + } + sb.append(arg.substring(idx + 1)); + sb.append(" "); + } else if (idx == -1) { + addOptions(arg.substring(2)); + } + } else { + sb.append(arg); + sb.append(" "); + } + } else { + redirectPipe = -1; + } + } + if (sb.length() > 0) { + String rest = sb.toString(); + for (String d : delims) { + rest = rest.replaceAll(d, " "); + } + String[] words = rest.split("\\s+"); + for (String w : words) { + if (w.length() < 3 || w.matches("\\d+")) { + continue; + } + if (isQuoted(w)) { + addQuoted(w.substring(1, w.length() - 1)); + } else if (w.contains(".")) { + for (String f : w.split("\\.")) { + if (!f.matches("\\d+") && f.matches("\\w+")) { + addFields(f); + } + } + } else if (w.matches("\\w+")) { + addValues(w); + } + } + } + } + namedPipes = null; + } + + public String encloseBy(String param) { + boolean quoted = + param.length() > 0 && (param.startsWith("\"") || param.startsWith("'") || param.startsWith("/")); + if (quoted && param.length() > 1) { + quoted = !param.endsWith(Character.toString(param.charAt(0))); + } + return quoted ? Character.toString(param.charAt(0)) : ""; + } + + private boolean isQuoted(String word) { + return word.length() > 1 + && ((word.startsWith("\"") && word.endsWith("\"")) + || (word.startsWith("'") && word.endsWith("'")) + || (word.startsWith("/") && word.endsWith("/"))); + } + + public int indexOfLastDelim(String word) { + int out = -1; + for (String d : delims) { + int x = word.lastIndexOf(d.replace("\\", "")); + if (x > out) { + out = x; + } + } + return out; + } + + private void addFields(String field) { + add("fields", field); + } + + private void addValues(String arg) { + add("values", arg); + } + + private void addQuoted(String arg) { + add("quoted", arg); + } + + private void addOptions(String arg) { + add("options", arg); + } + + private void add(String where, String value) { + if (value.length() < 3) { + return; + } + names.get(where).remove(value); + names.get(where).add(0, value); + } + + public List namedPipes() { + if (namedPipes == null) { + namedPipes = consoleId != null ? consoleEngine().getNamedPipes() : new ArrayList<>(); + } + return namedPipes; + } + + public List values() { + return names.get("values"); + } + + public List fields() { + return names.get("fields"); + } + + public List quoted() { + return names.get("quoted"); + } + + public List options() { + return names.get("options"); + } + + private Set fieldsAndValues() { + Set out = new HashSet<>(); + out.addAll(fields()); + out.addAll(values()); + return out; + } + + private void truncate(String where, int maxSize) { + if (names.get(where).size() > maxSize) { + names.put(where, names.get(where).subList(0, maxSize)); + } + } + + public void save() { + ConsoleEngine consoleEngine = consoleEngine(); + if (consoleEngine != null && fileNames != null) { + int maxSize = consoleEngine.consoleOption("maxValueNames", 100); + truncate("fields", maxSize); + truncate("values", maxSize); + truncate("quoted", maxSize); + truncate("options", maxSize); + consoleEngine.persist(fileNames, names); + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/keymap/BindingReader.java b/net-cli/src/main/java/org/jline/keymap/BindingReader.java new file mode 100644 index 0000000..708d789 --- /dev/null +++ b/net-cli/src/main/java/org/jline/keymap/BindingReader.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.keymap; + +import java.io.IOError; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; +import org.jline.reader.EndOfFileException; +import org.jline.utils.ClosedException; +import org.jline.utils.NonBlockingReader; + +/** + * The BindingReader will transform incoming chars into + * key bindings + * + * @author Guillaume Nodet + */ +public class BindingReader { + + protected final NonBlockingReader reader; + protected final StringBuilder opBuffer = new StringBuilder(); + protected final Deque pushBackChar = new ArrayDeque<>(); + protected String lastBinding; + + public BindingReader(NonBlockingReader reader) { + this.reader = reader; + } + + /** + * Read from the input stream and decode an operation from the key map. + *

+ * The input stream will be read character by character until a matching + * binding can be found. Characters that can't possibly be matched to + * any binding will be send with the {@link KeyMap#getNomatch()} binding. + * Unicode (>= 128) characters will be matched to {@link KeyMap#getUnicode()}. + * If the current key sequence is ambiguous, i.e. the sequence is bound but + * it's also a prefix to other sequences, then the {@link KeyMap#getAmbiguousTimeout()} + * timeout will be used to wait for another incoming character. + * If a character comes, the disambiguation will be done. If the timeout elapses + * and no character came in, or if the timeout is <= 0, the current bound operation + * will be returned. + * + * @param keys the KeyMap to use for decoding the input stream + * @param the type of bindings to be read + * @return the decoded binding or null if the end of + * stream has been reached + */ + public T readBinding(KeyMap keys) { + return readBinding(keys, null, true); + } + + public T readBinding(KeyMap keys, KeyMap local) { + return readBinding(keys, local, true); + } + + public T readBinding(KeyMap keys, KeyMap local, boolean block) { + lastBinding = null; + T o = null; + int[] remaining = new int[1]; + boolean hasRead = false; + for (; ; ) { + if (local != null) { + o = local.getBound(opBuffer, remaining); + } + if (o == null && (local == null || remaining[0] >= 0)) { + o = keys.getBound(opBuffer, remaining); + } + // We have a binding and additional chars + if (o != null) { + if (remaining[0] >= 0) { + runMacro(opBuffer.substring(opBuffer.length() - remaining[0])); + opBuffer.setLength(opBuffer.length() - remaining[0]); + } else { + long ambiguousTimeout = keys.getAmbiguousTimeout(); + if (ambiguousTimeout > 0 && peekCharacter(ambiguousTimeout) != NonBlockingReader.READ_EXPIRED) { + o = null; + } + } + if (o != null) { + lastBinding = opBuffer.toString(); + opBuffer.setLength(0); + return o; + } + // We don't match anything + } else if (remaining[0] > 0) { + int cp = opBuffer.codePointAt(0); + String rem = opBuffer.substring(Character.charCount(cp)); + lastBinding = opBuffer.substring(0, Character.charCount(cp)); + // Unicode character + o = (cp >= KeyMap.KEYMAP_LENGTH) ? keys.getUnicode() : keys.getNomatch(); + opBuffer.setLength(0); + opBuffer.append(rem); + if (o != null) { + return o; + } + } + + if (!block && hasRead) { + break; + } + int c = readCharacter(); + if (c == -1) { + return null; + } + opBuffer.appendCodePoint(c); + hasRead = true; + } + return null; + } + + public String readStringUntil(String sequence) { + StringBuilder sb = new StringBuilder(); + if (!pushBackChar.isEmpty()) { + pushBackChar.forEach(sb::appendCodePoint); + } + try { + char[] buf = new char[64]; + while (true) { + int idx = sb.indexOf(sequence, Math.max(0, sb.length() - buf.length - sequence.length())); + if (idx >= 0) { + String rem = sb.substring(idx + sequence.length()); + runMacro(rem); + return sb.substring(0, idx); + } + int l = reader.readBuffered(buf); + if (l < 0) { + throw new ClosedException(); + } + sb.append(buf, 0, l); + } + } catch (ClosedException e) { + throw new EndOfFileException(e); + } catch (IOException e) { + throw new IOError(e); + } + } + + /** + * Read a codepoint from the terminal. + * + * @return the character, or -1 if an EOF is received. + */ + public int readCharacter() { + if (!pushBackChar.isEmpty()) { + return pushBackChar.pop(); + } + try { + int c = NonBlockingReader.READ_EXPIRED; + int s = 0; + while (c == NonBlockingReader.READ_EXPIRED) { + c = reader.read(100L); + if (c >= 0 && Character.isHighSurrogate((char) c)) { + s = c; + c = NonBlockingReader.READ_EXPIRED; + } + } + return s != 0 ? Character.toCodePoint((char) s, (char) c) : c; + } catch (ClosedException e) { + throw new EndOfFileException(e); + } catch (IOException e) { + throw new IOError(e); + } + } + + public int readCharacterBuffered() { + try { + if (pushBackChar.isEmpty()) { + char[] buf = new char[32]; + int l = reader.readBuffered(buf); + if (l <= 0) { + return -1; + } + int s = 0; + for (int i = 0; i < l; ) { + int c = buf[i++]; + if (Character.isHighSurrogate((char) c)) { + s = c; + if (i < l) { + c = buf[i++]; + pushBackChar.addLast(Character.toCodePoint((char) s, (char) c)); + } else { + break; + } + } else { + s = 0; + pushBackChar.addLast(c); + } + } + if (s != 0) { + int c = reader.read(); + if (c >= 0) { + pushBackChar.addLast(Character.toCodePoint((char) s, (char) c)); + } else { + return -1; + } + } + } + return pushBackChar.pop(); + } catch (ClosedException e) { + throw new EndOfFileException(e); + } catch (IOException e) { + throw new IOError(e); + } + } + + public int peekCharacter(long timeout) { + if (!pushBackChar.isEmpty()) { + return pushBackChar.peek(); + } + try { + return reader.peek(timeout); + } catch (IOException e) { + throw new IOError(e); + } + } + + public void runMacro(String macro) { + macro.codePoints().forEachOrdered(pushBackChar::addLast); + } + + public String getCurrentBuffer() { + return opBuffer.toString(); + } + + public String getLastBinding() { + return lastBinding; + } +} diff --git a/net-cli/src/main/java/org/jline/keymap/KeyMap.java b/net-cli/src/main/java/org/jline/keymap/KeyMap.java new file mode 100644 index 0000000..40095ec --- /dev/null +++ b/net-cli/src/main/java/org/jline/keymap/KeyMap.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.keymap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.TreeMap; +import org.jline.terminal.Terminal; +import org.jline.utils.Curses; +import org.jline.utils.InfoCmp.Capability; + +/** + * The KeyMap class contains all bindings from keys to operations. + * + * @author Guillaume Nodet + * @since 2.6 + */ +public class KeyMap { + + public static final int KEYMAP_LENGTH = 128; + public static final long DEFAULT_AMBIGUOUS_TIMEOUT = 1000L; + public static final Comparator KEYSEQ_COMPARATOR = (s1, s2) -> { + int len1 = s1.length(); + int len2 = s2.length(); + int lim = Math.min(len1, len2); + int k = 0; + while (k < lim) { + char c1 = s1.charAt(k); + char c2 = s2.charAt(k); + if (c1 != c2) { + int l = len1 - len2; + return l != 0 ? l : c1 - c2; + } + k++; + } + return len1 - len2; + }; + private final Object[] mapping = new Object[KEYMAP_LENGTH]; + private T anotherKey = null; + private T unicode; + private T nomatch; + private long ambiguousTimeout = DEFAULT_AMBIGUOUS_TIMEOUT; + + public static String display(String key) { + StringBuilder sb = new StringBuilder(); + sb.append("\""); + for (int i = 0; i < key.length(); i++) { + char c = key.charAt(i); + if (c < 32) { + sb.append('^'); + sb.append((char) (c + 'A' - 1)); + } else if (c == 127) { + sb.append("^?"); + } else if (c == '^' || c == '\\') { + sb.append('\\').append(c); + } else if (c >= 128) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } + sb.append("\""); + return sb.toString(); + } + + public static String translate(String str) { + int i; + if (!str.isEmpty()) { + char c = str.charAt(0); + if ((c == '\'' || c == '"') && str.charAt(str.length() - 1) == c) { + str = str.substring(1, str.length() - 1); + } + } + StringBuilder keySeq = new StringBuilder(); + for (i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == '\\') { + if (++i >= str.length()) { + break; + } + c = str.charAt(i); + switch (c) { + case 'a': + c = 0x07; + break; + case 'b': + c = '\b'; + break; + case 'd': + c = 0x7f; + break; + case 'e': + case 'E': + c = 0x1b; + break; + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case 'v': + c = 0x0b; + break; + case '\\': + c = '\\'; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + c = 0; + for (int j = 0; j < 3; j++, i++) { + if (i >= str.length()) { + break; + } + int k = Character.digit(str.charAt(i), 8); + if (k < 0) { + break; + } + c = (char) (c * 8 + k); + } + i--; + c &= 0xFF; + break; + case 'x': + i++; + c = 0; + for (int j = 0; j < 2; j++, i++) { + if (i >= str.length()) { + break; + } + int k = Character.digit(str.charAt(i), 16); + if (k < 0) { + break; + } + c = (char) (c * 16 + k); + } + i--; + c &= 0xFF; + break; + case 'u': + i++; + c = 0; + for (int j = 0; j < 4; j++, i++) { + if (i >= str.length()) { + break; + } + int k = Character.digit(str.charAt(i), 16); + if (k < 0) { + break; + } + c = (char) (c * 16 + k); + } + break; + case 'C': + if (++i >= str.length()) { + break; + } + c = str.charAt(i); + if (c == '-') { + if (++i >= str.length()) { + break; + } + c = str.charAt(i); + } + c = c == '?' ? 0x7f : (char) (Character.toUpperCase(c) & 0x1f); + break; + } + } else if (c == '^') { + if (++i >= str.length()) { + break; + } + c = str.charAt(i); + if (c != '^') { + c = c == '?' ? 0x7f : (char) (Character.toUpperCase(c) & 0x1f); + } + } + keySeq.append(c); + } + return keySeq.toString(); + } + + public static Collection range(String range) { + String[] keys = range.split("-"); + if (keys.length != 2) { + return null; + } + keys[0] = translate(keys[0]); + keys[1] = translate(keys[1]); + if (keys[0].length() != keys[1].length()) { + return null; + } + String pfx; + if (keys[0].length() > 1) { + pfx = keys[0].substring(0, keys[0].length() - 1); + if (!keys[1].startsWith(pfx)) { + return null; + } + } else { + pfx = ""; + } + char c0 = keys[0].charAt(keys[0].length() - 1); + char c1 = keys[1].charAt(keys[1].length() - 1); + if (c0 > c1) { + return null; + } + Collection seqs = new ArrayList<>(); + for (char c = c0; c <= c1; c++) { + seqs.add(pfx + c); + } + return seqs; + } + + public static String esc() { + return "\033"; + } + + public static String alt(char c) { + return "\033" + c; + } + + public static String alt(String c) { + return "\033" + c; + } + + public static String del() { + return "\177"; + } + + public static String ctrl(char key) { + return key == '?' ? del() : Character.toString((char) (Character.toUpperCase(key) & 0x1f)); + } + + public static String key(Terminal terminal, Capability capability) { + return Curses.tputs(terminal.getStringCapability(capability)); + } + + // + // Methods + // + + @SuppressWarnings("unchecked") + private static void doGetBoundKeys(KeyMap keyMap, String prefix, Map bound) { + if (keyMap.anotherKey != null) { + bound.put(prefix, keyMap.anotherKey); + } + for (int c = 0; c < keyMap.mapping.length; c++) { + if (keyMap.mapping[c] instanceof KeyMap) { + doGetBoundKeys((KeyMap) keyMap.mapping[c], prefix + (char) (c), bound); + } else if (keyMap.mapping[c] != null) { + bound.put(prefix + (char) (c), (T) keyMap.mapping[c]); + } + } + } + + @SuppressWarnings("unchecked") + private static T unbind(KeyMap map, CharSequence keySeq) { + KeyMap prev = null; + if (keySeq != null && keySeq.length() > 0) { + for (int i = 0; i < keySeq.length() - 1; i++) { + char c = keySeq.charAt(i); + if (c > map.mapping.length) { + return null; + } + if (!(map.mapping[c] instanceof KeyMap)) { + return null; + } + prev = map; + map = (KeyMap) map.mapping[c]; + } + char c = keySeq.charAt(keySeq.length() - 1); + if (c > map.mapping.length) { + return null; + } + if (map.mapping[c] instanceof KeyMap) { + KeyMap sub = (KeyMap) map.mapping[c]; + Object res = sub.anotherKey; + sub.anotherKey = null; + return (T) res; + } else { + Object res = map.mapping[c]; + map.mapping[c] = null; + int nb = 0; + for (int i = 0; i < map.mapping.length; i++) { + if (map.mapping[i] != null) { + nb++; + } + } + if (nb == 0 && prev != null) { + prev.mapping[keySeq.charAt(keySeq.length() - 2)] = map.anotherKey; + } + return (T) res; + } + } + return null; + } + + @SuppressWarnings("unchecked") + private static void bind(KeyMap map, CharSequence keySeq, T function, boolean onlyIfNotBound) { + if (keySeq != null && keySeq.length() > 0) { + for (int i = 0; i < keySeq.length(); i++) { + char c = keySeq.charAt(i); + if (c >= map.mapping.length) { + return; + } + if (i < keySeq.length() - 1) { + if (!(map.mapping[c] instanceof KeyMap)) { + KeyMap m = new KeyMap<>(); + m.anotherKey = (T) map.mapping[c]; + map.mapping[c] = m; + } + map = (KeyMap) map.mapping[c]; + } else { + if (map.mapping[c] instanceof KeyMap) { + ((KeyMap) map.mapping[c]).anotherKey = function; + } else { + Object op = map.mapping[c]; + if (!onlyIfNotBound || op == null) { + map.mapping[c] = function; + } + } + } + } + } + } + + public T getUnicode() { + return unicode; + } + + public void setUnicode(T unicode) { + this.unicode = unicode; + } + + public T getNomatch() { + return nomatch; + } + + public void setNomatch(T nomatch) { + this.nomatch = nomatch; + } + + public long getAmbiguousTimeout() { + return ambiguousTimeout; + } + + public void setAmbiguousTimeout(long ambiguousTimeout) { + this.ambiguousTimeout = ambiguousTimeout; + } + + public T getAnotherKey() { + return anotherKey; + } + + public Map getBoundKeys() { + Map bound = new TreeMap<>(KEYSEQ_COMPARATOR); + doGetBoundKeys(this, "", bound); + return bound; + } + + @SuppressWarnings("unchecked") + public T getBound(CharSequence keySeq, int[] remaining) { + remaining[0] = -1; + if (keySeq != null && keySeq.length() > 0) { + char c = keySeq.charAt(0); + if (c >= mapping.length) { + remaining[0] = Character.codePointCount(keySeq, 0, keySeq.length()); + return null; + } else { + if (mapping[c] instanceof KeyMap) { + CharSequence sub = keySeq.subSequence(1, keySeq.length()); + return ((KeyMap) mapping[c]).getBound(sub, remaining); + } else if (mapping[c] != null) { + remaining[0] = keySeq.length() - 1; + return (T) mapping[c]; + } else { + remaining[0] = keySeq.length(); + return anotherKey; + } + } + } else { + return anotherKey; + } + } + + public T getBound(CharSequence keySeq) { + int[] remaining = new int[1]; + T res = getBound(keySeq, remaining); + return remaining[0] <= 0 ? res : null; + } + + public void bindIfNotBound(T function, CharSequence keySeq) { + if (function != null && keySeq != null) { + bind(this, keySeq, function, true); + } + } + + public void bind(T function, CharSequence... keySeqs) { + for (CharSequence keySeq : keySeqs) { + bind(function, keySeq); + } + } + + public void bind(T function, Iterable keySeqs) { + for (CharSequence keySeq : keySeqs) { + bind(function, keySeq); + } + } + + public void bind(T function, CharSequence keySeq) { + if (keySeq != null) { + if (function == null) { + unbind(keySeq); + } else { + bind(this, keySeq, function, false); + } + } + } + + public void unbind(CharSequence... keySeqs) { + for (CharSequence keySeq : keySeqs) { + unbind(keySeq); + } + } + + public void unbind(CharSequence keySeq) { + if (keySeq != null) { + unbind(this, keySeq); + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/Binding.java b/net-cli/src/main/java/org/jline/reader/Binding.java new file mode 100644 index 0000000..8ebc986 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Binding.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +/** + * Marker interface for objects bound to key sequences. + * + * @author Guillaume Nodet + * @see Macro + * @see Reference + * @see Widget + * @see org.jline.keymap.KeyMap + */ +public interface Binding { +} diff --git a/net-cli/src/main/java/org/jline/reader/Buffer.java b/net-cli/src/main/java/org/jline/reader/Buffer.java new file mode 100644 index 0000000..408b716 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Buffer.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2002-2017, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +public interface Buffer { + + /* + * Read access + */ + + int cursor(); + + int atChar(int i); + + int length(); + + int currChar(); + + int prevChar(); + + int nextChar(); + + /* + * Movement + */ + + boolean cursor(int position); + + int move(int num); + + boolean up(); + + boolean down(); + + boolean moveXY(int dx, int dy); + + /* + * Modification + */ + + boolean clear(); + + boolean currChar(int c); + + void write(int c); + + void write(int c, boolean overTyping); + + void write(CharSequence str); + + void write(CharSequence str, boolean overTyping); + + boolean backspace(); + + int backspace(int num); + + boolean delete(); + + int delete(int num); + + /* + * String + */ + + String substring(int start); + + String substring(int start, int end); + + String upToCursor(); + + String toString(); + + /* + * Copy + */ + + Buffer copy(); + + void copyFrom(Buffer buffer); + + /** + * Clear any internal buffer. + */ + void zeroOut(); +} diff --git a/net-cli/src/main/java/org/jline/reader/Candidate.java b/net-cli/src/main/java/org/jline/reader/Candidate.java new file mode 100644 index 0000000..eb0857e --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Candidate.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2002-2019, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.util.Objects; + +/** + * A completion candidate. + * + * @author Guillaume Nodet + */ +public class Candidate implements Comparable { + + private final String value; + private final String displ; + private final String group; + private final String descr; + private final String suffix; + private final String key; + private final boolean complete; + private final int sort; + + /** + * Simple constructor with only a single String as an argument. + * + * @param value the candidate + */ + public Candidate(String value) { + this(value, value, null, null, null, null, true, 0); + } + + /** + * Constructs a new Candidate. + * + * @param value the value + * @param displ the display string + * @param group the group + * @param descr the description + * @param suffix the suffix + * @param key the key + * @param complete the complete flag + * @param sort the sort flag + */ + public Candidate( + String value, + String displ, + String group, + String descr, + String suffix, + String key, + boolean complete, + int sort) { + this.value = Objects.requireNonNull(value); + this.displ = Objects.requireNonNull(displ); + this.group = group; + this.descr = descr; + this.suffix = suffix; + this.key = key; + this.complete = complete; + this.sort = sort; + } + + /** + * Constructs a new Candidate. + * + * @param value the value + * @param displ the display string + * @param group the group + * @param descr the description + * @param suffix the suffix + * @param key the key + * @param complete the complete flag + */ + public Candidate( + String value, String displ, String group, String descr, String suffix, String key, boolean complete) { + this(value, displ, group, descr, suffix, key, complete, 0); + } + + /** + * The value that will be used for the actual completion. + * This string should not contain ANSI sequences. + * + * @return the value + */ + public String value() { + return value; + } + + /** + * The string that will be displayed to the user. + * This string may contain ANSI sequences. + * + * @return the display string + */ + public String displ() { + return displ; + } + + /** + * The group name for this candidate. + * Candidates can be grouped together and this string is used + * as a key for the group and displayed to the user. + * + * @return the group + * @see LineReader.Option#GROUP + * @see LineReader.Option#AUTO_GROUP + */ + public String group() { + return group; + } + + /** + * Description of this candidate, usually a small help message + * to understand the meaning of this candidate. + * This string may contain ANSI sequences. + * + * @return the description + */ + public String descr() { + return descr; + } + + /** + * The suffix is added when this candidate is displayed. + * However, if the next character entered does not match, + * the suffix will be automatically removed. + * This string should not contain ANSI sequences. + * + * @return the suffix + * @see LineReader.Option#AUTO_REMOVE_SLASH + * @see LineReader#REMOVE_SUFFIX_CHARS + */ + public String suffix() { + return suffix; + } + + /** + * Candidates which have the same key will be merged together. + * For example, if a command has multiple aliases, they can be merged + * if they are using the same key. + * + * @return the key + */ + public String key() { + return key; + } + + /** + * Boolean indicating whether this candidate is complete or + * if the completer may further expand the candidate value + * after this candidate has been selected. + * This can be the case when completing folders for example. + * If the candidate is complete and is selected, a space + * separator will be added. + * + * @return the completion flag + */ + public boolean complete() { + return complete; + } + + /** + * Integer used to override default sort logic. + * + * @return the sort int + */ + public int sort() { + return sort; + } + + @Override + public int compareTo(Candidate o) { + // If both candidates have same sort, use default behavior + if (sort == o.sort()) { + return value.compareTo(o.value); + } else { + return Integer.compare(sort, o.sort()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Candidate candidate = (Candidate) o; + return Objects.equals(value, candidate.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return "Candidate{" + value + "}"; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/Completer.java b/net-cli/src/main/java/org/jline/reader/Completer.java new file mode 100644 index 0000000..1671932 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Completer.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.util.List; + +/** + * A completer is the mechanism by which tab-completion candidates will be resolved. + * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @author Guillaume Nodet + * @since 2.3 + */ +public interface Completer { + /** + * Populates candidates with a list of possible completions for the command line. + *

+ * The list of candidates will be sorted and filtered by the LineReader, so that + * the list of candidates displayed to the user will usually be smaller than + * the list given by the completer. Thus it is not necessary for the completer + * to do any matching based on the current buffer. On the contrary, in order + * for the typo matcher to work, all possible candidates for the word being + * completed should be returned. + * + * @param reader The line reader + * @param line The parsed command line + * @param candidates The {@link List} of candidates to populate + */ + void complete(LineReader reader, ParsedLine line, List candidates); +} diff --git a/net-cli/src/main/java/org/jline/reader/CompletingParsedLine.java b/net-cli/src/main/java/org/jline/reader/CompletingParsedLine.java new file mode 100644 index 0000000..01ea816 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/CompletingParsedLine.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +/** + * An extension of {@link ParsedLine} that, being aware of the quoting and escaping rules + * of the {@link Parser} that produced it, knows if and how a completion candidate + * should be escaped/quoted. + * + * @author Eric Bottard + */ +public interface CompletingParsedLine extends ParsedLine { + + CharSequence escape(CharSequence candidate, boolean complete); + + int rawWordCursor(); + + int rawWordLength(); +} diff --git a/net-cli/src/main/java/org/jline/reader/CompletionMatcher.java b/net-cli/src/main/java/org/jline/reader/CompletionMatcher.java new file mode 100644 index 0000000..00be1b7 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/CompletionMatcher.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.util.List; +import java.util.Map; + +public interface CompletionMatcher { + + /** + * Compiles completion matcher functions + * + * @param options LineReader options + * @param prefix invoked by complete-prefix or expand-or-complete-prefix widget + * @param line The parsed line within which completion has been requested + * @param caseInsensitive if completion is case insensitive or not + * @param errors number of errors accepted in matching + * @param originalGroupName value of JLineReader variable original-group-name + */ + void compile( + Map options, + boolean prefix, + CompletingParsedLine line, + boolean caseInsensitive, + int errors, + String originalGroupName); + + /** + * @param candidates list of candidates + * @return a list of candidates that completion matcher matches + */ + List matches(List candidates); + + /** + * @return a candidate that have exact match, null if no exact match found + */ + Candidate exactMatch(); + + /** + * @return a common prefix of matched candidates + */ + String getCommonPrefix(); +} diff --git a/net-cli/src/main/java/org/jline/reader/EOFError.java b/net-cli/src/main/java/org/jline/reader/EOFError.java new file mode 100644 index 0000000..31a69a1 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/EOFError.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +public class EOFError extends SyntaxError { + + private static final long serialVersionUID = 1L; + + private final String missing; + private final int openBrackets; + private final String nextClosingBracket; + + public EOFError(int line, int column, String message) { + this(line, column, message, null); + } + + public EOFError(int line, int column, String message, String missing) { + this(line, column, message, missing, 0, null); + } + + public EOFError(int line, int column, String message, String missing, int openBrackets, String nextClosingBracket) { + super(line, column, message); + this.missing = missing; + this.openBrackets = openBrackets; + this.nextClosingBracket = nextClosingBracket; + } + + public String getMissing() { + return missing; + } + + public int getOpenBrackets() { + return openBrackets; + } + + public String getNextClosingBracket() { + return nextClosingBracket; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/Editor.java b/net-cli/src/main/java/org/jline/reader/Editor.java new file mode 100644 index 0000000..0f79458 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Editor.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2002-2019, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.io.IOException; +import java.util.List; + +public interface Editor { + void open(List files) throws IOException; + + void run() throws IOException; + + void setRestricted(boolean restricted); +} diff --git a/net-cli/src/main/java/org/jline/reader/EndOfFileException.java b/net-cli/src/main/java/org/jline/reader/EndOfFileException.java new file mode 100644 index 0000000..1ff5943 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/EndOfFileException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +/** + * This exception is thrown by {@link LineReader#readLine} when + * user the user types ctrl-D). + */ +public class EndOfFileException extends RuntimeException { + + private static final long serialVersionUID = 528485360925144689L; + private String partialLine; + + public EndOfFileException() { + } + + public EndOfFileException(String message) { + super(message); + } + + public EndOfFileException(String message, Throwable cause) { + super(message, cause); + } + + public EndOfFileException(Throwable cause) { + super(cause); + } + + public EndOfFileException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public EndOfFileException partialLine(String partialLine) { + this.partialLine = partialLine; + return this; + } + + public String getPartialLine() { + return partialLine; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/Expander.java b/net-cli/src/main/java/org/jline/reader/Expander.java new file mode 100644 index 0000000..e17fe49 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Expander.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +public interface Expander { + + String expandHistory(History history, String line); + + String expandVar(String word); +} diff --git a/net-cli/src/main/java/org/jline/reader/Highlighter.java b/net-cli/src/main/java/org/jline/reader/Highlighter.java new file mode 100644 index 0000000..e3c72dd --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Highlighter.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.util.regex.Pattern; +import org.jline.utils.AttributedString; + +public interface Highlighter { + + /** + * Highlight buffer + * + * @param reader LineReader + * @param buffer the buffer to be highlighted + * @return highlighted buffer + */ + AttributedString highlight(LineReader reader, String buffer); + + /** + * Refresh highlight configuration + */ + default void refresh(LineReader reader) { + } + + /** + * Set error pattern to be highlighted + * + * @param errorPattern error pattern to be highlighted + */ + void setErrorPattern(Pattern errorPattern); + + /** + * Set error index to be highlighted + * + * @param errorIndex error index to be highlighted + */ + void setErrorIndex(int errorIndex); +} diff --git a/net-cli/src/main/java/org/jline/reader/History.java b/net-cli/src/main/java/org/jline/reader/History.java new file mode 100644 index 0000000..dc0292b --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/History.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Iterator; +import java.util.ListIterator; + +/** + * Console history. + * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @since 2.3 + */ +public interface History extends Iterable { + + /** + * Initialize the history for the given reader. + * + * @param reader the reader to attach to + */ + void attach(LineReader reader); + + /** + * Load history. + * + * @throws IOException if a problem occurs + */ + void load() throws IOException; + + /** + * Save history. + * + * @throws IOException if a problem occurs + */ + void save() throws IOException; + + /** + * Write history to the file. If incremental only the events that are new since the last incremental operation to + * the file are added. + * + * @param file History file + * @param incremental If true incremental write operation is performed. + * @throws IOException if a problem occurs + */ + void write(Path file, boolean incremental) throws IOException; + + /** + * Append history to the file. If incremental only the events that are new since the last incremental operation to + * the file are added. + * + * @param file History file + * @param incremental If true incremental append operation is performed. + * @throws IOException if a problem occurs + */ + void append(Path file, boolean incremental) throws IOException; + + /** + * Read history from the file. If checkDuplicates is true only the events that + * are not contained within the internal list are added. + * + * @param file History file + * @param checkDuplicates If true, duplicate history entries will be discarded + * @throws IOException if a problem occurs + */ + void read(Path file, boolean checkDuplicates) throws IOException; + + /** + * Purge history. + * + * @throws IOException if a problem occurs + */ + void purge() throws IOException; + + int size(); + + default boolean isEmpty() { + return size() == 0; + } + + int index(); + + int first(); + + int last(); + + String get(int index); + + default void add(String line) { + add(Instant.now(), line); + } + + void add(Instant time, String line); + + /** + * Check if an entry should be persisted or not. + * + * @param entry the entry to check + * @return true if the given entry should be persisted, false otherwise + */ + default boolean isPersistable(Entry entry) { + return true; + } + + // + // Entries + // + + ListIterator iterator(int index); + + default ListIterator iterator() { + return iterator(first()); + } + + default Iterator reverseIterator() { + return reverseIterator(last()); + } + + default Iterator reverseIterator(int index) { + return new Iterator() { + private final ListIterator it = iterator(index + 1); + + @Override + public boolean hasNext() { + return it.hasPrevious(); + } + + @Override + public Entry next() { + return it.previous(); + } + + @Override + public void remove() { + it.remove(); + resetIndex(); + } + }; + } + + /** + * Return the content of the current buffer. + * + * @return the content of the current buffer + */ + String current(); + + // + // Navigation + // + + /** + * Move the pointer to the previous element in the buffer. + * + * @return true if we successfully went to the previous element + */ + boolean previous(); + + /** + * Move the pointer to the next element in the buffer. + * + * @return true if we successfully went to the next element + */ + boolean next(); + + /** + * Moves the history index to the first entry. + * + * @return Return false if there are no iterator in the history or if the + * history is already at the beginning. + */ + boolean moveToFirst(); + + /** + * This moves the history to the last entry. This entry is one position + * before the moveToEnd() position. + * + * @return Returns false if there were no history iterator or the history + * index was already at the last entry. + */ + boolean moveToLast(); + + /** + * Move to the specified index in the history + * + * @param index The index to move to. + * @return Returns true if the index was moved. + */ + boolean moveTo(int index); + + /** + * Move to the end of the history buffer. This will be a blank entry, after + * all of the other iterator. + */ + void moveToEnd(); + + /** + * Reset index after remove + */ + void resetIndex(); + + interface Entry { + int index(); + + Instant time(); + + String line(); + } +} diff --git a/net-cli/src/main/java/org/jline/reader/LineReader.java b/net-cli/src/main/java/org/jline/reader/LineReader.java new file mode 100644 index 0000000..d21aec9 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/LineReader.java @@ -0,0 +1,829 @@ +/* + * Copyright (c) 2002-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.io.File; +import java.io.InputStream; +import java.util.Collection; +import java.util.Map; +import java.util.function.IntConsumer; +import org.jline.keymap.KeyMap; +import org.jline.terminal.MouseEvent; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedString; + +/** + * Read lines from the console, with input editing. + * + *

Thread safety

+ * The LineReader implementations are not thread safe, + * thus you should not attempt to use a single reader in several threads. + * Any attempt to call one of the readLine call while one is + * already executing in a different thread will immediately result in an + * IllegalStateException being thrown. Other calls may lead to + * unknown behaviors. There is one exception though: users are allowed to call + * {@link #printAbove(String)} or {@link #printAbove(AttributedString)} at + * any time to allow text to be printed above the current prompt. + * + *

Prompt strings

+ * It is traditional for an interactive console-based program + * to print a short prompt string to signal that the user is expected + * to type a command. JLine supports 3 kinds of prompt string: + *
    + *
  • The normal prompt at the start (left) of the initial line of a command. + *
  • An optional right prompt at the right border of the initial line. + *
  • A start (left) prompt for continuation lines. I.e. the lines + * after the first line of a multi-line command. + *
+ *

+ * All of these are specified with prompt templates, + * which are similar to {@code printf} format strings, + * using the character {@code '%'} to indicate special functionality. + *

+ * The pattern may include ANSI escapes. + * It may include these template markers: + *
+ *
{@code %N}
+ *
A line number. This is the sum of {@code getLineNumber()} + * and a counter starting with 1 for the first continuation line. + *
+ *
{@code %M}
+ *
A short word explaining what is "missing". This is supplied from + * the {@link EOFError#getMissing()} method, if provided. + * Defaults to an empty string. + *
+ *
{@code %}n{@code P}c
+ *
Insert padding at this position, repeating the following + * character c as needed to bring the total prompt + * column width as specified by the digits n. + *
+ *
{@code %P}c
+ *
As before, but use width from the initial prompt. + *
+ *
{@code %%}
+ *
A literal {@code '%'}. + *
+ *
%{
%}
+ *
Text between a %{...%} pair is printed as + * part of a prompt, but not interpreted by JLine + * (except that {@code '%'}-escapes are processed). The text is assumed + * to take zero columns (not move the cursor). If it changes the style, + * you're responsible for changing it back. Standard ANSI escape sequences + * do not need to be within a %{...%} pair + * (though can be) since JLine knows how to deal with them. However, + * these delimiters are needed for unusual non-standard escape sequences. + *
+ *
+ */ +public interface LineReader { + + /** + * System property that can be set to avoid a warning being logged + * when using a Parser which does not return {@link CompletingParsedLine} objects. + */ + String PROP_SUPPORT_PARSEDLINE = "org.jline.reader.support.parsedline"; + + // + // Widget names + // + String CALLBACK_INIT = "callback-init"; + String CALLBACK_FINISH = "callback-finish"; + String CALLBACK_KEYMAP = "callback-keymap"; + + String ACCEPT_AND_INFER_NEXT_HISTORY = "accept-and-infer-next-history"; + String ACCEPT_AND_HOLD = "accept-and-hold"; + String ACCEPT_LINE = "accept-line"; + String ACCEPT_LINE_AND_DOWN_HISTORY = "accept-line-and-down-history"; + String ARGUMENT_BASE = "argument-base"; + String BACKWARD_CHAR = "backward-char"; + String BACKWARD_DELETE_CHAR = "backward-delete-char"; + String BACKWARD_DELETE_WORD = "backward-delete-word"; + String BACKWARD_KILL_LINE = "backward-kill-line"; + String BACKWARD_KILL_WORD = "backward-kill-word"; + String BACKWARD_WORD = "backward-word"; + String BEEP = "beep"; + String BEGINNING_OF_BUFFER_OR_HISTORY = "beginning-of-buffer-or-history"; + String BEGINNING_OF_HISTORY = "beginning-of-history"; + String BEGINNING_OF_LINE = "beginning-of-line"; + String BEGINNING_OF_LINE_HIST = "beginning-of-line-hist"; + String CAPITALIZE_WORD = "capitalize-word"; + String CHARACTER_SEARCH = "character-search"; + String CHARACTER_SEARCH_BACKWARD = "character-search-backward"; + String CLEAR = "clear"; + String CLEAR_SCREEN = "clear-screen"; + String COMPLETE_PREFIX = "complete-prefix"; + String COMPLETE_WORD = "complete-word"; + String COPY_PREV_WORD = "copy-prev-word"; + String COPY_REGION_AS_KILL = "copy-region-as-kill"; + String DELETE_CHAR = "delete-char"; + String DELETE_CHAR_OR_LIST = "delete-char-or-list"; + String DELETE_WORD = "delete-word"; + String DIGIT_ARGUMENT = "digit-argument"; + String DO_LOWERCASE_VERSION = "do-lowercase-version"; + String DOWN_CASE_WORD = "down-case-word"; + String DOWN_HISTORY = "down-history"; + String DOWN_LINE = "down-line"; + String DOWN_LINE_OR_HISTORY = "down-line-or-history"; + String DOWN_LINE_OR_SEARCH = "down-line-or-search"; + String EDIT_AND_EXECUTE_COMMAND = "edit-and-execute-command"; + String EMACS_BACKWARD_WORD = "emacs-backward-word"; + String EMACS_EDITING_MODE = "emacs-editing-mode"; + String EMACS_FORWARD_WORD = "emacs-forward-word"; + String END_OF_BUFFER_OR_HISTORY = "end-of-buffer-or-history"; + String END_OF_HISTORY = "end-of-history"; + String END_OF_LINE = "end-of-line"; + String END_OF_LINE_HIST = "end-of-line-hist"; + String EXCHANGE_POINT_AND_MARK = "exchange-point-and-mark"; + String EXECUTE_NAMED_CMD = "execute-named-cmd"; + String EXPAND_HISTORY = "expand-history"; + String EXPAND_OR_COMPLETE = "expand-or-complete"; + String EXPAND_OR_COMPLETE_PREFIX = "expand-or-complete-prefix"; + String EXPAND_WORD = "expand-word"; + String FRESH_LINE = "fresh-line"; + String FORWARD_CHAR = "forward-char"; + String FORWARD_WORD = "forward-word"; + String HISTORY_BEGINNING_SEARCH_BACKWARD = "history-beginning-search-backward"; + String HISTORY_BEGINNING_SEARCH_FORWARD = "history-beginning-search-forward"; + String HISTORY_INCREMENTAL_PATTERN_SEARCH_BACKWARD = "history-incremental-pattern-search-backward"; + String HISTORY_INCREMENTAL_PATTERN_SEARCH_FORWARD = "history-incremental-pattern-search-forward"; + String HISTORY_INCREMENTAL_SEARCH_BACKWARD = "history-incremental-search-backward"; + String HISTORY_INCREMENTAL_SEARCH_FORWARD = "history-incremental-search-forward"; + String HISTORY_SEARCH_BACKWARD = "history-search-backward"; + String HISTORY_SEARCH_FORWARD = "history-search-forward"; + String INSERT_CLOSE_CURLY = "insert-close-curly"; + String INSERT_CLOSE_PAREN = "insert-close-paren"; + String INSERT_CLOSE_SQUARE = "insert-close-square"; + String INFER_NEXT_HISTORY = "infer-next-history"; + String INSERT_COMMENT = "insert-comment"; + String INSERT_LAST_WORD = "insert-last-word"; + String KILL_BUFFER = "kill-buffer"; + String KILL_LINE = "kill-line"; + String KILL_REGION = "kill-region"; + String KILL_WHOLE_LINE = "kill-whole-line"; + String KILL_WORD = "kill-word"; + String LIST_CHOICES = "list-choices"; + String LIST_EXPAND = "list-expand"; + String MAGIC_SPACE = "magic-space"; + String MENU_EXPAND_OR_COMPLETE = "menu-expand-or-complete"; + String MENU_COMPLETE = "menu-complete"; + String MENU_SELECT = "menu-select"; + String NEG_ARGUMENT = "neg-argument"; + String OVERWRITE_MODE = "overwrite-mode"; + String PUT_REPLACE_SELECTION = "put-replace-selection"; + String QUOTED_INSERT = "quoted-insert"; + String READ_COMMAND = "read-command"; + String RECURSIVE_EDIT = "recursive-edit"; + String REDISPLAY = "redisplay"; + String REDRAW_LINE = "redraw-line"; + String REDO = "redo"; + String REVERSE_MENU_COMPLETE = "reverse-menu-complete"; + String SELF_INSERT = "self-insert"; + String SELF_INSERT_UNMETA = "self-insert-unmeta"; + String SEND_BREAK = "abort"; + String SET_LOCAL_HISTORY = "set-local-history"; + String SET_MARK_COMMAND = "set-mark-command"; + String SPELL_WORD = "spell-word"; + String SPLIT_UNDO = "split-undo"; + String TRANSPOSE_CHARS = "transpose-chars"; + String TRANSPOSE_WORDS = "transpose-words"; + String UNDEFINED_KEY = "undefined-key"; + String UNDO = "undo"; + String UNIVERSAL_ARGUMENT = "universal-argument"; + String UP_CASE_WORD = "up-case-word"; + String UP_HISTORY = "up-history"; + String UP_LINE = "up-line"; + String UP_LINE_OR_HISTORY = "up-line-or-history"; + String UP_LINE_OR_SEARCH = "up-line-or-search"; + String VI_ADD_EOL = "vi-add-eol"; + String VI_ADD_NEXT = "vi-add-next"; + String VI_BACKWARD_BLANK_WORD = "vi-backward-blank-word"; + String VI_BACKWARD_BLANK_WORD_END = "vi-backward-blank-word-end"; + String VI_BACKWARD_CHAR = "vi-backward-char"; + String VI_BACKWARD_DELETE_CHAR = "vi-backward-delete-char"; + String VI_BACKWARD_KILL_WORD = "vi-backward-kill-word"; + String VI_BACKWARD_WORD = "vi-backward-word"; + String VI_BACKWARD_WORD_END = "vi-backward-word-end"; + String VI_BEGINNING_OF_LINE = "vi-beginning-of-line"; + String VI_CHANGE = "vi-change-to"; + String VI_CHANGE_EOL = "vi-change-eol"; + String VI_CHANGE_WHOLE_LINE = "vi-change-whole-line"; + String VI_CMD_MODE = "vi-cmd-mode"; + String VI_DELETE = "vi-delete"; + String VI_DELETE_CHAR = "vi-delete-char"; + String VI_DIGIT_OR_BEGINNING_OF_LINE = "vi-digit-or-beginning-of-line"; + String VI_DOWN_LINE_OR_HISTORY = "vi-down-line-or-history"; + String VI_END_OF_LINE = "vi-end-of-line"; + String VI_FETCH_HISTORY = "vi-fetch-history"; + String VI_FIND_NEXT_CHAR = "vi-find-next-char"; + String VI_FIND_NEXT_CHAR_SKIP = "vi-find-next-char-skip"; + String VI_FIND_PREV_CHAR = "vi-find-prev-char"; + String VI_FIND_PREV_CHAR_SKIP = "vi-find-prev-char-skip"; + String VI_FIRST_NON_BLANK = "vi-first-non-blank"; + String VI_FORWARD_BLANK_WORD = "vi-forward-blank-word"; + String VI_FORWARD_BLANK_WORD_END = "vi-forward-blank-word-end"; + String VI_FORWARD_CHAR = "vi-forward-char"; + String VI_FORWARD_WORD = "vi-forward-word"; + String VI_FORWARD_WORD_END = "vi-forward-word-end"; + String VI_GOTO_COLUMN = "vi-goto-column"; + String VI_HISTORY_SEARCH_BACKWARD = "vi-history-search-backward"; + String VI_HISTORY_SEARCH_FORWARD = "vi-history-search-forward"; + String VI_INSERT = "vi-insert"; + String VI_INSERT_BOL = "vi-insert-bol"; + String VI_INSERT_COMMENT = "vi-insert-comment"; + String VI_JOIN = "vi-join"; + String VI_KILL_EOL = "vi-kill-eol"; + String VI_KILL_LINE = "vi-kill-line"; + String VI_MATCH_BRACKET = "vi-match-bracket"; + String VI_OPEN_LINE_ABOVE = "vi-open-line-above"; + String VI_OPEN_LINE_BELOW = "vi-open-line-below"; + String VI_OPER_SWAP_CASE = "vi-oper-swap-case"; + String VI_PUT_AFTER = "vi-put-after"; + String VI_PUT_BEFORE = "vi-put-before"; + String VI_QUOTED_INSERT = "vi-quoted-insert"; + String VI_REPEAT_CHANGE = "vi-repeat-change"; + String VI_REPEAT_FIND = "vi-repeat-find"; + String VI_REPEAT_SEARCH = "vi-repeat-search"; + String VI_REPLACE = "vi-replace"; + String VI_REPLACE_CHARS = "vi-replace-chars"; + String VI_REV_REPEAT_FIND = "vi-rev-repeat-find"; + String VI_REV_REPEAT_SEARCH = "vi-rev-repeat-search"; + String VI_SET_BUFFER = "vi-set-buffer"; + String VI_SUBSTITUTE = "vi-substitute"; + String VI_SWAP_CASE = "vi-swap-case"; + String VI_UNDO_CHANGE = "vi-undo-change"; + String VI_UP_LINE_OR_HISTORY = "vi-up-line-or-history"; + String VI_YANK = "vi-yank"; + String VI_YANK_EOL = "vi-yank-eol"; + String VI_YANK_WHOLE_LINE = "vi-yank-whole-line"; + String VISUAL_LINE_MODE = "visual-line-mode"; + String VISUAL_MODE = "visual-mode"; + String WHAT_CURSOR_POSITION = "what-cursor-position"; + String YANK = "yank"; + String YANK_POP = "yank-pop"; + String MOUSE = "mouse"; + String FOCUS_IN = "terminal-focus-in"; + String FOCUS_OUT = "terminal-focus-out"; + + String BEGIN_PASTE = "begin-paste"; + + // + // KeyMap names + // + + String VICMD = "vicmd"; + String VIINS = "viins"; + String VIOPP = "viopp"; + String VISUAL = "visual"; + String MAIN = "main"; + String EMACS = "emacs"; + String SAFE = ".safe"; + String DUMB = "dumb"; + String MENU = "menu"; + + // + // Variable names + // + + String BIND_TTY_SPECIAL_CHARS = "bind-tty-special-chars"; + String COMMENT_BEGIN = "comment-begin"; + String BELL_STYLE = "bell-style"; + String PREFER_VISIBLE_BELL = "prefer-visible-bell"; + /** + * tab completion: if candidates are more than list-max a question will be asked before displaying them + */ + String LIST_MAX = "list-max"; + /** + * tab completion: if candidates are less than menu-list-max + * they are displayed in a list below the field to be completed + */ + String MENU_LIST_MAX = "menu-list-max"; + + String DISABLE_HISTORY = "disable-history"; + String DISABLE_COMPLETION = "disable-completion"; + String EDITING_MODE = "editing-mode"; + String KEYMAP = "keymap"; + String BLINK_MATCHING_PAREN = "blink-matching-paren"; + String WORDCHARS = "WORDCHARS"; + String REMOVE_SUFFIX_CHARS = "REMOVE_SUFFIX_CHARS"; + String SEARCH_TERMINATORS = "search-terminators"; + /** + * Number of matching errors that are accepted by the completion matcher + */ + String ERRORS = "errors"; + /** + * Property for the "others" group name + */ + String OTHERS_GROUP_NAME = "OTHERS_GROUP_NAME"; + /** + * Property for the "original" group name + */ + String ORIGINAL_GROUP_NAME = "ORIGINAL_GROUP_NAME"; + /** + * Completion style for displaying groups name + */ + String COMPLETION_STYLE_GROUP = "COMPLETION_STYLE_GROUP"; + + String COMPLETION_STYLE_LIST_GROUP = "COMPLETION_STYLE_LIST_GROUP"; + /** + * Completion style for displaying the current selected item + */ + String COMPLETION_STYLE_SELECTION = "COMPLETION_STYLE_SELECTION"; + + String COMPLETION_STYLE_LIST_SELECTION = "COMPLETION_STYLE_LIST_SELECTION"; + /** + * Completion style for displaying the candidate description + */ + String COMPLETION_STYLE_DESCRIPTION = "COMPLETION_STYLE_DESCRIPTION"; + + String COMPLETION_STYLE_LIST_DESCRIPTION = "COMPLETION_STYLE_LIST_DESCRIPTION"; + /** + * Completion style for displaying the matching part of candidates + */ + String COMPLETION_STYLE_STARTING = "COMPLETION_STYLE_STARTING"; + + String COMPLETION_STYLE_LIST_STARTING = "COMPLETION_STYLE_LIST_STARTING"; + /** + * Completion style for displaying the list + */ + String COMPLETION_STYLE_BACKGROUND = "COMPLETION_STYLE_BACKGROUND"; + + String COMPLETION_STYLE_LIST_BACKGROUND = "COMPLETION_STYLE_LIST_BACKGROUND"; + /** + * Set the template for prompts for secondary (continuation) lines. + * This is a prompt template as described in the class header. + */ + String SECONDARY_PROMPT_PATTERN = "secondary-prompt-pattern"; + /** + * When in multiline edit mode, this variable can be used + * to offset the line number displayed. + */ + String LINE_OFFSET = "line-offset"; + + /** + * Timeout for ambiguous key sequences. + * If the key sequence is ambiguous, i.e. there is a matching + * sequence but the sequence is also a prefix for other bindings, + * the next key press will be waited for a specified amount of + * time. If the timeout elapses, the matched sequence will be + * used. + */ + String AMBIGUOUS_BINDING = "ambiguous-binding"; + + /** + * Colon separated list of patterns that will not be saved in history. + */ + String HISTORY_IGNORE = "history-ignore"; + + /** + * File system history path. + */ + String HISTORY_FILE = "history-file"; + + /** + * Number of history items to keep in memory. + */ + String HISTORY_SIZE = "history-size"; + + /** + * Number of history items to keep in the history file. + */ + String HISTORY_FILE_SIZE = "history-file-size"; + + /** + * New line automatic indentation after opening/closing bracket. + */ + String INDENTATION = "indentation"; + + /** + * Max buffer size for advanced features. + * Once the length of the buffer reaches this threshold, no + * advanced features will be enabled. This includes the undo + * buffer, syntax highlighting, parsing, etc.... + */ + String FEATURES_MAX_BUFFER_SIZE = "features-max-buffer-size"; + + /** + * Min buffer size for tab auto-suggestions. + * For shorter buffer sizes auto-suggestions are not resolved. + */ + String SUGGESTIONS_MIN_BUFFER_SIZE = "suggestions-min-buffer-size"; + + /** + * Max number of times a command can be repeated. + */ + String MAX_REPEAT_COUNT = "max-repeat-count"; + + /** + * Number of spaces to display a tabulation, the default is 4. + */ + String TAB_WIDTH = "tab-width"; + + /** + * Name of inputrc to read at line reader creation time. + */ + String INPUT_RC_FILE_NAME = "input-rc-file-name"; + + /** + * Prefix to automatically delegate variables to system properties + */ + String SYSTEM_PROPERTY_PREFIX = "system-property-prefix"; + + Map> defaultKeyMaps(); + + /** + * Read the next line and return the contents of the buffer. + *

+ * Equivalent to readLine(null, null, null). + * + * @return the line read + * @throws UserInterruptException if readLine was interrupted (using Ctrl-C for example) + * @throws EndOfFileException if an EOF has been found (using Ctrl-D for example) + * @throws java.io.IOError in case of other i/o errors + */ + String readLine() throws UserInterruptException, EndOfFileException; + + /** + * Read the next line with the specified character mask. If null, then + * characters will be echoed. If 0, then no characters will be echoed. + *

+ * Equivalent to readLine(null, mask, null) + * + * @param mask The mask character, null or 0. + * @return A line that is read from the terminal, can never be null. + * @throws UserInterruptException if readLine was interrupted (using Ctrl-C for example) + * @throws EndOfFileException if an EOF has been found (using Ctrl-D for example) + * @throws java.io.IOError in case of other i/o errors + */ + String readLine(Character mask) throws UserInterruptException, EndOfFileException; + + /** + * Read the next line with the specified prompt. + * If null, then the default prompt will be used. + *

+ * Equivalent to readLine(prompt, null, null) + * + * @param prompt The prompt to issue to the terminal, may be null. + * @return A line that is read from the terminal, can never be null. + * @throws UserInterruptException if readLine was interrupted (using Ctrl-C for example) + * @throws EndOfFileException if an EOF has been found (using Ctrl-D for example) + * @throws java.io.IOError in case of other i/o errors + */ + String readLine(String prompt) throws UserInterruptException, EndOfFileException; + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + *

+ * Equivalent to readLine(prompt, mask, null) + * + * @param prompt The prompt to issue to the terminal, may be null. + * @param mask The mask character, null or 0. + * @return A line that is read from the terminal, can never be null. + * @throws UserInterruptException if readLine was interrupted (using Ctrl-C for example) + * @throws EndOfFileException if an EOF has been found (using Ctrl-D for example) + * @throws java.io.IOError in case of other i/o errors + */ + String readLine(String prompt, Character mask) throws UserInterruptException, EndOfFileException; + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + *

+ * Equivalent to readLine(prompt, null, mask, buffer) + * + * @param prompt The prompt to issue to the terminal, may be null. + * This is a template, with optional {@code '%'} escapes, as + * described in the class header. + * @param mask The character mask, may be null. + * @param buffer The default value presented to the user to edit, may be null. + * @return A line that is read from the terminal, can never be null. + * @throws UserInterruptException if readLine was interrupted (using Ctrl-C for example) + * @throws EndOfFileException if an EOF has been found (using Ctrl-D for example) + * @throws java.io.IOError in case of other i/o errors + */ + String readLine(String prompt, Character mask, String buffer) throws UserInterruptException, EndOfFileException; + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + * + * @param prompt The prompt to issue to the terminal, may be null. + * This is a template, with optional {@code '%'} escapes, as + * described in the class header. + * @param rightPrompt The right prompt + * This is a template, with optional {@code '%'} escapes, as + * described in the class header. + * @param mask The character mask, may be null. + * @param buffer The default value presented to the user to edit, may be null. + * @return A line that is read from the terminal, can never be null. + * @throws UserInterruptException if readLine was interrupted (using Ctrl-C for example) + * @throws EndOfFileException if an EOF has been found (using Ctrl-D for example) + * @throws java.io.IOError in case of other i/o errors + */ + String readLine(String prompt, String rightPrompt, Character mask, String buffer) + throws UserInterruptException, EndOfFileException; + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + * + * @param prompt The prompt to issue to the terminal, may be null. + * This is a template, with optional {@code '%'} escapes, as + * described in the class header. + * @param rightPrompt The right prompt + * This is a template, with optional {@code '%'} escapes, as + * described in the class header. + * @param maskingCallback The {@link MaskingCallback} to use when displaying lines and adding them to the line {@link History} + * @param buffer The default value presented to the user to edit, may be null. + * @return A line that is read from the terminal, can never be null. + * @throws UserInterruptException if readLine was interrupted (using Ctrl-C for example) + * @throws EndOfFileException if an EOF has been found (using Ctrl-D for example) + * @throws java.io.IOError in case of other i/o errors + */ + String readLine(String prompt, String rightPrompt, MaskingCallback maskingCallback, String buffer) + throws UserInterruptException, EndOfFileException; + + /** + * Prints a line above the prompt and redraw everything. + * If the LineReader is not actually reading a line, the string will simply be printed to the terminal. + * + * @param str the string to print + * @see #printAbove(AttributedString) + */ + void printAbove(String str); + + /** + * Prints a string before the prompt and redraw everything. + * If the LineReader is not actually reading a line, the string will simply be printed to the terminal. + * + * @param str the string to print + * @see #printAbove(String) + */ + void printAbove(AttributedString str); + + /** + * Check if a thread is currently in a readLine() call. + * + * @return true if there is an ongoing readLine() call. + */ + boolean isReading(); + + LineReader variable(String name, Object value); + + LineReader option(Option option, boolean value); + + void callWidget(String name); + + // + // Chainable setters + // + + Map getVariables(); + + Object getVariable(String name); + + void setVariable(String name, Object value); + + boolean isSet(Option option); + + void setOpt(Option option); + + void unsetOpt(Option option); + + Terminal getTerminal(); + + Map getWidgets(); + + Map getBuiltinWidgets(); + + Buffer getBuffer(); + + String getAppName(); + + /** + * Push back a key sequence that will be later consumed by the line reader. + * This method can be used after reading the cursor position using + * {@link Terminal#getCursorPosition(IntConsumer)}. + * + * @param macro the key sequence to push back + * @see Terminal#getCursorPosition(IntConsumer) + * @see #readMouseEvent() + */ + void runMacro(String macro); + + /** + * Read a mouse event when the {@link org.jline.utils.InfoCmp.Capability#key_mouse} sequence + * has just been read on the input stream. + * Compared to {@link Terminal#readMouseEvent()}, this method takes into account keys + * that have been pushed back using {@link #runMacro(String)}. + * + * @return the mouse event + * @see #runMacro(String) + * @see Terminal#getCursorPosition(IntConsumer) + */ + MouseEvent readMouseEvent(); + + History getHistory(); + + Parser getParser(); + + Highlighter getHighlighter(); + + Expander getExpander(); + + Map> getKeyMaps(); + + String getKeyMap(); + + boolean setKeyMap(String name); + + KeyMap getKeys(); + + ParsedLine getParsedLine(); + + String getSearchTerm(); + + RegionType getRegionActive(); + + int getRegionMark(); + + void addCommandsInBuffer(Collection commands); + + void editAndAddInBuffer(File file) throws Exception; + + String getLastBinding(); + + String getTailTip(); + + void setTailTip(String tailTip); + + SuggestionType getAutosuggestion(); + + void setAutosuggestion(SuggestionType type); + + /** + * Clear any internal buffers. + */ + void zeroOut(); + + enum Option { + COMPLETE_IN_WORD, + /** + * use camel case completion matcher + */ + COMPLETE_MATCHER_CAMELCASE, + /** + * use type completion matcher + */ + COMPLETE_MATCHER_TYPO(true), + DISABLE_EVENT_EXPANSION, + HISTORY_VERIFY, + HISTORY_IGNORE_SPACE(true), + HISTORY_IGNORE_DUPS(true), + HISTORY_REDUCE_BLANKS(true), + HISTORY_BEEP(true), + HISTORY_INCREMENTAL(true), + HISTORY_TIMESTAMPED(true), + /** + * when displaying candidates, group them by {@link Candidate#group()} + */ + AUTO_GROUP(true), + AUTO_MENU(true), + AUTO_LIST(true), + /** + * list candidates below the field to be completed + */ + AUTO_MENU_LIST, + RECOGNIZE_EXACT, + /** + * display group name before each group (else display all group names first) + */ + GROUP(true), + /** + * when double tab to select candidate keep candidates grouped (else loose grouping) + */ + GROUP_PERSIST, + /** + * if completion is case insensitive or not + */ + CASE_INSENSITIVE, + LIST_AMBIGUOUS, + LIST_PACKED, + LIST_ROWS_FIRST, + GLOB_COMPLETE, + MENU_COMPLETE, + /** + * if set and not at start of line before prompt, move to new line + */ + AUTO_FRESH_LINE, + + /** + * After writing into the rightmost column, do we immediately + * move to the next line (the default)? Or do we wait until + * the next character. + * If set, an input line that is exactly {@code N*columns} wide will + * use {@code N} screen lines; otherwise it will use {@code N+1} lines. + * When the cursor position is the right margin of the last line + * (i.e. after {@code N*columns} normal characters), if this option + * it set, the cursor will be remain on the last line (line {@code N-1}, + * zero-origin); if unset the cursor will be on the empty next line. + * Regardless, for all except the last screen line if the cursor is at + * the right margin, it will be shown at the start of the next line. + */ + DELAY_LINE_WRAP, + AUTO_PARAM_SLASH(true), + AUTO_REMOVE_SLASH(true), + /** + * FileNameCompleter: Use '/' character as a file directory separator + */ + USE_FORWARD_SLASH, + /** + * When hitting the <tab> key at the beginning of the line, insert a tabulation + * instead of completing. This is mainly useful when {@link #BRACKETED_PASTE} is + * disabled, so that copy/paste of indented text does not trigger completion. + */ + INSERT_TAB, + MOUSE, + DISABLE_HIGHLIGHTER, + BRACKETED_PASTE(true), + /** + * Instead of printing a new line when the line is read, the entire line + * (including the prompt) will be erased, thereby leaving the screen as it + * was before the readLine call. + */ + ERASE_LINE_ON_FINISH, + + /** + * if history search is fully case insensitive + */ + CASE_INSENSITIVE_SEARCH, + + /** + * Automatic insertion of closing bracket + */ + INSERT_BRACKET, + + /** + * Show command options tab completion candidates for zero length word + */ + EMPTY_WORD_OPTIONS(true), + + /** + * Disable the undo feature + */ + DISABLE_UNDO; + + private final boolean def; + + Option() { + this(false); + } + + Option(boolean def) { + this.def = def; + } + + public final boolean isSet(Map options) { + Boolean b = options.get(this); + return b != null ? b : this.isDef(); + } + + public boolean isDef() { + return def; + } + } + + enum RegionType { + NONE, + CHAR, + LINE, + PASTE + } + + enum SuggestionType { + /** + * As you type command line suggestions are disabled. + */ + NONE, + /** + * Prepare command line suggestions using command history. + * Requires an additional widgets implementation. + */ + HISTORY, + /** + * Prepare command line suggestions using command completer data. + */ + COMPLETER, + /** + * Prepare command line suggestions using command completer data and/or command positional argument descriptions. + * Requires an additional widgets implementation. + */ + TAIL_TIP + } +} diff --git a/net-cli/src/main/java/org/jline/reader/LineReaderBuilder.java b/net-cli/src/main/java/org/jline/reader/LineReaderBuilder.java new file mode 100644 index 0000000..b83aaf2 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/LineReaderBuilder.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.io.IOError; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jline.reader.impl.LineReaderImpl; +import org.jline.reader.impl.history.DefaultHistory; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.utils.Log; + +public final class LineReaderBuilder { + + Terminal terminal; + String appName; + Map variables = new HashMap<>(); + Map options = new HashMap<>(); + History history; + Completer completer; + History memoryHistory; + Highlighter highlighter; + Parser parser; + Expander expander; + CompletionMatcher completionMatcher; + private LineReaderBuilder() { + } + + public static LineReaderBuilder builder() { + return new LineReaderBuilder(); + } + + public LineReaderBuilder terminal(Terminal terminal) { + this.terminal = terminal; + return this; + } + + public LineReaderBuilder appName(String appName) { + this.appName = appName; + return this; + } + + public LineReaderBuilder variables(Map variables) { + Map old = this.variables; + this.variables = Objects.requireNonNull(variables); + this.variables.putAll(old); + return this; + } + + public LineReaderBuilder variable(String name, Object value) { + this.variables.put(name, value); + return this; + } + + public LineReaderBuilder option(LineReader.Option option, boolean value) { + this.options.put(option, value); + return this; + } + + public LineReaderBuilder history(History history) { + this.history = history; + return this; + } + + public LineReaderBuilder completer(Completer completer) { + this.completer = completer; + return this; + } + + public LineReaderBuilder highlighter(Highlighter highlighter) { + this.highlighter = highlighter; + return this; + } + + public LineReaderBuilder parser(Parser parser) { + if (parser != null) { + try { + if (!Boolean.getBoolean(LineReader.PROP_SUPPORT_PARSEDLINE) + && !(parser.parse("", 0) instanceof CompletingParsedLine)) { + Log.warn("The Parser of class " + parser.getClass().getName() + + " does not support the CompletingParsedLine interface. " + + "Completion with escaped or quoted words won't work correctly."); + } + } catch (Throwable t) { + // Ignore + } + } + this.parser = parser; + return this; + } + + public LineReaderBuilder expander(Expander expander) { + this.expander = expander; + return this; + } + + public LineReaderBuilder completionMatcher(CompletionMatcher completionMatcher) { + this.completionMatcher = completionMatcher; + return this; + } + + public LineReader build() { + Terminal terminal = this.terminal; + if (terminal == null) { + try { + terminal = TerminalBuilder.terminal(); + } catch (IOException e) { + throw new IOError(e); + } + } + + String appName = this.appName; + if (null == appName) { + appName = terminal.getName(); + } + + LineReaderImpl reader = new LineReaderImpl(terminal, appName, variables); + if (history != null) { + reader.setHistory(history); + } else { + if (memoryHistory == null) { + memoryHistory = new DefaultHistory(); + } + reader.setHistory(memoryHistory); + } + if (completer != null) { + reader.setCompleter(completer); + } + if (highlighter != null) { + reader.setHighlighter(highlighter); + } + if (parser != null) { + reader.setParser(parser); + } + if (expander != null) { + reader.setExpander(expander); + } + if (completionMatcher != null) { + reader.setCompletionMatcher(completionMatcher); + } + for (Map.Entry e : options.entrySet()) { + reader.option(e.getKey(), e.getValue()); + } + return reader; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/Macro.java b/net-cli/src/main/java/org/jline/reader/Macro.java new file mode 100644 index 0000000..89236d1 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Macro.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +public class Macro implements Binding { + + private final String sequence; + + public Macro(String sequence) { + this.sequence = sequence; + } + + public String getSequence() { + return sequence; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Macro macro = (Macro) o; + return sequence.equals(macro.sequence); + } + + @Override + public int hashCode() { + return sequence.hashCode(); + } + + @Override + public String toString() { + return "Macro[" + sequence + ']'; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/MaskingCallback.java b/net-cli/src/main/java/org/jline/reader/MaskingCallback.java new file mode 100644 index 0000000..b3d17fc --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/MaskingCallback.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +/** + * Callback used to mask parts of the line + */ +public interface MaskingCallback { + + /** + * Transforms the line before it is displayed so that + * some parts can be hidden. + * + * @param line the current line being edited + * @return the modified line to display + */ + String display(String line); + + /** + * Transforms the line before storing in the history. + * If the return value is empty or null, it will not be saved + * in the history. + * + * @param line the line to be added to history + * @return the modified line + */ + String history(String line); +} diff --git a/net-cli/src/main/java/org/jline/reader/ParsedLine.java b/net-cli/src/main/java/org/jline/reader/ParsedLine.java new file mode 100644 index 0000000..bd8599f --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/ParsedLine.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.util.List; + +/** + * ParsedLine objects are returned by the {@link Parser} + * during completion or when accepting the line. + *

+ * The instances should implement the {@link CompletingParsedLine} + * interface so that escape chars and quotes can be correctly handled. + * + * @see Parser + * @see CompletingParsedLine + */ +public interface ParsedLine { + + /** + * The current word being completed. + * If the cursor is after the last word, an empty string is returned. + * + * @return the word being completed or an empty string + */ + String word(); + + /** + * The cursor position within the current word. + * + * @return the cursor position within the current word + */ + int wordCursor(); + + /** + * The index of the current word in the list of words. + * + * @return the index of the current word in the list of words + */ + int wordIndex(); + + /** + * The list of words. + * + * @return the list of words + */ + List words(); + + /** + * The unparsed line. + * + * @return the unparsed line + */ + String line(); + + /** + * The cursor position within the line. + * + * @return the cursor position within the line + */ + int cursor(); +} diff --git a/net-cli/src/main/java/org/jline/reader/Parser.java b/net-cli/src/main/java/org/jline/reader/Parser.java new file mode 100644 index 0000000..f952dc3 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Parser.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public interface Parser { + String REGEX_VARIABLE = "[a-zA-Z_]+[a-zA-Z0-9_-]*"; + String REGEX_COMMAND = "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*"; + + ParsedLine parse(String line, int cursor, ParseContext context) throws SyntaxError; + + default ParsedLine parse(String line, int cursor) throws SyntaxError { + return parse(line, cursor, ParseContext.UNSPECIFIED); + } + + default boolean isEscapeChar(char ch) { + return ch == '\\'; + } + + default boolean validCommandName(String name) { + return name != null && name.matches(REGEX_COMMAND); + } + + default boolean validVariableName(String name) { + return name != null && name.matches(REGEX_VARIABLE); + } + + default String getCommand(final String line) { + String out; + Pattern patternCommand = Pattern.compile("^\\s*" + REGEX_VARIABLE + "=(" + REGEX_COMMAND + ")(\\s+|$)"); + Matcher matcher = patternCommand.matcher(line); + if (matcher.find()) { + out = matcher.group(1); + } else { + out = line.trim().split("\\s+")[0]; + if (!out.matches(REGEX_COMMAND)) { + out = ""; + } + } + return out; + } + + default String getVariable(final String line) { + String out = null; + Pattern patternCommand = Pattern.compile("^\\s*(" + REGEX_VARIABLE + ")\\s*=[^=~].*"); + Matcher matcher = patternCommand.matcher(line); + if (matcher.find()) { + out = matcher.group(1); + } + return out; + } + + enum ParseContext { + UNSPECIFIED, + + /** + * Try a real "final" parse. + * May throw EOFError in which case we have incomplete input. + */ + ACCEPT_LINE, + + /** + * Parsed words will have all characters present in input line + * including quotes and escape chars. + * We should tolerate and ignore errors. + */ + SPLIT_LINE, + + /** + * Parse to find completions (typically after a Tab). + * We should tolerate and ignore errors. + */ + COMPLETE, + + /** + * Called when we need to update the secondary prompts. + * Specifically, when we need the 'missing' field from EOFError, + * which is used by a "%M" in a prompt pattern. + */ + SECONDARY_PROMPT + } +} diff --git a/net-cli/src/main/java/org/jline/reader/PrintAboveWriter.java b/net-cli/src/main/java/org/jline/reader/PrintAboveWriter.java new file mode 100644 index 0000000..3a5ce30 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/PrintAboveWriter.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +import java.io.StringWriter; +import java.io.Writer; + +/** + * Redirects a {@link Writer} to a {@link LineReader}'s {@link LineReader#printAbove(String)} method, + * which draws output above the current prompt / input line. + * + *

Example:

+ *
+ *     LineReader reader = LineReaderBuilder.builder().terminal(terminal).parser(parser).build();
+ *     PrintAboveWriter printAbove = new PrintAboveWriter(reader);
+ *     printAbove.write(new char[] { 'h', 'i', '!', '\n'});
+ * 
+ */ +public class PrintAboveWriter extends StringWriter { + private final LineReader reader; + + public PrintAboveWriter(LineReader reader) { + this.reader = reader; + } + + @Override + public void flush() { + StringBuffer buffer = getBuffer(); + int lastNewline = buffer.lastIndexOf("\n"); + if (lastNewline >= 0) { + reader.printAbove(buffer.substring(0, lastNewline + 1)); + buffer.delete(0, lastNewline + 1); + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/Reference.java b/net-cli/src/main/java/org/jline/reader/Reference.java new file mode 100644 index 0000000..c40d061 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Reference.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +/** + * A reference to a {@link Widget}. + */ +public class Reference implements Binding { + + private final String name; + + public Reference(String name) { + this.name = name; + } + + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Reference func = (Reference) o; + return name.equals(func.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "Reference[" + name + ']'; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/SyntaxError.java b/net-cli/src/main/java/org/jline/reader/SyntaxError.java new file mode 100644 index 0000000..2831759 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/SyntaxError.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +public class SyntaxError extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final int line; + private final int column; + + public SyntaxError(int line, int column, String message) { + super(message); + this.line = line; + this.column = column; + } + + public int column() { + return column; + } + + public int line() { + return line; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/UserInterruptException.java b/net-cli/src/main/java/org/jline/reader/UserInterruptException.java new file mode 100644 index 0000000..1b41eab --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/UserInterruptException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +/** + * This exception is thrown by {@link LineReader#readLine} when + * user interrupt handling is enabled and the user types the + * interrupt character (ctrl-C). The partially entered line is + * available via the {@link #getPartialLine()} method. + */ +public class UserInterruptException extends RuntimeException { + private static final long serialVersionUID = 6172232572140736750L; + + private final String partialLine; + + public UserInterruptException(String partialLine) { + this.partialLine = partialLine; + } + + /** + * @return the partially entered line when ctrl-C was pressed + */ + public String getPartialLine() { + return partialLine; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/Widget.java b/net-cli/src/main/java/org/jline/reader/Widget.java new file mode 100644 index 0000000..b7ab012 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/Widget.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader; + +/** + * + */ +@FunctionalInterface +public interface Widget extends Binding { + + boolean apply(); +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/BufferImpl.java b/net-cli/src/main/java/org/jline/reader/impl/BufferImpl.java new file mode 100644 index 0000000..9760869 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/BufferImpl.java @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2002-2017, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import java.util.Arrays; +import java.util.Objects; +import org.jline.reader.Buffer; + +/** + * A holder for a {@link StringBuilder} that also contains the current cursor position. + * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @since 2.0 + */ +public class BufferImpl implements Buffer { + private int cursor = 0; + private int cursorCol = -1; + private int[] buffer; + private int g0; + private int g1; + + public BufferImpl() { + this(64); + } + + public BufferImpl(int size) { + buffer = new int[size]; + g0 = 0; + g1 = buffer.length; + } + + private BufferImpl(BufferImpl buffer) { + this.cursor = buffer.cursor; + this.cursorCol = buffer.cursorCol; + this.buffer = buffer.buffer.clone(); + this.g0 = buffer.g0; + this.g1 = buffer.g1; + } + + public BufferImpl copy() { + return new BufferImpl(this); + } + + public int cursor() { + return cursor; + } + + public int length() { + return buffer.length - (g1 - g0); + } + + public boolean currChar(int ch) { + if (cursor == length()) { + return false; + } else { + buffer[adjust(cursor)] = ch; + return true; + } + } + + public int currChar() { + if (cursor == length()) { + return 0; + } else { + return atChar(cursor); + } + } + + public int prevChar() { + if (cursor <= 0) { + return 0; + } + return atChar(cursor - 1); + } + + public int nextChar() { + if (cursor >= length() - 1) { + return 0; + } + return atChar(cursor + 1); + } + + public int atChar(int i) { + if (i < 0 || i >= length()) { + return 0; + } + return buffer[adjust(i)]; + } + + private int adjust(int i) { + return (i >= g0) ? i + g1 - g0 : i; + } + + /** + * Write the specific character into the buffer, setting the cursor position + * ahead one. + * + * @param c the character to insert + */ + public void write(int c) { + write(new int[]{c}); + } + + /** + * Write the specific character into the buffer, setting the cursor position + * ahead one. The text may overwrite or insert based on the current setting + * of {@code overTyping}. + * + * @param c the character to insert + */ + public void write(int c, boolean overTyping) { + if (overTyping) { + delete(1); + } + write(new int[]{c}); + } + + /** + * Insert the specified chars into the buffer, setting the cursor to the end of the insertion point. + */ + public void write(CharSequence str) { + Objects.requireNonNull(str); + write(str.codePoints().toArray()); + } + + public void write(CharSequence str, boolean overTyping) { + Objects.requireNonNull(str); + int[] ucps = str.codePoints().toArray(); + if (overTyping) { + delete(ucps.length); + } + write(ucps); + } + + private void write(int[] ucps) { + moveGapToCursor(); + int len = length() + ucps.length; + int sz = buffer.length; + if (sz < len) { + while (sz < len) { + sz *= 2; + } + int[] nb = new int[sz]; + System.arraycopy(buffer, 0, nb, 0, g0); + System.arraycopy(buffer, g1, nb, g1 + sz - buffer.length, buffer.length - g1); + g1 += sz - buffer.length; + buffer = nb; + } + System.arraycopy(ucps, 0, buffer, cursor, ucps.length); + g0 += ucps.length; + cursor += ucps.length; + cursorCol = -1; + } + + public boolean clear() { + if (length() == 0) { + return false; + } + g0 = 0; + g1 = buffer.length; + cursor = 0; + cursorCol = -1; + return true; + } + + public String substring(int start) { + return substring(start, length()); + } + + public String substring(int start, int end) { + if (start >= end || start < 0 || end > length()) { + return ""; + } + if (end <= g0) { + return new String(buffer, start, end - start); + } else if (start > g0) { + return new String(buffer, g1 - g0 + start, end - start); + } else { + int[] b = buffer.clone(); + System.arraycopy(b, g1, b, g0, b.length - g1); + return new String(b, start, end - start); + } + } + + public String upToCursor() { + return substring(0, cursor); + } + + /** + * Move the cursor position to the specified absolute index. + */ + public boolean cursor(int position) { + if (position == cursor) { + return true; + } + return move(position - cursor) != 0; + } + + /** + * Move the cursor where characters. + * + * @param num If less than 0, move abs(where) to the left, otherwise move where to the right. + * @return The number of spaces we moved + */ + public int move(final int num) { + int where = num; + + if ((cursor == 0) && (where <= 0)) { + return 0; + } + + if ((cursor == length()) && (where >= 0)) { + return 0; + } + + if ((cursor + where) < 0) { + where = -cursor; + } else if ((cursor + where) > length()) { + where = length() - cursor; + } + + cursor += where; + cursorCol = -1; + + return where; + } + + public boolean up() { + int col = getCursorCol(); + int pnl = cursor - 1; + while (pnl >= 0 && atChar(pnl) != '\n') { + pnl--; + } + if (pnl < 0) { + return false; + } + int ppnl = pnl - 1; + while (ppnl >= 0 && atChar(ppnl) != '\n') { + ppnl--; + } + cursor = Math.min(ppnl + col + 1, pnl); + return true; + } + + public boolean down() { + int col = getCursorCol(); + int nnl = cursor; + while (nnl < length() && atChar(nnl) != '\n') { + nnl++; + } + if (nnl >= length()) { + return false; + } + int nnnl = nnl + 1; + while (nnnl < length() && atChar(nnnl) != '\n') { + nnnl++; + } + cursor = Math.min(nnl + col + 1, nnnl); + return true; + } + + public boolean moveXY(int dx, int dy) { + int col = 0; + while (prevChar() != '\n' && move(-1) == -1) { + col++; + } + cursorCol = 0; + while (dy < 0) { + up(); + dy++; + } + while (dy > 0) { + down(); + dy--; + } + col = Math.max(col + dx, 0); + for (int i = 0; i < col; i++) { + if (move(1) != 1 || currChar() == '\n') { + break; + } + } + cursorCol = col; + return true; + } + + private int getCursorCol() { + if (cursorCol < 0) { + cursorCol = 0; + int pnl = cursor - 1; + while (pnl >= 0 && atChar(pnl) != '\n') { + pnl--; + } + cursorCol = cursor - pnl - 1; + } + return cursorCol; + } + + /** + * Issue num backspaces. + * + * @return the number of characters backed up + */ + public int backspace(final int num) { + int count = Math.max(Math.min(cursor, num), 0); + moveGapToCursor(); + cursor -= count; + g0 -= count; + cursorCol = -1; + return count; + } + + /** + * Issue a backspace. + * + * @return true if successful + */ + public boolean backspace() { + return backspace(1) == 1; + } + + public int delete(int num) { + int count = Math.max(Math.min(length() - cursor, num), 0); + moveGapToCursor(); + g1 += count; + cursorCol = -1; + return count; + } + + public boolean delete() { + return delete(1) == 1; + } + + @Override + public String toString() { + return substring(0, length()); + } + + public void copyFrom(Buffer buf) { + if (!(buf instanceof BufferImpl that)) { + throw new IllegalStateException(); + } + this.g0 = that.g0; + this.g1 = that.g1; + this.buffer = that.buffer.clone(); + this.cursor = that.cursor; + this.cursorCol = that.cursorCol; + } + + private void moveGapToCursor() { + if (cursor < g0) { + int l = g0 - cursor; + System.arraycopy(buffer, cursor, buffer, g1 - l, l); + g0 -= l; + g1 -= l; + } else if (cursor > g0) { + int l = cursor - g0; + System.arraycopy(buffer, g1, buffer, g0, l); + g0 += l; + g1 += l; + } + } + + @Override + public void zeroOut() { + Arrays.fill(buffer, 0); + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/CompletionMatcherImpl.java b/net-cli/src/main/java/org/jline/reader/impl/CompletionMatcherImpl.java new file mode 100644 index 0000000..d392d9f --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/CompletionMatcherImpl.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.jline.reader.Candidate; +import org.jline.reader.CompletingParsedLine; +import org.jline.reader.CompletionMatcher; +import org.jline.reader.LineReader; +import org.jline.utils.AttributedString; + +public class CompletionMatcherImpl implements CompletionMatcher { + protected Predicate exact; + protected List>, Map>>> matchers; + private Map> matching; + private boolean caseInsensitive; + + public CompletionMatcherImpl() { + } + + protected void reset(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + exact = s -> false; + matchers = new ArrayList<>(); + matching = null; + } + + @Override + public void compile( + Map options, + boolean prefix, + CompletingParsedLine line, + boolean caseInsensitive, + int errors, + String originalGroupName) { + reset(caseInsensitive); + defaultMatchers(options, prefix, line, caseInsensitive, errors, originalGroupName); + } + + @Override + public List matches(List candidates) { + matching = Collections.emptyMap(); + Map> sortedCandidates = sort(candidates); + for (Function>, Map>> matcher : matchers) { + matching = matcher.apply(sortedCandidates); + if (!matching.isEmpty()) { + break; + } + } + return !matching.isEmpty() + ? matching.entrySet().stream() + .flatMap(e -> e.getValue().stream()) + .distinct() + .collect(Collectors.toList()) + : new ArrayList<>(); + } + + @Override + public Candidate exactMatch() { + if (matching == null) { + throw new IllegalStateException(); + } + return matching.values().stream() + .flatMap(Collection::stream) + .filter(Candidate::complete) + .filter(c -> exact.test(c.value())) + .findFirst() + .orElse(null); + } + + @Override + public String getCommonPrefix() { + if (matching == null) { + throw new IllegalStateException(); + } + String commonPrefix = null; + for (String key : matching.keySet()) { + commonPrefix = commonPrefix == null ? key : getCommonStart(commonPrefix, key, caseInsensitive); + } + return commonPrefix; + } + + /** + * Default JLine matchers + */ + protected void defaultMatchers( + Map options, + boolean prefix, + CompletingParsedLine line, + boolean caseInsensitive, + int errors, + String originalGroupName) { + // Find matchers + // TODO: glob completion + String wd = line.word(); + String wdi = caseInsensitive ? wd.toLowerCase() : wd; + String wp = wdi.substring(0, line.wordCursor()); + if (prefix) { + matchers = new ArrayList<>(Arrays.asList( + simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).startsWith(wp)), + simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).contains(wp)))); + if (LineReader.Option.COMPLETE_MATCHER_TYPO.isSet(options)) { + matchers.add(typoMatcher(wp, errors, caseInsensitive, originalGroupName)); + } + exact = s -> caseInsensitive ? s.equalsIgnoreCase(wp) : s.equals(wp); + } else if (!LineReader.Option.EMPTY_WORD_OPTIONS.isSet(options) && wd.length() == 0) { + matchers = new ArrayList<>(Collections.singletonList(simpleMatcher(s -> !s.startsWith("-")))); + exact = s -> caseInsensitive ? s.equalsIgnoreCase(wd) : s.equals(wd); + } else { + if (LineReader.Option.COMPLETE_IN_WORD.isSet(options)) { + String ws = wdi.substring(line.wordCursor()); + Pattern p1 = Pattern.compile(Pattern.quote(wp) + ".*" + Pattern.quote(ws) + ".*"); + Pattern p2 = Pattern.compile(".*" + Pattern.quote(wp) + ".*" + Pattern.quote(ws) + ".*"); + matchers = new ArrayList<>(Arrays.asList( + simpleMatcher(s -> p1.matcher(caseInsensitive ? s.toLowerCase() : s) + .matches()), + simpleMatcher(s -> p2.matcher(caseInsensitive ? s.toLowerCase() : s) + .matches()))); + } else { + matchers = new ArrayList<>(Arrays.asList( + simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).startsWith(wdi)), + simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).contains(wdi)))); + } + if (LineReader.Option.COMPLETE_MATCHER_CAMELCASE.isSet(options)) { + matchers.add(simpleMatcher(s -> camelMatch(wd, 0, s, 0))); + } + if (LineReader.Option.COMPLETE_MATCHER_TYPO.isSet(options)) { + matchers.add(typoMatcher(wdi, errors, caseInsensitive, originalGroupName)); + } + exact = s -> caseInsensitive ? s.equalsIgnoreCase(wd) : s.equals(wd); + } + } + + protected Function>, Map>> simpleMatcher( + Predicate predicate) { + return m -> m.entrySet().stream() + .filter(e -> predicate.test(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + protected Function>, Map>> typoMatcher( + String word, int errors, boolean caseInsensitive, String originalGroupName) { + return m -> { + Map> map = m.entrySet().stream() + .filter(e -> ReaderUtils.distance( + word, caseInsensitive ? e.getKey().toLowerCase() : e.getKey()) + < errors) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + if (map.size() > 1) { + map.computeIfAbsent(word, w -> new ArrayList<>()) + .add(new Candidate(word, word, originalGroupName, null, null, null, false)); + } + return map; + }; + } + + protected boolean camelMatch(String word, int i, String candidate, int j) { + if (word.length() <= i) { + return true; + } else if (candidate.length() <= j) { + return false; + } else { + char c = word.charAt(i); + if (c == candidate.charAt(j)) { + return camelMatch(word, i + 1, candidate, j + 1); + } else { + for (int j1 = j; j1 < candidate.length(); j1++) { + if (Character.isUpperCase(candidate.charAt(j1))) { + if (Character.toUpperCase(c) == candidate.charAt(j1)) { + if (camelMatch(word, i + 1, candidate, j1 + 1)) { + return true; + } + } + } + } + return false; + } + } + } + + private Map> sort(List candidates) { + // Build a list of sorted candidates + Map> sortedCandidates = new HashMap<>(); + for (Candidate candidate : candidates) { + sortedCandidates + .computeIfAbsent( + AttributedString.fromAnsi(candidate.value()).toString(), s -> new ArrayList<>()) + .add(candidate); + } + return sortedCandidates; + } + + private String getCommonStart(String str1, String str2, boolean caseInsensitive) { + int[] s1 = str1.codePoints().toArray(); + int[] s2 = str2.codePoints().toArray(); + int len = 0; + while (len < Math.min(s1.length, s2.length)) { + int ch1 = s1[len]; + int ch2 = s2[len]; + if (ch1 != ch2 && caseInsensitive) { + ch1 = Character.toUpperCase(ch1); + ch2 = Character.toUpperCase(ch2); + if (ch1 != ch2) { + ch1 = Character.toLowerCase(ch1); + ch2 = Character.toLowerCase(ch2); + } + } + if (ch1 != ch2) { + break; + } + len++; + } + return new String(s1, 0, len); + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/DefaultExpander.java b/net-cli/src/main/java/org/jline/reader/impl/DefaultExpander.java new file mode 100644 index 0000000..5e28d40 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/DefaultExpander.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import java.util.ListIterator; +import org.jline.reader.Expander; +import org.jline.reader.History; +import org.jline.reader.History.Entry; + +public class DefaultExpander implements Expander { + + /** + * Expand event designator such as !!, !#, !3, etc... + * See http://www.gnu.org/software/bash/manual/html_node/Event-Designators.html + */ + @SuppressWarnings("fallthrough") + @Override + public String expandHistory(History history, String line) { + boolean inQuote = false; + StringBuilder sb = new StringBuilder(); + boolean escaped = false; + int unicode = 0; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (unicode > 0) { + escaped = (--unicode >= 0); + sb.append(c); + } else if (escaped) { + if (c == 'u') { + unicode = 4; + } else { + escaped = false; + } + sb.append(c); + } else if (c == '\'') { + inQuote = !inQuote; + sb.append(c); + } else if (inQuote) { + sb.append(c); + } else { + switch (c) { + case '\\': + // any '\!' should be considered an expansion escape, so skip expansion and strip the escape + // character + // a leading '\^' should be considered an expansion escape, so skip expansion and strip the + // escape character + // otherwise, add the escape + escaped = true; + sb.append(c); + break; + case '!': + if (i + 1 < line.length()) { + c = line.charAt(++i); + boolean neg = false; + String rep = null; + int i1, idx; + switch (c) { + case '!': + if (history.size() == 0) { + throw new IllegalArgumentException("!!: event not found"); + } + rep = history.get(history.index() - 1); + break; + case '#': + sb.append(sb); + break; + case '?': + i1 = line.indexOf('?', i + 1); + if (i1 < 0) { + i1 = line.length(); + } + String sc = line.substring(i + 1, i1); + i = i1; + idx = searchBackwards(history, sc, history.index(), false); + if (idx < 0) { + throw new IllegalArgumentException("!?" + sc + ": event not found"); + } else { + rep = history.get(idx); + } + break; + case '$': + if (history.size() == 0) { + throw new IllegalArgumentException("!$: event not found"); + } + String previous = + history.get(history.index() - 1).trim(); + int lastSpace = previous.lastIndexOf(' '); + if (lastSpace != -1) { + rep = previous.substring(lastSpace + 1); + } else { + rep = previous; + } + break; + case ' ': + case '\t': + sb.append('!'); + sb.append(c); + break; + case '-': + neg = true; + i++; + // fall through + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + i1 = i; + for (; i < line.length(); i++) { + c = line.charAt(i); + if (c < '0' || c > '9') { + break; + } + } + try { + idx = Integer.parseInt(line.substring(i1, i)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + (neg ? "!-" : "!") + line.substring(i1, i) + ": event not found"); + } + if (neg && idx > 0 && idx <= history.size()) { + rep = history.get(history.index() - idx); + } else if (!neg + && idx > history.index() - history.size() + && idx <= history.index()) { + rep = history.get(idx - 1); + } else { + throw new IllegalArgumentException( + (neg ? "!-" : "!") + line.substring(i1, i) + ": event not found"); + } + break; + default: + String ss = line.substring(i); + i = line.length(); + idx = searchBackwards(history, ss, history.index(), true); + if (idx < 0) { + throw new IllegalArgumentException("!" + ss + ": event not found"); + } else { + rep = history.get(idx); + } + break; + } + if (rep != null) { + sb.append(rep); + } + } else { + sb.append(c); + } + break; + case '^': + if (i == 0) { + int i1 = line.indexOf('^', i + 1); + int i2 = line.indexOf('^', i1 + 1); + if (i2 < 0) { + i2 = line.length(); + } + if (i1 > 0 && i2 > 0) { + String s1 = line.substring(i + 1, i1); + String s2 = line.substring(i1 + 1, i2); + String s = history.get(history.index() - 1).replace(s1, s2); + sb.append(s); + i = i2 + 1; + break; + } + } + sb.append(c); + break; + default: + sb.append(c); + break; + } + } + } + return sb.toString(); + } + + @Override + public String expandVar(String word) { + return word; + } + + protected int searchBackwards(History history, String searchTerm, int startIndex, boolean startsWith) { + ListIterator it = history.iterator(startIndex); + while (it.hasPrevious()) { + Entry e = it.previous(); + if (startsWith) { + if (e.line().startsWith(searchTerm)) { + return e.index(); + } + } else { + if (e.line().contains(searchTerm)) { + return e.index(); + } + } + } + return -1; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/DefaultHighlighter.java b/net-cli/src/main/java/org/jline/reader/impl/DefaultHighlighter.java new file mode 100644 index 0000000..5e0ddc2 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/DefaultHighlighter.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import java.util.regex.Pattern; +import org.jline.reader.Highlighter; +import org.jline.reader.LineReader; +import org.jline.reader.LineReader.RegionType; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.WCWidth; + +public class DefaultHighlighter implements Highlighter { + protected Pattern errorPattern; + protected int errorIndex = -1; + + @Override + public void setErrorPattern(Pattern errorPattern) { + this.errorPattern = errorPattern; + } + + @Override + public void setErrorIndex(int errorIndex) { + this.errorIndex = errorIndex; + } + + @Override + public AttributedString highlight(LineReader reader, String buffer) { + int underlineStart = -1; + int underlineEnd = -1; + int negativeStart = -1; + int negativeEnd = -1; + String search = reader.getSearchTerm(); + if (search != null && search.length() > 0) { + underlineStart = buffer.indexOf(search); + if (underlineStart >= 0) { + underlineEnd = underlineStart + search.length() - 1; + } + } + if (reader.getRegionActive() != RegionType.NONE) { + negativeStart = reader.getRegionMark(); + negativeEnd = reader.getBuffer().cursor(); + if (negativeStart > negativeEnd) { + int x = negativeEnd; + negativeEnd = negativeStart; + negativeStart = x; + } + if (reader.getRegionActive() == RegionType.LINE) { + while (negativeStart > 0 && reader.getBuffer().atChar(negativeStart - 1) != '\n') { + negativeStart--; + } + while (negativeEnd < reader.getBuffer().length() - 1 + && reader.getBuffer().atChar(negativeEnd + 1) != '\n') { + negativeEnd++; + } + } + } + + AttributedStringBuilder sb = new AttributedStringBuilder(); + for (int i = 0; i < buffer.length(); i++) { + if (i == underlineStart) { + sb.style(AttributedStyle::underline); + } + if (i == negativeStart) { + sb.style(AttributedStyle::inverse); + } + if (i == errorIndex) { + sb.style(AttributedStyle::inverse); + } + + char c = buffer.charAt(i); + if (c == '\t' || c == '\n') { + sb.append(c); + } else if (c < 32) { + sb.style(AttributedStyle::inverseNeg) + .append('^') + .append((char) (c + '@')) + .style(AttributedStyle::inverseNeg); + } else { + int w = WCWidth.wcwidth(c); + if (w > 0) { + sb.append(c); + } + } + if (i == underlineEnd) { + sb.style(AttributedStyle::underlineOff); + } + if (i == negativeEnd) { + sb.style(AttributedStyle::inverseOff); + } + if (i == errorIndex) { + sb.style(AttributedStyle::inverseOff); + } + } + if (errorPattern != null) { + sb.styleMatches(errorPattern, AttributedStyle.INVERSE); + } + return sb.toAttributedString(); + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/DefaultParser.java b/net-cli/src/main/java/org/jline/reader/impl/DefaultParser.java new file mode 100644 index 0000000..98e6337 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/DefaultParser.java @@ -0,0 +1,811 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jline.reader.CompletingParsedLine; +import org.jline.reader.EOFError; +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser; + +public class DefaultParser implements Parser { + + private char[] quoteChars = {'\'', '"'}; + private char[] escapeChars = {'\\'}; + private boolean eofOnUnclosedQuote; + private boolean eofOnEscapedNewLine; + private char[] openingBrackets = null; + private char[] closingBrackets = null; + private String[] lineCommentDelims = null; + private BlockCommentDelims blockCommentDelims = null; + private String regexVariable = "[a-zA-Z_]+[a-zA-Z0-9_-]*((\\.|\\['|\\[\"|\\[)[a-zA-Z0-9_-]*(|']|\"]|]))?"; + private String regexCommand = "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*"; + private int commandGroup = 4; + + public DefaultParser lineCommentDelims(final String[] lineCommentDelims) { + this.lineCommentDelims = lineCommentDelims; + return this; + } + + public DefaultParser blockCommentDelims(final BlockCommentDelims blockCommentDelims) { + this.blockCommentDelims = blockCommentDelims; + return this; + } + + // + // Chainable setters + // + + public DefaultParser quoteChars(final char[] chars) { + this.quoteChars = chars; + return this; + } + + public DefaultParser escapeChars(final char[] chars) { + this.escapeChars = chars; + return this; + } + + public DefaultParser eofOnUnclosedQuote(boolean eofOnUnclosedQuote) { + this.eofOnUnclosedQuote = eofOnUnclosedQuote; + return this; + } + + public DefaultParser eofOnUnclosedBracket(Bracket... brackets) { + setEofOnUnclosedBracket(brackets); + return this; + } + + public DefaultParser eofOnEscapedNewLine(boolean eofOnEscapedNewLine) { + this.eofOnEscapedNewLine = eofOnEscapedNewLine; + return this; + } + + public DefaultParser regexVariable(String regexVariable) { + this.regexVariable = regexVariable; + return this; + } + + public DefaultParser regexCommand(String regexCommand) { + this.regexCommand = regexCommand; + return this; + } + + public DefaultParser commandGroup(int commandGroup) { + this.commandGroup = commandGroup; + return this; + } + + public char[] getQuoteChars() { + return this.quoteChars; + } + + public void setQuoteChars(final char[] chars) { + this.quoteChars = chars; + } + + // + // Java bean getters and setters + // + + public char[] getEscapeChars() { + return this.escapeChars; + } + + public void setEscapeChars(final char[] chars) { + this.escapeChars = chars; + } + + public String[] getLineCommentDelims() { + return this.lineCommentDelims; + } + + public void setLineCommentDelims(String[] lineCommentDelims) { + this.lineCommentDelims = lineCommentDelims; + } + + public BlockCommentDelims getBlockCommentDelims() { + return blockCommentDelims; + } + + public void setBlockCommentDelims(BlockCommentDelims blockCommentDelims) { + this.blockCommentDelims = blockCommentDelims; + } + + public boolean isEofOnUnclosedQuote() { + return eofOnUnclosedQuote; + } + + public void setEofOnUnclosedQuote(boolean eofOnUnclosedQuote) { + this.eofOnUnclosedQuote = eofOnUnclosedQuote; + } + + public boolean isEofOnEscapedNewLine() { + return eofOnEscapedNewLine; + } + + public void setEofOnEscapedNewLine(boolean eofOnEscapedNewLine) { + this.eofOnEscapedNewLine = eofOnEscapedNewLine; + } + + public void setEofOnUnclosedBracket(Bracket... brackets) { + if (brackets == null) { + openingBrackets = null; + closingBrackets = null; + } else { + Set bs = new HashSet<>(Arrays.asList(brackets)); + openingBrackets = new char[bs.size()]; + closingBrackets = new char[bs.size()]; + int i = 0; + for (Bracket b : bs) { + switch (b) { + case ROUND: + openingBrackets[i] = '('; + closingBrackets[i] = ')'; + break; + case CURLY: + openingBrackets[i] = '{'; + closingBrackets[i] = '}'; + break; + case SQUARE: + openingBrackets[i] = '['; + closingBrackets[i] = ']'; + break; + case ANGLE: + openingBrackets[i] = '<'; + closingBrackets[i] = '>'; + break; + } + i++; + } + } + } + + public void setRegexVariable(String regexVariable) { + this.regexVariable = regexVariable; + } + + public void setRegexCommand(String regexCommand) { + this.regexCommand = regexCommand; + } + + public void setCommandGroup(int commandGroup) { + this.commandGroup = commandGroup; + } + + @Override + public boolean validCommandName(String name) { + return name != null && name.matches(regexCommand); + } + + @Override + public boolean validVariableName(String name) { + return name != null && regexVariable != null && name.matches(regexVariable); + } + + @Override + public String getCommand(final String line) { + String out = ""; + boolean checkCommandOnly = regexVariable == null; + if (!checkCommandOnly) { + Pattern patternCommand = Pattern.compile("^\\s*" + regexVariable + "=(" + regexCommand + ")(\\s+|$)"); + Matcher matcher = patternCommand.matcher(line); + if (matcher.find()) { + out = matcher.group(commandGroup); + } else { + checkCommandOnly = true; + } + } + if (checkCommandOnly) { + out = line.trim().split("\\s+")[0]; + if (!out.matches(regexCommand)) { + out = ""; + } + } + return out; + } + + @Override + public String getVariable(final String line) { + String out = null; + if (regexVariable != null) { + Pattern patternCommand = Pattern.compile("^\\s*(" + regexVariable + ")\\s*=[^=~].*"); + Matcher matcher = patternCommand.matcher(line); + if (matcher.find()) { + out = matcher.group(1); + } + } + return out; + } + + public ParsedLine parse(final String line, final int cursor, ParseContext context) { + List words = new LinkedList<>(); + StringBuilder current = new StringBuilder(); + int wordCursor = -1; + int wordIndex = -1; + int quoteStart = -1; + int rawWordCursor = -1; + int rawWordLength = -1; + int rawWordStart = 0; + BracketChecker bracketChecker = new BracketChecker(cursor); + boolean quotedWord = false; + boolean lineCommented = false; + boolean blockCommented = false; + boolean blockCommentInRightOrder = true; + final String blockCommentEnd = blockCommentDelims == null ? null : blockCommentDelims.end; + final String blockCommentStart = blockCommentDelims == null ? null : blockCommentDelims.start; + + for (int i = 0; (line != null) && (i < line.length()); i++) { + // once we reach the cursor, set the + // position of the selected index + if (i == cursor) { + wordIndex = words.size(); + // the position in the current argument is just the + // length of the current argument + wordCursor = current.length(); + rawWordCursor = i - rawWordStart; + } + + if (quoteStart < 0 && isQuoteChar(line, i) && !lineCommented && !blockCommented) { + // Start a quote block + quoteStart = i; + if (current.length() == 0) { + quotedWord = true; + if (context == ParseContext.SPLIT_LINE) { + current.append(line.charAt(i)); + } + } else { + current.append(line.charAt(i)); + } + } else if (quoteStart >= 0 && line.charAt(quoteStart) == line.charAt(i) && !isEscaped(line, i)) { + // End quote block + if (!quotedWord || context == ParseContext.SPLIT_LINE) { + current.append(line.charAt(i)); + } else if (rawWordCursor >= 0 && rawWordLength < 0) { + rawWordLength = i - rawWordStart + 1; + } + quoteStart = -1; + quotedWord = false; + } else if (quoteStart < 0 && isDelimiter(line, i)) { + if (lineCommented) { + if (isCommentDelim(line, i, System.lineSeparator())) { + lineCommented = false; + } + } else if (blockCommented) { + if (isCommentDelim(line, i, blockCommentEnd)) { + blockCommented = false; + } + } else { + // Delimiter + rawWordLength = handleDelimiterAndGetRawWordLength( + current, words, rawWordStart, rawWordCursor, rawWordLength, i); + rawWordStart = i + 1; + } + } else { + if (quoteStart < 0 && !blockCommented && (lineCommented || isLineCommentStarted(line, i))) { + lineCommented = true; + } else if (quoteStart < 0 + && !lineCommented + && (blockCommented || isCommentDelim(line, i, blockCommentStart))) { + if (blockCommented) { + if (blockCommentEnd != null && isCommentDelim(line, i, blockCommentEnd)) { + blockCommented = false; + i += blockCommentEnd.length() - 1; + } + } else { + blockCommented = true; + rawWordLength = handleDelimiterAndGetRawWordLength( + current, words, rawWordStart, rawWordCursor, rawWordLength, i); + i += blockCommentStart == null ? 0 : blockCommentStart.length() - 1; + rawWordStart = i + 1; + } + } else if (quoteStart < 0 && !lineCommented && isCommentDelim(line, i, blockCommentEnd)) { + current.append(line.charAt(i)); + blockCommentInRightOrder = false; + } else if (!isEscapeChar(line, i)) { + current.append(line.charAt(i)); + if (quoteStart < 0) { + bracketChecker.check(line, i); + } + } else if (context == ParseContext.SPLIT_LINE) { + current.append(line.charAt(i)); + } + } + } + + if (current.length() > 0 || cursor == line.length()) { + words.add(current.toString()); + if (rawWordCursor >= 0 && rawWordLength < 0) { + rawWordLength = line.length() - rawWordStart; + } + } + + if (cursor == line.length()) { + wordIndex = words.size() - 1; + wordCursor = words.get(words.size() - 1).length(); + rawWordCursor = cursor - rawWordStart; + rawWordLength = rawWordCursor; + } + + if (context != ParseContext.COMPLETE && context != ParseContext.SPLIT_LINE) { + if (eofOnEscapedNewLine && isEscapeChar(line, line.length() - 1)) { + throw new EOFError(-1, -1, "Escaped new line", "newline"); + } + if (eofOnUnclosedQuote && quoteStart >= 0) { + throw new EOFError( + -1, -1, "Missing closing quote", line.charAt(quoteStart) == '\'' ? "quote" : "dquote"); + } + if (blockCommented) { + throw new EOFError(-1, -1, "Missing closing block comment delimiter", "add: " + blockCommentEnd); + } + if (!blockCommentInRightOrder) { + throw new EOFError(-1, -1, "Missing opening block comment delimiter", "missing: " + blockCommentStart); + } + if (bracketChecker.isClosingBracketMissing() || bracketChecker.isOpeningBracketMissing()) { + String message = null; + String missing = null; + if (bracketChecker.isClosingBracketMissing()) { + message = "Missing closing brackets"; + missing = "add: " + bracketChecker.getMissingClosingBrackets(); + } else { + message = "Missing opening bracket"; + missing = "missing: " + bracketChecker.getMissingOpeningBracket(); + } + throw new EOFError( + -1, + -1, + message, + missing, + bracketChecker.getOpenBrackets(), + bracketChecker.getNextClosingBracket()); + } + } + + String openingQuote = quotedWord ? line.substring(quoteStart, quoteStart + 1) : null; + return new ArgumentList(line, words, wordIndex, wordCursor, cursor, openingQuote, rawWordCursor, rawWordLength); + } + + /** + * Returns true if the specified character is a whitespace parameter. Check to ensure that the character is not + * escaped by any of {@link #getQuoteChars}, and is not escaped by any of the {@link #getEscapeChars}, and + * returns true from {@link #isDelimiterChar}. + * + * @param buffer The complete command buffer + * @param pos The index of the character in the buffer + * @return True if the character should be a delimiter + */ + public boolean isDelimiter(final CharSequence buffer, final int pos) { + return !isQuoted(buffer, pos) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos); + } + + private int handleDelimiterAndGetRawWordLength( + StringBuilder current, + List words, + int rawWordStart, + int rawWordCursor, + int rawWordLength, + int pos) { + if (current.length() > 0) { + words.add(current.toString()); + current.setLength(0); // reset the arg + if (rawWordCursor >= 0 && rawWordLength < 0) { + return pos - rawWordStart; + } + } + return rawWordLength; + } + + public boolean isQuoted(final CharSequence buffer, final int pos) { + return false; + } + + public boolean isQuoteChar(final CharSequence buffer, final int pos) { + if (pos < 0) { + return false; + } + if (quoteChars != null) { + for (char e : quoteChars) { + if (e == buffer.charAt(pos)) { + return !isEscaped(buffer, pos); + } + } + } + return false; + } + + private boolean isCommentDelim(final CharSequence buffer, final int pos, final String pattern) { + if (pos < 0) { + return false; + } + + if (pattern != null) { + final int length = pattern.length(); + if (length <= buffer.length() - pos) { + for (int i = 0; i < length; i++) { + if (pattern.charAt(i) != buffer.charAt(pos + i)) { + return false; + } + } + return true; + } + } + return false; + } + + public boolean isLineCommentStarted(final CharSequence buffer, final int pos) { + if (lineCommentDelims != null) { + for (String comment : lineCommentDelims) { + if (isCommentDelim(buffer, pos, comment)) { + return true; + } + } + } + return false; + } + + @Override + public boolean isEscapeChar(char ch) { + if (escapeChars != null) { + for (char e : escapeChars) { + if (e == ch) { + return true; + } + } + } + return false; + } + + /** + * Check if this character is a valid escape char (i.e. one that has not been escaped) + * + * @param buffer the buffer to check in + * @param pos the position of the character to check + * @return true if the character at the specified position in the given buffer is an escape + * character and the character immediately preceding it is not an escape character. + */ + public boolean isEscapeChar(final CharSequence buffer, final int pos) { + if (pos < 0) { + return false; + } + char ch = buffer.charAt(pos); + return isEscapeChar(ch) && !isEscaped(buffer, pos); + } + + /** + * Check if a character is escaped (i.e. if the previous character is an escape) + * + * @param buffer the buffer to check in + * @param pos the position of the character to check + * @return true if the character at the specified position in the given buffer is an escape + * character and the character immediately preceding it is an escape character. + */ + public boolean isEscaped(final CharSequence buffer, final int pos) { + if (pos <= 0) { + return false; + } + return isEscapeChar(buffer, pos - 1); + } + + /** + * Returns true if the character at the specified position if a delimiter. This method will only be called if + * the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by any of the + * {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead. + * + * @param buffer the buffer to check in + * @param pos the position of the character to check + * @return true if the character at the specified position in the given buffer is a delimiter. + */ + public boolean isDelimiterChar(CharSequence buffer, int pos) { + return Character.isWhitespace(buffer.charAt(pos)); + } + + private boolean isRawEscapeChar(char key) { + if (escapeChars != null) { + for (char e : escapeChars) { + if (e == key) { + return true; + } + } + } + return false; + } + + private boolean isRawQuoteChar(char key) { + if (quoteChars != null) { + for (char e : quoteChars) { + if (e == key) { + return true; + } + } + } + return false; + } + + public enum Bracket { + ROUND, // () + CURLY, // {} + SQUARE, // [] + ANGLE // <> + } + + public static class BlockCommentDelims { + private final String start; + private final String end; + + public BlockCommentDelims(String start, String end) { + if (start == null || end == null || start.isEmpty() || end.isEmpty() || start.equals(end)) { + throw new IllegalArgumentException("Bad block comment delimiter!"); + } + this.start = start; + this.end = end; + } + + public String getStart() { + return start; + } + + public String getEnd() { + return end; + } + } + + private class BracketChecker { + private int missingOpeningBracket = -1; + private final List nested = new ArrayList<>(); + private int openBrackets = 0; + private final int cursor; + private String nextClosingBracket; + + public BracketChecker(int cursor) { + this.cursor = cursor; + } + + public void check(final CharSequence buffer, final int pos) { + if (openingBrackets == null || pos < 0) { + return; + } + int bid = bracketId(openingBrackets, buffer, pos); + if (bid >= 0) { + nested.add(bid); + } else { + bid = bracketId(closingBrackets, buffer, pos); + if (bid >= 0) { + if (!nested.isEmpty() && bid == nested.get(nested.size() - 1)) { + nested.remove(nested.size() - 1); + } else { + missingOpeningBracket = bid; + } + } + } + if (cursor > pos) { + openBrackets = nested.size(); + if (nested.size() > 0) { + nextClosingBracket = String.valueOf(closingBrackets[nested.get(nested.size() - 1)]); + } + } + } + + public boolean isOpeningBracketMissing() { + return missingOpeningBracket != -1; + } + + public String getMissingOpeningBracket() { + if (!isOpeningBracketMissing()) { + return null; + } + return Character.toString(openingBrackets[missingOpeningBracket]); + } + + public boolean isClosingBracketMissing() { + return !nested.isEmpty(); + } + + public String getMissingClosingBrackets() { + if (!isClosingBracketMissing()) { + return null; + } + StringBuilder out = new StringBuilder(); + for (int i = nested.size() - 1; i > -1; i--) { + out.append(closingBrackets[nested.get(i)]); + } + return out.toString(); + } + + public int getOpenBrackets() { + return openBrackets; + } + + public String getNextClosingBracket() { + return nested.size() == 2 ? nextClosingBracket : null; + } + + private int bracketId(final char[] brackets, final CharSequence buffer, final int pos) { + for (int i = 0; i < brackets.length; i++) { + if (buffer.charAt(pos) == brackets[i]) { + return i; + } + } + return -1; + } + } + + /** + * The result of a delimited buffer. + * + * @author Marc Prud'hommeaux + */ + public class ArgumentList implements ParsedLine, CompletingParsedLine { + private final String line; + + private final List words; + + private final int wordIndex; + + private final int wordCursor; + + private final int cursor; + + private final String openingQuote; + + private final int rawWordCursor; + + private final int rawWordLength; + + @Deprecated + public ArgumentList( + final String line, + final List words, + final int wordIndex, + final int wordCursor, + final int cursor) { + this( + line, + words, + wordIndex, + wordCursor, + cursor, + null, + wordCursor, + words.get(wordIndex).length()); + } + + /** + * @param line the command line being edited + * @param words the list of words + * @param wordIndex the index of the current word in the list of words + * @param wordCursor the cursor position within the current word + * @param cursor the cursor position within the line + * @param openingQuote the opening quote (usually '\"' or '\'') or null + * @param rawWordCursor the cursor position inside the raw word (i.e. including quotes and escape characters) + * @param rawWordLength the raw word length, including quotes and escape characters + */ + public ArgumentList( + final String line, + final List words, + final int wordIndex, + final int wordCursor, + final int cursor, + final String openingQuote, + final int rawWordCursor, + final int rawWordLength) { + this.line = line; + this.words = Collections.unmodifiableList(Objects.requireNonNull(words)); + this.wordIndex = wordIndex; + this.wordCursor = wordCursor; + this.cursor = cursor; + this.openingQuote = openingQuote; + this.rawWordCursor = rawWordCursor; + this.rawWordLength = rawWordLength; + } + + public int wordIndex() { + return this.wordIndex; + } + + public String word() { + // TODO: word() should always be contained in words() + if ((wordIndex < 0) || (wordIndex >= words.size())) { + return ""; + } + return words.get(wordIndex); + } + + public int wordCursor() { + return this.wordCursor; + } + + public List words() { + return this.words; + } + + public int cursor() { + return this.cursor; + } + + public String line() { + return line; + } + + public CharSequence escape(CharSequence candidate, boolean complete) { + StringBuilder sb = new StringBuilder(candidate); + Predicate needToBeEscaped; + String quote = openingQuote; + boolean middleQuotes = false; + if (openingQuote == null) { + for (int i = 0; i < sb.length(); i++) { + if (isQuoteChar(sb, i)) { + middleQuotes = true; + break; + } + } + } + if (escapeChars != null) { + if (escapeChars.length > 0) { + // Completion is protected by an opening quote: + // Delimiters (spaces) don't need to be escaped, nor do other quotes, but everything else does. + // Also, close the quote at the end + if (openingQuote != null) { + needToBeEscaped = i -> isRawEscapeChar(sb.charAt(i)) + || String.valueOf(sb.charAt(i)).equals(openingQuote); + } + // Completion is protected by middle quotes: + // Delimiters (spaces) don't need to be escaped, nor do quotes, but everything else does. + else if (middleQuotes) { + needToBeEscaped = i -> isRawEscapeChar(sb.charAt(i)); + } + // No quote protection, need to escape everything: delimiter chars (spaces), quote chars + // and escapes themselves + else { + needToBeEscaped = i -> + isDelimiterChar(sb, i) || isRawEscapeChar(sb.charAt(i)) || isRawQuoteChar(sb.charAt(i)); + } + for (int i = 0; i < sb.length(); i++) { + if (needToBeEscaped.test(i)) { + sb.insert(i++, escapeChars[0]); + } + } + } + } else if (openingQuote == null && !middleQuotes) { + for (int i = 0; i < sb.length(); i++) { + if (isDelimiterChar(sb, i)) { + quote = "'"; + break; + } + } + } + if (quote != null) { + sb.insert(0, quote); + if (complete) { + sb.append(quote); + } + } + return sb; + } + + @Override + public int rawWordCursor() { + return rawWordCursor; + } + + @Override + public int rawWordLength() { + return rawWordLength; + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/InputRC.java b/net-cli/src/main/java/org/jline/reader/impl/InputRC.java new file mode 100644 index 0000000..56b1465 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/InputRC.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2002-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.jline.reader.LineReader; +import org.jline.reader.Macro; +import org.jline.reader.Reference; +import org.jline.terminal.Terminal; +import org.jline.utils.Log; + +public final class InputRC { + + private final LineReader reader; + + private InputRC(LineReader reader) { + this.reader = reader; + } + + public static void configure(LineReader reader, URL url) throws IOException { + try (InputStream is = url.openStream()) { + configure(reader, is); + } + } + + public static void configure(LineReader reader, InputStream is) throws IOException { + try (InputStreamReader r = new InputStreamReader(is)) { + configure(reader, r); + } + } + + public static void configure(LineReader reader, Reader r) throws IOException { + BufferedReader br; + if (r instanceof BufferedReader) { + br = (BufferedReader) r; + } else { + br = new BufferedReader(r); + } + + Terminal terminal = reader.getTerminal(); + + if (Terminal.TYPE_DUMB.equals(terminal.getType()) || Terminal.TYPE_DUMB_COLOR.equals(terminal.getType())) { + reader.getVariables().putIfAbsent(LineReader.EDITING_MODE, "dumb"); + } else { + reader.getVariables().putIfAbsent(LineReader.EDITING_MODE, "emacs"); + } + + reader.setKeyMap(LineReader.MAIN); + new InputRC(reader).parse(br); + if ("vi".equals(reader.getVariable(LineReader.EDITING_MODE))) { + reader.getKeyMaps().put(LineReader.MAIN, reader.getKeyMaps().get(LineReader.VIINS)); + } else if ("emacs".equals(reader.getVariable(LineReader.EDITING_MODE))) { + reader.getKeyMaps().put(LineReader.MAIN, reader.getKeyMaps().get(LineReader.EMACS)); + } else if ("dumb".equals(reader.getVariable(LineReader.EDITING_MODE))) { + reader.getKeyMaps().put(LineReader.MAIN, reader.getKeyMaps().get(LineReader.DUMB)); + } + } + + private static String translateQuoted(String keySeq) { + int i; + String str = keySeq.substring(1, keySeq.length() - 1); + StringBuilder sb = new StringBuilder(); + for (i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == '\\') { + boolean ctrl = str.regionMatches(i, "\\C-", 0, 3) || str.regionMatches(i, "\\M-\\C-", 0, 6); + boolean meta = str.regionMatches(i, "\\M-", 0, 3) || str.regionMatches(i, "\\C-\\M-", 0, 6); + i += (meta ? 3 : 0) + (ctrl ? 3 : 0) + (!meta && !ctrl ? 1 : 0); + if (i >= str.length()) { + break; + } + c = str.charAt(i); + if (meta) { + sb.append("\u001b"); + } + if (ctrl) { + c = c == '?' ? 0x7f : (char) (Character.toUpperCase(c) & 0x1f); + } + if (!meta && !ctrl) { + switch (c) { + case 'a': + c = 0x07; + break; + case 'b': + c = '\b'; + break; + case 'd': + c = 0x7f; + break; + case 'e': + c = 0x1b; + break; + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case 'v': + c = 0x0b; + break; + case '\\': + c = '\\'; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + c = 0; + for (int j = 0; j < 3; j++, i++) { + if (i >= str.length()) { + break; + } + int k = Character.digit(str.charAt(i), 8); + if (k < 0) { + break; + } + c = (char) (c * 8 + k); + } + c &= 0xFF; + break; + case 'x': + i++; + c = 0; + for (int j = 0; j < 2; j++, i++) { + if (i >= str.length()) { + break; + } + int k = Character.digit(str.charAt(i), 16); + if (k < 0) { + break; + } + c = (char) (c * 16 + k); + } + c &= 0xFF; + break; + case 'u': + i++; + c = 0; + for (int j = 0; j < 4; j++, i++) { + if (i >= str.length()) { + break; + } + int k = Character.digit(str.charAt(i), 16); + if (k < 0) { + break; + } + c = (char) (c * 16 + k); + } + break; + } + } + sb.append(c); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + private static char getKeyFromName(String name) { + if ("DEL".equalsIgnoreCase(name) || "Rubout".equalsIgnoreCase(name)) { + return 0x7f; + } else if ("ESC".equalsIgnoreCase(name) || "Escape".equalsIgnoreCase(name)) { + return '\033'; + } else if ("LFD".equalsIgnoreCase(name) || "NewLine".equalsIgnoreCase(name)) { + return '\n'; + } else if ("RET".equalsIgnoreCase(name) || "Return".equalsIgnoreCase(name)) { + return '\r'; + } else if ("SPC".equalsIgnoreCase(name) || "Space".equalsIgnoreCase(name)) { + return ' '; + } else if ("Tab".equalsIgnoreCase(name)) { + return '\t'; + } else { + return name.charAt(0); + } + } + + static void setVar(LineReader reader, String key, String val) { + if (LineReader.KEYMAP.equalsIgnoreCase(key)) { + reader.setKeyMap(val); + return; + } + + for (LineReader.Option option : LineReader.Option.values()) { + if (option.name().toLowerCase(Locale.ENGLISH).replace('_', '-').equals(val)) { + if ("on".equalsIgnoreCase(val)) { + reader.setOpt(option); + } else if ("off".equalsIgnoreCase(val)) { + reader.unsetOpt(option); + } + return; + } + } + + reader.setVariable(key, val); + } + + private void parse(BufferedReader br) throws IOException, IllegalArgumentException { + String line; + boolean parsing = true; + List ifsStack = new ArrayList<>(); + while ((line = br.readLine()) != null) { + try { + line = line.trim(); + if (line.length() == 0) { + continue; + } + if (line.charAt(0) == '#') { + continue; + } + int i = 0; + if (line.charAt(i) == '$') { + String cmd; + String args; + ++i; + while (i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t')) { + i++; + } + int s = i; + while (i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t')) { + i++; + } + cmd = line.substring(s, i); + while (i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t')) { + i++; + } + s = i; + while (i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t')) { + i++; + } + args = line.substring(s, i); + if ("if".equalsIgnoreCase(cmd)) { + ifsStack.add(parsing); + if (!parsing) { + continue; + } + if (args.startsWith("term=")) { + // TODO + } else if (args.startsWith("mode=")) { + String mode = (String) reader.getVariable(LineReader.EDITING_MODE); + parsing = args.substring("mode=".length()).equalsIgnoreCase(mode); + } else { + parsing = args.equalsIgnoreCase(reader.getAppName()); + } + } else if ("else".equalsIgnoreCase(cmd)) { + if (ifsStack.isEmpty()) { + throw new IllegalArgumentException("$else found without matching $if"); + } + boolean invert = true; + for (boolean b : ifsStack) { + if (!b) { + invert = false; + break; + } + } + if (invert) { + parsing = !parsing; + } + } else if ("endif".equalsIgnoreCase(cmd)) { + if (ifsStack.isEmpty()) { + throw new IllegalArgumentException("endif found without matching $if"); + } + parsing = ifsStack.remove(ifsStack.size() - 1); + } else if ("include".equalsIgnoreCase(cmd)) { + // TODO + } + continue; + } + if (!parsing) { + continue; + } + if (line.charAt(i++) == '"') { + boolean esc = false; + for (; ; i++) { + if (i >= line.length()) { + throw new IllegalArgumentException("Missing closing quote on line '" + line + "'"); + } + if (esc) { + esc = false; + } else if (line.charAt(i) == '\\') { + esc = true; + } else if (line.charAt(i) == '"') { + break; + } + } + } + while (i < line.length() && line.charAt(i) != ':' && line.charAt(i) != ' ' && line.charAt(i) != '\t') { + i++; + } + String keySeq = line.substring(0, i); + boolean equivalency = i + 1 < line.length() && line.charAt(i) == ':' && line.charAt(i + 1) == '='; + i++; + if (equivalency) { + i++; + } + if (keySeq.equalsIgnoreCase("set")) { + String key; + String val; + while (i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t')) { + i++; + } + int s = i; + while (i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t')) { + i++; + } + key = line.substring(s, i); + while (i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t')) { + i++; + } + s = i; + while (i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t')) { + i++; + } + val = line.substring(s, i); + setVar(reader, key, val); + } else { + while (i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t')) { + i++; + } + int start = i; + if (i < line.length() && (line.charAt(i) == '\'' || line.charAt(i) == '\"')) { + char delim = line.charAt(i++); + boolean esc = false; + for (; ; i++) { + if (i >= line.length()) { + break; + } + if (esc) { + esc = false; + } else if (line.charAt(i) == '\\') { + esc = true; + } else if (line.charAt(i) == delim) { + break; + } + } + } + for (; i < line.length() && line.charAt(i) != ' ' && line.charAt(i) != '\t'; i++) + ; + String val = line.substring(Math.min(start, line.length()), Math.min(i, line.length())); + if (keySeq.charAt(0) == '"') { + keySeq = translateQuoted(keySeq); + } else { + // Bind key name + String keyName = + keySeq.lastIndexOf('-') > 0 ? keySeq.substring(keySeq.lastIndexOf('-') + 1) : keySeq; + char key = getKeyFromName(keyName); + keyName = keySeq.toLowerCase(); + keySeq = ""; + if (keyName.contains("meta-") || keyName.contains("m-")) { + keySeq += "\u001b"; + } + if (keyName.contains("control-") || keyName.contains("c-") || keyName.contains("ctrl-")) { + key = (char) (Character.toUpperCase(key) & 0x1f); + } + keySeq += key; + } + if (val.length() > 0 && (val.charAt(0) == '\'' || val.charAt(0) == '\"')) { + reader.getKeys().bind(new Macro(translateQuoted(val)), keySeq); + } else { + reader.getKeys().bind(new Reference(val), keySeq); + } + } + } catch (IllegalArgumentException e) { + Log.warn("Unable to parse user configuration: ", e); + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/KillRing.java b/net-cli/src/main/java/org/jline/reader/impl/KillRing.java new file mode 100644 index 0000000..c7182e5 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/KillRing.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +/** + * The kill ring class keeps killed text in a fixed size ring. In this + * class we also keep record of whether or not the last command was a + * kill or a yank. Depending on this, the class may behave + * different. For instance, two consecutive kill-word commands fill + * the same slot such that the next yank will return the two + * previously killed words instead that only the last one. Likewise + * yank pop requires that the previous command was either a yank or a + * yank-pop. + */ +public final class KillRing { + + /** + * Default size is 60, like in emacs. + */ + private static final int DEFAULT_SIZE = 60; + + private final String[] slots; + private int head = 0; + private boolean lastKill = false; + private boolean lastYank = false; + + /** + * Creates a new kill ring of the given size. + * + * @param size the size of the ring + */ + public KillRing(int size) { + slots = new String[size]; + } + + /** + * Creates a new kill ring of the default size. See {@link #DEFAULT_SIZE}. + */ + public KillRing() { + this(DEFAULT_SIZE); + } + + /** + * Resets the last-yank state. + */ + public void resetLastYank() { + lastYank = false; + } + + /** + * Resets the last-kill state. + */ + public void resetLastKill() { + lastKill = false; + } + + /** + * Returns {@code true} if the last command was a yank. + * + * @return {@code true} if the last command was a yank + */ + public boolean lastYank() { + return lastYank; + } + + /** + * Adds the string to the kill-ring. Also sets lastYank to false + * and lastKill to true. + * + * @param str the string to add + */ + public void add(String str) { + lastYank = false; + + if (lastKill) { + if (slots[head] != null) { + slots[head] += str; + return; + } + } + + lastKill = true; + next(); + slots[head] = str; + } + + /** + * Adds the string to the kill-ring product of killing + * backwards. If the previous command was a kill text one then + * adds the text at the beginning of the previous kill to avoid + * that two consecutive backwards kills followed by a yank leaves + * things reversed. + * + * @param str the string to add + */ + public void addBackwards(String str) { + lastYank = false; + + if (lastKill) { + if (slots[head] != null) { + slots[head] = str + slots[head]; + return; + } + } + + lastKill = true; + next(); + slots[head] = str; + } + + /** + * Yanks a previously killed text. Returns {@code null} if the + * ring is empty. + * + * @return the text in the current position + */ + public String yank() { + lastKill = false; + lastYank = true; + return slots[head]; + } + + /** + * Moves the pointer to the current slot back and returns the text + * in that position. If the previous command was not yank returns + * null. + * + * @return the text in the previous position + */ + public String yankPop() { + lastKill = false; + if (lastYank) { + prev(); + return slots[head]; + } + return null; + } + + /** + * Moves the pointer to the current slot forward. If the end of + * the slots is reached then points back to the beginning. + */ + private void next() { + if (head == 0 && slots[0] == null) { + return; + } + head++; + if (head == slots.length) { + head = 0; + } + } + + /** + * Moves the pointer to the current slot backwards. If the + * beginning of the slots is reached then traverses the slot + * backwards until one with not null content is found. + */ + private void prev() { + head--; + if (head == -1) { + int x = (slots.length - 1); + for (; x >= 0; x--) { + if (slots[x] != null) { + break; + } + } + head = x; + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/LineReaderImpl.java b/net-cli/src/main/java/org/jline/reader/impl/LineReaderImpl.java new file mode 100644 index 0000000..18f7fe8 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/LineReaderImpl.java @@ -0,0 +1,6429 @@ +/* + * Copyright (c) 2002-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.Flushable; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.lang.reflect.Constructor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.IntBinaryOperator; +import java.util.function.Supplier; +import java.util.function.ToIntFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import org.jline.keymap.BindingReader; +import org.jline.keymap.KeyMap; +import org.jline.reader.Binding; +import org.jline.reader.Buffer; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.CompletingParsedLine; +import org.jline.reader.CompletionMatcher; +import org.jline.reader.EOFError; +import org.jline.reader.Editor; +import org.jline.reader.EndOfFileException; +import org.jline.reader.Expander; +import org.jline.reader.Highlighter; +import org.jline.reader.History; +import org.jline.reader.LineReader; +import org.jline.reader.Macro; +import org.jline.reader.MaskingCallback; +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser; +import org.jline.reader.Parser.ParseContext; +import org.jline.reader.Reference; +import org.jline.reader.SyntaxError; +import org.jline.reader.UserInterruptException; +import org.jline.reader.Widget; +import org.jline.reader.impl.history.DefaultHistory; +import org.jline.terminal.Attributes; +import org.jline.terminal.Attributes.ControlChar; +import org.jline.terminal.Cursor; +import org.jline.terminal.MouseEvent; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.Terminal.Signal; +import org.jline.terminal.Terminal.SignalHandler; +import org.jline.terminal.impl.AbstractWindowsTerminal; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.Curses; +import org.jline.utils.Display; +import org.jline.utils.InfoCmp.Capability; +import org.jline.utils.Log; +import org.jline.utils.Status; +import org.jline.utils.StyleResolver; +import org.jline.utils.WCWidth; +import static org.jline.keymap.KeyMap.alt; +import static org.jline.keymap.KeyMap.ctrl; +import static org.jline.keymap.KeyMap.del; +import static org.jline.keymap.KeyMap.esc; +import static org.jline.keymap.KeyMap.range; +import static org.jline.keymap.KeyMap.translate; +import static org.jline.terminal.TerminalBuilder.PROP_DISABLE_ALTERNATE_CHARSET; + +/** + * A reader for terminal applications. It supports custom tab-completion, + * saveable command history, and command line editing. + * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @author Guillaume Nodet + */ +@SuppressWarnings("StatementWithEmptyBody") +public class LineReaderImpl implements LineReader, Flushable { + public static final char NULL_MASK = 0; + + /** + * @deprecated use {@link #DEFAULT_TAB_WIDTH} and {@link #getTabWidth()} + */ + @Deprecated + public static final int TAB_WIDTH = 4; + + public static final int DEFAULT_TAB_WIDTH = 4; + + public static final String DEFAULT_WORDCHARS = "*?_-.[]~=/&;!#$%^(){}<>"; + public static final String DEFAULT_REMOVE_SUFFIX_CHARS = " \t\n;&|"; + public static final String DEFAULT_COMMENT_BEGIN = "#"; + public static final String DEFAULT_SEARCH_TERMINATORS = "\033\012"; + public static final String DEFAULT_BELL_STYLE = ""; + public static final int DEFAULT_LIST_MAX = 100; + public static final int DEFAULT_MENU_LIST_MAX = Integer.MAX_VALUE; + public static final int DEFAULT_ERRORS = 2; + public static final long DEFAULT_BLINK_MATCHING_PAREN = 500L; + public static final long DEFAULT_AMBIGUOUS_BINDING = 1000L; + public static final String DEFAULT_SECONDARY_PROMPT_PATTERN = "%M> "; + public static final String DEFAULT_OTHERS_GROUP_NAME = "others"; + public static final String DEFAULT_ORIGINAL_GROUP_NAME = "original"; + public static final String DEFAULT_COMPLETION_STYLE_STARTING = "fg:cyan"; + public static final String DEFAULT_COMPLETION_STYLE_DESCRIPTION = "fg:bright-black"; + public static final String DEFAULT_COMPLETION_STYLE_GROUP = "fg:bright-magenta,bold"; + public static final String DEFAULT_COMPLETION_STYLE_SELECTION = "inverse"; + public static final String DEFAULT_COMPLETION_STYLE_BACKGROUND = "bg:default"; + public static final String DEFAULT_COMPLETION_STYLE_LIST_STARTING = DEFAULT_COMPLETION_STYLE_STARTING; + public static final String DEFAULT_COMPLETION_STYLE_LIST_DESCRIPTION = DEFAULT_COMPLETION_STYLE_DESCRIPTION; + public static final String DEFAULT_COMPLETION_STYLE_LIST_GROUP = "fg:black,bold"; + public static final String DEFAULT_COMPLETION_STYLE_LIST_SELECTION = DEFAULT_COMPLETION_STYLE_SELECTION; + public static final String DEFAULT_COMPLETION_STYLE_LIST_BACKGROUND = "bg:bright-magenta"; + public static final int DEFAULT_INDENTATION = 0; + public static final int DEFAULT_FEATURES_MAX_BUFFER_SIZE = 1000; + public static final int DEFAULT_SUGGESTIONS_MIN_BUFFER_SIZE = 1; + public static final String DEFAULT_SYSTEM_PROPERTY_PREFIX = "org.jline.reader.props."; + public static final String BRACKETED_PASTE_ON = "\033[?2004h"; + public static final String BRACKETED_PASTE_OFF = "\033[?2004l"; + public static final String BRACKETED_PASTE_BEGIN = "\033[200~"; + public static final String BRACKETED_PASTE_END = "\033[201~"; + public static final String FOCUS_IN_SEQ = "\033[I"; + public static final String FOCUS_OUT_SEQ = "\033[O"; + public static final int DEFAULT_MAX_REPEAT_COUNT = 9999; + private static final int MIN_ROWS = 3; + private static final String DESC_PREFIX = "("; + private static final String DESC_SUFFIX = ")"; + private static final int MARGIN_BETWEEN_DISPLAY_AND_DESC = 1; + + // + // Constructor variables + // + private static final int MARGIN_BETWEEN_COLUMNS = 3; + private static final int MENU_LIST_WIDTH = 25; + /** + * The terminal to use + */ + protected final Terminal terminal; + /** + * The application name + */ + protected final String appName; + /** + * The terminal keys mapping + */ + protected final Map> keyMaps; + // + // Configuration + // + protected final Map variables; + protected final Map options = new HashMap<>(); + protected final Buffer buf = new BufferImpl(); + protected final Size size = new Size(); + // Reading buffers + protected final BindingReader bindingReader; + + // + // State variables + // + /** + * State lock + */ + protected final ReentrantLock lock = new ReentrantLock(); + protected final AtomicBoolean startedReading = new AtomicBoolean(); + protected History history = new DefaultHistory(); + protected Completer completer = null; + protected Highlighter highlighter = new DefaultHighlighter(); + protected Parser parser = new DefaultParser(); + protected Expander expander = new DefaultExpander(); + protected CompletionMatcher completionMatcher = new CompletionMatcherImpl(); + protected String tailTip = ""; + protected SuggestionType autosuggestion = SuggestionType.NONE; + protected AttributedString prompt = AttributedString.EMPTY; + protected AttributedString rightPrompt = AttributedString.EMPTY; + protected MaskingCallback maskingCallback; + protected Map modifiedHistory = new HashMap<>(); + protected Buffer historyBuffer = null; + protected CharSequence searchBuffer; + protected StringBuffer searchTerm = null; + protected boolean searchFailing; + protected boolean searchBackward; + protected int searchIndex = -1; + protected boolean doAutosuggestion; + /** + * VI character find + */ + protected int findChar; + protected int findDir; + protected int findTailAdd; + /** + * Region state + */ + protected int regionMark; + protected RegionType regionActive; + /** + * The vi yank buffer + */ + protected String yankBuffer = ""; + + protected ViMoveMode viMoveMode = ViMoveMode.NORMAL; + + protected KillRing killRing = new KillRing(); + + protected UndoTree undo; + protected boolean isUndo; + /* + * Current internal state of the line reader + */ + protected State state = State.DONE; + protected boolean reading; + protected Supplier post; + protected Map builtinWidgets; + protected Map widgets; + protected int count; + protected int mult; + protected int universal = 4; + protected int repeatCount; + protected boolean isArgDigit; + protected ParsedLine parsedLine; + protected boolean skipRedisplay; + protected Display display; + protected boolean overTyping = false; + protected String keyMap; + protected int smallTerminalOffset = 0; + /* + * accept-and-infer-next-history, accept-and-hold & accept-line-and-down-history + */ + protected boolean nextCommandFromHistory = false; + protected int nextHistoryId = -1; + /* + * execute commands from commandsBuffer + */ + protected List commandsBuffer = new ArrayList<>(); + protected int candidateStartPosition = 0; + protected String alternateIn; + protected String alternateOut; + /** + * VI history string search + */ + private int searchDir; + private String searchString; + private boolean forceChar; + private boolean forceLine; + + public LineReaderImpl(Terminal terminal) throws IOException { + this(terminal, terminal.getName(), null); + } + + public LineReaderImpl(Terminal terminal, String appName) throws IOException { + this(terminal, appName, null); + } + + @SuppressWarnings("this-escape") + public LineReaderImpl(Terminal terminal, String appName, Map variables) { + Objects.requireNonNull(terminal, "terminal can not be null"); + this.terminal = terminal; + if (appName == null) { + appName = "JLine"; + } + this.appName = appName; + if (variables != null) { + this.variables = variables; + } else { + this.variables = new HashMap<>(); + } + String prefix = getString(SYSTEM_PROPERTY_PREFIX, DEFAULT_SYSTEM_PROPERTY_PREFIX); + if (prefix != null) { + Properties sysProps = System.getProperties(); + for (String s : sysProps.stringPropertyNames()) { + if (s.startsWith(prefix)) { + String key = s.substring(prefix.length()); + InputRC.setVar(this, key, sysProps.getProperty(s)); + } + } + } + + this.keyMaps = defaultKeyMaps(); + if (!Boolean.getBoolean(PROP_DISABLE_ALTERNATE_CHARSET)) { + this.alternateIn = Curses.tputs(terminal.getStringCapability(Capability.enter_alt_charset_mode)); + this.alternateOut = Curses.tputs(terminal.getStringCapability(Capability.exit_alt_charset_mode)); + } + + undo = new UndoTree<>(this::setBuffer); + builtinWidgets = builtinWidgets(); + widgets = new HashMap<>(builtinWidgets); + bindingReader = new BindingReader(terminal.reader()); + + String inputRc = getString(INPUT_RC_FILE_NAME, null); + if (inputRc != null) { + Path inputRcPath = Paths.get(inputRc); + if (Files.exists(inputRcPath)) { + try (InputStream is = Files.newInputStream(inputRcPath)) { + InputRC.configure(this, is); + } catch (Exception e) { + Log.debug("Error reading inputRc config file: ", inputRc, e); + } + } + } + + doDisplay(); + } + + protected static CompletingParsedLine wrap(ParsedLine line) { + if (line instanceof CompletingParsedLine) { + return (CompletingParsedLine) line; + } else { + return new CompletingParsedLine() { + public String word() { + return line.word(); + } + + public int wordCursor() { + return line.wordCursor(); + } + + public int wordIndex() { + return line.wordIndex(); + } + + public List words() { + return line.words(); + } + + public String line() { + return line.line(); + } + + public int cursor() { + return line.cursor(); + } + + public CharSequence escape(CharSequence candidate, boolean complete) { + return candidate; + } + + public int rawWordCursor() { + return wordCursor(); + } + + public int rawWordLength() { + return word().length(); + } + }; + } + } + + public Terminal getTerminal() { + return terminal; + } + + public String getAppName() { + return appName; + } + + public Map> getKeyMaps() { + return keyMaps; + } + + public KeyMap getKeys() { + return keyMaps.get(keyMap); + } + + @Override + public Map getWidgets() { + return widgets; + } + + @Override + public Map getBuiltinWidgets() { + return Collections.unmodifiableMap(builtinWidgets); + } + + @Override + public Buffer getBuffer() { + return buf; + } + + protected void setBuffer(Buffer buffer) { + buf.copyFrom(buffer); + } + + /** + * Set the current buffer's content to the specified {@link String}. The + * visual terminal will be modified to show the current buffer. + * + * @param buffer the new contents of the buffer. + */ + protected void setBuffer(final String buffer) { + buf.clear(); + buf.write(buffer); + } + + @Override + public SuggestionType getAutosuggestion() { + return autosuggestion; + } + + @Override + public void setAutosuggestion(SuggestionType type) { + this.autosuggestion = type; + } + + @Override + public String getTailTip() { + return tailTip; + } + + // + // History + // + + @Override + public void setTailTip(String tailTip) { + this.tailTip = tailTip; + } + + @Override + public void runMacro(String macro) { + bindingReader.runMacro(macro); + } + + // + // Highlighter + // + + @Override + public MouseEvent readMouseEvent() { + return terminal.readMouseEvent(bindingReader::readCharacter); + } + + /** + * Returns the completer. + * + * @return the completer + */ + public Completer getCompleter() { + return completer; + } + + /** + * Set the completer. + * + * @param completer the completer to use + */ + public void setCompleter(Completer completer) { + this.completer = completer; + } + + public History getHistory() { + return history; + } + + public void setHistory(final History history) { + Objects.requireNonNull(history); + this.history = history; + } + + public Highlighter getHighlighter() { + return highlighter; + } + + public void setHighlighter(Highlighter highlighter) { + this.highlighter = highlighter; + } + + // + // Line Reading + // + + public Parser getParser() { + return parser; + } + + public void setParser(Parser parser) { + this.parser = parser; + } + + @Override + public Expander getExpander() { + return expander; + } + + public void setExpander(Expander expander) { + this.expander = expander; + } + + public void setCompletionMatcher(CompletionMatcher completionMatcher) { + this.completionMatcher = completionMatcher; + } + + /** + * Read the next line and return the contents of the buffer. + * + * @return A line that is read from the terminal, can never be null. + */ + public String readLine() throws UserInterruptException, EndOfFileException { + return readLine(null, null, (MaskingCallback) null, null); + } + + /** + * Read the next line with the specified character mask. If null, then + * characters will be echoed. If 0, then no characters will be echoed. + * + * @param mask The mask character, null or 0. + * @return A line that is read from the terminal, can never be null. + */ + public String readLine(Character mask) throws UserInterruptException, EndOfFileException { + return readLine(null, null, mask, null); + } + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + * + * @param prompt The prompt to issue to the terminal, may be null. + * @return A line that is read from the terminal, can never be null. + */ + public String readLine(String prompt) throws UserInterruptException, EndOfFileException { + return readLine(prompt, null, (MaskingCallback) null, null); + } + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + * + * @param prompt The prompt to issue to the terminal, may be null. + * @param mask The mask character, null or 0. + * @return A line that is read from the terminal, can never be null. + */ + public String readLine(String prompt, Character mask) throws UserInterruptException, EndOfFileException { + return readLine(prompt, null, mask, null); + } + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + * + * @param prompt The prompt to issue to the terminal, may be null. + * @param mask The mask character, null or 0. + * @param buffer A string that will be set for editing. + * @return A line that is read from the terminal, can never be null. + */ + public String readLine(String prompt, Character mask, String buffer) + throws UserInterruptException, EndOfFileException { + return readLine(prompt, null, mask, buffer); + } + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + * + * @param prompt The prompt to issue to the terminal, may be null. + * @param rightPrompt The prompt to issue to the right of the terminal, may be null. + * @param mask The mask character, null or 0. + * @param buffer A string that will be set for editing. + * @return A line that is read from the terminal, can never be null. + */ + public String readLine(String prompt, String rightPrompt, Character mask, String buffer) + throws UserInterruptException, EndOfFileException { + return readLine(prompt, rightPrompt, mask != null ? new SimpleMaskingCallback(mask) : null, buffer); + } + + /** + * Read a line from the in {@link InputStream}, and return the line + * (without any trailing newlines). + * + * @param prompt The prompt to issue to the terminal, may be null. + * @param rightPrompt The prompt to issue to the right of the terminal, may be null. + * @param maskingCallback The callback used to mask parts of the edited line. + * @param buffer A string that will be set for editing. + * @return A line that is read from the terminal, can never be null. + */ + public String readLine(String prompt, String rightPrompt, MaskingCallback maskingCallback, String buffer) + throws UserInterruptException, EndOfFileException { + // prompt may be null + // maskingCallback may be null + // buffer may be null + if (!commandsBuffer.isEmpty()) { + String cmd = commandsBuffer.remove(0); + boolean done = false; + do { + try { + parser.parse(cmd, cmd.length() + 1, ParseContext.ACCEPT_LINE); + done = true; + } catch (EOFError e) { + if (commandsBuffer.isEmpty()) { + throw new IllegalArgumentException("Incompleted command: \n" + cmd); + } + cmd += "\n"; + cmd += commandsBuffer.remove(0); + } catch (SyntaxError e) { + done = true; + } catch (Exception e) { + commandsBuffer.clear(); + throw new IllegalArgumentException(e.getMessage()); + } + } while (!done); + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.styled(AttributedStyle::bold, cmd); + sb.toAttributedString().println(terminal); + terminal.flush(); + return finish(cmd); + } + + if (!startedReading.compareAndSet(false, true)) { + throw new IllegalStateException(); + } + + Thread readLineThread = Thread.currentThread(); + SignalHandler previousIntrHandler = null; + SignalHandler previousWinchHandler = null; + SignalHandler previousContHandler = null; + Attributes originalAttributes = null; + boolean dumb = isTerminalDumb(); + try { + + this.maskingCallback = maskingCallback; + + /* + * This is the accumulator for VI-mode repeat count. That is, while in + * move mode, if you type 30x it will delete 30 characters. This is + * where the "30" is accumulated until the command is struck. + */ + repeatCount = 0; + mult = 1; + regionActive = RegionType.NONE; + regionMark = -1; + + smallTerminalOffset = 0; + + state = State.NORMAL; + + modifiedHistory.clear(); + + setPrompt(prompt); + setRightPrompt(rightPrompt); + buf.clear(); + if (buffer != null) { + buf.write(buffer); + } + if (nextCommandFromHistory && nextHistoryId > 0) { + if (history.size() > nextHistoryId) { + history.moveTo(nextHistoryId); + } else { + history.moveTo(history.last()); + } + buf.write(history.current()); + } else { + nextHistoryId = -1; + } + nextCommandFromHistory = false; + undo.clear(); + parsedLine = null; + keyMap = MAIN; + + if (history != null) { + history.attach(this); + } + + try { + lock.lock(); + + this.reading = true; + + previousIntrHandler = terminal.handle(Signal.INT, signal -> readLineThread.interrupt()); + previousWinchHandler = terminal.handle(Signal.WINCH, this::handleSignal); + previousContHandler = terminal.handle(Signal.CONT, this::handleSignal); + originalAttributes = terminal.enterRawMode(); + + doDisplay(); + + // Move into application mode + if (!dumb) { + terminal.puts(Capability.keypad_xmit); + if (isSet(Option.AUTO_FRESH_LINE)) callWidget(FRESH_LINE); + if (isSet(Option.MOUSE)) terminal.trackMouse(Terminal.MouseTracking.Normal); + if (isSet(Option.BRACKETED_PASTE)) terminal.writer().write(BRACKETED_PASTE_ON); + } + + callWidget(CALLBACK_INIT); + + if (!isSet(Option.DISABLE_UNDO)) undo.newState(buf.copy()); + + // Draw initial prompt + redrawLine(); + redisplay(); + } finally { + lock.unlock(); + } + + while (true) { + + KeyMap local = null; + if (isInViCmdMode() && regionActive != RegionType.NONE) { + local = keyMaps.get(VISUAL); + } + Binding o = readBinding(getKeys(), local); + if (o == null) { + throw new EndOfFileException().partialLine(buf.length() > 0 ? buf.toString() : null); + } + Log.trace("Binding: ", o); + if (buf.length() == 0 + && getLastBinding().charAt(0) == originalAttributes.getControlChar(ControlChar.VEOF)) { + throw new EndOfFileException(); + } + + // If this is still false after handling the binding, then + // we reset our repeatCount to 0. + isArgDigit = false; + // Every command that can be repeated a specified number + // of times, needs to know how many times to repeat, so + // we figure that out here. + count = ((repeatCount == 0) ? 1 : repeatCount) * mult; + // Reset undo/redo flag + isUndo = false; + // Reset region after a paste + if (regionActive == RegionType.PASTE) { + regionActive = RegionType.NONE; + } + + try { + lock.lock(); + // Get executable widget + Buffer copy = buf.length() <= getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE) + ? buf.copy() + : null; + Widget w = getWidget(o); + if (!w.apply()) { + beep(); + } + if (!isSet(Option.DISABLE_UNDO) + && !isUndo + && copy != null + && buf.length() <= getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE) + && !copy.toString().equals(buf.toString())) { + undo.newState(buf.copy()); + } + + switch (state) { + case DONE: + return finishBuffer(); + case IGNORE: + return ""; + case EOF: + throw new EndOfFileException(); + case INTERRUPT: + throw new UserInterruptException(buf.toString()); + } + + if (!isArgDigit) { + /* + * If the operation performed wasn't a vi argument + * digit, then clear out the current repeatCount; + */ + repeatCount = 0; + mult = 1; + } + + if (!dumb) { + redisplay(); + } + } finally { + lock.unlock(); + } + } + } catch (IOError e) { + if (e.getCause() instanceof InterruptedIOException) { + throw new UserInterruptException(buf.toString()); + } else { + throw e; + } + } finally { + AtomicBoolean interrupted = new AtomicBoolean(Thread.interrupted()); + try { + lock.lock(); + + this.reading = false; + + Terminal.SignalHandler tmpHandler = terminal.handle(Signal.INT, s -> interrupted.set(true)); + if (previousIntrHandler == null) { + previousIntrHandler = tmpHandler; + } + + cleanup(); + if (originalAttributes != null) { + terminal.setAttributes(originalAttributes); + } + if (previousIntrHandler != null) { + terminal.handle(Signal.INT, previousIntrHandler); + } + if (previousWinchHandler != null) { + terminal.handle(Signal.WINCH, previousWinchHandler); + } + if (previousContHandler != null) { + terminal.handle(Signal.CONT, previousContHandler); + } + } finally { + lock.unlock(); + startedReading.set(false); + if (interrupted.get()) { + Thread.currentThread().interrupt(); + } + } + } + } + + private boolean isTerminalDumb() { + return Terminal.TYPE_DUMB.equals(terminal.getType()) || Terminal.TYPE_DUMB_COLOR.equals(terminal.getType()); + } + + private void doDisplay() { + // Cache terminal size for the duration of the call to readLine() + // It will eventually be updated with WINCH signals + size.copy(terminal.getBufferSize()); + + display = new Display(terminal, false); + display.resize(size.getRows(), size.getColumns()); + if (isSet(Option.DELAY_LINE_WRAP)) display.setDelayLineWrap(true); + } + + @Override + public void printAbove(String str) { + try { + lock.lock(); + + boolean reading = this.reading; + if (reading) { + display.update(Collections.emptyList(), 0); + } + if (str.endsWith("\n") || str.endsWith("\n\033[m") || str.endsWith("\n\033[0m")) { + terminal.writer().print(str); + } else { + terminal.writer().println(str); + } + if (reading) { + redisplay(false); + } + terminal.flush(); + } finally { + lock.unlock(); + } + } + + @Override + public void printAbove(AttributedString str) { + printAbove(str.toAnsi(terminal)); + } + + @Override + public boolean isReading() { + try { + lock.lock(); + return reading; + } finally { + lock.unlock(); + } + } + + /* Make sure we position the cursor on column 0 */ + protected boolean freshLine() { + boolean wrapAtEol = terminal.getBooleanCapability(Capability.auto_right_margin); + boolean delayedWrapAtEol = wrapAtEol && terminal.getBooleanCapability(Capability.eat_newline_glitch); + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.BLACK + AttributedStyle.BRIGHT)); + sb.append("~"); + sb.style(AttributedStyle.DEFAULT); + if (!wrapAtEol || delayedWrapAtEol) { + for (int i = 0; i < size.getColumns() - 1; i++) { + sb.append(" "); + } + sb.append(KeyMap.key(terminal, Capability.carriage_return)); + sb.append(" "); + sb.append(KeyMap.key(terminal, Capability.carriage_return)); + } else { + // Given the terminal will wrap automatically, + // we need to print one less than needed. + // This means that the last character will not + // be overwritten, and that's why we're using + // a clr_eol first if possible. + String el = terminal.getStringCapability(Capability.clr_eol); + if (el != null) { + Curses.tputs(sb, el); + } + for (int i = 0; i < size.getColumns() - 2; i++) { + sb.append(" "); + } + sb.append(KeyMap.key(terminal, Capability.carriage_return)); + sb.append(" "); + sb.append(KeyMap.key(terminal, Capability.carriage_return)); + } + sb.print(terminal); + return true; + } + + @Override + public void callWidget(String name) { + try { + lock.lock(); + if (!reading) { + throw new IllegalStateException("Widgets can only be called during a `readLine` call"); + } + try { + Widget w; + if (name.startsWith(".")) { + w = builtinWidgets.get(name.substring(1)); + } else { + w = widgets.get(name); + } + if (w != null) { + w.apply(); + } + } catch (Throwable t) { + Log.debug("Error executing widget '", name, "'", t); + } + } finally { + lock.unlock(); + } + } + + /** + * Clear the line and redraw it. + * + * @return true + */ + public boolean redrawLine() { + display.reset(); + return true; + } + + /** + * Write out the specified string to the buffer and the output stream. + * + * @param str the char sequence to write in the buffer + */ + public void putString(final CharSequence str) { + buf.write(str, overTyping); + } + + /** + * Flush the terminal output stream. This is important for printout out single + * characters (like a buf.backspace or keyboard) that we want the terminal to + * handle immediately. + */ + public void flush() { + terminal.flush(); + } + + public boolean isKeyMap(String name) { + return keyMap.equals(name); + } + + /** + * Read a character from the terminal. + * + * @return the character, or -1 if an EOF is received. + */ + public int readCharacter() { + if (lock.isHeldByCurrentThread()) { + try { + lock.unlock(); + return bindingReader.readCharacter(); + } finally { + lock.lock(); + } + } else { + return bindingReader.readCharacter(); + } + } + + public int peekCharacter(long timeout) { + return bindingReader.peekCharacter(timeout); + } + + protected T doReadBinding(KeyMap keys, KeyMap local) { + if (lock.isHeldByCurrentThread()) { + try { + lock.unlock(); + return bindingReader.readBinding(keys, local); + } finally { + lock.lock(); + } + } else { + return bindingReader.readBinding(keys, local); + } + } + + protected String doReadStringUntil(String sequence) { + if (lock.isHeldByCurrentThread()) { + try { + lock.unlock(); + return bindingReader.readStringUntil(sequence); + } finally { + lock.lock(); + } + } else { + return bindingReader.readStringUntil(sequence); + } + } + + /** + * Read from the input stream and decode an operation from the key map. + *

+ * The input stream will be read character by character until a matching + * binding can be found. Characters that can't possibly be matched to + * any binding will be discarded. + * + * @param keys the KeyMap to use for decoding the input stream + * @return the decoded binding or null if the end of + * stream has been reached + */ + public Binding readBinding(KeyMap keys) { + return readBinding(keys, null); + } + + public Binding readBinding(KeyMap keys, KeyMap local) { + Binding o = doReadBinding(keys, local); + /* + * The kill ring keeps record of whether or not the + * previous command was a yank or a kill. We reset + * that state here if needed. + */ + if (o instanceof Reference) { + String ref = ((Reference) o).name(); + if (!YANK_POP.equals(ref) && !YANK.equals(ref)) { + killRing.resetLastYank(); + } + if (!KILL_LINE.equals(ref) + && !KILL_WHOLE_LINE.equals(ref) + && !BACKWARD_KILL_WORD.equals(ref) + && !KILL_WORD.equals(ref)) { + killRing.resetLastKill(); + } + } + return o; + } + + // + // Key Bindings + // + + @Override + public ParsedLine getParsedLine() { + return parsedLine; + } + + @Override + public String getLastBinding() { + return bindingReader.getLastBinding(); + } + + @Override + public String getSearchTerm() { + return searchTerm != null ? searchTerm.toString() : null; + } + + @Override + public RegionType getRegionActive() { + return regionActive; + } + + @Override + public int getRegionMark() { + return regionMark; + } + + /** + * Sets the current keymap by name. Supported keymaps are "emacs", + * "viins", "vicmd". + * + * @param name The name of the keymap to switch to + * @return true if the keymap was set, or false if the keymap is + * not recognized. + */ + public boolean setKeyMap(String name) { + KeyMap map = keyMaps.get(name); + if (map == null) { + return false; + } + this.keyMap = name; + if (reading) { + callWidget(CALLBACK_KEYMAP); + } + return true; + } + + /** + * Returns the name of the current key mapping. + * + * @return the name of the key mapping. This will be the canonical name + * of the current mode of the key map and may not reflect the name that + * was used with {@link #setKeyMap(String)}. + */ + public String getKeyMap() { + return keyMap; + } + + @Override + public LineReader variable(String name, Object value) { + variables.put(name, value); + return this; + } + + @Override + public Map getVariables() { + return variables; + } + + @Override + public Object getVariable(String name) { + return variables.get(name); + } + + @Override + public void setVariable(String name, Object value) { + variables.put(name, value); + } + + @Override + public LineReader option(Option option, boolean value) { + options.put(option, value); + return this; + } + + @Override + public boolean isSet(Option option) { + return option.isSet(options); + } + + // + // Widget implementation + // + + @Override + public void setOpt(Option option) { + options.put(option, Boolean.TRUE); + } + + @Override + public void unsetOpt(Option option) { + options.put(option, Boolean.FALSE); + } + + @Override + public void addCommandsInBuffer(Collection commands) { + commandsBuffer.addAll(commands); + } + + @Override + public void editAndAddInBuffer(File file) throws Exception { + if (isSet(Option.BRACKETED_PASTE)) { + terminal.writer().write(BRACKETED_PASTE_OFF); + } + Constructor ctor = Class.forName("org.jline.builtins.Nano").getConstructor(Terminal.class, File.class); + Editor editor = (Editor) ctor.newInstance(terminal, new File(file.getParent())); + editor.setRestricted(true); + editor.open(Collections.singletonList(file.getName())); + editor.run(); + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + String line; + commandsBuffer.clear(); + while ((line = br.readLine()) != null) { + commandsBuffer.add(line); + } + } + } + + // + // Helper methods + // + + protected int getTabWidth() { + return getInt(LineReader.TAB_WIDTH, DEFAULT_TAB_WIDTH); + } + + /** + * Clear the buffer and add its contents to the history. + * + * @return the former contents of the buffer. + */ + protected String finishBuffer() { + return finish(buf.toString()); + } + + protected String finish(String str) { + String historyLine = str; + + if (!isSet(Option.DISABLE_EVENT_EXPANSION)) { + StringBuilder sb = new StringBuilder(); + boolean escaped = false; + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (escaped) { + escaped = false; + if (ch != '\n') { + sb.append(ch); + } + } else if (parser.isEscapeChar(ch)) { + escaped = true; + } else { + sb.append(ch); + } + } + str = sb.toString(); + } + + if (maskingCallback != null) { + historyLine = maskingCallback.history(historyLine); + } + + // we only add it to the history if the buffer is not empty + if (historyLine != null && historyLine.length() > 0) { + history.add(Instant.now(), historyLine); + } + return str; + } + + protected synchronized void handleSignal(Signal signal) { + doAutosuggestion = false; + if (signal == Signal.WINCH) { + size.copy(terminal.getBufferSize()); + display.resize(size.getRows(), size.getColumns()); + Status status = Status.getStatus(terminal, false); + if (status != null) { + status.resize(size); + status.reset(); + } + terminal.puts(Capability.carriage_return); + terminal.puts(Capability.clr_eos); + redrawLine(); + redisplay(); + } else if (signal == Signal.CONT) { + terminal.enterRawMode(); + size.copy(terminal.getBufferSize()); + display.resize(size.getRows(), size.getColumns()); + terminal.puts(Capability.keypad_xmit); + redrawLine(); + redisplay(); + } + } + + @SuppressWarnings("unchecked") + protected Widget getWidget(Object binding) { + Widget w; + if (binding instanceof Widget) { + w = (Widget) binding; + } else if (binding instanceof Macro) { + String macro = ((Macro) binding).getSequence(); + w = () -> { + bindingReader.runMacro(macro); + return true; + }; + } else if (binding instanceof Reference) { + String name = ((Reference) binding).name(); + w = widgets.get(name); + if (w == null) { + w = () -> { + post = () -> new AttributedString("No such widget `" + name + "'"); + return false; + }; + } + } else { + w = () -> { + post = () -> new AttributedString("Unsupported widget"); + return false; + }; + } + return w; + } + + public void setPrompt(final String prompt) { + this.prompt = (prompt == null ? AttributedString.EMPTY : expandPromptPattern(prompt, 0, "", 0)); + } + + public void setRightPrompt(final String rightPrompt) { + this.rightPrompt = (rightPrompt == null ? AttributedString.EMPTY : expandPromptPattern(rightPrompt, 0, "", 0)); + } + + /** + * This method is calling while doing a delete-to ("d"), change-to ("c"), + * or yank-to ("y") and it filters out only those movement operations + * that are allowable during those operations. Any operation that isn't + * allow drops you back into movement mode. + * + * @param op The incoming operation to remap + * @return The remaped operation + */ + protected String viDeleteChangeYankToRemap(String op) { + switch (op) { + case SEND_BREAK: + case BACKWARD_CHAR: + case FORWARD_CHAR: + case END_OF_LINE: + case VI_MATCH_BRACKET: + case VI_DIGIT_OR_BEGINNING_OF_LINE: + case NEG_ARGUMENT: + case DIGIT_ARGUMENT: + case VI_BACKWARD_CHAR: + case VI_BACKWARD_WORD: + case VI_FORWARD_CHAR: + case VI_FORWARD_WORD: + case VI_FORWARD_WORD_END: + case VI_FIRST_NON_BLANK: + case VI_GOTO_COLUMN: + case VI_DELETE: + case VI_YANK: + case VI_CHANGE: + case VI_FIND_NEXT_CHAR: + case VI_FIND_NEXT_CHAR_SKIP: + case VI_FIND_PREV_CHAR: + case VI_FIND_PREV_CHAR_SKIP: + case VI_REPEAT_FIND: + case VI_REV_REPEAT_FIND: + return op; + + default: + return VI_CMD_MODE; + } + } + + protected int switchCase(int ch) { + if (Character.isUpperCase(ch)) { + return Character.toLowerCase(ch); + } else if (Character.isLowerCase(ch)) { + return Character.toUpperCase(ch); + } else { + return ch; + } + } + + // + // Movement + // + + /** + * @return true if line reader is in the middle of doing a change-to + * delete-to or yank-to. + */ + protected boolean isInViMoveOperation() { + return viMoveMode != ViMoveMode.NORMAL; + } + + protected boolean isInViChangeOperation() { + return viMoveMode == ViMoveMode.CHANGE; + } + + // + // Word movement + // + + protected boolean isInViCmdMode() { + return VICMD.equals(keyMap); + } + + protected boolean viForwardChar() { + if (count < 0) { + return callNeg(this::viBackwardChar); + } + int lim = findeol(); + if (isInViCmdMode() && !isInViMoveOperation()) { + lim--; + } + if (buf.cursor() >= lim) { + return false; + } + while (count-- > 0 && buf.cursor() < lim) { + buf.move(1); + } + return true; + } + + protected boolean viBackwardChar() { + if (count < 0) { + return callNeg(this::viForwardChar); + } + int lim = findbol(); + if (buf.cursor() == lim) { + return false; + } + while (count-- > 0 && buf.cursor() > 0) { + buf.move(-1); + if (buf.currChar() == '\n') { + buf.move(1); + break; + } + } + return true; + } + + protected boolean forwardWord() { + if (count < 0) { + return callNeg(this::backwardWord); + } + while (count-- > 0) { + while (buf.cursor() < buf.length() && isWord(buf.currChar())) { + buf.move(1); + } + if (isInViChangeOperation() && count == 0) { + break; + } + while (buf.cursor() < buf.length() && !isWord(buf.currChar())) { + buf.move(1); + } + } + return true; + } + + protected boolean viForwardWord() { + if (count < 0) { + return callNeg(this::viBackwardWord); + } + while (count-- > 0) { + if (isViAlphaNum(buf.currChar())) { + while (buf.cursor() < buf.length() && isViAlphaNum(buf.currChar())) { + buf.move(1); + } + } else { + while (buf.cursor() < buf.length() && !isViAlphaNum(buf.currChar()) && !isWhitespace(buf.currChar())) { + buf.move(1); + } + } + if (isInViChangeOperation() && count == 0) { + return true; + } + int nl = buf.currChar() == '\n' ? 1 : 0; + while (buf.cursor() < buf.length() && nl < 2 && isWhitespace(buf.currChar())) { + buf.move(1); + nl += buf.currChar() == '\n' ? 1 : 0; + } + } + return true; + } + + protected boolean viForwardBlankWord() { + if (count < 0) { + return callNeg(this::viBackwardBlankWord); + } + while (count-- > 0) { + while (buf.cursor() < buf.length() && !isWhitespace(buf.currChar())) { + buf.move(1); + } + if (isInViChangeOperation() && count == 0) { + return true; + } + int nl = buf.currChar() == '\n' ? 1 : 0; + while (buf.cursor() < buf.length() && nl < 2 && isWhitespace(buf.currChar())) { + buf.move(1); + nl += buf.currChar() == '\n' ? 1 : 0; + } + } + return true; + } + + protected boolean emacsForwardWord() { + return forwardWord(); + } + + protected boolean viForwardBlankWordEnd() { + if (count < 0) { + return false; + } + while (count-- > 0) { + while (buf.cursor() < buf.length()) { + buf.move(1); + if (!isWhitespace(buf.currChar())) { + break; + } + } + while (buf.cursor() < buf.length()) { + buf.move(1); + if (isWhitespace(buf.currChar())) { + break; + } + } + } + return true; + } + + protected boolean viForwardWordEnd() { + if (count < 0) { + return callNeg(this::backwardWord); + } + while (count-- > 0) { + while (buf.cursor() < buf.length()) { + if (!isWhitespace(buf.nextChar())) { + break; + } + buf.move(1); + } + if (buf.cursor() < buf.length()) { + if (isViAlphaNum(buf.nextChar())) { + buf.move(1); + while (buf.cursor() < buf.length() && isViAlphaNum(buf.nextChar())) { + buf.move(1); + } + } else { + buf.move(1); + while (buf.cursor() < buf.length() + && !isViAlphaNum(buf.nextChar()) + && !isWhitespace(buf.nextChar())) { + buf.move(1); + } + } + } + } + if (buf.cursor() < buf.length() && isInViMoveOperation()) { + buf.move(1); + } + return true; + } + + protected boolean backwardWord() { + if (count < 0) { + return callNeg(this::forwardWord); + } + while (count-- > 0) { + while (buf.cursor() > 0 && !isWord(buf.atChar(buf.cursor() - 1))) { + buf.move(-1); + } + while (buf.cursor() > 0 && isWord(buf.atChar(buf.cursor() - 1))) { + buf.move(-1); + } + } + return true; + } + + protected boolean viBackwardWord() { + if (count < 0) { + return callNeg(this::viForwardWord); + } + while (count-- > 0) { + int nl = 0; + while (buf.cursor() > 0) { + buf.move(-1); + if (!isWhitespace(buf.currChar())) { + break; + } + nl += buf.currChar() == '\n' ? 1 : 0; + if (nl == 2) { + buf.move(1); + break; + } + } + if (buf.cursor() > 0) { + if (isViAlphaNum(buf.currChar())) { + while (buf.cursor() > 0) { + if (!isViAlphaNum(buf.prevChar())) { + break; + } + buf.move(-1); + } + } else { + while (buf.cursor() > 0) { + if (isViAlphaNum(buf.prevChar()) || isWhitespace(buf.prevChar())) { + break; + } + buf.move(-1); + } + } + } + } + return true; + } + + protected boolean viBackwardBlankWord() { + if (count < 0) { + return callNeg(this::viForwardBlankWord); + } + while (count-- > 0) { + while (buf.cursor() > 0) { + buf.move(-1); + if (!isWhitespace(buf.currChar())) { + break; + } + } + while (buf.cursor() > 0) { + buf.move(-1); + if (isWhitespace(buf.currChar())) { + break; + } + } + } + return true; + } + + protected boolean viBackwardWordEnd() { + if (count < 0) { + return callNeg(this::viForwardWordEnd); + } + while (count-- > 0 && buf.cursor() > 1) { + int start; + if (isViAlphaNum(buf.currChar())) { + start = 1; + } else if (!isWhitespace(buf.currChar())) { + start = 2; + } else { + start = 0; + } + while (buf.cursor() > 0) { + boolean same = (start != 1) && isWhitespace(buf.currChar()); + if (start != 0) { + same |= isViAlphaNum(buf.currChar()); + } + if (same == (start == 2)) { + break; + } + buf.move(-1); + } + while (buf.cursor() > 0 && isWhitespace(buf.currChar())) { + buf.move(-1); + } + } + return true; + } + + protected boolean viBackwardBlankWordEnd() { + if (count < 0) { + return callNeg(this::viForwardBlankWordEnd); + } + while (count-- > 0) { + while (buf.cursor() > 0 && !isWhitespace(buf.currChar())) { + buf.move(-1); + } + while (buf.cursor() > 0 && isWhitespace(buf.currChar())) { + buf.move(-1); + } + } + return true; + } + + protected boolean emacsBackwardWord() { + return backwardWord(); + } + + protected boolean backwardDeleteWord() { + if (count < 0) { + return callNeg(this::deleteWord); + } + int cursor = buf.cursor(); + while (count-- > 0) { + while (cursor > 0 && !isWord(buf.atChar(cursor - 1))) { + cursor--; + } + while (cursor > 0 && isWord(buf.atChar(cursor - 1))) { + cursor--; + } + } + buf.backspace(buf.cursor() - cursor); + return true; + } + + protected boolean viBackwardKillWord() { + if (count < 0) { + return false; + } + int lim = findbol(); + int x = buf.cursor(); + while (count-- > 0) { + while (x > lim && isWhitespace(buf.atChar(x - 1))) { + x--; + } + if (x > lim) { + if (isViAlphaNum(buf.atChar(x - 1))) { + while (x > lim && isViAlphaNum(buf.atChar(x - 1))) { + x--; + } + } else { + while (x > lim && !isViAlphaNum(buf.atChar(x - 1)) && !isWhitespace(buf.atChar(x - 1))) { + x--; + } + } + } + } + killRing.addBackwards(buf.substring(x, buf.cursor())); + buf.backspace(buf.cursor() - x); + return true; + } + + protected boolean backwardKillWord() { + if (count < 0) { + return callNeg(this::killWord); + } + int x = buf.cursor(); + while (count-- > 0) { + while (x > 0 && !isWord(buf.atChar(x - 1))) { + x--; + } + while (x > 0 && isWord(buf.atChar(x - 1))) { + x--; + } + } + killRing.addBackwards(buf.substring(x, buf.cursor())); + buf.backspace(buf.cursor() - x); + return true; + } + + protected boolean copyPrevWord() { + if (count <= 0) { + return false; + } + int t1, t0 = buf.cursor(); + while (true) { + t1 = t0; + while (t0 > 0 && !isWord(buf.atChar(t0 - 1))) { + t0--; + } + while (t0 > 0 && isWord(buf.atChar(t0 - 1))) { + t0--; + } + if (--count == 0) { + break; + } + if (t0 == 0) { + return false; + } + } + buf.write(buf.substring(t0, t1)); + return true; + } + + protected boolean upCaseWord() { + int count = Math.abs(this.count); + int cursor = buf.cursor(); + while (count-- > 0) { + while (buf.cursor() < buf.length() && !isWord(buf.currChar())) { + buf.move(1); + } + while (buf.cursor() < buf.length() && isWord(buf.currChar())) { + buf.currChar(Character.toUpperCase(buf.currChar())); + buf.move(1); + } + } + if (this.count < 0) { + buf.cursor(cursor); + } + return true; + } + + protected boolean downCaseWord() { + int count = Math.abs(this.count); + int cursor = buf.cursor(); + while (count-- > 0) { + while (buf.cursor() < buf.length() && !isWord(buf.currChar())) { + buf.move(1); + } + while (buf.cursor() < buf.length() && isWord(buf.currChar())) { + buf.currChar(Character.toLowerCase(buf.currChar())); + buf.move(1); + } + } + if (this.count < 0) { + buf.cursor(cursor); + } + return true; + } + + protected boolean capitalizeWord() { + int count = Math.abs(this.count); + int cursor = buf.cursor(); + while (count-- > 0) { + boolean first = true; + while (buf.cursor() < buf.length() && !isWord(buf.currChar())) { + buf.move(1); + } + while (buf.cursor() < buf.length() && isWord(buf.currChar()) && !isAlpha(buf.currChar())) { + buf.move(1); + } + while (buf.cursor() < buf.length() && isWord(buf.currChar())) { + buf.currChar(first ? Character.toUpperCase(buf.currChar()) : Character.toLowerCase(buf.currChar())); + buf.move(1); + first = false; + } + } + if (this.count < 0) { + buf.cursor(cursor); + } + return true; + } + + protected boolean deleteWord() { + if (count < 0) { + return callNeg(this::backwardDeleteWord); + } + int x = buf.cursor(); + while (count-- > 0) { + while (x < buf.length() && !isWord(buf.atChar(x))) { + x++; + } + while (x < buf.length() && isWord(buf.atChar(x))) { + x++; + } + } + buf.delete(x - buf.cursor()); + return true; + } + + protected boolean killWord() { + if (count < 0) { + return callNeg(this::backwardKillWord); + } + int x = buf.cursor(); + while (count-- > 0) { + while (x < buf.length() && !isWord(buf.atChar(x))) { + x++; + } + while (x < buf.length() && isWord(buf.atChar(x))) { + x++; + } + } + killRing.add(buf.substring(buf.cursor(), x)); + buf.delete(x - buf.cursor()); + return true; + } + + protected boolean transposeWords() { + int lstart = buf.cursor() - 1; + int lend = buf.cursor(); + while (buf.atChar(lstart) != 0 && buf.atChar(lstart) != '\n') { + lstart--; + } + lstart++; + while (buf.atChar(lend) != 0 && buf.atChar(lend) != '\n') { + lend++; + } + if (lend - lstart < 2) { + return false; + } + int words = 0; + boolean inWord = false; + if (!isDelimiter(buf.atChar(lstart))) { + words++; + inWord = true; + } + for (int i = lstart; i < lend; i++) { + if (isDelimiter(buf.atChar(i))) { + inWord = false; + } else { + if (!inWord) { + words++; + } + inWord = true; + } + } + if (words < 2) { + return false; + } + // TODO: use isWord instead of isDelimiter + boolean neg = this.count < 0; + for (int count = Math.max(this.count, -this.count); count > 0; --count) { + int sta1, end1, sta2, end2; + // Compute current word boundaries + sta1 = buf.cursor(); + while (sta1 > lstart && !isDelimiter(buf.atChar(sta1 - 1))) { + sta1--; + } + end1 = sta1; + while (end1 < lend && !isDelimiter(buf.atChar(++end1))) + ; + if (neg) { + end2 = sta1 - 1; + while (end2 > lstart && isDelimiter(buf.atChar(end2 - 1))) { + end2--; + } + if (end2 < lstart) { + // No word before, use the word after + sta2 = end1; + while (isDelimiter(buf.atChar(++sta2))) + ; + end2 = sta2; + while (end2 < lend && !isDelimiter(buf.atChar(++end2))) + ; + } else { + sta2 = end2; + while (sta2 > lstart && !isDelimiter(buf.atChar(sta2 - 1))) { + sta2--; + } + } + } else { + sta2 = end1; + while (sta2 < lend && isDelimiter(buf.atChar(++sta2))) + ; + if (sta2 == lend) { + // No word after, use the word before + end2 = sta1; + while (isDelimiter(buf.atChar(end2 - 1))) { + end2--; + } + sta2 = end2; + while (sta2 > lstart && !isDelimiter(buf.atChar(sta2 - 1))) { + sta2--; + } + } else { + end2 = sta2; + while (end2 < lend && !isDelimiter(buf.atChar(++end2))) + ; + } + } + if (sta1 < sta2) { + String res = buf.substring(0, sta1) + + buf.substring(sta2, end2) + + buf.substring(end1, sta2) + + buf.substring(sta1, end1) + + buf.substring(end2); + buf.clear(); + buf.write(res); + buf.cursor(neg ? end1 : end2); + } else { + String res = buf.substring(0, sta2) + + buf.substring(sta1, end1) + + buf.substring(end2, sta1) + + buf.substring(sta2, end2) + + buf.substring(end1); + buf.clear(); + buf.write(res); + buf.cursor(neg ? end2 : end1); + } + } + return true; + } + + private int findbol() { + int x = buf.cursor(); + while (x > 0 && buf.atChar(x - 1) != '\n') { + x--; + } + return x; + } + + private int findeol() { + int x = buf.cursor(); + while (x < buf.length() && buf.atChar(x) != '\n') { + x++; + } + return x; + } + + protected boolean insertComment() { + return doInsertComment(false); + } + + protected boolean viInsertComment() { + return doInsertComment(true); + } + + protected boolean doInsertComment(boolean isViMode) { + String comment = getString(COMMENT_BEGIN, DEFAULT_COMMENT_BEGIN); + beginningOfLine(); + putString(comment); + if (isViMode) { + setKeyMap(VIINS); + } + return acceptLine(); + } + + protected boolean viFindNextChar() { + if ((findChar = vigetkey()) > 0) { + findDir = 1; + findTailAdd = 0; + return vifindchar(false); + } + return false; + } + + protected boolean viFindPrevChar() { + if ((findChar = vigetkey()) > 0) { + findDir = -1; + findTailAdd = 0; + return vifindchar(false); + } + return false; + } + + protected boolean viFindNextCharSkip() { + if ((findChar = vigetkey()) > 0) { + findDir = 1; + findTailAdd = -1; + return vifindchar(false); + } + return false; + } + + protected boolean viFindPrevCharSkip() { + if ((findChar = vigetkey()) > 0) { + findDir = -1; + findTailAdd = 1; + return vifindchar(false); + } + return false; + } + + protected boolean viRepeatFind() { + return vifindchar(true); + } + + protected boolean viRevRepeatFind() { + if (count < 0) { + return callNeg(() -> vifindchar(true)); + } + findTailAdd = -findTailAdd; + findDir = -findDir; + boolean ret = vifindchar(true); + findTailAdd = -findTailAdd; + findDir = -findDir; + return ret; + } + + private int vigetkey() { + int ch = readCharacter(); + KeyMap km = keyMaps.get(MAIN); + if (km != null) { + Binding b = km.getBound(new String(Character.toChars(ch))); + if (b instanceof Reference) { + String func = ((Reference) b).name(); + if (SEND_BREAK.equals(func)) { + return -1; + } + } + } + return ch; + } + + private boolean vifindchar(boolean repeat) { + if (findDir == 0) { + return false; + } + if (count < 0) { + return callNeg(this::viRevRepeatFind); + } + if (repeat && findTailAdd != 0) { + if (findDir > 0) { + if (buf.cursor() < buf.length() && buf.nextChar() == findChar) { + buf.move(1); + } + } else { + if (buf.cursor() > 0 && buf.prevChar() == findChar) { + buf.move(-1); + } + } + } + int cursor = buf.cursor(); + while (count-- > 0) { + do { + buf.move(findDir); + } while (buf.cursor() > 0 + && buf.cursor() < buf.length() + && buf.currChar() != findChar + && buf.currChar() != '\n'); + if (buf.cursor() <= 0 || buf.cursor() >= buf.length() || buf.currChar() == '\n') { + buf.cursor(cursor); + return false; + } + } + if (findTailAdd != 0) { + buf.move(findTailAdd); + } + if (findDir == 1 && isInViMoveOperation()) { + buf.move(1); + } + return true; + } + + private boolean callNeg(Widget widget) { + this.count = -this.count; + boolean ret = widget.apply(); + this.count = -this.count; + return ret; + } + + /** + * Implements vi search ("/" or "?"). + * + * @return true if the search was successful + */ + protected boolean viHistorySearchForward() { + searchDir = 1; + searchIndex = 0; + return getViSearchString() && viRepeatSearch(); + } + + protected boolean viHistorySearchBackward() { + searchDir = -1; + searchIndex = history.size() - 1; + return getViSearchString() && viRepeatSearch(); + } + + protected boolean viRepeatSearch() { + if (searchDir == 0) { + return false; + } + int si = searchDir < 0 + ? searchBackwards(searchString, searchIndex, false) + : searchForwards(searchString, searchIndex, false); + if (si == -1 || si == history.index()) { + return false; + } + searchIndex = si; + + /* + * Show the match. + */ + buf.clear(); + history.moveTo(searchIndex); + buf.write(history.get(searchIndex)); + if (VICMD.equals(keyMap)) { + buf.move(-1); + } + return true; + } + + protected boolean viRevRepeatSearch() { + boolean ret; + searchDir = -searchDir; + ret = viRepeatSearch(); + searchDir = -searchDir; + return ret; + } + + private boolean getViSearchString() { + if (searchDir == 0) { + return false; + } + String searchPrompt = searchDir < 0 ? "?" : "/"; + Buffer searchBuffer = new BufferImpl(); + + KeyMap keyMap = keyMaps.get(MAIN); + if (keyMap == null) { + keyMap = keyMaps.get(SAFE); + } + while (true) { + post = () -> new AttributedString(searchPrompt + searchBuffer + "_"); + redisplay(); + Binding b = doReadBinding(keyMap, null); + if (b instanceof Reference) { + String func = ((Reference) b).name(); + switch (func) { + case SEND_BREAK: + post = null; + return false; + case ACCEPT_LINE: + case VI_CMD_MODE: + searchString = searchBuffer.toString(); + post = null; + return true; + case MAGIC_SPACE: + searchBuffer.write(' '); + break; + case REDISPLAY: + redisplay(); + break; + case CLEAR_SCREEN: + clearScreen(); + break; + case SELF_INSERT: + searchBuffer.write(getLastBinding()); + break; + case SELF_INSERT_UNMETA: + if (getLastBinding().charAt(0) == '\u001b') { + String s = getLastBinding().substring(1); + if ("\r".equals(s)) { + s = "\n"; + } + searchBuffer.write(s); + } + break; + case BACKWARD_DELETE_CHAR: + case VI_BACKWARD_DELETE_CHAR: + if (searchBuffer.length() > 0) { + searchBuffer.backspace(); + } + break; + case BACKWARD_KILL_WORD: + case VI_BACKWARD_KILL_WORD: + if (searchBuffer.length() > 0 && !isWhitespace(searchBuffer.prevChar())) { + searchBuffer.backspace(); + } + if (searchBuffer.length() > 0 && isWhitespace(searchBuffer.prevChar())) { + searchBuffer.backspace(); + } + break; + case QUOTED_INSERT: + case VI_QUOTED_INSERT: + int c = readCharacter(); + if (c >= 0) { + searchBuffer.write(c); + } else { + beep(); + } + break; + default: + beep(); + break; + } + } + } + } + + protected boolean insertCloseCurly() { + return insertClose("}"); + } + + protected boolean insertCloseParen() { + return insertClose(")"); + } + + protected boolean insertCloseSquare() { + return insertClose("]"); + } + + protected boolean insertClose(String s) { + putString(s); + + long blink = getLong(BLINK_MATCHING_PAREN, DEFAULT_BLINK_MATCHING_PAREN); + if (blink <= 0) { + removeIndentation(); + return true; + } + + int closePosition = buf.cursor(); + + buf.move(-1); + doViMatchBracket(); + redisplay(); + + peekCharacter(blink); + int blinkPosition = buf.cursor(); + buf.cursor(closePosition); + + if (blinkPosition != closePosition - 1) { + removeIndentation(); + } + return true; + } + + private void removeIndentation() { + int indent = getInt(INDENTATION, DEFAULT_INDENTATION); + if (indent > 0) { + buf.move(-1); + for (int i = 0; i < indent; i++) { + buf.move(-1); + if (buf.currChar() == ' ') { + buf.delete(); + } else { + buf.move(1); + break; + } + } + buf.move(1); + } + } + + protected boolean viMatchBracket() { + return doViMatchBracket(); + } + + protected boolean undefinedKey() { + return false; + } + + /** + * Implements vi style bracket matching ("%" command). The matching + * bracket for the current bracket type that you are sitting on is matched. + * + * @return true if it worked, false if the cursor was not on a bracket + * character or if there was no matching bracket. + */ + protected boolean doViMatchBracket() { + int pos = buf.cursor(); + + if (pos == buf.length()) { + return false; + } + + int type = getBracketType(buf.atChar(pos)); + int move = (type < 0) ? -1 : 1; + int count = 1; + + if (type == 0) return false; + + while (count > 0) { + pos += move; + + // Fell off the start or end. + if (pos < 0 || pos >= buf.length()) { + return false; + } + + int curType = getBracketType(buf.atChar(pos)); + if (curType == type) { + ++count; + } else if (curType == -type) { + --count; + } + } + + /* + * Slight adjustment for delete-to, yank-to, change-to to ensure + * that the matching paren is consumed + */ + if (move > 0 && isInViMoveOperation()) ++pos; + + buf.cursor(pos); + return true; + } + + /** + * Given a character determines what type of bracket it is (paren, + * square, curly, or none). + * + * @param ch The character to check + * @return 1 is square, 2 curly, 3 parent, or zero for none. The value + * will be negated if it is the closing form of the bracket. + */ + protected int getBracketType(int ch) { + switch (ch) { + case '[': + return 1; + case ']': + return -1; + case '{': + return 2; + case '}': + return -2; + case '(': + return 3; + case ')': + return -3; + default: + return 0; + } + } + + /** + * Performs character transpose. The character prior to the cursor and the + * character under the cursor are swapped and the cursor is advanced one. + * Do not cross line breaks. + * + * @return true + */ + protected boolean transposeChars() { + int lstart = buf.cursor() - 1; + int lend = buf.cursor(); + while (buf.atChar(lstart) != 0 && buf.atChar(lstart) != '\n') { + lstart--; + } + lstart++; + while (buf.atChar(lend) != 0 && buf.atChar(lend) != '\n') { + lend++; + } + if (lend - lstart < 2) { + return false; + } + boolean neg = this.count < 0; + for (int count = Math.max(this.count, -this.count); count > 0; --count) { + while (buf.cursor() <= lstart) { + buf.move(1); + } + while (buf.cursor() >= lend) { + buf.move(-1); + } + int c = buf.currChar(); + buf.currChar(buf.prevChar()); + buf.move(-1); + buf.currChar(c); + buf.move(neg ? 0 : 2); + } + return true; + } + + protected boolean undo() { + isUndo = true; + if (undo.canUndo()) { + undo.undo(); + return true; + } + return false; + } + + protected boolean redo() { + isUndo = true; + if (undo.canRedo()) { + undo.redo(); + return true; + } + return false; + } + + protected boolean sendBreak() { + if (searchTerm == null) { + buf.clear(); + println(); + redrawLine(); + // state = State.INTERRUPT; + return false; + } + return true; + } + + protected boolean backwardChar() { + return buf.move(-count) != 0; + } + + protected boolean forwardChar() { + return buf.move(count) != 0; + } + + protected boolean viDigitOrBeginningOfLine() { + if (repeatCount > 0) { + return digitArgument(); + } else { + return beginningOfLine(); + } + } + + protected boolean universalArgument() { + mult *= universal; + isArgDigit = true; + return true; + } + + protected boolean argumentBase() { + if (repeatCount > 0 && repeatCount < 32) { + universal = repeatCount; + isArgDigit = true; + return true; + } else { + return false; + } + } + + protected boolean negArgument() { + mult *= -1; + isArgDigit = true; + return true; + } + + protected boolean digitArgument() { + String s = getLastBinding(); + repeatCount = (repeatCount * 10) + s.charAt(s.length() - 1) - '0'; + int maxRepeatCount = getInt(MAX_REPEAT_COUNT, DEFAULT_MAX_REPEAT_COUNT); + if (repeatCount > maxRepeatCount) { + throw new IllegalArgumentException("digit argument should be less than " + maxRepeatCount); + } + isArgDigit = true; + return true; + } + + protected boolean viDelete() { + int cursorStart = buf.cursor(); + Binding o = readBinding(getKeys()); + if (o instanceof Reference) { + // TODO: be smarter on how to get the vi range + String op = viDeleteChangeYankToRemap(((Reference) o).name()); + // This is a weird special case. In vi + // "dd" deletes the current line. So if we + // get a delete-to, followed by a delete-to, + // we delete the line. + if (VI_DELETE.equals(op)) { + killWholeLine(); + } else { + viMoveMode = ViMoveMode.DELETE; + Widget widget = widgets.get(op); + if (widget != null && !widget.apply()) { + viMoveMode = ViMoveMode.NORMAL; + return false; + } + viMoveMode = ViMoveMode.NORMAL; + } + return viDeleteTo(cursorStart, buf.cursor()); + } else { + pushBackBinding(); + return false; + } + } + + /* + protected int getViRange(Reference cmd, ViMoveMode mode) { + Buffer buffer = buf.copy(); + int oldMark = mark; + int pos = buf.cursor(); + String bind = getLastBinding(); + + if (visual != 0) { + if (buf.length() == 0) { + return -1; + } + pos = mark; + v + } else { + viMoveMode = mode; + mark = -1; + Binding b = doReadBinding(getKeys(), keyMaps.get(VIOPP)); + if (b == null || new Reference(SEND_BREAK).equals(b)) { + viMoveMode = ViMoveMode.NORMAL; + mark = oldMark; + return -1; + } + if (cmd.equals(b)) { + doViLineRange(); + } + Widget w = getWidget(b); + if (w ) + if (b instanceof Reference) { + + } + } + + } + */ + + protected boolean viYankTo() { + int cursorStart = buf.cursor(); + Binding o = readBinding(getKeys()); + if (o instanceof Reference) { + // TODO: be smarter on how to get the vi range + String op = viDeleteChangeYankToRemap(((Reference) o).name()); + // Similar to delete-to, a "yy" yanks the whole line. + if (VI_YANK.equals(op)) { + yankBuffer = buf.toString(); + return true; + } else { + viMoveMode = ViMoveMode.YANK; + Widget widget = widgets.get(op); + if (widget != null && !widget.apply()) { + return false; + } + viMoveMode = ViMoveMode.NORMAL; + } + return viYankTo(cursorStart, buf.cursor()); + } else { + pushBackBinding(); + return false; + } + } + + protected boolean viYankWholeLine() { + int s, e; + int p = buf.cursor(); + while (buf.move(-1) == -1 && buf.prevChar() != '\n') + ; + s = buf.cursor(); + for (int i = 0; i < repeatCount; i++) { + while (buf.move(1) == 1 && buf.prevChar() != '\n') + ; + } + e = buf.cursor(); + yankBuffer = buf.substring(s, e); + if (!yankBuffer.endsWith("\n")) { + yankBuffer += "\n"; + } + buf.cursor(p); + return true; + } + + protected boolean viChange() { + int cursorStart = buf.cursor(); + Binding o = readBinding(getKeys()); + if (o instanceof Reference) { + // TODO: be smarter on how to get the vi range + String op = viDeleteChangeYankToRemap(((Reference) o).name()); + // change whole line + if (VI_CHANGE.equals(op)) { + killWholeLine(); + } else { + viMoveMode = ViMoveMode.CHANGE; + Widget widget = widgets.get(op); + if (widget != null && !widget.apply()) { + viMoveMode = ViMoveMode.NORMAL; + return false; + } + viMoveMode = ViMoveMode.NORMAL; + } + boolean res = viChange(cursorStart, buf.cursor()); + setKeyMap(VIINS); + return res; + } else { + pushBackBinding(); + return false; + } + } + + protected void cleanup() { + if (isSet(Option.ERASE_LINE_ON_FINISH)) { + Buffer oldBuffer = buf.copy(); + AttributedString oldPrompt = prompt; + buf.clear(); + prompt = new AttributedString(""); + doCleanup(false); + prompt = oldPrompt; + buf.copyFrom(oldBuffer); + } else { + doCleanup(true); + } + } + + protected void doCleanup(boolean nl) { + buf.cursor(buf.length()); + post = null; + if (size.getColumns() > 0 || size.getRows() > 0) { + doAutosuggestion = false; + redisplay(false); + if (nl) { + println(); + } + terminal.puts(Capability.keypad_local); + terminal.trackMouse(Terminal.MouseTracking.Off); + if (isSet(Option.BRACKETED_PASTE) && !isTerminalDumb()) + terminal.writer().write(BRACKETED_PASTE_OFF); + flush(); + } + history.moveToEnd(); + } + + protected boolean historyIncrementalSearchForward() { + return doSearchHistory(false); + } + + protected boolean historyIncrementalSearchBackward() { + return doSearchHistory(true); + } + + protected boolean doSearchHistory(boolean backward) { + if (history.isEmpty()) { + return false; + } + + KeyMap terminators = new KeyMap<>(); + getString(SEARCH_TERMINATORS, DEFAULT_SEARCH_TERMINATORS) + .codePoints() + .forEach(c -> bind(terminators, ACCEPT_LINE, new String(Character.toChars(c)))); + + Buffer originalBuffer = buf.copy(); + searchIndex = -1; + searchTerm = new StringBuffer(); + searchBackward = backward; + searchFailing = false; + post = () -> new AttributedString((searchFailing ? "failing" + " " : "") + + (searchBackward ? "bck-i-search" : "fwd-i-search") + + ": " + searchTerm + "_"); + + redisplay(); + try { + while (true) { + int prevSearchIndex = searchIndex; + Binding operation = readBinding(getKeys(), terminators); + String ref = (operation instanceof Reference) ? ((Reference) operation).name() : ""; + boolean next = false; + switch (ref) { + case SEND_BREAK: + beep(); + buf.copyFrom(originalBuffer); + return true; + case HISTORY_INCREMENTAL_SEARCH_BACKWARD: + searchBackward = true; + next = true; + break; + case HISTORY_INCREMENTAL_SEARCH_FORWARD: + searchBackward = false; + next = true; + break; + case BACKWARD_DELETE_CHAR: + if (searchTerm.length() > 0) { + searchTerm.deleteCharAt(searchTerm.length() - 1); + } + break; + case SELF_INSERT: + searchTerm.append(getLastBinding()); + break; + default: + // Set buffer and cursor position to the found string. + if (searchIndex != -1) { + history.moveTo(searchIndex); + } + pushBackBinding(); + return true; + } + + // print the search status + String pattern = doGetSearchPattern(); + if (pattern.length() == 0) { + buf.copyFrom(originalBuffer); + searchFailing = false; + } else { + boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); + Pattern pat = Pattern.compile( + pattern, + caseInsensitive ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE : Pattern.UNICODE_CASE); + Pair pair = null; + if (searchBackward) { + boolean nextOnly = next; + pair = matches(pat, buf.toString(), searchIndex).stream() + .filter(p -> nextOnly ? p.v < buf.cursor() : p.v <= buf.cursor()) + .max(Comparator.comparing(Pair::getV)) + .orElse(null); + if (pair == null) { + pair = StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + history.reverseIterator( + searchIndex < 0 ? history.last() : searchIndex - 1), + Spliterator.ORDERED), + false) + .flatMap(e -> matches(pat, e.line(), e.index()).stream()) + .findFirst() + .orElse(null); + } + } else { + boolean nextOnly = next; + pair = matches(pat, buf.toString(), searchIndex).stream() + .filter(p -> nextOnly ? p.v > buf.cursor() : p.v >= buf.cursor()) + .min(Comparator.comparing(Pair::getV)) + .orElse(null); + if (pair == null) { + pair = StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + history.iterator( + (searchIndex < 0 ? history.last() : searchIndex) + 1), + Spliterator.ORDERED), + false) + .flatMap(e -> matches(pat, e.line(), e.index()).stream()) + .findFirst() + .orElse(null); + if (pair == null && searchIndex >= 0) { + pair = matches(pat, originalBuffer.toString(), -1).stream() + .min(Comparator.comparing(Pair::getV)) + .orElse(null); + } + } + } + if (pair != null) { + searchIndex = pair.u; + buf.clear(); + if (searchIndex >= 0) { + buf.write(history.get(searchIndex)); + } else { + buf.write(originalBuffer.toString()); + } + buf.cursor(pair.v); + searchFailing = false; + } else { + searchFailing = true; + beep(); + } + } + redisplay(); + } + } catch (IOError e) { + // Ignore Ctrl+C interrupts and just exit the loop + if (!(e.getCause() instanceof InterruptedException)) { + throw e; + } + return true; + } finally { + searchTerm = null; + searchIndex = -1; + post = null; + } + } + + private List> matches(Pattern p, String line, int index) { + List> starts = new ArrayList<>(); + Matcher m = p.matcher(line); + while (m.find()) { + starts.add(new Pair<>(index, m.start())); + } + return starts; + } + + private String doGetSearchPattern() { + StringBuilder sb = new StringBuilder(); + boolean inQuote = false; + for (int i = 0; i < searchTerm.length(); i++) { + char c = searchTerm.charAt(i); + if (Character.isLowerCase(c)) { + if (inQuote) { + sb.append("\\E"); + inQuote = false; + } + sb.append("[") + .append(Character.toLowerCase(c)) + .append(Character.toUpperCase(c)) + .append("]"); + } else { + if (!inQuote) { + sb.append("\\Q"); + inQuote = true; + } + sb.append(c); + } + } + if (inQuote) { + sb.append("\\E"); + } + return sb.toString(); + } + + private void pushBackBinding() { + pushBackBinding(false); + } + + private void pushBackBinding(boolean skip) { + String s = getLastBinding(); + if (s != null) { + bindingReader.runMacro(s); + skipRedisplay = skip; + } + } + + protected boolean historySearchForward() { + if (historyBuffer == null || buf.length() == 0 || !buf.toString().equals(history.current())) { + historyBuffer = buf.copy(); + searchBuffer = getFirstWord(); + } + int index = history.index() + 1; + + if (index < history.last() + 1) { + int searchIndex = searchForwards(searchBuffer.toString(), index, true); + if (searchIndex == -1) { + history.moveToEnd(); + if (!buf.toString().equals(historyBuffer.toString())) { + setBuffer(historyBuffer.toString()); + historyBuffer = null; + } else { + return false; + } + } else { + // Maintain cursor position while searching. + if (history.moveTo(searchIndex)) { + setBuffer(history.current()); + } else { + history.moveToEnd(); + setBuffer(historyBuffer.toString()); + return false; + } + } + } else { + history.moveToEnd(); + if (!buf.toString().equals(historyBuffer.toString())) { + setBuffer(historyBuffer.toString()); + historyBuffer = null; + } else { + return false; + } + } + return true; + } + + // + // History search + // + + private CharSequence getFirstWord() { + String s = buf.toString(); + int i = 0; + while (i < s.length() && !Character.isWhitespace(s.charAt(i))) { + i++; + } + return s.substring(0, i); + } + + protected boolean historySearchBackward() { + if (historyBuffer == null || buf.length() == 0 || !buf.toString().equals(history.current())) { + historyBuffer = buf.copy(); + searchBuffer = getFirstWord(); + } + int searchIndex = searchBackwards(searchBuffer.toString(), history.index(), true); + + if (searchIndex == -1) { + return false; + } else { + // Maintain cursor position while searching. + if (history.moveTo(searchIndex)) { + setBuffer(history.current()); + } else { + return false; + } + } + return true; + } + + /** + * Search backward in history from a given position. + * + * @param searchTerm substring to search for. + * @param startIndex the index from which on to search + * @return index where this substring has been found, or -1 else. + */ + public int searchBackwards(String searchTerm, int startIndex) { + return searchBackwards(searchTerm, startIndex, false); + } + + /** + * Search backwards in history from the current position. + * + * @param searchTerm substring to search for. + * @return index where the substring has been found, or -1 else. + */ + public int searchBackwards(String searchTerm) { + return searchBackwards(searchTerm, history.index(), false); + } + + public int searchBackwards(String searchTerm, int startIndex, boolean startsWith) { + boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); + if (caseInsensitive) { + searchTerm = searchTerm.toLowerCase(); + } + ListIterator it = history.iterator(startIndex); + while (it.hasPrevious()) { + History.Entry e = it.previous(); + String line = e.line(); + if (caseInsensitive) { + line = line.toLowerCase(); + } + int idx = line.indexOf(searchTerm); + if ((startsWith && idx == 0) || (!startsWith && idx >= 0)) { + return e.index(); + } + } + return -1; + } + + public int searchForwards(String searchTerm, int startIndex, boolean startsWith) { + boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); + if (caseInsensitive) { + searchTerm = searchTerm.toLowerCase(); + } + if (startIndex > history.last()) { + startIndex = history.last(); + } + ListIterator it = history.iterator(startIndex); + if (searchIndex != -1 && it.hasNext()) { + it.next(); + } + while (it.hasNext()) { + History.Entry e = it.next(); + String line = e.line(); + if (caseInsensitive) { + line = line.toLowerCase(); + } + int idx = line.indexOf(searchTerm); + if ((startsWith && idx == 0) || (!startsWith && idx >= 0)) { + return e.index(); + } + } + return -1; + } + + /** + * Search forward in history from a given position. + * + * @param searchTerm substring to search for. + * @param startIndex the index from which on to search + * @return index where this substring has been found, or -1 else. + */ + public int searchForwards(String searchTerm, int startIndex) { + return searchForwards(searchTerm, startIndex, false); + } + + /** + * Search forwards in history from the current position. + * + * @param searchTerm substring to search for. + * @return index where the substring has been found, or -1 else. + */ + public int searchForwards(String searchTerm) { + return searchForwards(searchTerm, history.index()); + } + + protected boolean quit() { + getBuffer().clear(); + return acceptLine(); + } + + protected boolean acceptAndHold() { + nextCommandFromHistory = false; + acceptLine(); + if (!buf.toString().isEmpty()) { + nextHistoryId = Integer.MAX_VALUE; + nextCommandFromHistory = true; + } + return nextCommandFromHistory; + } + + protected boolean acceptLineAndDownHistory() { + nextCommandFromHistory = false; + acceptLine(); + if (nextHistoryId < 0) { + nextHistoryId = history.index(); + } + if (history.size() > nextHistoryId + 1) { + nextHistoryId++; + nextCommandFromHistory = true; + } + return nextCommandFromHistory; + } + + protected boolean acceptAndInferNextHistory() { + nextCommandFromHistory = false; + acceptLine(); + if (!buf.toString().isEmpty()) { + nextHistoryId = searchBackwards(buf.toString(), history.last()); + if (nextHistoryId >= 0 && history.size() > nextHistoryId + 1) { + nextHistoryId++; + nextCommandFromHistory = true; + } + } + return nextCommandFromHistory; + } + + protected boolean acceptLine() { + parsedLine = null; + int curPos = 0; + if (!isSet(Option.DISABLE_EVENT_EXPANSION)) { + try { + String str = buf.toString(); + String exp = expander.expandHistory(history, str); + if (!exp.equals(str)) { + buf.clear(); + buf.write(exp); + if (isSet(Option.HISTORY_VERIFY)) { + return true; + } + } + } catch (IllegalArgumentException e) { + // Ignore + } + } + try { + curPos = buf.cursor(); + parsedLine = parser.parse(buf.toString(), buf.cursor(), ParseContext.ACCEPT_LINE); + } catch (EOFError e) { + StringBuilder sb = new StringBuilder("\n"); + indention(e.getOpenBrackets(), sb); + int curMove = sb.length(); + if (isSet(Option.INSERT_BRACKET) && e.getOpenBrackets() > 1 && e.getNextClosingBracket() != null) { + sb.append('\n'); + indention(e.getOpenBrackets() - 1, sb); + sb.append(e.getNextClosingBracket()); + } + buf.write(sb.toString()); + buf.cursor(curPos + curMove); + return true; + } catch (SyntaxError e) { + // do nothing + } + callWidget(CALLBACK_FINISH); + state = State.DONE; + return true; + } + + void indention(int nb, StringBuilder sb) { + int indent = getInt(INDENTATION, DEFAULT_INDENTATION) * nb; + for (int i = 0; i < indent; i++) { + sb.append(' '); + } + } + + protected boolean selfInsert() { + for (int count = this.count; count > 0; count--) { + putString(getLastBinding()); + } + return true; + } + + // + // History Control + // + + protected boolean selfInsertUnmeta() { + if (getLastBinding().charAt(0) == '\u001b') { + String s = getLastBinding().substring(1); + if ("\r".equals(s)) { + s = "\n"; + } + for (int count = this.count; count > 0; count--) { + putString(s); + } + return true; + } else { + return false; + } + } + + protected boolean overwriteMode() { + overTyping = !overTyping; + return true; + } + + protected boolean beginningOfBufferOrHistory() { + if (findbol() != 0) { + buf.cursor(0); + return true; + } else { + return beginningOfHistory(); + } + } + + protected boolean beginningOfHistory() { + if (history.moveToFirst()) { + setBuffer(history.current()); + return true; + } else { + return false; + } + } + + protected boolean endOfBufferOrHistory() { + if (findeol() != buf.length()) { + buf.cursor(buf.length()); + return true; + } else { + return endOfHistory(); + } + } + + protected boolean endOfHistory() { + if (history.moveToLast()) { + setBuffer(history.current()); + return true; + } else { + return false; + } + } + + protected boolean beginningOfLineHist() { + if (count < 0) { + return callNeg(this::endOfLineHist); + } + while (count-- > 0) { + int bol = findbol(); + if (bol != buf.cursor()) { + buf.cursor(bol); + } else { + moveHistory(false); + buf.cursor(0); + } + } + return true; + } + + protected boolean endOfLineHist() { + if (count < 0) { + return callNeg(this::beginningOfLineHist); + } + while (count-- > 0) { + int eol = findeol(); + if (eol != buf.cursor()) { + buf.cursor(eol); + } else { + moveHistory(true); + } + } + return true; + } + + protected boolean upHistory() { + while (count-- > 0) { + if (!moveHistory(false)) { + return !isSet(Option.HISTORY_BEEP); + } + } + return true; + } + + protected boolean downHistory() { + while (count-- > 0) { + if (!moveHistory(true)) { + return !isSet(Option.HISTORY_BEEP); + } + } + return true; + } + + protected boolean viUpLineOrHistory() { + return upLine() || upHistory() && viFirstNonBlank(); + } + + protected boolean viDownLineOrHistory() { + return downLine() || downHistory() && viFirstNonBlank(); + } + + protected boolean upLine() { + return buf.up(); + } + + protected boolean downLine() { + return buf.down(); + } + + protected boolean upLineOrHistory() { + return upLine() || upHistory(); + } + + protected boolean upLineOrSearch() { + return upLine() || historySearchBackward(); + } + + protected boolean downLineOrHistory() { + return downLine() || downHistory(); + } + + protected boolean downLineOrSearch() { + return downLine() || historySearchForward(); + } + + protected boolean viCmdMode() { + // If we are re-entering move mode from an + // aborted yank-to, delete-to, change-to then + // don't move the cursor back. The cursor is + // only move on an explicit entry to movement + // mode. + if (state == State.NORMAL) { + buf.move(-1); + } + return setKeyMap(VICMD); + } + + protected boolean viInsert() { + return setKeyMap(VIINS); + } + + protected boolean viAddNext() { + buf.move(1); + return setKeyMap(VIINS); + } + + protected boolean viAddEol() { + return endOfLine() && setKeyMap(VIINS); + } + + protected boolean emacsEditingMode() { + return setKeyMap(EMACS); + } + + protected boolean viChangeWholeLine() { + return viFirstNonBlank() && viChangeEol(); + } + + protected boolean viChangeEol() { + return viChange(buf.cursor(), buf.length()) && setKeyMap(VIINS); + } + + protected boolean viKillEol() { + int eol = findeol(); + if (buf.cursor() == eol) { + return false; + } + killRing.add(buf.substring(buf.cursor(), eol)); + buf.delete(eol - buf.cursor()); + return true; + } + + protected boolean quotedInsert() { + int c = readCharacter(); + while (count-- > 0) { + putString(new String(Character.toChars(c))); + } + return true; + } + + protected boolean viJoin() { + if (buf.down()) { + while (buf.move(-1) == -1 && buf.prevChar() != '\n') + ; + buf.backspace(); + buf.write(' '); + buf.move(-1); + return true; + } + return false; + } + + protected boolean viKillWholeLine() { + return killWholeLine() && setKeyMap(VIINS); + } + + protected boolean viInsertBol() { + return beginningOfLine() && setKeyMap(VIINS); + } + + protected boolean backwardDeleteChar() { + if (count < 0) { + return callNeg(this::deleteChar); + } + if (buf.cursor() == 0) { + return false; + } + buf.backspace(count); + return true; + } + + protected boolean viFirstNonBlank() { + beginningOfLine(); + while (buf.cursor() < buf.length() && isWhitespace(buf.currChar())) { + buf.move(1); + } + return true; + } + + protected boolean viBeginningOfLine() { + buf.cursor(findbol()); + return true; + } + + protected boolean viEndOfLine() { + if (count < 0) { + return false; + } + while (count-- > 0) { + buf.cursor(findeol() + 1); + } + buf.move(-1); + return true; + } + + protected boolean beginningOfLine() { + while (count-- > 0) { + while (buf.move(-1) == -1 && buf.prevChar() != '\n') + ; + } + return true; + } + + protected boolean endOfLine() { + while (count-- > 0) { + while (buf.move(1) == 1 && buf.currChar() != '\n') + ; + } + return true; + } + + protected boolean deleteChar() { + if (count < 0) { + return callNeg(this::backwardDeleteChar); + } + if (buf.cursor() == buf.length()) { + return false; + } + buf.delete(count); + return true; + } + + /** + * Deletes the previous character from the cursor position + * + * @return true if it succeeded, false otherwise + */ + protected boolean viBackwardDeleteChar() { + for (int i = 0; i < count; i++) { + if (!buf.backspace()) { + return false; + } + } + return true; + } + + /** + * Deletes the character you are sitting on and sucks the rest of + * the line in from the right. + * + * @return true if it succeeded, false otherwise + */ + protected boolean viDeleteChar() { + for (int i = 0; i < count; i++) { + if (!buf.delete()) { + return false; + } + } + return true; + } + + /** + * Switches the case of the current character from upper to lower + * or lower to upper as necessary and advances the cursor one + * position to the right. + * + * @return true if it succeeded, false otherwise + */ + protected boolean viSwapCase() { + for (int i = 0; i < count; i++) { + if (buf.cursor() < buf.length()) { + int ch = buf.atChar(buf.cursor()); + ch = switchCase(ch); + buf.currChar(ch); + buf.move(1); + } else { + return false; + } + } + return true; + } + + /** + * Implements the vi change character command (in move-mode "r" + * followed by the character to change to). + * + * @return true if it succeeded, false otherwise + */ + protected boolean viReplaceChars() { + int c = readCharacter(); + // EOF, ESC, or CTRL-C aborts. + if (c < 0 || c == '\033' || c == '\003') { + return true; + } + + for (int i = 0; i < count; i++) { + if (buf.currChar((char) c)) { + if (i < count - 1) { + buf.move(1); + } + } else { + return false; + } + } + return true; + } + + protected boolean viChange(int startPos, int endPos) { + return doViDeleteOrChange(startPos, endPos, true); + } + + protected boolean viDeleteTo(int startPos, int endPos) { + return doViDeleteOrChange(startPos, endPos, false); + } + + /** + * Performs the vi "delete-to" action, deleting characters between a given + * span of the input line. + * + * @param startPos The start position + * @param endPos The end position. + * @param isChange If true, then the delete is part of a change operationg + * (e.g. "c$" is change-to-end-of line, so we first must delete to end + * of line to start the change + * @return true if it succeeded, false otherwise + */ + protected boolean doViDeleteOrChange(int startPos, int endPos, boolean isChange) { + if (startPos == endPos) { + return true; + } + + if (endPos < startPos) { + int tmp = endPos; + endPos = startPos; + startPos = tmp; + } + + buf.cursor(startPos); + buf.delete(endPos - startPos); + + // If we are doing a delete operation (e.g. "d$") then don't leave the + // cursor dangling off the end. In reality the "isChange" flag is silly + // what is really happening is that if we are in "move-mode" then the + // cursor can't be moved off the end of the line, but in "edit-mode" it + // is ok, but I have no easy way of knowing which mode we are in. + if (!isChange && startPos > 0 && startPos == buf.length()) { + buf.move(-1); + } + return true; + } + + /** + * Implement the "vi" yank-to operation. This operation allows you + * to yank the contents of the current line based upon a move operation, + * for example "yw" yanks the current word, "3yw" yanks 3 words, etc. + * + * @param startPos The starting position from which to yank + * @param endPos The ending position to which to yank + * @return true if the yank succeeded + */ + protected boolean viYankTo(int startPos, int endPos) { + int cursorPos = startPos; + + if (endPos < startPos) { + int tmp = endPos; + endPos = startPos; + startPos = tmp; + } + + if (startPos == endPos) { + yankBuffer = ""; + return true; + } + + yankBuffer = buf.substring(startPos, endPos); + + /* + * It was a movement command that moved the cursor to find the + * end position, so put the cursor back where it started. + */ + buf.cursor(cursorPos); + return true; + } + + protected boolean viOpenLineAbove() { + while (buf.move(-1) == -1 && buf.prevChar() != '\n') + ; + buf.write('\n'); + buf.move(-1); + return setKeyMap(VIINS); + } + + protected boolean viOpenLineBelow() { + while (buf.move(1) == 1 && buf.currChar() != '\n') + ; + buf.write('\n'); + return setKeyMap(VIINS); + } + + /** + * Pasts the yank buffer to the right of the current cursor position + * and moves the cursor to the end of the pasted region. + * + * @return true + */ + protected boolean viPutAfter() { + if (yankBuffer.indexOf('\n') >= 0) { + while (buf.move(1) == 1 && buf.currChar() != '\n') + ; + buf.move(1); + putString(yankBuffer); + buf.move(-yankBuffer.length()); + } else if (yankBuffer.length() != 0) { + if (buf.cursor() < buf.length()) { + buf.move(1); + } + for (int i = 0; i < count; i++) { + putString(yankBuffer); + } + buf.move(-1); + } + return true; + } + + protected boolean viPutBefore() { + if (yankBuffer.indexOf('\n') >= 0) { + while (buf.move(-1) == -1 && buf.prevChar() != '\n') + ; + putString(yankBuffer); + buf.move(-yankBuffer.length()); + } else if (yankBuffer.length() != 0) { + if (buf.cursor() > 0) { + buf.move(-1); + } + for (int i = 0; i < count; i++) { + putString(yankBuffer); + } + buf.move(-1); + } + return true; + } + + protected boolean doLowercaseVersion() { + bindingReader.runMacro(getLastBinding().toLowerCase()); + return true; + } + + protected boolean setMarkCommand() { + if (count < 0) { + regionActive = RegionType.NONE; + return true; + } + regionMark = buf.cursor(); + regionActive = RegionType.CHAR; + return true; + } + + protected boolean exchangePointAndMark() { + if (count == 0) { + regionActive = RegionType.CHAR; + return true; + } + int x = regionMark; + regionMark = buf.cursor(); + buf.cursor(x); + if (buf.cursor() > buf.length()) { + buf.cursor(buf.length()); + } + if (count > 0) { + regionActive = RegionType.CHAR; + } + return true; + } + + protected boolean visualMode() { + if (isInViMoveOperation()) { + isArgDigit = true; + forceLine = false; + forceChar = true; + return true; + } + if (regionActive == RegionType.NONE) { + regionMark = buf.cursor(); + regionActive = RegionType.CHAR; + } else if (regionActive == RegionType.CHAR) { + regionActive = RegionType.NONE; + } else if (regionActive == RegionType.LINE) { + regionActive = RegionType.CHAR; + } + return true; + } + + protected boolean visualLineMode() { + if (isInViMoveOperation()) { + isArgDigit = true; + forceLine = true; + forceChar = false; + return true; + } + if (regionActive == RegionType.NONE) { + regionMark = buf.cursor(); + regionActive = RegionType.LINE; + } else if (regionActive == RegionType.CHAR) { + regionActive = RegionType.LINE; + } else if (regionActive == RegionType.LINE) { + regionActive = RegionType.NONE; + } + return true; + } + + protected boolean deactivateRegion() { + regionActive = RegionType.NONE; + return true; + } + + protected boolean whatCursorPosition() { + post = () -> { + AttributedStringBuilder sb = new AttributedStringBuilder(); + if (buf.cursor() < buf.length()) { + int c = buf.currChar(); + sb.append("Char: "); + if (c == ' ') { + sb.append("SPC"); + } else if (c == '\n') { + sb.append("LFD"); + } else if (c < 32) { + sb.append('^'); + sb.append((char) (c + 'A' - 1)); + } else if (c == 127) { + sb.append("^?"); + } else { + sb.append((char) c); + } + sb.append(" ("); + sb.append("0").append(Integer.toOctalString(c)).append(" "); + sb.append(Integer.toString(c)).append(" "); + sb.append("0x").append(Integer.toHexString(c)).append(" "); + sb.append(")"); + } else { + sb.append("EOF"); + } + sb.append(" "); + sb.append("point "); + sb.append(Integer.toString(buf.cursor() + 1)); + sb.append(" of "); + sb.append(Integer.toString(buf.length() + 1)); + sb.append(" ("); + sb.append(Integer.toString(buf.length() == 0 ? 100 : ((100 * buf.cursor()) / buf.length()))); + sb.append("%)"); + sb.append(" "); + sb.append("column "); + sb.append(Integer.toString(buf.cursor() - findbol())); + return sb.toAttributedString(); + }; + return true; + } + + protected Map builtinWidgets() { + Map widgets = new HashMap<>(); + addBuiltinWidget(widgets, ACCEPT_AND_INFER_NEXT_HISTORY, this::acceptAndInferNextHistory); + addBuiltinWidget(widgets, ACCEPT_AND_HOLD, this::acceptAndHold); + addBuiltinWidget(widgets, ACCEPT_LINE, this::acceptLine); + addBuiltinWidget(widgets, ACCEPT_LINE_AND_DOWN_HISTORY, this::acceptLineAndDownHistory); + addBuiltinWidget(widgets, ARGUMENT_BASE, this::argumentBase); + addBuiltinWidget(widgets, BACKWARD_CHAR, this::backwardChar); + addBuiltinWidget(widgets, BACKWARD_DELETE_CHAR, this::backwardDeleteChar); + addBuiltinWidget(widgets, BACKWARD_DELETE_WORD, this::backwardDeleteWord); + addBuiltinWidget(widgets, BACKWARD_KILL_LINE, this::backwardKillLine); + addBuiltinWidget(widgets, BACKWARD_KILL_WORD, this::backwardKillWord); + addBuiltinWidget(widgets, BACKWARD_WORD, this::backwardWord); + addBuiltinWidget(widgets, BEEP, this::beep); + addBuiltinWidget(widgets, BEGINNING_OF_BUFFER_OR_HISTORY, this::beginningOfBufferOrHistory); + addBuiltinWidget(widgets, BEGINNING_OF_HISTORY, this::beginningOfHistory); + addBuiltinWidget(widgets, BEGINNING_OF_LINE, this::beginningOfLine); + addBuiltinWidget(widgets, BEGINNING_OF_LINE_HIST, this::beginningOfLineHist); + addBuiltinWidget(widgets, CAPITALIZE_WORD, this::capitalizeWord); + addBuiltinWidget(widgets, CLEAR, this::clear); + addBuiltinWidget(widgets, CLEAR_SCREEN, this::clearScreen); + addBuiltinWidget(widgets, COMPLETE_PREFIX, this::completePrefix); + addBuiltinWidget(widgets, COMPLETE_WORD, this::completeWord); + addBuiltinWidget(widgets, COPY_PREV_WORD, this::copyPrevWord); + addBuiltinWidget(widgets, COPY_REGION_AS_KILL, this::copyRegionAsKill); + addBuiltinWidget(widgets, DELETE_CHAR, this::deleteChar); + addBuiltinWidget(widgets, DELETE_CHAR_OR_LIST, this::deleteCharOrList); + addBuiltinWidget(widgets, DELETE_WORD, this::deleteWord); + addBuiltinWidget(widgets, DIGIT_ARGUMENT, this::digitArgument); + addBuiltinWidget(widgets, DO_LOWERCASE_VERSION, this::doLowercaseVersion); + addBuiltinWidget(widgets, DOWN_CASE_WORD, this::downCaseWord); + addBuiltinWidget(widgets, DOWN_LINE, this::downLine); + addBuiltinWidget(widgets, DOWN_LINE_OR_HISTORY, this::downLineOrHistory); + addBuiltinWidget(widgets, DOWN_LINE_OR_SEARCH, this::downLineOrSearch); + addBuiltinWidget(widgets, DOWN_HISTORY, this::downHistory); + addBuiltinWidget(widgets, EMACS_EDITING_MODE, this::emacsEditingMode); + addBuiltinWidget(widgets, EMACS_BACKWARD_WORD, this::emacsBackwardWord); + addBuiltinWidget(widgets, EMACS_FORWARD_WORD, this::emacsForwardWord); + addBuiltinWidget(widgets, END_OF_BUFFER_OR_HISTORY, this::endOfBufferOrHistory); + addBuiltinWidget(widgets, END_OF_HISTORY, this::endOfHistory); + addBuiltinWidget(widgets, END_OF_LINE, this::endOfLine); + addBuiltinWidget(widgets, END_OF_LINE_HIST, this::endOfLineHist); + addBuiltinWidget(widgets, EXCHANGE_POINT_AND_MARK, this::exchangePointAndMark); + addBuiltinWidget(widgets, EXPAND_HISTORY, this::expandHistory); + addBuiltinWidget(widgets, EXPAND_OR_COMPLETE, this::expandOrComplete); + addBuiltinWidget(widgets, EXPAND_OR_COMPLETE_PREFIX, this::expandOrCompletePrefix); + addBuiltinWidget(widgets, EXPAND_WORD, this::expandWord); + addBuiltinWidget(widgets, FRESH_LINE, this::freshLine); + addBuiltinWidget(widgets, FORWARD_CHAR, this::forwardChar); + addBuiltinWidget(widgets, FORWARD_WORD, this::forwardWord); + addBuiltinWidget(widgets, HISTORY_INCREMENTAL_SEARCH_BACKWARD, this::historyIncrementalSearchBackward); + addBuiltinWidget(widgets, HISTORY_INCREMENTAL_SEARCH_FORWARD, this::historyIncrementalSearchForward); + addBuiltinWidget(widgets, HISTORY_SEARCH_BACKWARD, this::historySearchBackward); + addBuiltinWidget(widgets, HISTORY_SEARCH_FORWARD, this::historySearchForward); + addBuiltinWidget(widgets, INSERT_CLOSE_CURLY, this::insertCloseCurly); + addBuiltinWidget(widgets, INSERT_CLOSE_PAREN, this::insertCloseParen); + addBuiltinWidget(widgets, INSERT_CLOSE_SQUARE, this::insertCloseSquare); + addBuiltinWidget(widgets, INSERT_COMMENT, this::insertComment); + addBuiltinWidget(widgets, KILL_BUFFER, this::killBuffer); + addBuiltinWidget(widgets, KILL_LINE, this::killLine); + addBuiltinWidget(widgets, KILL_REGION, this::killRegion); + addBuiltinWidget(widgets, KILL_WHOLE_LINE, this::killWholeLine); + addBuiltinWidget(widgets, KILL_WORD, this::killWord); + addBuiltinWidget(widgets, LIST_CHOICES, this::listChoices); + addBuiltinWidget(widgets, MENU_COMPLETE, this::menuComplete); + addBuiltinWidget(widgets, MENU_EXPAND_OR_COMPLETE, this::menuExpandOrComplete); + addBuiltinWidget(widgets, NEG_ARGUMENT, this::negArgument); + addBuiltinWidget(widgets, OVERWRITE_MODE, this::overwriteMode); + // addBuiltinWidget(widgets, QUIT, this::quit); + addBuiltinWidget(widgets, QUOTED_INSERT, this::quotedInsert); + addBuiltinWidget(widgets, REDISPLAY, this::redisplay); + addBuiltinWidget(widgets, REDRAW_LINE, this::redrawLine); + addBuiltinWidget(widgets, REDO, this::redo); + addBuiltinWidget(widgets, SELF_INSERT, this::selfInsert); + addBuiltinWidget(widgets, SELF_INSERT_UNMETA, this::selfInsertUnmeta); + addBuiltinWidget(widgets, SEND_BREAK, this::sendBreak); + addBuiltinWidget(widgets, SET_MARK_COMMAND, this::setMarkCommand); + addBuiltinWidget(widgets, TRANSPOSE_CHARS, this::transposeChars); + addBuiltinWidget(widgets, TRANSPOSE_WORDS, this::transposeWords); + addBuiltinWidget(widgets, UNDEFINED_KEY, this::undefinedKey); + addBuiltinWidget(widgets, UNIVERSAL_ARGUMENT, this::universalArgument); + addBuiltinWidget(widgets, UNDO, this::undo); + addBuiltinWidget(widgets, UP_CASE_WORD, this::upCaseWord); + addBuiltinWidget(widgets, UP_HISTORY, this::upHistory); + addBuiltinWidget(widgets, UP_LINE, this::upLine); + addBuiltinWidget(widgets, UP_LINE_OR_HISTORY, this::upLineOrHistory); + addBuiltinWidget(widgets, UP_LINE_OR_SEARCH, this::upLineOrSearch); + addBuiltinWidget(widgets, VI_ADD_EOL, this::viAddEol); + addBuiltinWidget(widgets, VI_ADD_NEXT, this::viAddNext); + addBuiltinWidget(widgets, VI_BACKWARD_CHAR, this::viBackwardChar); + addBuiltinWidget(widgets, VI_BACKWARD_DELETE_CHAR, this::viBackwardDeleteChar); + addBuiltinWidget(widgets, VI_BACKWARD_BLANK_WORD, this::viBackwardBlankWord); + addBuiltinWidget(widgets, VI_BACKWARD_BLANK_WORD_END, this::viBackwardBlankWordEnd); + addBuiltinWidget(widgets, VI_BACKWARD_KILL_WORD, this::viBackwardKillWord); + addBuiltinWidget(widgets, VI_BACKWARD_WORD, this::viBackwardWord); + addBuiltinWidget(widgets, VI_BACKWARD_WORD_END, this::viBackwardWordEnd); + addBuiltinWidget(widgets, VI_BEGINNING_OF_LINE, this::viBeginningOfLine); + addBuiltinWidget(widgets, VI_CMD_MODE, this::viCmdMode); + addBuiltinWidget(widgets, VI_DIGIT_OR_BEGINNING_OF_LINE, this::viDigitOrBeginningOfLine); + addBuiltinWidget(widgets, VI_DOWN_LINE_OR_HISTORY, this::viDownLineOrHistory); + addBuiltinWidget(widgets, VI_CHANGE, this::viChange); + addBuiltinWidget(widgets, VI_CHANGE_EOL, this::viChangeEol); + addBuiltinWidget(widgets, VI_CHANGE_WHOLE_LINE, this::viChangeWholeLine); + addBuiltinWidget(widgets, VI_DELETE_CHAR, this::viDeleteChar); + addBuiltinWidget(widgets, VI_DELETE, this::viDelete); + addBuiltinWidget(widgets, VI_END_OF_LINE, this::viEndOfLine); + addBuiltinWidget(widgets, VI_KILL_EOL, this::viKillEol); + addBuiltinWidget(widgets, VI_FIRST_NON_BLANK, this::viFirstNonBlank); + addBuiltinWidget(widgets, VI_FIND_NEXT_CHAR, this::viFindNextChar); + addBuiltinWidget(widgets, VI_FIND_NEXT_CHAR_SKIP, this::viFindNextCharSkip); + addBuiltinWidget(widgets, VI_FIND_PREV_CHAR, this::viFindPrevChar); + addBuiltinWidget(widgets, VI_FIND_PREV_CHAR_SKIP, this::viFindPrevCharSkip); + addBuiltinWidget(widgets, VI_FORWARD_BLANK_WORD, this::viForwardBlankWord); + addBuiltinWidget(widgets, VI_FORWARD_BLANK_WORD_END, this::viForwardBlankWordEnd); + addBuiltinWidget(widgets, VI_FORWARD_CHAR, this::viForwardChar); + addBuiltinWidget(widgets, VI_FORWARD_WORD, this::viForwardWord); + addBuiltinWidget(widgets, VI_FORWARD_WORD, this::viForwardWord); + addBuiltinWidget(widgets, VI_FORWARD_WORD_END, this::viForwardWordEnd); + addBuiltinWidget(widgets, VI_HISTORY_SEARCH_BACKWARD, this::viHistorySearchBackward); + addBuiltinWidget(widgets, VI_HISTORY_SEARCH_FORWARD, this::viHistorySearchForward); + addBuiltinWidget(widgets, VI_INSERT, this::viInsert); + addBuiltinWidget(widgets, VI_INSERT_BOL, this::viInsertBol); + addBuiltinWidget(widgets, VI_INSERT_COMMENT, this::viInsertComment); + addBuiltinWidget(widgets, VI_JOIN, this::viJoin); + addBuiltinWidget(widgets, VI_KILL_LINE, this::viKillWholeLine); + addBuiltinWidget(widgets, VI_MATCH_BRACKET, this::viMatchBracket); + addBuiltinWidget(widgets, VI_OPEN_LINE_ABOVE, this::viOpenLineAbove); + addBuiltinWidget(widgets, VI_OPEN_LINE_BELOW, this::viOpenLineBelow); + addBuiltinWidget(widgets, VI_PUT_AFTER, this::viPutAfter); + addBuiltinWidget(widgets, VI_PUT_BEFORE, this::viPutBefore); + addBuiltinWidget(widgets, VI_REPEAT_FIND, this::viRepeatFind); + addBuiltinWidget(widgets, VI_REPEAT_SEARCH, this::viRepeatSearch); + addBuiltinWidget(widgets, VI_REPLACE_CHARS, this::viReplaceChars); + addBuiltinWidget(widgets, VI_REV_REPEAT_FIND, this::viRevRepeatFind); + addBuiltinWidget(widgets, VI_REV_REPEAT_SEARCH, this::viRevRepeatSearch); + addBuiltinWidget(widgets, VI_SWAP_CASE, this::viSwapCase); + addBuiltinWidget(widgets, VI_UP_LINE_OR_HISTORY, this::viUpLineOrHistory); + addBuiltinWidget(widgets, VI_YANK, this::viYankTo); + addBuiltinWidget(widgets, VI_YANK_WHOLE_LINE, this::viYankWholeLine); + addBuiltinWidget(widgets, VISUAL_LINE_MODE, this::visualLineMode); + addBuiltinWidget(widgets, VISUAL_MODE, this::visualMode); + addBuiltinWidget(widgets, WHAT_CURSOR_POSITION, this::whatCursorPosition); + addBuiltinWidget(widgets, YANK, this::yank); + addBuiltinWidget(widgets, YANK_POP, this::yankPop); + addBuiltinWidget(widgets, MOUSE, this::mouse); + addBuiltinWidget(widgets, BEGIN_PASTE, this::beginPaste); + addBuiltinWidget(widgets, FOCUS_IN, this::focusIn); + addBuiltinWidget(widgets, FOCUS_OUT, this::focusOut); + return widgets; + } + + private void addBuiltinWidget(Map widgets, String name, Widget widget) { + widgets.put(name, namedWidget("." + name, widget)); + } + + private Widget namedWidget(String name, Widget widget) { + return new Widget() { + @Override + public String toString() { + return name; + } + + @Override + public boolean apply() { + return widget.apply(); + } + }; + } + + public boolean redisplay() { + redisplay(true); + return true; + } + + protected void redisplay(boolean flush) { + try { + lock.lock(); + + if (skipRedisplay) { + skipRedisplay = false; + return; + } + + Status status = Status.getStatus(terminal, false); + if (status != null) { + status.redraw(); + } + + if (size.getRows() > 0 && size.getRows() < MIN_ROWS) { + AttributedStringBuilder sb = new AttributedStringBuilder().tabs(getTabWidth()); + + sb.append(prompt); + concat(getHighlightedBuffer(buf.toString()).columnSplitLength(Integer.MAX_VALUE), sb); + AttributedString full = sb.toAttributedString(); + + sb.setLength(0); + sb.append(prompt); + String line = buf.upToCursor(); + if (maskingCallback != null) { + line = maskingCallback.display(line); + } + + concat(new AttributedString(line).columnSplitLength(Integer.MAX_VALUE), sb); + AttributedString toCursor = sb.toAttributedString(); + + int w = WCWidth.wcwidth('…'); + int width = size.getColumns(); + int cursor = toCursor.columnLength(); + int inc = width / 2 + 1; + while (cursor <= smallTerminalOffset + w) { + smallTerminalOffset -= inc; + } + while (cursor >= smallTerminalOffset + width - w) { + smallTerminalOffset += inc; + } + if (smallTerminalOffset > 0) { + sb.setLength(0); + sb.append("…"); + sb.append(full.columnSubSequence(smallTerminalOffset + w, Integer.MAX_VALUE)); + full = sb.toAttributedString(); + } + int length = full.columnLength(); + if (length >= smallTerminalOffset + width) { + sb.setLength(0); + sb.append(full.columnSubSequence(0, width - w)); + sb.append("…"); + full = sb.toAttributedString(); + } + + display.update(Collections.singletonList(full), cursor - smallTerminalOffset, flush); + return; + } + + List secondaryPrompts = new ArrayList<>(); + AttributedString full = getDisplayedBufferWithPrompts(secondaryPrompts); + + List newLines; + if (size.getColumns() <= 0) { + newLines = new ArrayList<>(); + newLines.add(full); + } else { + newLines = full.columnSplitLength(size.getColumns(), true, display.delayLineWrap()); + } + + List rightPromptLines; + if (rightPrompt.length() == 0 || size.getColumns() <= 0) { + rightPromptLines = new ArrayList<>(); + } else { + rightPromptLines = rightPrompt.columnSplitLength(size.getColumns()); + } + while (newLines.size() < rightPromptLines.size()) { + newLines.add(new AttributedString("")); + } + for (int i = 0; i < rightPromptLines.size(); i++) { + AttributedString line = rightPromptLines.get(i); + newLines.set(i, addRightPrompt(line, newLines.get(i))); + } + + int cursorPos = -1; + int cursorNewLinesId = -1; + int cursorColPos = -1; + if (size.getColumns() > 0) { + AttributedStringBuilder sb = new AttributedStringBuilder().tabs(getTabWidth()); + sb.append(prompt); + String buffer = buf.upToCursor(); + if (maskingCallback != null) { + buffer = maskingCallback.display(buffer); + } + sb.append(insertSecondaryPrompts(new AttributedString(buffer), secondaryPrompts, false)); + List promptLines = + sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap()); + if (!promptLines.isEmpty()) { + cursorNewLinesId = promptLines.size() - 1; + cursorColPos = promptLines.get(promptLines.size() - 1).columnLength(); + cursorPos = size.cursorPos(cursorNewLinesId, cursorColPos); + } + } + + List newLinesToDisplay = new ArrayList<>(); + int displaySize = displayRows(status); + if (newLines.size() > displaySize && !isTerminalDumb()) { + StringBuilder sb = new StringBuilder(">...."); + // blanks are needed when displaying command completion candidate list + for (int i = sb.toString().length(); i < size.getColumns(); i++) { + sb.append(" "); + } + AttributedString partialCommandInfo = new AttributedString(sb.toString()); + int lineId = newLines.size() - displaySize + 1; + int endId = displaySize; + int startId = 1; + if (lineId > cursorNewLinesId) { + lineId = cursorNewLinesId; + endId = displaySize - 1; + startId = 0; + } else { + newLinesToDisplay.add(partialCommandInfo); + } + int cursorRowPos = 0; + for (int i = startId; i < endId; i++) { + if (cursorNewLinesId == lineId) { + cursorRowPos = i; + } + newLinesToDisplay.add(newLines.get(lineId++)); + } + if (startId == 0) { + newLinesToDisplay.add(partialCommandInfo); + } + cursorPos = size.cursorPos(cursorRowPos, cursorColPos); + } else { + newLinesToDisplay = newLines; + } + display.update(newLinesToDisplay, cursorPos, flush); + } finally { + lock.unlock(); + } + } + + private void concat(List lines, AttributedStringBuilder sb) { + if (lines.size() > 1) { + for (int i = 0; i < lines.size() - 1; i++) { + sb.append(lines.get(i)); + sb.style(sb.style().inverse()); + sb.append("\\n"); + sb.style(sb.style().inverseOff()); + } + } + sb.append(lines.get(lines.size() - 1)); + } + + private String matchPreviousCommand(String buffer) { + if (buffer.length() == 0) { + return ""; + } + History history = getHistory(); + StringBuilder sb = new StringBuilder(); + for (char c : buffer.replace("\\", "\\\\").toCharArray()) { + if (c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '^' || c == '*' || c == '$' + || c == '.' || c == '?' || c == '+' || c == '|' || c == '<' || c == '>' || c == '!' || c == '-') { + sb.append('\\'); + } + sb.append(c); + } + Pattern pattern = Pattern.compile(sb + ".*", Pattern.DOTALL); + Iterator iter = history.reverseIterator(history.last()); + String suggestion = ""; + int tot = 0; + while (iter.hasNext()) { + History.Entry entry = iter.next(); + Matcher matcher = pattern.matcher(entry.line()); + if (matcher.matches()) { + suggestion = entry.line().substring(buffer.length()); + break; + } else if (tot > 200) { + break; + } + tot++; + } + return suggestion; + } + + /** + * Compute the full string to be displayed with the left, right and secondary prompts + * + * @param secondaryPrompts a list to store the secondary prompts + * @return the displayed string including the buffer, left prompts and the help below + */ + public AttributedString getDisplayedBufferWithPrompts(List secondaryPrompts) { + AttributedString attBuf = getHighlightedBuffer(buf.toString()); + + AttributedString tNewBuf = insertSecondaryPrompts(attBuf, secondaryPrompts); + AttributedStringBuilder full = new AttributedStringBuilder().tabs(getTabWidth()); + full.append(prompt); + full.append(tNewBuf); + if (doAutosuggestion && !isTerminalDumb()) { + String lastBinding = getLastBinding() != null ? getLastBinding() : ""; + if (autosuggestion == SuggestionType.HISTORY) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + tailTip = matchPreviousCommand(buf.toString()); + sb.styled(AttributedStyle::faint, tailTip); + full.append(sb.toAttributedString()); + } else if (autosuggestion == SuggestionType.COMPLETER) { + if (buf.length() >= getInt(SUGGESTIONS_MIN_BUFFER_SIZE, DEFAULT_SUGGESTIONS_MIN_BUFFER_SIZE) + && buf.length() == buf.cursor() + && (!lastBinding.equals("\t") || buf.prevChar() == ' ' || buf.prevChar() == '=')) { + clearChoices(); + listChoices(true); + } else if (!lastBinding.equals("\t")) { + clearChoices(); + } + } else if (autosuggestion == SuggestionType.TAIL_TIP) { + if (buf.length() == buf.cursor()) { + if (!lastBinding.equals("\t") || buf.prevChar() == ' ') { + clearChoices(); + } + AttributedStringBuilder sb = new AttributedStringBuilder(); + if (buf.prevChar() != ' ') { + if (!tailTip.startsWith("[")) { + int idx = tailTip.indexOf(' '); + int idb = buf.toString().lastIndexOf(' '); + int idd = buf.toString().lastIndexOf('-'); + if (idx > 0 && ((idb == -1 && idb == idd) || (idb >= 0 && idb > idd))) { + tailTip = tailTip.substring(idx); + } else if (idb >= 0 && idb < idd) { + sb.append(" "); + } + } else { + sb.append(" "); + } + } + sb.styled(AttributedStyle::faint, tailTip); + full.append(sb.toAttributedString()); + } + } + } + if (post != null) { + full.append("\n"); + full.append(post.get()); + } + doAutosuggestion = true; + return full.toAttributedString(); + } + + private AttributedString getHighlightedBuffer(String buffer) { + if (maskingCallback != null) { + buffer = maskingCallback.display(buffer); + } + if (highlighter != null + && !isSet(Option.DISABLE_HIGHLIGHTER) + && buffer.length() < getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE)) { + return highlighter.highlight(this, buffer); + } + return new AttributedString(buffer); + } + + private AttributedString expandPromptPattern(String pattern, int padToWidth, String message, int line) { + ArrayList parts = new ArrayList<>(); + boolean isHidden = false; + int padPartIndex = -1; + StringBuilder padPartString = null; + StringBuilder sb = new StringBuilder(); + // Add "%{" to avoid special case for end of string. + pattern = pattern + "%{"; + int plen = pattern.length(); + int padChar = -1; + int padPos = -1; + int cols = 0; + for (int i = 0; i < plen; ) { + char ch = pattern.charAt(i++); + if (ch == '%' && i < plen) { + int count = 0; + boolean countSeen = false; + decode: + while (true) { + ch = pattern.charAt(i++); + switch (ch) { + case '{': + case '}': + String str = sb.toString(); + AttributedString astr; + if (!isHidden) { + astr = fromAnsi(str); + cols += astr.columnLength(); + } else { + astr = new AttributedString(str, AttributedStyle.HIDDEN); + } + if (padPartIndex == parts.size()) { + padPartString = sb; + if (i < plen) { + sb = new StringBuilder(); + } + } else { + sb.setLength(0); + } + parts.add(astr); + isHidden = ch == '{'; + break decode; + case '%': + sb.append(ch); + break decode; + case 'N': + sb.append(getInt(LINE_OFFSET, 0) + line); + break decode; + case 'M': + if (message != null) sb.append(message); + break decode; + case 'P': + if (countSeen && count >= 0) padToWidth = count; + if (i < plen) { + padChar = pattern.charAt(i++); + // FIXME check surrogate + } + padPos = sb.length(); + padPartIndex = parts.size(); + break decode; + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + boolean neg = false; + if (ch == '-') { + neg = true; + ch = pattern.charAt(i++); + } + countSeen = true; + count = 0; + while (ch >= '0' && ch <= '9') { + count = (count < 0 ? 0 : 10 * count) + (ch - '0'); + ch = pattern.charAt(i++); + } + if (neg) { + count = -count; + } + i--; + break; + default: + break decode; + } + } + } else sb.append(ch); + } + if (padToWidth > cols) { + int padCharCols = WCWidth.wcwidth(padChar); + int padCount = (padToWidth - cols) / padCharCols; + sb = padPartString; + while (--padCount >= 0) sb.insert(padPos, (char) padChar); // FIXME if wide + parts.set(padPartIndex, fromAnsi(sb.toString())); + } + return AttributedString.join(null, parts); + } + + private AttributedString fromAnsi(String str) { + return AttributedString.fromAnsi(str, Collections.singletonList(0), alternateIn, alternateOut); + } + + private AttributedString insertSecondaryPrompts(AttributedString str, List prompts) { + return insertSecondaryPrompts(str, prompts, true); + } + + // + // Completion + // + + private AttributedString insertSecondaryPrompts( + AttributedString strAtt, List prompts, boolean computePrompts) { + Objects.requireNonNull(prompts); + List lines = strAtt.columnSplitLength(Integer.MAX_VALUE); + AttributedStringBuilder sb = new AttributedStringBuilder(); + String secondaryPromptPattern = getString(SECONDARY_PROMPT_PATTERN, DEFAULT_SECONDARY_PROMPT_PATTERN); + boolean needsMessage = secondaryPromptPattern.contains("%M") + && strAtt.length() < getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE); + AttributedStringBuilder buf = new AttributedStringBuilder(); + int width = 0; + List missings = new ArrayList<>(); + if (computePrompts && secondaryPromptPattern.contains("%P")) { + width = prompt.columnLength(); + if (width > size.getColumns() || prompt.contains('\n')) { + width = new TerminalLine(prompt.toString(), 0, size.getColumns()) + .getEndLine() + .length(); + } + for (int line = 0; line < lines.size() - 1; line++) { + AttributedString prompt; + buf.append(lines.get(line)).append("\n"); + String missing = ""; + if (needsMessage) { + try { + parser.parse(buf.toString(), buf.length(), ParseContext.SECONDARY_PROMPT); + } catch (EOFError e) { + missing = e.getMissing(); + } catch (SyntaxError e) { + // Ignore + } + } + missings.add(missing); + prompt = expandPromptPattern(secondaryPromptPattern, 0, missing, line + 1); + width = Math.max(width, prompt.columnLength()); + } + buf.setLength(0); + } + int line = 0; + while (line < lines.size() - 1) { + sb.append(lines.get(line)).append("\n"); + buf.append(lines.get(line)).append("\n"); + AttributedString prompt; + if (computePrompts) { + String missing = ""; + if (needsMessage) { + if (missings.isEmpty()) { + try { + parser.parse(buf.toString(), buf.length(), ParseContext.SECONDARY_PROMPT); + } catch (EOFError e) { + missing = e.getMissing(); + } catch (SyntaxError e) { + // Ignore + } + } else { + missing = missings.get(line); + } + } + prompt = expandPromptPattern(secondaryPromptPattern, width, missing, line + 1); + } else { + prompt = prompts.get(line); + } + prompts.add(prompt); + sb.append(prompt); + line++; + } + sb.append(lines.get(line)); + buf.append(lines.get(line)); + return sb.toAttributedString(); + } + + private AttributedString addRightPrompt(AttributedString prompt, AttributedString line) { + int width = prompt.columnLength(); + boolean endsWithNl = line.length() > 0 && line.charAt(line.length() - 1) == '\n'; + // columnLength counts -1 for the final newline; adjust for that + int nb = size.getColumns() - width - (line.columnLength() + (endsWithNl ? 1 : 0)); + if (nb >= 3) { + AttributedStringBuilder sb = new AttributedStringBuilder(size.getColumns()); + sb.append(line, 0, endsWithNl ? line.length() - 1 : line.length()); + for (int j = 0; j < nb; j++) { + sb.append(' '); + } + sb.append(prompt); + if (endsWithNl) { + sb.append('\n'); + } + line = sb.toAttributedString(); + } + return line; + } + + protected boolean insertTab() { + return isSet(Option.INSERT_TAB) + && getLastBinding().equals("\t") + && buf.toString().matches("(^|[\\s\\S]*\n)[\r\n\t ]*"); + } + + protected boolean expandHistory() { + String str = buf.toString(); + String exp = expander.expandHistory(history, str); + if (!exp.equals(str)) { + buf.clear(); + buf.write(exp); + return true; + } else { + return false; + } + } + + protected boolean expandWord() { + if (insertTab()) { + return selfInsert(); + } else { + return doComplete(CompletionType.Expand, isSet(Option.MENU_COMPLETE), false); + } + } + + protected boolean expandOrComplete() { + if (insertTab()) { + return selfInsert(); + } else { + return doComplete(CompletionType.ExpandComplete, isSet(Option.MENU_COMPLETE), false); + } + } + + protected boolean expandOrCompletePrefix() { + if (insertTab()) { + return selfInsert(); + } else { + return doComplete(CompletionType.ExpandComplete, isSet(Option.MENU_COMPLETE), true); + } + } + + protected boolean completeWord() { + if (insertTab()) { + return selfInsert(); + } else { + return doComplete(CompletionType.Complete, isSet(Option.MENU_COMPLETE), false); + } + } + + protected boolean menuComplete() { + if (insertTab()) { + return selfInsert(); + } else { + return doComplete(CompletionType.Complete, true, false); + } + } + + protected boolean menuExpandOrComplete() { + if (insertTab()) { + return selfInsert(); + } else { + return doComplete(CompletionType.ExpandComplete, true, false); + } + } + + protected boolean completePrefix() { + if (insertTab()) { + return selfInsert(); + } else { + return doComplete(CompletionType.Complete, isSet(Option.MENU_COMPLETE), true); + } + } + + protected boolean listChoices() { + return listChoices(false); + } + + private boolean listChoices(boolean forSuggestion) { + return doComplete(CompletionType.List, isSet(Option.MENU_COMPLETE), false, forSuggestion); + } + + protected boolean deleteCharOrList() { + if (buf.cursor() != buf.length() || buf.length() == 0) { + return deleteChar(); + } else { + return doComplete(CompletionType.List, isSet(Option.MENU_COMPLETE), false); + } + } + + protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix) { + return doComplete(lst, useMenu, prefix, false); + } + + protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix, boolean forSuggestion) { + // If completion is disabled, just bail out + if (getBoolean(DISABLE_COMPLETION, false)) { + return true; + } + // Try to expand history first + // If there is actually an expansion, bail out now + if (!isSet(Option.DISABLE_EVENT_EXPANSION)) { + try { + if (expandHistory()) { + return true; + } + } catch (Exception e) { + Log.info("Error while expanding history", e); + return false; + } + } + + // Parse the command line + CompletingParsedLine line; + try { + line = wrap(parser.parse(buf.toString(), buf.cursor(), ParseContext.COMPLETE)); + } catch (Exception e) { + Log.info("Error while parsing line", e); + return false; + } + + // Find completion candidates + List candidates = new ArrayList<>(); + try { + if (completer != null) { + completer.complete(this, line, candidates); + } + } catch (Exception e) { + Log.info("Error while finding completion candidates", e); + if (Log.isDebugEnabled()) { + e.printStackTrace(); + } + return false; + } + + if (lst == CompletionType.ExpandComplete || lst == CompletionType.Expand) { + String w = expander.expandVar(line.word()); + if (!line.word().equals(w)) { + if (prefix) { + buf.backspace(line.wordCursor()); + } else { + buf.move(line.word().length() - line.wordCursor()); + buf.backspace(line.word().length()); + } + buf.write(w); + return true; + } + if (lst == CompletionType.Expand) { + return false; + } else { + lst = CompletionType.Complete; + } + } + + boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE); + int errors = getInt(ERRORS, DEFAULT_ERRORS); + + completionMatcher.compile(options, prefix, line, caseInsensitive, errors, getOriginalGroupName()); + // Find matching candidates + List possible = completionMatcher.matches(candidates); + // If we have no matches, bail out + if (possible.isEmpty()) { + return false; + } + size.copy(terminal.getSize()); + try { + // If we only need to display the list, do it now + if (lst == CompletionType.List) { + doList(possible, line.word(), false, line::escape, forSuggestion); + return !possible.isEmpty(); + } + + // Check if there's a single possible match + Candidate completion = null; + // If there's a single possible completion + if (possible.size() == 1) { + completion = possible.get(0); + } + // Or if RECOGNIZE_EXACT is set, try to find an exact match + else if (isSet(Option.RECOGNIZE_EXACT)) { + completion = completionMatcher.exactMatch(); + } + // Complete and exit + if (completion != null && !completion.value().isEmpty()) { + if (prefix) { + buf.backspace(line.rawWordCursor()); + } else { + buf.move(line.rawWordLength() - line.rawWordCursor()); + buf.backspace(line.rawWordLength()); + } + buf.write(line.escape(completion.value(), completion.complete())); + if (completion.complete()) { + if (buf.currChar() != ' ') { + buf.write(" "); + } else { + buf.move(1); + } + } + if (completion.suffix() != null) { + if (autosuggestion == SuggestionType.COMPLETER) { + listChoices(true); + } + redisplay(); + Binding op = readBinding(getKeys()); + if (op != null) { + String chars = getString(REMOVE_SUFFIX_CHARS, DEFAULT_REMOVE_SUFFIX_CHARS); + String ref = op instanceof Reference ? ((Reference) op).name() : null; + if (SELF_INSERT.equals(ref) + && chars.indexOf(getLastBinding().charAt(0)) >= 0 + || ACCEPT_LINE.equals(ref)) { + buf.backspace(completion.suffix().length()); + if (getLastBinding().charAt(0) != ' ') { + buf.write(' '); + } + } + pushBackBinding(true); + } + } + return true; + } + + if (useMenu) { + buf.move(line.word().length() - line.wordCursor()); + buf.backspace(line.word().length()); + doMenu(possible, line.word(), line::escape); + return true; + } + + // Find current word and move to end + String current; + if (prefix) { + current = line.word().substring(0, line.wordCursor()); + } else { + current = line.word(); + buf.move(line.rawWordLength() - line.rawWordCursor()); + } + // Now, we need to find the unambiguous completion + // TODO: need to find common suffix + String commonPrefix = completionMatcher.getCommonPrefix(); + boolean hasUnambiguous = commonPrefix.startsWith(current) && !commonPrefix.equals(current); + + if (hasUnambiguous) { + buf.backspace(line.rawWordLength()); + buf.write(line.escape(commonPrefix, false)); + callWidget(REDISPLAY); + current = commonPrefix; + if ((!isSet(Option.AUTO_LIST) && isSet(Option.AUTO_MENU)) + || (isSet(Option.AUTO_LIST) && isSet(Option.LIST_AMBIGUOUS))) { + if (!nextBindingIsComplete()) { + return true; + } + } + } + if (isSet(Option.AUTO_LIST)) { + if (!doList(possible, current, true, line::escape)) { + return true; + } + } + if (isSet(Option.AUTO_MENU)) { + buf.backspace(current.length()); + doMenu(possible, line.word(), line::escape); + } + return true; + } finally { + size.copy(terminal.getBufferSize()); + } + } + + protected Comparator getCandidateComparator(boolean caseInsensitive, String word) { + String wdi = caseInsensitive ? word.toLowerCase() : word; + ToIntFunction wordDistance = w -> ReaderUtils.distance(wdi, caseInsensitive ? w.toLowerCase() : w); + return Comparator.comparing(Candidate::value, Comparator.comparingInt(wordDistance)) + .thenComparing(Comparator.naturalOrder()); + } + + protected String getOthersGroupName() { + return getString(OTHERS_GROUP_NAME, DEFAULT_OTHERS_GROUP_NAME); + } + + protected String getOriginalGroupName() { + return getString(ORIGINAL_GROUP_NAME, DEFAULT_ORIGINAL_GROUP_NAME); + } + + protected Comparator getGroupComparator() { + return Comparator.comparingInt(s -> getOthersGroupName().equals(s) + ? 1 + : getOriginalGroupName().equals(s) ? -1 : 0) + .thenComparing(String::toLowerCase, Comparator.naturalOrder()); + } + + private void mergeCandidates(List possible) { + // Merge candidates if the have the same key + Map> keyedCandidates = new HashMap<>(); + for (Candidate candidate : possible) { + if (candidate.key() != null) { + List cands = keyedCandidates.computeIfAbsent(candidate.key(), s -> new ArrayList<>()); + cands.add(candidate); + } + } + if (!keyedCandidates.isEmpty()) { + for (List candidates : keyedCandidates.values()) { + if (candidates.size() >= 1) { + possible.removeAll(candidates); + // Candidates with the same key are supposed to have + // the same description + candidates.sort(Comparator.comparing(Candidate::value)); + Candidate first = candidates.get(0); + String disp = candidates.stream().map(Candidate::displ).collect(Collectors.joining(" ")); + possible.add(new Candidate( + first.value(), disp, first.group(), first.descr(), first.suffix(), null, first.complete())); + } + } + } + } + + protected boolean nextBindingIsComplete() { + redisplay(); + KeyMap keyMap = keyMaps.get(MENU); + Binding operation = readBinding(getKeys(), keyMap); + if (operation instanceof Reference && MENU_COMPLETE.equals(((Reference) operation).name())) { + return true; + } else { + pushBackBinding(); + return false; + } + } + + private int displayRows() { + return displayRows(Status.getStatus(terminal, false)); + } + + private int displayRows(Status status) { + return size.getRows() - (status != null ? status.size() : 0); + } + + private int visibleDisplayRows() { + Status status = Status.getStatus(terminal, false); + return terminal.getSize().getRows() - (status != null ? status.size() : 0); + } + + private int promptLines() { + AttributedString text = + insertSecondaryPrompts(AttributedStringBuilder.append(prompt, buf.toString()), new ArrayList<>()); + return text.columnSplitLength(size.getColumns(), false, display.delayLineWrap()) + .size(); + } + + protected boolean doMenu( + List original, String completed, BiFunction escaper) { + // Reorder candidates according to display order + final List possible = new ArrayList<>(); + boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE); + original.sort(getCandidateComparator(caseInsensitive, completed)); + mergeCandidates(original); + computePost(original, null, possible, completed); + // candidate grouping is not supported by MenuSupport + boolean defaultAutoGroup = isSet(Option.AUTO_GROUP); + boolean defaultGroup = isSet(Option.GROUP); + if (!isSet(Option.GROUP_PERSIST)) { + option(Option.AUTO_GROUP, false); + option(Option.GROUP, false); + } + // Build menu support + MenuSupport menuSupport = new MenuSupport(original, completed, escaper); + post = menuSupport; + callWidget(REDISPLAY); + + // Loop + KeyMap keyMap = keyMaps.get(MENU); + Binding operation; + while ((operation = readBinding(getKeys(), keyMap)) != null) { + String ref = (operation instanceof Reference) ? ((Reference) operation).name() : ""; + switch (ref) { + case MENU_COMPLETE: + menuSupport.next(); + break; + case REVERSE_MENU_COMPLETE: + menuSupport.previous(); + break; + case UP_LINE_OR_HISTORY: + case UP_LINE_OR_SEARCH: + menuSupport.up(); + break; + case DOWN_LINE_OR_HISTORY: + case DOWN_LINE_OR_SEARCH: + menuSupport.down(); + break; + case FORWARD_CHAR: + menuSupport.right(); + break; + case BACKWARD_CHAR: + menuSupport.left(); + break; + case CLEAR_SCREEN: + clearScreen(); + break; + default: { + Candidate completion = menuSupport.completion(); + if (completion.suffix() != null) { + String chars = getString(REMOVE_SUFFIX_CHARS, DEFAULT_REMOVE_SUFFIX_CHARS); + if (SELF_INSERT.equals(ref) + && chars.indexOf(getLastBinding().charAt(0)) >= 0 + || BACKWARD_DELETE_CHAR.equals(ref)) { + buf.backspace(completion.suffix().length()); + } + } + if (completion.complete() + && getLastBinding().charAt(0) != ' ' + && (SELF_INSERT.equals(ref) || getLastBinding().charAt(0) != ' ')) { + buf.write(' '); + } + if (!ACCEPT_LINE.equals(ref) + && !(SELF_INSERT.equals(ref) + && completion.suffix() != null + && completion.suffix().startsWith(getLastBinding()))) { + pushBackBinding(true); + } + post = null; + option(Option.AUTO_GROUP, defaultAutoGroup); + option(Option.GROUP, defaultGroup); + return true; + } + } + doAutosuggestion = false; + callWidget(REDISPLAY); + } + option(Option.AUTO_GROUP, defaultAutoGroup); + option(Option.GROUP, defaultGroup); + return false; + } + + protected boolean clearChoices() { + return doList(new ArrayList<>(), "", false, null, false); + } + + protected boolean doList( + List possible, + String completed, + boolean runLoop, + BiFunction escaper) { + return doList(possible, completed, runLoop, escaper, false); + } + + protected boolean doList( + List possible, + String completed, + boolean runLoop, + BiFunction escaper, + boolean forSuggestion) { + // If we list only and if there's a big + // number of items, we should ask the user + // for confirmation, display the list + // and redraw the line at the bottom + mergeCandidates(possible); + AttributedString text = + insertSecondaryPrompts(AttributedStringBuilder.append(prompt, buf.toString()), new ArrayList<>()); + int promptLines = text.columnSplitLength(size.getColumns(), false, display.delayLineWrap()) + .size(); + PostResult postResult = computePost(possible, null, null, completed); + int lines = postResult.lines; + int listMax = getInt(LIST_MAX, DEFAULT_LIST_MAX); + int possibleSize = possible.size(); + if (possibleSize == 0 || size.getRows() == 0) { + return false; + } + if (listMax > 0 && possibleSize >= listMax || lines >= size.getRows() - promptLines) { + if (!forSuggestion) { + // prompt + post = () -> new AttributedString(getAppName() + ": do you wish to see all " + possibleSize + + " possibilities (" + lines + " lines)?"); + redisplay(true); + int c = readCharacter(); + if (c != 'y' && c != 'Y' && c != '\t') { + post = null; + return false; + } + } else { + return false; + } + } + + boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE); + StringBuilder sb = new StringBuilder(); + candidateStartPosition = 0; + while (true) { + String current = completed + sb; + List cands; + if (sb.length() > 0) { + completionMatcher.compile(options, false, new CompletingWord(current), caseInsensitive, 0, null); + cands = completionMatcher.matches(possible).stream() + .sorted(getCandidateComparator(caseInsensitive, current)) + .collect(Collectors.toList()); + } else { + cands = possible.stream() + .sorted(getCandidateComparator(caseInsensitive, current)) + .collect(Collectors.toList()); + } + if (isSet(Option.AUTO_MENU_LIST) && candidateStartPosition == 0) { + candidateStartPosition = candidateStartPosition(cands); + } + post = () -> { + AttributedString t = insertSecondaryPrompts( + AttributedStringBuilder.append(prompt, buf.toString()), new ArrayList<>()); + int pl = t.columnSplitLength(size.getColumns(), false, display.delayLineWrap()) + .size(); + PostResult pr = computePost(cands, null, null, current); + if (pr.lines >= size.getRows() - pl) { + post = null; + int oldCursor = buf.cursor(); + buf.cursor(buf.length()); + redisplay(false); + buf.cursor(oldCursor); + println(); + List ls = + pr.post.columnSplitLength(size.getColumns(), false, display.delayLineWrap()); + Display d = new Display(terminal, false); + d.resize(size.getRows(), size.getColumns()); + d.update(ls, -1); + println(); + redrawLine(); + return new AttributedString(""); + } + return pr.post; + }; + if (!runLoop) { + return false; + } + redisplay(); + // TODO: use a different keyMap ? + Binding b = doReadBinding(getKeys(), null); + if (b instanceof Reference) { + String name = ((Reference) b).name(); + if (BACKWARD_DELETE_CHAR.equals(name) || VI_BACKWARD_DELETE_CHAR.equals(name)) { + if (sb.length() == 0) { + pushBackBinding(); + post = null; + return false; + } else { + sb.setLength(sb.length() - 1); + buf.backspace(); + } + } else if (SELF_INSERT.equals(name)) { + sb.append(getLastBinding()); + callWidget(name); + if (cands.isEmpty()) { + post = null; + return false; + } + } else if ("\t".equals(getLastBinding())) { + if (cands.size() == 1 || sb.length() > 0) { + post = null; + pushBackBinding(); + } else if (isSet(Option.AUTO_MENU)) { + buf.backspace(escaper.apply(current, false).length()); + doMenu(cands, current, escaper); + } + return false; + } else { + pushBackBinding(); + post = null; + return false; + } + } else if (b == null) { + post = null; + return false; + } + } + } + + protected PostResult computePost( + List possible, Candidate selection, List ordered, String completed) { + return computePost( + possible, + selection, + ordered, + completed, + display::wcwidth, + size.getColumns(), + isSet(Option.AUTO_GROUP), + isSet(Option.GROUP), + isSet(Option.LIST_ROWS_FIRST)); + } + + protected PostResult computePost( + List possible, + Candidate selection, + List ordered, + String completed, + Function wcwidth, + int width, + boolean autoGroup, + boolean groupName, + boolean rowsFirst) { + List strings = new ArrayList<>(); + if (groupName) { + Comparator groupComparator = getGroupComparator(); + Map> sorted; + sorted = groupComparator != null ? new TreeMap<>(groupComparator) : new LinkedHashMap<>(); + for (Candidate cand : possible) { + String group = cand.group(); + sorted.computeIfAbsent(group != null ? group : "", s -> new ArrayList<>()) + .add(cand); + } + for (Map.Entry> entry : sorted.entrySet()) { + String group = entry.getKey(); + if (group.isEmpty() && sorted.size() > 1) { + group = getOthersGroupName(); + } + if (!group.isEmpty() && autoGroup) { + strings.add(group); + } + List candidates = entry.getValue(); + Collections.sort(candidates); + strings.add(candidates); + if (ordered != null) { + ordered.addAll(candidates); + } + } + } else { + Set groups = new LinkedHashSet<>(); + List sorted = new ArrayList<>(); + for (Candidate cand : possible) { + String group = cand.group(); + if (group != null) { + groups.add(group); + } + sorted.add(cand); + } + if (autoGroup) { + strings.addAll(groups); + } + Collections.sort(sorted); + strings.add(sorted); + if (ordered != null) { + ordered.addAll(sorted); + } + } + return toColumns(strings, selection, completed, wcwidth, width, rowsFirst); + } + + private int candidateStartPosition(List cands) { + List values = cands.stream() + .map(c -> AttributedString.stripAnsi(c.displ())) + .filter(c -> !c.matches("\\w+") && c.length() > 1) + .collect(Collectors.toList()); + Set notDelimiters = new HashSet<>(); + values.forEach(v -> v.substring(0, v.length() - 1) + .chars() + .filter(c -> !Character.isDigit(c) && !Character.isAlphabetic(c)) + .forEach(c -> notDelimiters.add(Character.toString((char) c)))); + int width = size.getColumns(); + int promptLength = prompt != null ? prompt.length() : 0; + if (promptLength > 0) { + TerminalLine tp = new TerminalLine(prompt.toString(), 0, width); + promptLength = tp.getEndLine().length(); + } + TerminalLine tl = new TerminalLine(buf.substring(0, buf.cursor()), promptLength, width); + int out = tl.getStartPos(); + String buffer = tl.getEndLine(); + for (int i = buffer.length(); i > 0; i--) { + if (buffer.substring(0, i).matches(".*\\W") && !notDelimiters.contains(buffer.substring(i - 1, i))) { + out += i; + break; + } + } + return out; + } + + @SuppressWarnings("unchecked") + protected PostResult toColumns( + List items, + Candidate selection, + String completed, + Function wcwidth, + int width, + boolean rowsFirst) { + int[] out = new int[2]; + // TODO: support Option.LIST_PACKED + // Compute column width + int maxWidth = 0; + int listSize = 0; + for (Object item : items) { + if (item instanceof String) { + int len = wcwidth.apply((String) item); + maxWidth = Math.max(maxWidth, len); + } else if (item instanceof List) { + for (Candidate cand : (List) item) { + listSize++; + int len = wcwidth.apply(cand.displ()); + if (cand.descr() != null) { + len += MARGIN_BETWEEN_DISPLAY_AND_DESC; + len += DESC_PREFIX.length(); + len += wcwidth.apply(cand.descr()); + len += DESC_SUFFIX.length(); + } + maxWidth = Math.max(maxWidth, len); + } + } + } + // Build columns + AttributedStringBuilder sb = new AttributedStringBuilder(); + if (listSize > 0) { + if (isSet(Option.AUTO_MENU_LIST) + && listSize + < Math.min( + getInt(MENU_LIST_MAX, DEFAULT_MENU_LIST_MAX), + visibleDisplayRows() - promptLines())) { + maxWidth = Math.max(maxWidth, MENU_LIST_WIDTH); + sb.tabs(Math.max(Math.min(candidateStartPosition, width - maxWidth - 1), 1)); + width = maxWidth + 2; + if (!isSet(Option.GROUP_PERSIST)) { + List list = new ArrayList<>(); + for (Object o : items) { + if (o instanceof Collection) { + list.addAll((Collection) o); + } + } + list = list.stream() + .sorted(getCandidateComparator(isSet(Option.CASE_INSENSITIVE), "")) + .collect(Collectors.toList()); + toColumns(list, width, maxWidth, sb, selection, completed, rowsFirst, true, out); + } else { + for (Object list : items) { + toColumns(list, width, maxWidth, sb, selection, completed, rowsFirst, true, out); + } + } + } else { + for (Object list : items) { + toColumns(list, width, maxWidth, sb, selection, completed, rowsFirst, false, out); + } + } + } + if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\n') { + sb.setLength(sb.length() - 1); + } + return new PostResult(sb.toAttributedString(), out[0], out[1]); + } + + @SuppressWarnings("unchecked") + protected void toColumns( + Object items, + int width, + int maxWidth, + AttributedStringBuilder sb, + Candidate selection, + String completed, + boolean rowsFirst, + boolean doMenuList, + int[] out) { + if (maxWidth <= 0 || width <= 0) { + return; + } + // This is a group + if (items instanceof String) { + if (doMenuList) { + sb.style(AttributedStyle.DEFAULT); + sb.append('\t'); + } + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.style(getCompletionStyleGroup(doMenuList)) + .append((String) items) + .style(AttributedStyle.DEFAULT); + if (doMenuList) { + for (int k = ((String) items).length(); k < maxWidth + 1; k++) { + asb.append(' '); + } + } + sb.style(getCompletionStyleBackground(doMenuList)); + sb.append(asb); + sb.append("\n"); + out[0]++; + } + // This is a Candidate list + else if (items instanceof List) { + List candidates = (List) items; + maxWidth = Math.min(width, maxWidth); + int c = width / maxWidth; + while (c > 1 && c * maxWidth + (c - 1) * MARGIN_BETWEEN_COLUMNS >= width) { + c--; + } + int lines = (candidates.size() + c - 1) / c; + // Try to minimize the number of columns for the given number of rows + // Prevents eg 9 candiates being split 6/3 instead of 5/4. + final int columns = (candidates.size() + lines - 1) / lines; + IntBinaryOperator index; + if (rowsFirst) { + index = (i, j) -> i * columns + j; + } else { + index = (i, j) -> j * lines + i; + } + for (int i = 0; i < lines; i++) { + if (doMenuList) { + sb.style(AttributedStyle.DEFAULT); + sb.append('\t'); + } + AttributedStringBuilder asb = new AttributedStringBuilder(); + for (int j = 0; j < columns; j++) { + int idx = index.applyAsInt(i, j); + if (idx < candidates.size()) { + Candidate cand = candidates.get(idx); + boolean hasRightItem = j < columns - 1 && index.applyAsInt(i, j + 1) < candidates.size(); + AttributedString left = fromAnsi(cand.displ()); + AttributedString right = fromAnsi(cand.descr()); + int lw = left.columnLength(); + int rw = 0; + if (right != null) { + int rem = maxWidth + - (lw + + MARGIN_BETWEEN_DISPLAY_AND_DESC + + DESC_PREFIX.length() + + DESC_SUFFIX.length()); + rw = right.columnLength(); + if (rw > rem) { + right = AttributedStringBuilder.append( + right.columnSubSequence(0, rem - WCWidth.wcwidth('…')), "…"); + rw = right.columnLength(); + } + right = AttributedStringBuilder.append(DESC_PREFIX, right, DESC_SUFFIX); + rw += DESC_PREFIX.length() + DESC_SUFFIX.length(); + } + if (cand == selection) { + out[1] = i; + asb.style(getCompletionStyleSelection(doMenuList)); + if (left.toString() + .regionMatches( + isSet(Option.CASE_INSENSITIVE), 0, completed, 0, completed.length())) { + asb.append(left.toString(), 0, completed.length()); + asb.append(left.toString(), completed.length(), left.length()); + } else { + asb.append(left.toString()); + } + for (int k = 0; k < maxWidth - lw - rw; k++) { + asb.append(' '); + } + if (right != null) { + asb.append(right); + } + asb.style(AttributedStyle.DEFAULT); + } else { + if (left.toString() + .regionMatches( + isSet(Option.CASE_INSENSITIVE), 0, completed, 0, completed.length())) { + asb.style(getCompletionStyleStarting(doMenuList)); + asb.append(left, 0, completed.length()); + asb.style(AttributedStyle.DEFAULT); + asb.append(left, completed.length(), left.length()); + } else { + asb.append(left); + } + if (right != null || hasRightItem) { + for (int k = 0; k < maxWidth - lw - rw; k++) { + asb.append(' '); + } + } + if (right != null) { + asb.style(getCompletionStyleDescription(doMenuList)); + asb.append(right); + asb.style(AttributedStyle.DEFAULT); + } else if (doMenuList) { + for (int k = lw; k < maxWidth; k++) { + asb.append(' '); + } + } + } + if (hasRightItem) { + for (int k = 0; k < MARGIN_BETWEEN_COLUMNS; k++) { + asb.append(' '); + } + } + if (doMenuList) { + asb.append(' '); + } + } + } + sb.style(getCompletionStyleBackground(doMenuList)); + sb.append(asb); + sb.append('\n'); + } + out[0] += lines; + } + } + + protected AttributedStyle getCompletionStyleStarting(boolean menuList) { + return menuList ? getCompletionStyleListStarting() : getCompletionStyleStarting(); + } + + protected AttributedStyle getCompletionStyleDescription(boolean menuList) { + return menuList ? getCompletionStyleListDescription() : getCompletionStyleDescription(); + } + + protected AttributedStyle getCompletionStyleGroup(boolean menuList) { + return menuList ? getCompletionStyleListGroup() : getCompletionStyleGroup(); + } + + protected AttributedStyle getCompletionStyleSelection(boolean menuList) { + return menuList ? getCompletionStyleListSelection() : getCompletionStyleSelection(); + } + + protected AttributedStyle getCompletionStyleBackground(boolean menuList) { + return menuList ? getCompletionStyleListBackground() : getCompletionStyleBackground(); + } + + protected AttributedStyle getCompletionStyleStarting() { + return getCompletionStyle(COMPLETION_STYLE_STARTING, DEFAULT_COMPLETION_STYLE_STARTING); + } + + protected AttributedStyle getCompletionStyleDescription() { + return getCompletionStyle(COMPLETION_STYLE_DESCRIPTION, DEFAULT_COMPLETION_STYLE_DESCRIPTION); + } + + protected AttributedStyle getCompletionStyleGroup() { + return getCompletionStyle(COMPLETION_STYLE_GROUP, DEFAULT_COMPLETION_STYLE_GROUP); + } + + protected AttributedStyle getCompletionStyleSelection() { + return getCompletionStyle(COMPLETION_STYLE_SELECTION, DEFAULT_COMPLETION_STYLE_SELECTION); + } + + protected AttributedStyle getCompletionStyleBackground() { + return getCompletionStyle(COMPLETION_STYLE_BACKGROUND, DEFAULT_COMPLETION_STYLE_BACKGROUND); + } + + protected AttributedStyle getCompletionStyleListStarting() { + return getCompletionStyle(COMPLETION_STYLE_LIST_STARTING, DEFAULT_COMPLETION_STYLE_LIST_STARTING); + } + + protected AttributedStyle getCompletionStyleListDescription() { + return getCompletionStyle(COMPLETION_STYLE_LIST_DESCRIPTION, DEFAULT_COMPLETION_STYLE_LIST_DESCRIPTION); + } + + protected AttributedStyle getCompletionStyleListGroup() { + return getCompletionStyle(COMPLETION_STYLE_LIST_GROUP, DEFAULT_COMPLETION_STYLE_LIST_GROUP); + } + + protected AttributedStyle getCompletionStyleListSelection() { + return getCompletionStyle(COMPLETION_STYLE_LIST_SELECTION, DEFAULT_COMPLETION_STYLE_LIST_SELECTION); + } + + protected AttributedStyle getCompletionStyleListBackground() { + return getCompletionStyle(COMPLETION_STYLE_LIST_BACKGROUND, DEFAULT_COMPLETION_STYLE_LIST_BACKGROUND); + } + + protected AttributedStyle getCompletionStyle(String name, String value) { + return new StyleResolver(s -> getString(s, null)).resolve("." + name, value); + } + + protected AttributedStyle buildStyle(String str) { + return fromAnsi("\u001b[" + str + "m ").styleAt(0); + } + + /** + * Used in "vi" mode for argumented history move, to move a specific + * number of history entries forward or back. + * + * @param next If true, move forward + * @param count The number of entries to move + * @return true if the move was successful + */ + protected boolean moveHistory(final boolean next, int count) { + boolean ok = true; + for (int i = 0; i < count && (ok = moveHistory(next)); i++) { + /* empty */ + } + return ok; + } + + /** + * Move up or down the history tree. + * + * @param next true to go to the next, false for the previous. + * @return true if successful, false otherwise + */ + protected boolean moveHistory(final boolean next) { + if (!buf.toString().equals(history.current())) { + modifiedHistory.put(history.index(), buf.toString()); + } + if (next && !history.next()) { + return false; + } else if (!next && !history.previous()) { + return false; + } + + setBuffer( + modifiedHistory.containsKey(history.index()) + ? modifiedHistory.get(history.index()) + : history.current()); + + return true; + } + + /** + * Raw output printing. + * + * @param str the string to print to the terminal + */ + void print(String str) { + terminal.writer().write(str); + } + + void println(String s) { + print(s); + println(); + } + + /** + * Output a platform-dependant newline. + */ + void println() { + terminal.puts(Capability.carriage_return); + print("\n"); + redrawLine(); + } + + protected boolean killBuffer() { + killRing.add(buf.toString()); + buf.clear(); + return true; + } + + protected boolean killWholeLine() { + if (buf.length() == 0) { + return false; + } + int start; + int end; + if (count < 0) { + end = buf.cursor(); + while (buf.atChar(end) != 0 && buf.atChar(end) != '\n') { + end++; + } + start = end; + for (int count = -this.count; count > 0; --count) { + while (start > 0 && buf.atChar(start - 1) != '\n') { + start--; + } + start--; + } + } else { + start = buf.cursor(); + while (start > 0 && buf.atChar(start - 1) != '\n') { + start--; + } + end = start; + while (count-- > 0) { + while (end < buf.length() && buf.atChar(end) != '\n') { + end++; + } + if (end < buf.length()) { + end++; + } + } + } + String killed = buf.substring(start, end); + buf.cursor(start); + buf.delete(end - start); + killRing.add(killed); + return true; + } + + /** + * Kill the buffer ahead of the current cursor position. + * + * @return true if successful + */ + public boolean killLine() { + if (count < 0) { + return callNeg(this::backwardKillLine); + } + if (buf.cursor() == buf.length()) { + return false; + } + int cp = buf.cursor(); + int len = cp; + while (count-- > 0) { + if (buf.atChar(len) == '\n') { + len++; + } else { + while (buf.atChar(len) != 0 && buf.atChar(len) != '\n') { + len++; + } + } + } + int num = len - cp; + String killed = buf.substring(cp, cp + num); + buf.delete(num); + killRing.add(killed); + return true; + } + + public boolean backwardKillLine() { + if (count < 0) { + return callNeg(this::killLine); + } + if (buf.cursor() == 0) { + return false; + } + int cp = buf.cursor(); + int beg = cp; + while (count-- > 0) { + if (beg == 0) { + break; + } + if (buf.atChar(beg - 1) == '\n') { + beg--; + } else { + while (beg > 0 && buf.atChar(beg - 1) != 0 && buf.atChar(beg - 1) != '\n') { + beg--; + } + } + } + int num = cp - beg; + String killed = buf.substring(cp - beg, cp); + buf.cursor(beg); + buf.delete(num); + killRing.add(killed); + return true; + } + + public boolean killRegion() { + return doCopyKillRegion(true); + } + + public boolean copyRegionAsKill() { + return doCopyKillRegion(false); + } + + // + // Printing + // + + private boolean doCopyKillRegion(boolean kill) { + if (regionMark > buf.length()) { + regionMark = buf.length(); + } + if (regionActive == RegionType.LINE) { + int start = regionMark; + int end = buf.cursor(); + if (start < end) { + while (start > 0 && buf.atChar(start - 1) != '\n') { + start--; + } + while (end < buf.length() - 1 && buf.atChar(end + 1) != '\n') { + end++; + } + if (isInViCmdMode()) { + end++; + } + killRing.add(buf.substring(start, end)); + if (kill) { + buf.backspace(end - start); + } + } else { + while (end > 0 && buf.atChar(end - 1) != '\n') { + end--; + } + while (start < buf.length() && buf.atChar(start) != '\n') { + start++; + } + if (isInViCmdMode()) { + start++; + } + killRing.addBackwards(buf.substring(end, start)); + if (kill) { + buf.cursor(end); + buf.delete(start - end); + } + } + } else if (regionMark > buf.cursor()) { + if (isInViCmdMode()) { + regionMark++; + } + killRing.add(buf.substring(buf.cursor(), regionMark)); + if (kill) { + buf.delete(regionMark - buf.cursor()); + } + } else { + if (isInViCmdMode()) { + buf.move(1); + } + killRing.add(buf.substring(regionMark, buf.cursor())); + if (kill) { + buf.backspace(buf.cursor() - regionMark); + } + } + if (kill) { + regionActive = RegionType.NONE; + } + return true; + } + + public boolean yank() { + String yanked = killRing.yank(); + if (yanked == null) { + return false; + } else { + putString(yanked); + return true; + } + } + + public boolean yankPop() { + if (!killRing.lastYank()) { + return false; + } + String current = killRing.yank(); + if (current == null) { + // This shouldn't happen. + return false; + } + buf.backspace(current.length()); + String yanked = killRing.yankPop(); + if (yanked == null) { + // This shouldn't happen. + return false; + } + + putString(yanked); + return true; + } + + // + // Actions + // + + public boolean mouse() { + MouseEvent event = readMouseEvent(); + if (event.getType() == MouseEvent.Type.Released && event.getButton() == MouseEvent.Button.Button1) { + StringBuilder tsb = new StringBuilder(); + Cursor cursor = terminal.getCursorPosition(c -> tsb.append((char) c)); + bindingReader.runMacro(tsb.toString()); + + List secondaryPrompts = new ArrayList<>(); + getDisplayedBufferWithPrompts(secondaryPrompts); + + AttributedStringBuilder sb = new AttributedStringBuilder().tabs(getTabWidth()); + sb.append(prompt); + sb.append(insertSecondaryPrompts(new AttributedString(buf.upToCursor()), secondaryPrompts, false)); + List promptLines = + sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap()); + + int currentLine = promptLines.size() - 1; + int wantedLine = Math.max(0, Math.min(currentLine + event.getY() - cursor.getY(), secondaryPrompts.size())); + int pl0 = currentLine == 0 + ? prompt.columnLength() + : secondaryPrompts.get(currentLine - 1).columnLength(); + int pl1 = wantedLine == 0 + ? prompt.columnLength() + : secondaryPrompts.get(wantedLine - 1).columnLength(); + int adjust = pl1 - pl0; + buf.moveXY(event.getX() - cursor.getX() - adjust, event.getY() - cursor.getY()); + } + return true; + } + + public boolean beginPaste() { + String str = doReadStringUntil(BRACKETED_PASTE_END); + regionActive = RegionType.PASTE; + regionMark = getBuffer().cursor(); + getBuffer().write(str.replace('\r', '\n')); + return true; + } + + public boolean focusIn() { + return false; + } + + public boolean focusOut() { + return false; + } + + /** + * Clean the used display + * + * @return true + */ + public boolean clear() { + display.update(Collections.emptyList(), 0); + return true; + } + + /** + * Clear the screen by issuing the ANSI "clear screen" code. + * + * @return true + */ + public boolean clearScreen() { + if (terminal.puts(Capability.clear_screen)) { + // ConEMU extended fonts support + if (AbstractWindowsTerminal.TYPE_WINDOWS_CONEMU.equals(terminal.getType()) + && !Boolean.getBoolean("org.jline.terminal.conemu.disable-activate")) { + terminal.writer().write("\u001b[9999E"); + } + Status status = Status.getStatus(terminal, false); + if (status != null) { + status.reset(); + } + redrawLine(); + } else { + println(); + } + return true; + } + + /** + * Issue an audible keyboard bell. + * + * @return true + */ + public boolean beep() { + BellType bell_preference = BellType.AUDIBLE; + switch (getString(BELL_STYLE, DEFAULT_BELL_STYLE).toLowerCase()) { + case "none": + case "off": + bell_preference = BellType.NONE; + break; + case "audible": + bell_preference = BellType.AUDIBLE; + break; + case "visible": + bell_preference = BellType.VISIBLE; + break; + case "on": + bell_preference = getBoolean(PREFER_VISIBLE_BELL, false) ? BellType.VISIBLE : BellType.AUDIBLE; + break; + } + if (bell_preference == BellType.VISIBLE) { + if (terminal.puts(Capability.flash_screen) || terminal.puts(Capability.bell)) { + flush(); + } + } else if (bell_preference == BellType.AUDIBLE) { + if (terminal.puts(Capability.bell)) { + flush(); + } + } + return true; + } + + /** + * Checks to see if the specified character is a delimiter. We consider a + * character a delimiter if it is anything but a letter or digit. + * + * @param c The character to test + * @return True if it is a delimiter + */ + protected boolean isDelimiter(int c) { + return !Character.isLetterOrDigit(c); + } + + /** + * Checks to see if a character is a whitespace character. Currently + * this delegates to {@link Character#isWhitespace(char)}, however + * eventually it should be hooked up so that the definition of whitespace + * can be configured, as readline does. + * + * @param c The character to check + * @return true if the character is a whitespace + */ + protected boolean isWhitespace(int c) { + return Character.isWhitespace(c); + } + + protected boolean isViAlphaNum(int c) { + return c == '_' || Character.isLetterOrDigit(c); + } + + protected boolean isAlpha(int c) { + return Character.isLetter(c); + } + + protected boolean isWord(int c) { + String wordchars = getString(WORDCHARS, DEFAULT_WORDCHARS); + return Character.isLetterOrDigit(c) || (c < 128 && wordchars.indexOf((char) c) >= 0); + } + + String getString(String name, String def) { + return ReaderUtils.getString(this, name, def); + } + + boolean getBoolean(String name, boolean def) { + return ReaderUtils.getBoolean(this, name, def); + } + + int getInt(String name, int def) { + return ReaderUtils.getInt(this, name, def); + } + + long getLong(String name, long def) { + return ReaderUtils.getLong(this, name, def); + } + + // + // Helpers + // + + @Override + public Map> defaultKeyMaps() { + Map> keyMaps = new HashMap<>(); + keyMaps.put(EMACS, emacs()); + keyMaps.put(VICMD, viCmd()); + keyMaps.put(VIINS, viInsertion()); + keyMaps.put(MENU, menu()); + keyMaps.put(VIOPP, viOpp()); + keyMaps.put(VISUAL, visual()); + keyMaps.put(SAFE, safe()); + keyMaps.put(DUMB, dumb()); + + if (getBoolean(BIND_TTY_SPECIAL_CHARS, true)) { + Attributes attr = terminal.getAttributes(); + bindConsoleChars(keyMaps.get(EMACS), attr); + bindConsoleChars(keyMaps.get(VIINS), attr); + } + // Put default + for (KeyMap keyMap : keyMaps.values()) { + keyMap.setUnicode(new Reference(SELF_INSERT)); + keyMap.setAmbiguousTimeout(getLong(AMBIGUOUS_BINDING, DEFAULT_AMBIGUOUS_BINDING)); + } + // By default, link main to emacs unless the temrinal is dumb + keyMaps.put(MAIN, keyMaps.get(isTerminalDumb() ? DUMB : EMACS)); + + return keyMaps; + } + + public KeyMap emacs() { + KeyMap emacs = new KeyMap<>(); + bindKeys(emacs); + bind(emacs, SET_MARK_COMMAND, ctrl('@')); + bind(emacs, BEGINNING_OF_LINE, ctrl('A')); + bind(emacs, BACKWARD_CHAR, ctrl('B')); + bind(emacs, DELETE_CHAR_OR_LIST, ctrl('D')); + bind(emacs, END_OF_LINE, ctrl('E')); + bind(emacs, FORWARD_CHAR, ctrl('F')); + bind(emacs, SEND_BREAK, ctrl('G')); + bind(emacs, BACKWARD_DELETE_CHAR, ctrl('H')); + bind(emacs, EXPAND_OR_COMPLETE, ctrl('I')); + bind(emacs, ACCEPT_LINE, ctrl('J')); + bind(emacs, KILL_LINE, ctrl('K')); + bind(emacs, CLEAR_SCREEN, ctrl('L')); + bind(emacs, ACCEPT_LINE, ctrl('M')); + bind(emacs, DOWN_LINE_OR_HISTORY, ctrl('N')); + bind(emacs, ACCEPT_LINE_AND_DOWN_HISTORY, ctrl('O')); + bind(emacs, UP_LINE_OR_HISTORY, ctrl('P')); + bind(emacs, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('R')); + bind(emacs, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('S')); + bind(emacs, TRANSPOSE_CHARS, ctrl('T')); + bind(emacs, KILL_WHOLE_LINE, ctrl('U')); + bind(emacs, QUOTED_INSERT, ctrl('V')); + bind(emacs, BACKWARD_KILL_WORD, ctrl('W')); + bind(emacs, YANK, ctrl('Y')); + bind(emacs, CHARACTER_SEARCH, ctrl(']')); + bind(emacs, UNDO, ctrl('_')); + bind(emacs, SELF_INSERT, range(" -~")); + bind(emacs, INSERT_CLOSE_PAREN, ")"); + bind(emacs, INSERT_CLOSE_SQUARE, "]"); + bind(emacs, INSERT_CLOSE_CURLY, "}"); + bind(emacs, BACKWARD_DELETE_CHAR, del()); + bind(emacs, VI_MATCH_BRACKET, translate("^X^B")); + bind(emacs, SEND_BREAK, translate("^X^G")); + bind(emacs, EDIT_AND_EXECUTE_COMMAND, translate("^X^E")); + bind(emacs, VI_FIND_NEXT_CHAR, translate("^X^F")); + bind(emacs, VI_JOIN, translate("^X^J")); + bind(emacs, KILL_BUFFER, translate("^X^K")); + bind(emacs, INFER_NEXT_HISTORY, translate("^X^N")); + bind(emacs, OVERWRITE_MODE, translate("^X^O")); + bind(emacs, REDO, translate("^X^R")); + bind(emacs, UNDO, translate("^X^U")); + bind(emacs, VI_CMD_MODE, translate("^X^V")); + bind(emacs, EXCHANGE_POINT_AND_MARK, translate("^X^X")); + bind(emacs, DO_LOWERCASE_VERSION, translate("^XA-^XZ")); + bind(emacs, WHAT_CURSOR_POSITION, translate("^X=")); + bind(emacs, KILL_LINE, translate("^X^?")); + bind(emacs, SEND_BREAK, alt(ctrl('G'))); + bind(emacs, BACKWARD_KILL_WORD, alt(ctrl('H'))); + bind(emacs, SELF_INSERT_UNMETA, alt(ctrl('M'))); + bind(emacs, COMPLETE_WORD, alt(esc())); + bind(emacs, CHARACTER_SEARCH_BACKWARD, alt(ctrl(']'))); + bind(emacs, COPY_PREV_WORD, alt(ctrl('_'))); + bind(emacs, SET_MARK_COMMAND, alt(' ')); + bind(emacs, NEG_ARGUMENT, alt('-')); + bind(emacs, DIGIT_ARGUMENT, range("\\E0-\\E9")); + bind(emacs, BEGINNING_OF_HISTORY, alt('<')); + bind(emacs, LIST_CHOICES, alt('=')); + bind(emacs, END_OF_HISTORY, alt('>')); + bind(emacs, LIST_CHOICES, alt('?')); + bind(emacs, DO_LOWERCASE_VERSION, range("^[A-^[Z")); + bind(emacs, ACCEPT_AND_HOLD, alt('a')); + bind(emacs, BACKWARD_WORD, alt('b')); + bind(emacs, CAPITALIZE_WORD, alt('c')); + bind(emacs, KILL_WORD, alt('d')); + bind(emacs, KILL_WORD, translate("^[[3;5~")); // ctrl-delete + bind(emacs, FORWARD_WORD, alt('f')); + bind(emacs, DOWN_CASE_WORD, alt('l')); + bind(emacs, HISTORY_SEARCH_FORWARD, alt('n')); + bind(emacs, HISTORY_SEARCH_BACKWARD, alt('p')); + bind(emacs, TRANSPOSE_WORDS, alt('t')); + bind(emacs, UP_CASE_WORD, alt('u')); + bind(emacs, YANK_POP, alt('y')); + bind(emacs, BACKWARD_KILL_WORD, alt(del())); + bindArrowKeys(emacs); + bind(emacs, FORWARD_WORD, translate("^[[1;5C")); // ctrl-left + bind(emacs, BACKWARD_WORD, translate("^[[1;5D")); // ctrl-right + bind(emacs, FORWARD_WORD, alt(key(Capability.key_right))); + bind(emacs, BACKWARD_WORD, alt(key(Capability.key_left))); + bind(emacs, FORWARD_WORD, alt(translate("^[[C"))); + bind(emacs, BACKWARD_WORD, alt(translate("^[[D"))); + return emacs; + } + + public KeyMap viInsertion() { + KeyMap viins = new KeyMap<>(); + bindKeys(viins); + bind(viins, SELF_INSERT, range("^@-^_")); + bind(viins, LIST_CHOICES, ctrl('D')); + bind(viins, SEND_BREAK, ctrl('G')); + bind(viins, BACKWARD_DELETE_CHAR, ctrl('H')); + bind(viins, EXPAND_OR_COMPLETE, ctrl('I')); + bind(viins, ACCEPT_LINE, ctrl('J')); + bind(viins, CLEAR_SCREEN, ctrl('L')); + bind(viins, ACCEPT_LINE, ctrl('M')); + bind(viins, MENU_COMPLETE, ctrl('N')); + bind(viins, REVERSE_MENU_COMPLETE, ctrl('P')); + bind(viins, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('R')); + bind(viins, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('S')); + bind(viins, TRANSPOSE_CHARS, ctrl('T')); + bind(viins, KILL_WHOLE_LINE, ctrl('U')); + bind(viins, QUOTED_INSERT, ctrl('V')); + bind(viins, BACKWARD_KILL_WORD, ctrl('W')); + bind(viins, YANK, ctrl('Y')); + bind(viins, VI_CMD_MODE, ctrl('[')); + bind(viins, UNDO, ctrl('_')); + bind(viins, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('X') + "r"); + bind(viins, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('X') + "s"); + bind(viins, SELF_INSERT, range(" -~")); + bind(viins, INSERT_CLOSE_PAREN, ")"); + bind(viins, INSERT_CLOSE_SQUARE, "]"); + bind(viins, INSERT_CLOSE_CURLY, "}"); + bind(viins, BACKWARD_DELETE_CHAR, del()); + bindArrowKeys(viins); + return viins; + } + + public KeyMap viCmd() { + KeyMap vicmd = new KeyMap<>(); + bind(vicmd, LIST_CHOICES, ctrl('D')); + bind(vicmd, EMACS_EDITING_MODE, ctrl('E')); + bind(vicmd, SEND_BREAK, ctrl('G')); + bind(vicmd, VI_BACKWARD_CHAR, ctrl('H')); + bind(vicmd, ACCEPT_LINE, ctrl('J')); + bind(vicmd, KILL_LINE, ctrl('K')); + bind(vicmd, CLEAR_SCREEN, ctrl('L')); + bind(vicmd, ACCEPT_LINE, ctrl('M')); + bind(vicmd, VI_DOWN_LINE_OR_HISTORY, ctrl('N')); + bind(vicmd, VI_UP_LINE_OR_HISTORY, ctrl('P')); + bind(vicmd, QUOTED_INSERT, ctrl('Q')); + bind(vicmd, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('R')); + bind(vicmd, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('S')); + bind(vicmd, TRANSPOSE_CHARS, ctrl('T')); + bind(vicmd, KILL_WHOLE_LINE, ctrl('U')); + bind(vicmd, QUOTED_INSERT, ctrl('V')); + bind(vicmd, BACKWARD_KILL_WORD, ctrl('W')); + bind(vicmd, YANK, ctrl('Y')); + bind(vicmd, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('X') + "r"); + bind(vicmd, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('X') + "s"); + bind(vicmd, SEND_BREAK, alt(ctrl('G'))); + bind(vicmd, BACKWARD_KILL_WORD, alt(ctrl('H'))); + bind(vicmd, SELF_INSERT_UNMETA, alt(ctrl('M'))); + bind(vicmd, COMPLETE_WORD, alt(esc())); + bind(vicmd, CHARACTER_SEARCH_BACKWARD, alt(ctrl(']'))); + bind(vicmd, SET_MARK_COMMAND, alt(' ')); + // bind(vicmd, INSERT_COMMENT, alt('#')); + // bind(vicmd, INSERT_COMPLETIONS, alt('*')); + bind(vicmd, DIGIT_ARGUMENT, alt('-')); + bind(vicmd, BEGINNING_OF_HISTORY, alt('<')); + bind(vicmd, LIST_CHOICES, alt('=')); + bind(vicmd, END_OF_HISTORY, alt('>')); + bind(vicmd, LIST_CHOICES, alt('?')); + bind(vicmd, DO_LOWERCASE_VERSION, range("^[A-^[Z")); + bind(vicmd, BACKWARD_WORD, alt('b')); + bind(vicmd, CAPITALIZE_WORD, alt('c')); + bind(vicmd, KILL_WORD, alt('d')); + bind(vicmd, FORWARD_WORD, alt('f')); + bind(vicmd, DOWN_CASE_WORD, alt('l')); + bind(vicmd, HISTORY_SEARCH_FORWARD, alt('n')); + bind(vicmd, HISTORY_SEARCH_BACKWARD, alt('p')); + bind(vicmd, TRANSPOSE_WORDS, alt('t')); + bind(vicmd, UP_CASE_WORD, alt('u')); + bind(vicmd, YANK_POP, alt('y')); + bind(vicmd, BACKWARD_KILL_WORD, alt(del())); + + bind(vicmd, FORWARD_CHAR, " "); + bind(vicmd, VI_INSERT_COMMENT, "#"); + bind(vicmd, END_OF_LINE, "$"); + bind(vicmd, VI_MATCH_BRACKET, "%"); + bind(vicmd, VI_DOWN_LINE_OR_HISTORY, "+"); + bind(vicmd, VI_REV_REPEAT_FIND, ","); + bind(vicmd, VI_UP_LINE_OR_HISTORY, "-"); + bind(vicmd, VI_REPEAT_CHANGE, "."); + bind(vicmd, VI_HISTORY_SEARCH_BACKWARD, "/"); + bind(vicmd, VI_DIGIT_OR_BEGINNING_OF_LINE, "0"); + bind(vicmd, DIGIT_ARGUMENT, range("1-9")); + bind(vicmd, VI_REPEAT_FIND, ";"); + bind(vicmd, LIST_CHOICES, "="); + bind(vicmd, VI_HISTORY_SEARCH_FORWARD, "?"); + bind(vicmd, VI_ADD_EOL, "A"); + bind(vicmd, VI_BACKWARD_BLANK_WORD, "B"); + bind(vicmd, VI_CHANGE_EOL, "C"); + bind(vicmd, VI_KILL_EOL, "D"); + bind(vicmd, VI_FORWARD_BLANK_WORD_END, "E"); + bind(vicmd, VI_FIND_PREV_CHAR, "F"); + bind(vicmd, VI_FETCH_HISTORY, "G"); + bind(vicmd, VI_INSERT_BOL, "I"); + bind(vicmd, VI_JOIN, "J"); + bind(vicmd, VI_REV_REPEAT_SEARCH, "N"); + bind(vicmd, VI_OPEN_LINE_ABOVE, "O"); + bind(vicmd, VI_PUT_BEFORE, "P"); + bind(vicmd, VI_REPLACE, "R"); + bind(vicmd, VI_KILL_LINE, "S"); + bind(vicmd, VI_FIND_PREV_CHAR_SKIP, "T"); + bind(vicmd, REDO, "U"); + bind(vicmd, VISUAL_LINE_MODE, "V"); + bind(vicmd, VI_FORWARD_BLANK_WORD, "W"); + bind(vicmd, VI_BACKWARD_DELETE_CHAR, "X"); + bind(vicmd, VI_YANK_WHOLE_LINE, "Y"); + bind(vicmd, VI_FIRST_NON_BLANK, "^"); + bind(vicmd, VI_ADD_NEXT, "a"); + bind(vicmd, VI_BACKWARD_WORD, "b"); + bind(vicmd, VI_CHANGE, "c"); + bind(vicmd, VI_DELETE, "d"); + bind(vicmd, VI_FORWARD_WORD_END, "e"); + bind(vicmd, VI_FIND_NEXT_CHAR, "f"); + bind(vicmd, WHAT_CURSOR_POSITION, "ga"); + bind(vicmd, VI_BACKWARD_BLANK_WORD_END, "gE"); + bind(vicmd, VI_BACKWARD_WORD_END, "ge"); + bind(vicmd, VI_BACKWARD_CHAR, "h"); + bind(vicmd, VI_INSERT, "i"); + bind(vicmd, DOWN_LINE_OR_HISTORY, "j"); + bind(vicmd, UP_LINE_OR_HISTORY, "k"); + bind(vicmd, VI_FORWARD_CHAR, "l"); + bind(vicmd, VI_REPEAT_SEARCH, "n"); + bind(vicmd, VI_OPEN_LINE_BELOW, "o"); + bind(vicmd, VI_PUT_AFTER, "p"); + bind(vicmd, VI_REPLACE_CHARS, "r"); + bind(vicmd, VI_SUBSTITUTE, "s"); + bind(vicmd, VI_FIND_NEXT_CHAR_SKIP, "t"); + bind(vicmd, UNDO, "u"); + bind(vicmd, VISUAL_MODE, "v"); + bind(vicmd, VI_FORWARD_WORD, "w"); + bind(vicmd, VI_DELETE_CHAR, "x"); + bind(vicmd, VI_YANK, "y"); + bind(vicmd, VI_GOTO_COLUMN, "|"); + bind(vicmd, VI_SWAP_CASE, "~"); + bind(vicmd, VI_BACKWARD_CHAR, del()); + + bindArrowKeys(vicmd); + return vicmd; + } + + public KeyMap menu() { + KeyMap menu = new KeyMap<>(); + bind(menu, MENU_COMPLETE, "\t"); + bind(menu, REVERSE_MENU_COMPLETE, key(Capability.back_tab)); + bind(menu, ACCEPT_LINE, "\r", "\n"); + bindArrowKeys(menu); + return menu; + } + + public KeyMap safe() { + KeyMap safe = new KeyMap<>(); + bind(safe, SELF_INSERT, range("^@-^?")); + bind(safe, ACCEPT_LINE, "\r", "\n"); + bind(safe, SEND_BREAK, ctrl('G')); + return safe; + } + + public KeyMap dumb() { + KeyMap dumb = new KeyMap<>(); + bind(dumb, SELF_INSERT, range("^@-^?")); + bind(dumb, ACCEPT_LINE, "\r", "\n"); + bind(dumb, BEEP, ctrl('G')); + return dumb; + } + + public KeyMap visual() { + KeyMap visual = new KeyMap<>(); + bind(visual, UP_LINE, key(Capability.key_up), "k"); + bind(visual, DOWN_LINE, key(Capability.key_down), "j"); + bind(visual, this::deactivateRegion, esc()); + bind(visual, EXCHANGE_POINT_AND_MARK, "o"); + bind(visual, PUT_REPLACE_SELECTION, "p"); + bind(visual, VI_DELETE, "x"); + bind(visual, VI_OPER_SWAP_CASE, "~"); + return visual; + } + + public KeyMap viOpp() { + KeyMap viOpp = new KeyMap<>(); + bind(viOpp, UP_LINE, key(Capability.key_up), "k"); + bind(viOpp, DOWN_LINE, key(Capability.key_down), "j"); + bind(viOpp, VI_CMD_MODE, esc()); + return viOpp; + } + + private void bind(KeyMap map, String widget, Iterable keySeqs) { + map.bind(new Reference(widget), keySeqs); + } + + private void bind(KeyMap map, String widget, CharSequence... keySeqs) { + map.bind(new Reference(widget), keySeqs); + } + + private void bind(KeyMap map, Widget widget, CharSequence... keySeqs) { + map.bind(widget, keySeqs); + } + + private String key(Capability capability) { + return KeyMap.key(terminal, capability); + } + + private void bindKeys(KeyMap emacs) { + Widget beep = namedWidget("beep", this::beep); + Stream.of(Capability.values()) + .filter(c -> c.name().startsWith("key_")) + .map(this::key) + .forEach(k -> bind(emacs, beep, k)); + } + + private void bindArrowKeys(KeyMap map) { + bind(map, UP_LINE_OR_SEARCH, key(Capability.key_up)); + bind(map, DOWN_LINE_OR_SEARCH, key(Capability.key_down)); + bind(map, BACKWARD_CHAR, key(Capability.key_left)); + bind(map, FORWARD_CHAR, key(Capability.key_right)); + bind(map, BEGINNING_OF_LINE, key(Capability.key_home)); + bind(map, END_OF_LINE, key(Capability.key_end)); + bind(map, DELETE_CHAR, key(Capability.key_dc)); + bind(map, KILL_WHOLE_LINE, key(Capability.key_dl)); + bind(map, OVERWRITE_MODE, key(Capability.key_ic)); + bind(map, MOUSE, key(Capability.key_mouse)); + bind(map, BEGIN_PASTE, BRACKETED_PASTE_BEGIN); + bind(map, FOCUS_IN, FOCUS_IN_SEQ); + bind(map, FOCUS_OUT, FOCUS_OUT_SEQ); + } + + /** + * Bind special chars defined by the terminal instead of + * the default bindings + */ + private void bindConsoleChars(KeyMap keyMap, Attributes attr) { + if (attr != null) { + rebind(keyMap, BACKWARD_DELETE_CHAR, del(), (char) attr.getControlChar(ControlChar.VERASE)); + rebind(keyMap, BACKWARD_KILL_WORD, ctrl('W'), (char) attr.getControlChar(ControlChar.VWERASE)); + rebind(keyMap, KILL_WHOLE_LINE, ctrl('U'), (char) attr.getControlChar(ControlChar.VKILL)); + rebind(keyMap, QUOTED_INSERT, ctrl('V'), (char) attr.getControlChar(ControlChar.VLNEXT)); + } + } + + private void rebind(KeyMap keyMap, String operation, String prevBinding, char newBinding) { + if (newBinding > 0 && newBinding < 128) { + Reference ref = new Reference(operation); + bind(keyMap, SELF_INSERT, prevBinding); + keyMap.bind(ref, Character.toString(newBinding)); + } + } + + @Override + public void zeroOut() { + buf.zeroOut(); + parsedLine = null; + } + + /** + * Possible states in which the current readline operation may be in. + */ + protected enum State { + /** + * The user is just typing away + */ + NORMAL, + /** + * readLine should exit and return the buffer content + */ + DONE, + /** + * readLine should exit and return empty String + */ + IGNORE, + /** + * readLine should exit and throw an EOFException + */ + EOF, + /** + * readLine should exit and throw an UserInterruptException + */ + INTERRUPT + } + + protected enum ViMoveMode { + NORMAL, + YANK, + DELETE, + CHANGE + } + + protected enum BellType { + NONE, + AUDIBLE, + VISIBLE + } + + protected enum CompletionType { + Expand, + ExpandComplete, + Complete, + List, + } + + static class Pair { + final U u; + final V v; + + public Pair(U u, V v) { + this.u = u; + this.v = v; + } + + public U getU() { + return u; + } + + public V getV() { + return v; + } + } + + private static class CompletingWord implements CompletingParsedLine { + private final String word; + + public CompletingWord(String word) { + this.word = word; + } + + @Override + public CharSequence escape(CharSequence candidate, boolean complete) { + return null; + } + + @Override + public int rawWordCursor() { + return word.length(); + } + + @Override + public int rawWordLength() { + return word.length(); + } + + @Override + public String word() { + return word; + } + + @Override + public int wordCursor() { + return word.length(); + } + + @Override + public int wordIndex() { + return 0; + } + + @Override + public List words() { + return null; + } + + @Override + public String line() { + return word; + } + + @Override + public int cursor() { + return word.length(); + } + } + + protected static class PostResult { + final AttributedString post; + final int lines; + final int selectedLine; + + public PostResult(AttributedString post, int lines, int selectedLine) { + this.post = post; + this.lines = lines; + this.selectedLine = selectedLine; + } + } + + private static class TerminalLine { + private String endLine; + private int startPos; + + public TerminalLine(String line, int startPos, int width) { + this.startPos = startPos; + endLine = line.substring(line.lastIndexOf('\n') + 1); + boolean first = true; + while (endLine.length() + (first ? startPos : 0) > width && width > 0) { + if (first) { + endLine = endLine.substring(width - startPos); + } else { + endLine = endLine.substring(width); + } + first = false; + } + if (!first) { + this.startPos = 0; + } + } + + public int getStartPos() { + return startPos; + } + + public String getEndLine() { + return endLine; + } + } + + private class MenuSupport implements Supplier { + final List possible; + final BiFunction escaper; + int selection; + int topLine; + String word; + AttributedString computed; + int lines; + int columns; + String completed; + + public MenuSupport( + List original, String completed, BiFunction escaper) { + this.possible = new ArrayList<>(); + this.escaper = escaper; + this.selection = -1; + this.topLine = 0; + this.word = ""; + this.completed = completed; + computePost(original, null, possible, completed); + next(); + } + + public Candidate completion() { + return possible.get(selection); + } + + public void next() { + selection = (selection + 1) % possible.size(); + update(); + } + + public void previous() { + selection = (selection + possible.size() - 1) % possible.size(); + update(); + } + + /** + * Move 'step' options along the major axis of the menu.

+ * ie. if the menu is listing rows first, change row (up/down); + * otherwise move column (left/right) + * + * @param step number of options to move by + */ + private void major(int step) { + int axis = isSet(Option.LIST_ROWS_FIRST) ? columns : lines; + int sel = selection + step * axis; + if (sel < 0) { + int pos = (sel + axis) % axis; // needs +axis as (-1)%x == -1 + int remainders = possible.size() % axis; + sel = possible.size() - remainders + pos; + if (sel >= possible.size()) { + sel -= axis; + } + } else if (sel >= possible.size()) { + sel = sel % axis; + } + selection = sel; + update(); + } + + /** + * Move 'step' options along the minor axis of the menu.

+ * ie. if the menu is listing rows first, move along the row (left/right); + * otherwise move along the column (up/down) + * + * @param step number of options to move by + */ + private void minor(int step) { + int axis = isSet(Option.LIST_ROWS_FIRST) ? columns : lines; + int row = selection % axis; + int options = possible.size(); + if (selection - row + axis > options) { + // selection is the last row/column + // so there are fewer options than other rows + axis = options % axis; + } + selection = selection - row + ((axis + row + step) % axis); + update(); + } + + public void up() { + if (isSet(Option.LIST_ROWS_FIRST)) { + major(-1); + } else { + minor(-1); + } + } + + public void down() { + if (isSet(Option.LIST_ROWS_FIRST)) { + major(1); + } else { + minor(1); + } + } + + public void left() { + if (isSet(Option.LIST_ROWS_FIRST)) { + minor(-1); + } else { + major(-1); + } + } + + public void right() { + if (isSet(Option.LIST_ROWS_FIRST)) { + minor(1); + } else { + major(1); + } + } + + private void update() { + buf.backspace(word.length()); + word = escaper.apply(completion().value(), true).toString(); + buf.write(word); + + // Compute displayed prompt + PostResult pr = computePost(possible, completion(), null, completed); + int displaySize = displayRows() - promptLines(); + if (pr.lines > displaySize) { + int displayed = displaySize - 1; + if (pr.selectedLine >= 0) { + if (pr.selectedLine < topLine) { + topLine = pr.selectedLine; + } else if (pr.selectedLine >= topLine + displayed) { + topLine = pr.selectedLine - displayed + 1; + } + } + AttributedString post = pr.post; + if (post.length() > 0 && post.charAt(post.length() - 1) != '\n') { + post = new AttributedStringBuilder(post.length() + 1) + .append(post) + .append("\n") + .toAttributedString(); + } + List lines = post.columnSplitLength(size.getColumns(), true, display.delayLineWrap()); + List sub = new ArrayList<>(lines.subList(topLine, topLine + displayed)); + sub.add(new AttributedStringBuilder() + .style(AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN)) + .append("rows ") + .append(Integer.toString(topLine + 1)) + .append(" to ") + .append(Integer.toString(topLine + displayed)) + .append(" of ") + .append(Integer.toString(lines.size())) + .append("\n") + .style(AttributedStyle.DEFAULT) + .toAttributedString()); + computed = AttributedString.join(AttributedString.EMPTY, sub); + } else { + computed = pr.post; + } + lines = pr.lines; + columns = (possible.size() + lines - 1) / lines; + } + + @Override + public AttributedString get() { + return computed; + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/ReaderUtils.java b/net-cli/src/main/java/org/jline/reader/impl/ReaderUtils.java new file mode 100644 index 0000000..66218bd --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/ReaderUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import org.jline.reader.LineReader; +import org.jline.utils.Levenshtein; + +public class ReaderUtils { + + private ReaderUtils() { + } + + public static boolean isSet(LineReader reader, LineReader.Option option) { + return reader != null && reader.isSet(option); + } + + public static String getString(LineReader reader, String name, String def) { + Object v = reader != null ? reader.getVariable(name) : null; + return v != null ? v.toString() : def; + } + + public static boolean getBoolean(LineReader reader, String name, boolean def) { + Object v = reader != null ? reader.getVariable(name) : null; + if (v instanceof Boolean) { + return (Boolean) v; + } else if (v != null) { + String s = v.toString(); + return s.isEmpty() || s.equalsIgnoreCase("on") || s.equalsIgnoreCase("1") || s.equalsIgnoreCase("true"); + } + return def; + } + + public static int getInt(LineReader reader, String name, int def) { + int nb = def; + Object v = reader != null ? reader.getVariable(name) : null; + if (v instanceof Number) { + return ((Number) v).intValue(); + } else if (v != null) { + nb = 0; + try { + nb = Integer.parseInt(v.toString()); + } catch (NumberFormatException e) { + // Ignore + } + } + return nb; + } + + public static long getLong(LineReader reader, String name, long def) { + long nb = def; + Object v = reader != null ? reader.getVariable(name) : null; + if (v instanceof Number) { + return ((Number) v).longValue(); + } else if (v != null) { + nb = 0; + try { + nb = Long.parseLong(v.toString()); + } catch (NumberFormatException e) { + // Ignore + } + } + return nb; + } + + public static int distance(String word, String cand) { + if (word.length() < cand.length()) { + int d1 = Levenshtein.distance(word, cand.substring(0, Math.min(cand.length(), word.length()))); + int d2 = Levenshtein.distance(word, cand); + return Math.min(d1, d2); + } else { + return Levenshtein.distance(word, cand); + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/SimpleMaskingCallback.java b/net-cli/src/main/java/org/jline/reader/impl/SimpleMaskingCallback.java new file mode 100644 index 0000000..d641607 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/SimpleMaskingCallback.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2002-2017, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import java.util.Objects; +import org.jline.reader.MaskingCallback; + +/** + * Simple {@link MaskingCallback} that will replace all the characters in the line with the given mask. + * If the given mask is equal to {@link LineReaderImpl#NULL_MASK} then the line will be replaced with an empty String. + */ +public final class SimpleMaskingCallback implements MaskingCallback { + private final Character mask; + + public SimpleMaskingCallback(Character mask) { + this.mask = Objects.requireNonNull(mask, "mask must be a non null character"); + } + + @Override + public String display(String line) { + if (mask.equals(LineReaderImpl.NULL_MASK)) { + return ""; + } else { + StringBuilder sb = new StringBuilder(line.length()); + for (int i = line.length(); i-- > 0; ) { + sb.append((char) mask); + } + return sb.toString(); + } + } + + @Override + public String history(String line) { + return null; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/UndoTree.java b/net-cli/src/main/java/org/jline/reader/impl/UndoTree.java new file mode 100644 index 0000000..3c9736e --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/UndoTree.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl; + +import java.util.function.Consumer; + +/** + * Simple undo tree. + * Note that the first added state can't be undone + */ +public class UndoTree { + + private final Consumer state; + private final Node parent; + private Node current; + + @SuppressWarnings("this-escape") + public UndoTree(Consumer s) { + state = s; + parent = new Node(null); + parent.left = parent; + clear(); + } + + public void clear() { + current = parent; + } + + public void newState(T state) { + Node node = new Node(state); + current.right = node; + node.left = current; + current = node; + } + + public boolean canUndo() { + return current.left != parent; + } + + public boolean canRedo() { + return current.right != null; + } + + public void undo() { + if (!canUndo()) { + throw new IllegalStateException("Cannot undo."); + } + current = current.left; + state.accept(current.state); + } + + public void redo() { + if (!canRedo()) { + throw new IllegalStateException("Cannot redo."); + } + current = current.right; + state.accept(current.state); + } + + private class Node { + private final T state; + private Node left = null; + private Node right = null; + + public Node(T s) { + state = s; + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/completer/AggregateCompleter.java b/net-cli/src/main/java/org/jline/reader/impl/completer/AggregateCompleter.java new file mode 100644 index 0000000..82907fb --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/completer/AggregateCompleter.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl.completer; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +/** + * Completer which contains multiple completers and aggregates them together. + * + * @author Jason Dillon + * @since 2.3 + */ +public class AggregateCompleter implements Completer { + private final Collection completers; + + /** + * Construct an AggregateCompleter with the given completers. + * The completers will be used in the order given. + * + * @param completers the completers + */ + public AggregateCompleter(final Completer... completers) { + this(Arrays.asList(completers)); + } + + /** + * Construct an AggregateCompleter with the given completers. + * The completers will be used in the order given. + * + * @param completers the completers + */ + public AggregateCompleter(Collection completers) { + assert completers != null; + this.completers = completers; + } + + /** + * Retrieve the collection of completers currently being aggregated. + * + * @return the aggregated completers + */ + public Collection getCompleters() { + return completers; + } + + /** + * Perform a completion operation across all aggregated completers. + *

+ * The effect is similar to the following code: + *

{@code completers.forEach(c -> c.complete(reader, line, candidates));}
+ * + * @see Completer#complete(LineReader, ParsedLine, List) + */ + public void complete(LineReader reader, final ParsedLine line, final List candidates) { + Objects.requireNonNull(line); + Objects.requireNonNull(candidates); + completers.forEach(c -> c.complete(reader, line, candidates)); + } + + /** + * @return a string representing the aggregated completers + */ + @Override + public String toString() { + return getClass().getSimpleName() + "{" + "completers=" + completers + '}'; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/completer/ArgumentCompleter.java b/net-cli/src/main/java/org/jline/reader/impl/completer/ArgumentCompleter.java new file mode 100644 index 0000000..61e75c6 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/completer/ArgumentCompleter.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2002-2019, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl.completer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +/** + * A {@link Completer} implementation that invokes a child completer using the appropriate separator argument. + * This can be used instead of the individual completers having to know about argument parsing semantics. + * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @since 2.3 + */ +public class ArgumentCompleter implements Completer { + private final List completers = new ArrayList<>(); + + private boolean strict = true; + private boolean strictCommand = true; + + /** + * Create a new completer. + * + * @param completers The embedded completers + */ + public ArgumentCompleter(final Collection completers) { + Objects.requireNonNull(completers); + this.completers.addAll(completers); + } + + /** + * Create a new completer. + * + * @param completers The embedded completers + */ + public ArgumentCompleter(final Completer... completers) { + this(Arrays.asList(completers)); + } + + /** + * If true, a completion at argument index N will only succeed + * if all the completions from 1-(N-1) also succeed. + * + * @param strictCommand the strictCommand flag + */ + public void setStrictCommand(final boolean strictCommand) { + this.strictCommand = strictCommand; + } + + /** + * Returns whether a completion at argument index N will success + * if all the completions from arguments 0-(N-1) also succeed. + * + * @return True if strict. + * @since 2.3 + */ + public boolean isStrict() { + return this.strict; + } + + /** + * If true, a completion at argument index N will only succeed + * if all the completions from 0-(N-1) also succeed. + * + * @param strict the strict flag + */ + public void setStrict(final boolean strict) { + this.strict = strict; + } + + /** + * Returns the list of completers used inside this ArgumentCompleter. + * + * @return The list of completers. + * @since 2.3 + */ + public List getCompleters() { + return completers; + } + + public void complete(LineReader reader, ParsedLine line, final List candidates) { + Objects.requireNonNull(line); + Objects.requireNonNull(candidates); + + if (line.wordIndex() < 0) { + return; + } + + List completers = getCompleters(); + Completer completer; + + // if we are beyond the end of the completers, just use the last one + if (line.wordIndex() >= completers.size()) { + completer = completers.get(completers.size() - 1); + } else { + completer = completers.get(line.wordIndex()); + } + + // ensure that all the previous completers are successful before allowing this completer to pass (only if + // strict). + for (int i = strictCommand ? 0 : 1; isStrict() && (i < line.wordIndex()); i++) { + int idx = i >= completers.size() ? (completers.size() - 1) : i; + if (idx == 0 && !strictCommand) { + continue; + } + Completer sub = completers.get(idx); + List args = line.words(); + String arg = (args == null || i >= args.size()) ? "" : args.get(i).toString(); + + List subCandidates = new LinkedList<>(); + sub.complete(reader, new ArgumentLine(arg, arg.length()), subCandidates); + + boolean found = false; + for (Candidate cand : subCandidates) { + if (cand.value().equals(arg)) { + found = true; + break; + } + } + if (!found) { + return; + } + } + + completer.complete(reader, line, candidates); + } + + public static class ArgumentLine implements ParsedLine { + private final String word; + private final int cursor; + + public ArgumentLine(String word, int cursor) { + this.word = word; + this.cursor = cursor; + } + + @Override + public String word() { + return word; + } + + @Override + public int wordCursor() { + return cursor; + } + + @Override + public int wordIndex() { + return 0; + } + + @Override + public List words() { + return Collections.singletonList(word); + } + + @Override + public String line() { + return word; + } + + @Override + public int cursor() { + return cursor; + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/completer/EnumCompleter.java b/net-cli/src/main/java/org/jline/reader/impl/completer/EnumCompleter.java new file mode 100644 index 0000000..3346ad2 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/completer/EnumCompleter.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl.completer; + +import java.util.Objects; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; + +/** + * {@link Completer} for {@link Enum} names. + * + * @author Jason Dillon + * @since 2.3 + */ +public class EnumCompleter extends StringsCompleter { + public EnumCompleter(Class> source) { + Objects.requireNonNull(source); + for (Enum n : source.getEnumConstants()) { + candidates.add(new Candidate(n.name().toLowerCase())); + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/completer/FileNameCompleter.java b/net-cli/src/main/java/org/jline/reader/impl/completer/FileNameCompleter.java new file mode 100644 index 0000000..ab04643 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/completer/FileNameCompleter.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl.completer; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.LineReader.Option; +import org.jline.reader.ParsedLine; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; + +/** + * A file name completer takes the buffer and issues a list of + * potential completions. + *

+ * This completer tries to behave as similar as possible to + * bash's file name completion (using GNU readline) + * with the following exceptions: + *

    + *
  • Candidates that are directories will end with "/"
  • + *
  • Wildcard regular expressions are not evaluated or replaced
  • + *
  • The "~" character can be used to represent the user's home, + * but it cannot complete to other users' homes, since java does + * not provide any way of determining that easily
  • + *
+ * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @since 2.3 + * @deprecated use org.jline.builtins.Completers$FileNameCompleter instead + */ +@Deprecated +public class FileNameCompleter implements Completer { + + public void complete(LineReader reader, ParsedLine commandLine, final List candidates) { + assert commandLine != null; + assert candidates != null; + + String buffer = commandLine.word().substring(0, commandLine.wordCursor()); + + Path current; + String curBuf; + String sep = getUserDir().getFileSystem().getSeparator(); + int lastSep = buffer.lastIndexOf(sep); + if (lastSep >= 0) { + curBuf = buffer.substring(0, lastSep + 1); + if (curBuf.startsWith("~")) { + if (curBuf.startsWith("~" + sep)) { + current = getUserHome().resolve(curBuf.substring(2)); + } else { + current = getUserHome().getParent().resolve(curBuf.substring(1)); + } + } else { + current = getUserDir().resolve(curBuf); + } + } else { + curBuf = ""; + current = getUserDir(); + } + try (DirectoryStream directoryStream = Files.newDirectoryStream(current, this::accept)) { + directoryStream.forEach(p -> { + String value = curBuf + p.getFileName().toString(); + if (Files.isDirectory(p)) { + candidates.add(new Candidate( + value + (reader.isSet(Option.AUTO_PARAM_SLASH) ? sep : ""), + getDisplay(reader.getTerminal(), p), + null, + null, + reader.isSet(Option.AUTO_REMOVE_SLASH) ? sep : null, + null, + false)); + } else { + candidates.add( + new Candidate(value, getDisplay(reader.getTerminal(), p), null, null, null, null, true)); + } + }); + } catch (IOException e) { + // Ignore + } + } + + protected boolean accept(Path path) { + try { + return !Files.isHidden(path); + } catch (IOException e) { + return false; + } + } + + protected Path getUserDir() { + return Paths.get(System.getProperty("user.dir")); + } + + protected Path getUserHome() { + return Paths.get(System.getProperty("user.home")); + } + + protected String getDisplay(Terminal terminal, Path p) { + // TODO: use $LS_COLORS for output + String name = p.getFileName().toString(); + if (Files.isDirectory(p)) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.styled(AttributedStyle.BOLD.foreground(AttributedStyle.RED), name); + sb.append("/"); + name = sb.toAnsi(terminal); + } else if (Files.isSymbolicLink(p)) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.styled(AttributedStyle.BOLD.foreground(AttributedStyle.RED), name); + sb.append("@"); + name = sb.toAnsi(terminal); + } + return name; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/completer/NullCompleter.java b/net-cli/src/main/java/org/jline/reader/impl/completer/NullCompleter.java new file mode 100644 index 0000000..26c37e5 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/completer/NullCompleter.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl.completer; + +import java.util.List; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +/** + * Null completer. + * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @since 2.3 + */ +public final class NullCompleter implements Completer { + public static final NullCompleter INSTANCE = new NullCompleter(); + + public void complete(LineReader reader, final ParsedLine line, final List candidates) { + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/completer/StringsCompleter.java b/net-cli/src/main/java/org/jline/reader/impl/completer/StringsCompleter.java new file mode 100644 index 0000000..da37b1c --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/completer/StringsCompleter.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2002-2019, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl.completer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +import org.jline.utils.AttributedString; + +/** + * Completer for a set of strings. + * + * @author Jason Dillon + * @since 2.3 + */ +public class StringsCompleter implements Completer { + protected Collection candidates; + protected Supplier> stringsSupplier; + + public StringsCompleter() { + this(Collections.emptyList()); + } + + public StringsCompleter(Supplier> stringsSupplier) { + assert stringsSupplier != null; + candidates = null; + this.stringsSupplier = stringsSupplier; + } + + public StringsCompleter(String... strings) { + this(Arrays.asList(strings)); + } + + public StringsCompleter(Iterable strings) { + assert strings != null; + this.candidates = new ArrayList<>(); + for (String string : strings) { + candidates.add(new Candidate(AttributedString.stripAnsi(string), string, null, null, null, null, true)); + } + } + + public StringsCompleter(Candidate... candidates) { + this(Arrays.asList(candidates)); + } + + public StringsCompleter(Collection candidates) { + assert candidates != null; + this.candidates = new ArrayList<>(candidates); + } + + @Override + public void complete(LineReader reader, final ParsedLine commandLine, final List candidates) { + assert commandLine != null; + assert candidates != null; + if (this.candidates != null) { + candidates.addAll(this.candidates); + } else { + for (String string : stringsSupplier.get()) { + candidates.add(new Candidate(AttributedString.stripAnsi(string), string, null, null, null, null, true)); + } + } + } + + @Override + public String toString() { + String value = candidates != null ? candidates.toString() : "{" + stringsSupplier.toString() + "}"; + return "StringsCompleter" + value; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/completer/SystemCompleter.java b/net-cli/src/main/java/org/jline/reader/impl/completer/SystemCompleter.java new file mode 100644 index 0000000..d6d97c5 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/completer/SystemCompleter.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl.completer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +import org.jline.utils.AttributedString; + +/** + * Completer which contains multiple completers and aggregates them together. + * + * @author Matti Rinta-Nikkola + */ +public class SystemCompleter implements Completer { + private Map> completers = new HashMap<>(); + private final Map aliasCommand = new HashMap<>(); + private StringsCompleter commands; + private boolean compiled = false; + + public SystemCompleter() { + } + + @Override + public void complete(LineReader reader, ParsedLine commandLine, List candidates) { + if (!compiled) { + throw new IllegalStateException(); + } + assert commandLine != null; + assert candidates != null; + if (commandLine.words().size() > 0) { + if (commandLine.words().size() == 1) { + String buffer = commandLine.words().get(0); + int eq = buffer.indexOf('='); + if (eq < 0) { + commands.complete(reader, commandLine, candidates); + } else if (reader.getParser().validVariableName(buffer.substring(0, eq))) { + String curBuf = buffer.substring(0, eq + 1); + for (String c : completers.keySet()) { + candidates.add( + new Candidate(AttributedString.stripAnsi(curBuf + c), c, null, null, null, null, true)); + } + } + } else { + String cmd = reader.getParser().getCommand(commandLine.words().get(0)); + if (command(cmd) != null) { + completers.get(command(cmd)).get(0).complete(reader, commandLine, candidates); + } + } + } + } + + public boolean isCompiled() { + return compiled; + } + + private String command(String cmd) { + String out = null; + if (cmd != null) { + if (completers.containsKey(cmd)) { + out = cmd; + } else { + out = aliasCommand.get(cmd); + } + } + return out; + } + + public void add(String command, List completers) { + for (Completer c : completers) { + add(command, c); + } + } + + public void add(List commands, Completer completer) { + for (String c : commands) { + add(c, completer); + } + } + + public void add(String command, Completer completer) { + Objects.requireNonNull(command); + if (compiled) { + throw new IllegalStateException(); + } + if (!completers.containsKey(command)) { + completers.put(command, new ArrayList()); + } + if (completer instanceof ArgumentCompleter) { + ((ArgumentCompleter) completer).setStrictCommand(false); + } + completers.get(command).add(completer); + } + + public void add(SystemCompleter other) { + if (other.isCompiled()) { + throw new IllegalStateException(); + } + for (Map.Entry> entry : other.getCompleters().entrySet()) { + for (Completer c : entry.getValue()) { + add(entry.getKey(), c); + } + } + addAliases(other.getAliases()); + } + + public void addAliases(Map aliasCommand) { + if (compiled) { + throw new IllegalStateException(); + } + this.aliasCommand.putAll(aliasCommand); + } + + private Map getAliases() { + return aliasCommand; + } + + public void compile() { + if (compiled) { + return; + } + Map> compiledCompleters = new HashMap<>(); + for (Map.Entry> entry : completers.entrySet()) { + if (entry.getValue().size() == 1) { + compiledCompleters.put(entry.getKey(), entry.getValue()); + } else { + compiledCompleters.put(entry.getKey(), new ArrayList()); + compiledCompleters.get(entry.getKey()).add(new AggregateCompleter(entry.getValue())); + } + } + completers = compiledCompleters; + Set cmds = new HashSet<>(completers.keySet()); + cmds.addAll(aliasCommand.keySet()); + commands = new StringsCompleter(cmds); + compiled = true; + } + + public Map> getCompleters() { + return completers; + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/completer/package-info.java b/net-cli/src/main/java/org/jline/reader/impl/completer/package-info.java new file mode 100644 index 0000000..609f719 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/completer/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2002-2016, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +/** + * JLine 3. + * + * @since 3.0 + */ +package org.jline.reader.impl.completer; diff --git a/net-cli/src/main/java/org/jline/reader/impl/history/DefaultHistory.java b/net-cli/src/main/java/org/jline/reader/impl/history/DefaultHistory.java new file mode 100644 index 0000000..ab825ab --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/history/DefaultHistory.java @@ -0,0 +1,691 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.reader.impl.history; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.time.DateTimeException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Objects; +import java.util.Spliterator; +import org.jline.reader.History; +import org.jline.reader.LineReader; +import org.jline.utils.Log; +import static org.jline.reader.LineReader.HISTORY_IGNORE; +import static org.jline.reader.impl.ReaderUtils.getBoolean; +import static org.jline.reader.impl.ReaderUtils.getInt; +import static org.jline.reader.impl.ReaderUtils.getString; +import static org.jline.reader.impl.ReaderUtils.isSet; + +/** + * {@link History} using a file for persistent backing. + *

+ * Implementers should install shutdown hook to call {@link DefaultHistory#save} + * to save history to disk. + *

+ */ +public class DefaultHistory implements History { + + public static final int DEFAULT_HISTORY_SIZE = 500; + public static final int DEFAULT_HISTORY_FILE_SIZE = 10000; + + private final LinkedList items = new LinkedList<>(); + + private LineReader reader; + + private Map historyFiles = new HashMap<>(); + private int offset = 0; + private int index = 0; + + public DefaultHistory() { + } + + @SuppressWarnings("this-escape") + public DefaultHistory(LineReader reader) { + attach(reader); + } + + static List doTrimHistory(List allItems, int max) { + int idx = 0; + while (idx < allItems.size()) { + int ridx = allItems.size() - idx - 1; + String line = allItems.get(ridx).line().trim(); + ListIterator iterator = allItems.listIterator(ridx); + while (iterator.hasPrevious()) { + String l = iterator.previous().line(); + if (line.equals(l.trim())) { + iterator.remove(); + } + } + idx++; + } + while (allItems.size() > max) { + allItems.remove(0); + } + int index = allItems.get(allItems.size() - 1).index() - allItems.size() + 1; + List out = new ArrayList<>(); + for (Entry e : allItems) { + out.add(new EntryImpl(index++, e.time(), e.line())); + } + return out; + } + + private static String escape(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + switch (ch) { + case '\n': + sb.append('\\'); + sb.append('n'); + break; + case '\r': + sb.append('\\'); + sb.append('r'); + break; + case '\\': + sb.append('\\'); + sb.append('\\'); + break; + default: + sb.append(ch); + break; + } + } + return sb.toString(); + } + + static String unescape(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + switch (ch) { + case '\\': + ch = s.charAt(++i); + if (ch == 'n') { + sb.append('\n'); + } else if (ch == 'r') { + sb.append('\r'); + } else { + sb.append(ch); + } + break; + default: + sb.append(ch); + break; + } + } + return sb.toString(); + } + + private Path getPath() { + Object obj = reader != null ? reader.getVariables().get(LineReader.HISTORY_FILE) : null; + if (obj instanceof Path) { + return (Path) obj; + } else if (obj instanceof File) { + return ((File) obj).toPath(); + } else if (obj != null) { + return Paths.get(obj.toString()); + } else { + return null; + } + } + + @Override + public void attach(LineReader reader) { + if (this.reader != reader) { + this.reader = reader; + try { + load(); + } catch (IllegalArgumentException | IOException e) { + Log.warn("Failed to load history", e); + } + } + } + + @Override + public void load() throws IOException { + Path path = getPath(); + if (path != null) { + try { + if (Files.exists(path)) { + Log.trace("Loading history from: ", path); + try (BufferedReader reader = Files.newBufferedReader(path)) { + internalClear(); + reader.lines().forEach(line -> addHistoryLine(path, line)); + setHistoryFileData(path, new HistoryFileData(items.size(), offset + items.size())); + maybeResize(); + } + } + } catch (IllegalArgumentException | IOException e) { + Log.debug("Failed to load history; clearing", e); + internalClear(); + throw e; + } + } + } + + @Override + public void read(Path file, boolean checkDuplicates) throws IOException { + Path path = file != null ? file : getPath(); + if (path != null) { + try { + if (Files.exists(path)) { + Log.trace("Reading history from: ", path); + try (BufferedReader reader = Files.newBufferedReader(path)) { + reader.lines().forEach(line -> addHistoryLine(path, line, checkDuplicates)); + setHistoryFileData(path, new HistoryFileData(items.size(), offset + items.size())); + maybeResize(); + } + } + } catch (IllegalArgumentException | IOException e) { + Log.debug("Failed to read history; clearing", e); + internalClear(); + throw e; + } + } + } + + private String doHistoryFileDataKey(Path path) { + return path != null ? path.toAbsolutePath().toString() : null; + } + + private HistoryFileData getHistoryFileData(Path path) { + String key = doHistoryFileDataKey(path); + if (!historyFiles.containsKey(key)) { + historyFiles.put(key, new HistoryFileData()); + } + return historyFiles.get(key); + } + + private void setHistoryFileData(Path path, HistoryFileData historyFileData) { + historyFiles.put(doHistoryFileDataKey(path), historyFileData); + } + + private boolean isLineReaderHistory(Path path) throws IOException { + Path lrp = getPath(); + if (lrp == null) { + return path == null; + } + return Files.isSameFile(lrp, path); + } + + private void setLastLoaded(Path path, int lastloaded) { + getHistoryFileData(path).setLastLoaded(lastloaded); + } + + private void setEntriesInFile(Path path, int entriesInFile) { + getHistoryFileData(path).setEntriesInFile(entriesInFile); + } + + private void incEntriesInFile(Path path, int amount) { + getHistoryFileData(path).incEntriesInFile(amount); + } + + private int getLastLoaded(Path path) { + return getHistoryFileData(path).getLastLoaded(); + } + + private int getEntriesInFile(Path path) { + return getHistoryFileData(path).getEntriesInFile(); + } + + protected void addHistoryLine(Path path, String line) { + addHistoryLine(path, line, false); + } + + protected void addHistoryLine(Path path, String line, boolean checkDuplicates) { + if (reader.isSet(LineReader.Option.HISTORY_TIMESTAMPED)) { + int idx = line.indexOf(':'); + final String badHistoryFileSyntax = "Bad history file syntax! " + "The history file `" + path + + "` may be an older history: " + "please remove it or use a different history file."; + if (idx < 0) { + throw new IllegalArgumentException(badHistoryFileSyntax); + } + Instant time; + try { + time = Instant.ofEpochMilli(Long.parseLong(line.substring(0, idx))); + } catch (DateTimeException | NumberFormatException e) { + throw new IllegalArgumentException(badHistoryFileSyntax); + } + + String unescaped = unescape(line.substring(idx + 1)); + internalAdd(time, unescaped, checkDuplicates); + } else { + internalAdd(Instant.now(), unescape(line), checkDuplicates); + } + } + + @Override + public void purge() throws IOException { + internalClear(); + Path path = getPath(); + if (path != null) { + Log.trace("Purging history from: ", path); + Files.deleteIfExists(path); + } + } + + @Override + public void write(Path file, boolean incremental) throws IOException { + Path path = file != null ? file : getPath(); + if (path != null && Files.exists(path)) { + path.toFile().delete(); + } + internalWrite(path, incremental ? getLastLoaded(path) : 0); + } + + @Override + public void append(Path file, boolean incremental) throws IOException { + internalWrite(file != null ? file : getPath(), incremental ? getLastLoaded(file) : 0); + } + + @Override + public void save() throws IOException { + internalWrite(getPath(), getLastLoaded(getPath())); + } + + private void internalWrite(Path path, int from) throws IOException { + if (path != null) { + Log.trace("Saving history to: ", path); + Path parent = path.toAbsolutePath().getParent(); + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + // Append new items to the history file + try (BufferedWriter writer = Files.newBufferedWriter( + path.toAbsolutePath(), + StandardOpenOption.WRITE, + StandardOpenOption.APPEND, + StandardOpenOption.CREATE)) { + for (Entry entry : items.subList(from, items.size())) { + if (isPersistable(entry)) { + writer.append(format(entry)); + } + } + } + incEntriesInFile(path, items.size() - from); + int max = getInt(reader, LineReader.HISTORY_FILE_SIZE, DEFAULT_HISTORY_FILE_SIZE); + if (getEntriesInFile(path) > max + max / 4) { + trimHistory(path, max); + } + } + setLastLoaded(path, items.size()); + } + + protected void trimHistory(Path path, int max) throws IOException { + Log.trace("Trimming history path: ", path); + // Load all history entries + LinkedList allItems = new LinkedList<>(); + try (BufferedReader historyFileReader = Files.newBufferedReader(path)) { + historyFileReader.lines().forEach(l -> { + if (reader.isSet(LineReader.Option.HISTORY_TIMESTAMPED)) { + int idx = l.indexOf(':'); + Instant time = Instant.ofEpochMilli(Long.parseLong(l.substring(0, idx))); + String line = unescape(l.substring(idx + 1)); + allItems.add(createEntry(allItems.size(), time, line)); + } else { + allItems.add(createEntry(allItems.size(), Instant.now(), unescape(l))); + } + }); + } + // Remove duplicates + List trimmedItems = doTrimHistory(allItems, max); + // Write history + Path temp = Files.createTempFile( + path.toAbsolutePath().getParent(), path.getFileName().toString(), ".tmp"); + try (BufferedWriter writer = Files.newBufferedWriter(temp, StandardOpenOption.WRITE)) { + for (Entry entry : trimmedItems) { + writer.append(format(entry)); + } + } + Files.move(temp, path, StandardCopyOption.REPLACE_EXISTING); + // Keep items in memory + if (isLineReaderHistory(path)) { + internalClear(); + offset = trimmedItems.get(0).index(); + items.addAll(trimmedItems); + setHistoryFileData(path, new HistoryFileData(items.size(), items.size())); + } else { + setEntriesInFile(path, allItems.size()); + } + maybeResize(); + } + + /** + * Create a history entry. Subclasses may override to use their own entry implementations. + * + * @param index index of history entry + * @param time entry creation time + * @param line the entry text + * @return entry object + */ + protected EntryImpl createEntry(int index, Instant time, String line) { + return new EntryImpl(index, time, line); + } + + private void internalClear() { + offset = 0; + index = 0; + historyFiles = new HashMap<>(); + items.clear(); + } + + public int size() { + return items.size(); + } + + public boolean isEmpty() { + return items.isEmpty(); + } + + public int index() { + return offset + index; + } + + public int first() { + return offset; + } + + public int last() { + return offset + items.size() - 1; + } + + private String format(Entry entry) { + if (reader.isSet(LineReader.Option.HISTORY_TIMESTAMPED)) { + return entry.time().toEpochMilli() + ":" + escape(entry.line()) + "\n"; + } + return escape(entry.line()) + "\n"; + } + + public String get(final int index) { + int idx = index - offset; + if (idx >= items.size() || idx < 0) { + throw new IllegalArgumentException("IndexOutOfBounds: Index:" + idx + ", Size:" + items.size()); + } + return items.get(idx).line(); + } + + @Override + public void add(Instant time, String line) { + Objects.requireNonNull(time); + Objects.requireNonNull(line); + + if (getBoolean(reader, LineReader.DISABLE_HISTORY, false)) { + return; + } + if (isSet(reader, LineReader.Option.HISTORY_IGNORE_SPACE) && line.startsWith(" ")) { + return; + } + if (isSet(reader, LineReader.Option.HISTORY_REDUCE_BLANKS)) { + line = line.trim(); + } + if (isSet(reader, LineReader.Option.HISTORY_IGNORE_DUPS)) { + if (!items.isEmpty() && line.equals(items.getLast().line())) { + return; + } + } + if (matchPatterns(getString(reader, HISTORY_IGNORE, ""), line)) { + return; + } + internalAdd(time, line); + if (isSet(reader, LineReader.Option.HISTORY_INCREMENTAL)) { + try { + save(); + } catch (IOException e) { + Log.warn("Failed to save history", e); + } + } + } + + protected boolean matchPatterns(String patterns, String line) { + if (patterns == null || patterns.isEmpty()) { + return false; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < patterns.length(); i++) { + char ch = patterns.charAt(i); + if (ch == '\\') { + ch = patterns.charAt(++i); + sb.append(ch); + } else if (ch == ':') { + sb.append('|'); + } else if (ch == '*') { + sb.append('.').append('*'); + } else { + sb.append(ch); + } + } + return line.matches(sb.toString()); + } + + protected void internalAdd(Instant time, String line) { + internalAdd(time, line, false); + } + + protected void internalAdd(Instant time, String line, boolean checkDuplicates) { + Entry entry = new EntryImpl(offset + items.size(), time, line); + if (checkDuplicates) { + for (Entry e : items) { + if (e.line().trim().equals(line.trim())) { + return; + } + } + } + items.add(entry); + maybeResize(); + } + + private void maybeResize() { + while (size() > getInt(reader, LineReader.HISTORY_SIZE, DEFAULT_HISTORY_SIZE)) { + items.removeFirst(); + for (HistoryFileData hfd : historyFiles.values()) { + hfd.decLastLoaded(); + } + offset++; + } + index = size(); + } + + public ListIterator iterator(int index) { + return items.listIterator(index - offset); + } + + @Override + public Spliterator spliterator() { + return items.spliterator(); + } + + // + // Navigation + // + + public void resetIndex() { + index = Math.min(index, items.size()); + } + + /** + * This moves the history to the last entry. This entry is one position + * before the moveToEnd() position. + * + * @return Returns false if there were no history iterator or the history + * index was already at the last entry. + */ + public boolean moveToLast() { + int lastEntry = size() - 1; + if (lastEntry >= 0 && lastEntry != index) { + index = size() - 1; + return true; + } + + return false; + } + + /** + * Move to the specified index in the history + */ + public boolean moveTo(int index) { + index -= offset; + if (index >= 0 && index < size()) { + this.index = index; + return true; + } + return false; + } + + /** + * Moves the history index to the first entry. + * + * @return Return false if there are no iterator in the history or if the + * history is already at the beginning. + */ + public boolean moveToFirst() { + if (size() > 0 && index != 0) { + index = 0; + return true; + } + return false; + } + + /** + * Move to the end of the history buffer. This will be a blank entry, after + * all of the other iterator. + */ + public void moveToEnd() { + index = size(); + } + + /** + * Return the content of the current buffer. + */ + public String current() { + if (index >= size()) { + return ""; + } + return items.get(index).line(); + } + + /** + * Move the pointer to the previous element in the buffer. + * + * @return true if we successfully went to the previous element + */ + public boolean previous() { + if (index <= 0) { + return false; + } + index--; + return true; + } + + /** + * Move the pointer to the next element in the buffer. + * + * @return true if we successfully went to the next element + */ + public boolean next() { + if (index >= size()) { + return false; + } + index++; + return true; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Entry e : this) { + sb.append(e.toString()).append("\n"); + } + return sb.toString(); + } + + protected static class EntryImpl implements Entry { + + private final int index; + private final Instant time; + private final String line; + + public EntryImpl(int index, Instant time, String line) { + this.index = index; + this.time = time; + this.line = line; + } + + public int index() { + return index; + } + + public Instant time() { + return time; + } + + public String line() { + return line; + } + + @Override + public String toString() { + return String.format("%d: %s", index, line); + } + } + + private static class HistoryFileData { + private int lastLoaded = 0; + private int entriesInFile = 0; + + public HistoryFileData() { + } + + public HistoryFileData(int lastLoaded, int entriesInFile) { + this.lastLoaded = lastLoaded; + this.entriesInFile = entriesInFile; + } + + public int getLastLoaded() { + return lastLoaded; + } + + public void setLastLoaded(int lastLoaded) { + this.lastLoaded = lastLoaded; + } + + public void decLastLoaded() { + lastLoaded = lastLoaded - 1; + if (lastLoaded < 0) { + lastLoaded = 0; + } + } + + public int getEntriesInFile() { + return entriesInFile; + } + + public void setEntriesInFile(int entriesInFile) { + this.entriesInFile = entriesInFile; + } + + public void incEntriesInFile(int amount) { + entriesInFile = entriesInFile + amount; + } + } +} diff --git a/net-cli/src/main/java/org/jline/reader/impl/history/package-info.java b/net-cli/src/main/java/org/jline/reader/impl/history/package-info.java new file mode 100644 index 0000000..66c83f4 --- /dev/null +++ b/net-cli/src/main/java/org/jline/reader/impl/history/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2002-2016, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +/** + * JLine 3. + * + * @since 3.0 + */ +package org.jline.reader.impl.history; diff --git a/net-cli/src/main/java/org/jline/terminal/Attributes.java b/net-cli/src/main/java/org/jline/terminal/Attributes.java new file mode 100644 index 0000000..eccc260 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/Attributes.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class Attributes { + + final EnumSet iflag = EnumSet.noneOf(InputFlag.class); + final EnumSet oflag = EnumSet.noneOf(OutputFlag.class); + final EnumSet cflag = EnumSet.noneOf(ControlFlag.class); + final EnumSet lflag = EnumSet.noneOf(LocalFlag.class); + final EnumMap cchars = new EnumMap<>(ControlChar.class); + + public Attributes() { + } + @SuppressWarnings("this-escape") + public Attributes(Attributes attr) { + copy(attr); + } + + public EnumSet getInputFlags() { + return iflag; + } + + public void setInputFlags(EnumSet flags) { + iflag.clear(); + iflag.addAll(flags); + } + + public boolean getInputFlag(InputFlag flag) { + return iflag.contains(flag); + } + + public void setInputFlags(EnumSet flags, boolean value) { + if (value) { + iflag.addAll(flags); + } else { + iflag.removeAll(flags); + } + } + + public void setInputFlag(InputFlag flag, boolean value) { + if (value) { + iflag.add(flag); + } else { + iflag.remove(flag); + } + } + + // + // Input flags + // + + public EnumSet getOutputFlags() { + return oflag; + } + + public void setOutputFlags(EnumSet flags) { + oflag.clear(); + oflag.addAll(flags); + } + + public boolean getOutputFlag(OutputFlag flag) { + return oflag.contains(flag); + } + + public void setOutputFlags(EnumSet flags, boolean value) { + if (value) { + oflag.addAll(flags); + } else { + oflag.removeAll(flags); + } + } + + public void setOutputFlag(OutputFlag flag, boolean value) { + if (value) { + oflag.add(flag); + } else { + oflag.remove(flag); + } + } + + // + // Output flags + // + + public EnumSet getControlFlags() { + return cflag; + } + + public void setControlFlags(EnumSet flags) { + cflag.clear(); + cflag.addAll(flags); + } + + public boolean getControlFlag(ControlFlag flag) { + return cflag.contains(flag); + } + + public void setControlFlags(EnumSet flags, boolean value) { + if (value) { + cflag.addAll(flags); + } else { + cflag.removeAll(flags); + } + } + + public void setControlFlag(ControlFlag flag, boolean value) { + if (value) { + cflag.add(flag); + } else { + cflag.remove(flag); + } + } + + // + // Control flags + // + + public EnumSet getLocalFlags() { + return lflag; + } + + public void setLocalFlags(EnumSet flags) { + lflag.clear(); + lflag.addAll(flags); + } + + public boolean getLocalFlag(LocalFlag flag) { + return lflag.contains(flag); + } + + public void setLocalFlags(EnumSet flags, boolean value) { + if (value) { + lflag.addAll(flags); + } else { + lflag.removeAll(flags); + } + } + + public void setLocalFlag(LocalFlag flag, boolean value) { + if (value) { + lflag.add(flag); + } else { + lflag.remove(flag); + } + } + + // + // Local flags + // + + public EnumMap getControlChars() { + return cchars; + } + + public void setControlChars(EnumMap chars) { + cchars.clear(); + cchars.putAll(chars); + } + + public int getControlChar(ControlChar c) { + Integer v = cchars.get(c); + return v != null ? v : -1; + } + + public void setControlChar(ControlChar c, int value) { + cchars.put(c, value); + } + + public void copy(Attributes attributes) { + setControlFlags(attributes.getControlFlags()); + setInputFlags(attributes.getInputFlags()); + setLocalFlags(attributes.getLocalFlags()); + setOutputFlags(attributes.getOutputFlags()); + setControlChars(attributes.getControlChars()); + } + + // + // Control chars + // + + @Override + public String toString() { + return "Attributes[" + "lflags: " + + append(lflag) + ", " + "iflags: " + + append(iflag) + ", " + "oflags: " + + append(oflag) + ", " + "cflags: " + + append(cflag) + ", " + "cchars: " + + append(EnumSet.allOf(ControlChar.class), this::display) + "]"; + } + + private String display(ControlChar c) { + String value; + int ch = getControlChar(c); + if (c == ControlChar.VMIN || c == ControlChar.VTIME) { + value = Integer.toString(ch); + } else if (ch < 0) { + value = ""; + } else if (ch < 32) { + value = "^" + (char) (ch + 'A' - 1); + } else if (ch == 127) { + value = "^?"; + } else if (ch >= 128) { + value = String.format("\\u%04x", ch); + } else { + value = String.valueOf((char) ch); + } + return c.name().toLowerCase().substring(1) + "=" + value; + } + + private > String append(EnumSet set) { + return append(set, e -> e.name().toLowerCase()); + } + + private > String append(EnumSet set, Function toString) { + return set.stream().map(toString).collect(Collectors.joining(" ")); + } + + // + // Miscellaneous methods + // + + /** + * Control characters + */ + public enum ControlChar { + VEOF, + VEOL, + VEOL2, + VERASE, + VWERASE, + VKILL, + VREPRINT, + VINTR, + VQUIT, + VSUSP, + VDSUSP, + VSTART, + VSTOP, + VLNEXT, + VDISCARD, + VMIN, + VTIME, + VSTATUS + } + + /** + * Input flags - software input processing + */ + public enum InputFlag { + IGNBRK, /* ignore BREAK condition */ + BRKINT, /* map BREAK to SIGINTR */ + IGNPAR, /* ignore (discard) parity errors */ + PARMRK, /* mark parity and framing errors */ + INPCK, /* enable checking of parity errors */ + ISTRIP, /* strip 8th bit off chars */ + INLCR, /* map NL into CR */ + IGNCR, /* ignore CR */ + ICRNL, /* map CR to NL (ala CRMOD) */ + IXON, /* enable output flow control */ + IXOFF, /* enable input flow control */ + IXANY, /* any char will restart after stop */ + IMAXBEL, /* ring bell on input queue full */ + IUTF8, /* maintain state for UTF-8 VERASE */ + + INORMEOL /* normalize end-of-line */ + } + + /* + * Output flags - software output processing + */ + public enum OutputFlag { + OPOST, /* enable following output processing */ + ONLCR, /* map NL to CR-NL (ala CRMOD) */ + OXTABS, /* expand tabs to spaces */ + ONOEOT, /* discard EOT's (^D) on output) */ + OCRNL, /* map CR to NL on output */ + ONOCR, /* no CR output at column 0 */ + ONLRET, /* NL performs CR function */ + OFILL, /* use fill characters for delay */ + NLDLY, /* \n delay */ + TABDLY, /* horizontal tab delay */ + CRDLY, /* \r delay */ + FFDLY, /* form feed delay */ + BSDLY, /* \b delay */ + VTDLY, /* vertical tab delay */ + OFDEL /* fill is DEL, else NUL */ + } + + /* + * Control flags - hardware control of terminal + */ + public enum ControlFlag { + CIGNORE, /* ignore control flags */ + CS5, /* 5 bits (pseudo) */ + CS6, /* 6 bits */ + CS7, /* 7 bits */ + CS8, /* 8 bits */ + CSTOPB, /* send 2 stop bits */ + CREAD, /* enable receiver */ + PARENB, /* parity enable */ + PARODD, /* odd parity, else even */ + HUPCL, /* hang up on last close */ + CLOCAL, /* ignore modem status lines */ + CCTS_OFLOW, /* CTS flow control of output */ + CRTS_IFLOW, /* RTS flow control of input */ + CDTR_IFLOW, /* DTR flow control of input */ + CDSR_OFLOW, /* DSR flow control of output */ + CCAR_OFLOW /* DCD flow control of output */ + } + + /* + * "Local" flags - dumping ground for other state + * + * Warning: some flags in this structure begin with + * the letter "I" and look like they belong in the + * input flag. + */ + public enum LocalFlag { + ECHOKE, /* visual erase for line kill */ + ECHOE, /* visually erase chars */ + ECHOK, /* echo NL after line kill */ + ECHO, /* enable echoing */ + ECHONL, /* echo NL even if ECHO is off */ + ECHOPRT, /* visual erase mode for hardcopy */ + ECHOCTL, /* echo control chars as ^(Char) */ + ISIG, /* enable signals INTR, QUIT, [D]SUSP */ + ICANON, /* canonicalize input lines */ + ALTWERASE, /* use alternate WERASE algorithm */ + IEXTEN, /* enable DISCARD and LNEXT */ + EXTPROC, /* external processing */ + TOSTOP, /* stop background jobs from output */ + FLUSHO, /* output being flushed (state) */ + NOKERNINFO, /* no kernel output from VSTATUS */ + PENDIN, /* XXX retype pending input (state) */ + NOFLSH /* don't flush after interrupt */ + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/Cursor.java b/net-cli/src/main/java/org/jline/terminal/Cursor.java new file mode 100644 index 0000000..fad2368 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/Cursor.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal; + +/** + * Class holding the cursor position. + * + * @see Terminal#getCursorPosition(java.util.function.IntConsumer) + */ +public class Cursor { + + private final int x; + private final int y; + + public Cursor(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Cursor c) { + return x == c.x && y == c.y; + } else { + return false; + } + } + + @Override + public int hashCode() { + return x * 31 + y; + } + + @Override + public String toString() { + return "Cursor[" + "x=" + x + ", y=" + y + ']'; + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/MouseEvent.java b/net-cli/src/main/java/org/jline/terminal/MouseEvent.java new file mode 100644 index 0000000..6167f07 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/MouseEvent.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal; + +import java.util.EnumSet; + +public class MouseEvent { + + private final Type type; + private final Button button; + private final EnumSet modifiers; + private final int x; + private final int y; + public MouseEvent(Type type, Button button, EnumSet modifiers, int x, int y) { + this.type = type; + this.button = button; + this.modifiers = modifiers; + this.x = x; + this.y = y; + } + + public Type getType() { + return type; + } + + public Button getButton() { + return button; + } + + public EnumSet getModifiers() { + return modifiers; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public String toString() { + return "MouseEvent[" + "type=" + + type + ", button=" + + button + ", modifiers=" + + modifiers + ", x=" + + x + ", y=" + + y + ']'; + } + + public enum Type { + Released, + Pressed, + Wheel, + Moved, + Dragged + } + + public enum Button { + NoButton, + Button1, + Button2, + Button3, + WheelUp, + WheelDown + } + + public enum Modifier { + Shift, + Alt, + Control + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/Size.java b/net-cli/src/main/java/org/jline/terminal/Size.java new file mode 100644 index 0000000..0c840b0 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/Size.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal; + +public class Size { + + private int rows; + private int cols; + + public Size() { + } + + @SuppressWarnings("this-escape") + public Size(int columns, int rows) { + this(); + setColumns(columns); + setRows(rows); + } + + public int getColumns() { + return cols; + } + + public void setColumns(int columns) { + cols = (short) columns; + } + + public int getRows() { + return rows; + } + + public void setRows(int rows) { + this.rows = (short) rows; + } + + /** + * A cursor position combines a row number with a column position. + *

+ * Note each row has {@code col+1} different column positions, + * including the right margin. + *

+ * + * @param col the new column + * @param row the new row + * @return the cursor position + */ + public int cursorPos(int row, int col) { + return row * (cols + 1) + col; + } + + public void copy(Size size) { + setColumns(size.getColumns()); + setRows(size.getRows()); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Size size) { + return rows == size.rows && cols == size.cols; + } else { + return false; + } + } + + @Override + public int hashCode() { + return rows * 31 + cols; + } + + @Override + public String toString() { + return "Size[" + "cols=" + cols + ", rows=" + rows + ']'; + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/Terminal.java b/net-cli/src/main/java/org/jline/terminal/Terminal.java new file mode 100644 index 0000000..d08a194 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/Terminal.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +import org.jline.terminal.impl.NativeSignalHandler; +import org.jline.utils.ColorPalette; +import org.jline.utils.InfoCmp.Capability; +import org.jline.utils.NonBlockingReader; + +/** + * A terminal representing a virtual terminal on the computer. + *

+ * Terminals should be closed by calling the {@link #close()} method + * in order to restore their original state. + */ +public interface Terminal extends Closeable, Flushable { + + /** + * Type used for dumb terminals. + */ + String TYPE_DUMB = "dumb"; + + String TYPE_DUMB_COLOR = "dumb-color"; + + String getName(); + + // + // Signal support + // + + /** + * Registers a handler for the given {@link Signal}. + *

+ * Note that the JVM does not easily allow catching the {@link Signal#QUIT} signal, which causes a thread dump + * to be displayed. This signal is mainly used when connecting through an SSH socket to a virtual terminal. + * + * @param signal the signal to register a handler for + * @param handler the handler + * @return the previous signal handler + */ + SignalHandler handle(Signal signal, SignalHandler handler); + + /** + * Raise the specific signal. + * This is not method usually called by non system terminals. + * When accessing a terminal through a SSH or Telnet connection, signals may be + * conveyed by the protocol and thus need to be raised when reaching the terminal code. + * The terminals do that automatically when the terminal input stream has a character + * mapped to {@link Attributes.ControlChar#VINTR}, {@link Attributes.ControlChar#VQUIT}, + * or {@link Attributes.ControlChar#VSUSP}. + * + * @param signal the signal to raise + */ + void raise(Signal signal); + + /** + * Retrieve the Reader for this terminal. + * This is the standard way to read input from this terminal. + * The reader is non blocking. + * + * @return The non blocking reader + */ + NonBlockingReader reader(); + + /** + * Retrieve the Writer for this terminal. + * This is the standard way to write to this terminal. + * + * @return The writer + */ + PrintWriter writer(); + + // + // Input / output + // + + /** + * Returns the {@link Charset} that should be used to encode characters + * for {@link #input()} and {@link #output()}. + * + * @return The terminal encoding + */ + Charset encoding(); + + /** + * Retrieve the input stream for this terminal. + * In some rare cases, there may be a need to access the + * terminal input stream directly. In the usual cases, + * use the {@link #reader()} instead. + * + * @return The input stream + * @see #reader() + */ + InputStream input(); + + /** + * Retrieve the output stream for this terminal. + * In some rare cases, there may be a need to access the + * terminal output stream directly. In the usual cases, + * use the {@link #writer()} instead. + * + * @return The output stream + * @see #writer() + */ + OutputStream output(); + + /** + * Whether this terminal supports {@link #pause()} and {@link #resume()} calls. + * + * @return whether this terminal supports {@link #pause()} and {@link #resume()} calls. + * @see #paused() + * @see #pause() + * @see #resume() + */ + boolean canPauseResume(); + + /** + * Stop reading the input stream. + * + * @see #resume() + * @see #paused() + */ + void pause(); + + // + // Input control + // + + /** + * Stop reading the input stream and optionally wait for the underlying threads to finish. + * + * @param wait true to wait until the terminal is actually paused + * @throws InterruptedException if the call has been interrupted + */ + void pause(boolean wait) throws InterruptedException; + + /** + * Resume reading the input stream. + * + * @see #pause() + * @see #paused() + */ + void resume(); + + /** + * Check whether the terminal is currently reading the input stream or not. + * In order to process signal as quickly as possible, the terminal need to read + * the input stream and buffer it internally so that it can detect specific + * characters in the input stream (Ctrl+C, Ctrl+D, etc...) and raise the + * appropriate signals. + * However, there are some cases where this processing should be disabled, for + * example when handing the terminal control to a subprocess. + * + * @return whether the terminal is currently reading the input stream or not + * @see #pause() + * @see #resume() + */ + boolean paused(); + + Attributes enterRawMode(); + + boolean echo(); + + // + // Pty settings + // + + boolean echo(boolean echo); + + /** + * Returns the terminal attributes. + * The returned object can be safely modified + * further used in a call to {@link #setAttributes(Attributes)}. + * + * @return the terminal attributes. + */ + Attributes getAttributes(); + + /** + * Set the terminal attributes. + * The terminal will perform a copy of the given attributes. + * + * @param attr the new attributes + */ + void setAttributes(Attributes attr); + + /** + * Retrieve the size of the visible window + * + * @return the visible terminal size + * @see #getBufferSize() + */ + Size getSize(); + + void setSize(Size size); + + default int getWidth() { + return getSize().getColumns(); + } + + default int getHeight() { + return getSize().getRows(); + } + + /** + * Retrieve the size of the window buffer. + * Some terminals can be configured to have a buffer size + * larger than the visible window size and provide scroll bars. + * In such cases, this method should attempt to return the size + * of the whole buffer. The getBufferSize() method + * can be used to avoid wrapping when using the terminal in a line + * editing mode, while the {@link #getSize()} method should be + * used when using full screen mode. + * + * @return the terminal buffer size + * @see #getSize() + */ + default Size getBufferSize() { + return getSize(); + } + + void flush(); + + String getType(); + + boolean puts(Capability capability, Object... params); + + // + // Infocmp capabilities + // + + boolean getBooleanCapability(Capability capability); + + Integer getNumericCapability(Capability capability); + + String getStringCapability(Capability capability); + + /** + * Query the terminal to report the cursor position. + *

+ * As the response is read from the input stream, some + * characters may be read before the cursor position is actually + * read. Those characters can be given back using + * org.jline.keymap.BindingReader#runMacro(String) + * + * @param discarded a consumer receiving discarded characters + * @return null if cursor position reporting + * is not supported or a valid cursor position + */ + Cursor getCursorPosition(IntConsumer discarded); + + /** + * Returns true if the terminal has support for mouse. + * + * @return whether mouse is supported by the terminal + * @see #trackMouse(MouseTracking) + */ + boolean hasMouseSupport(); + + // + // Cursor support + // + + /** + * Change the mouse tracking mouse. + * To start mouse tracking, this method must be called with a valid mouse tracking mode. + * Mouse events will be reported by writing the {@link Capability#key_mouse} to the input stream. + * When this character sequence is detected, the {@link #readMouseEvent()} method can be + * called to actually read the corresponding mouse event. + * + * @param tracking the mouse tracking mode + * @return true if mouse tracking is supported + */ + boolean trackMouse(MouseTracking tracking); + + // + // Mouse support + // + + /** + * Read a MouseEvent from the terminal input stream. + * Such an event must have been detected by scanning the terminal's {@link Capability#key_mouse} + * in the stream immediately before reading the event. + * + * @return the decoded mouse event. + * @see #trackMouse(MouseTracking) + */ + MouseEvent readMouseEvent(); + + /** + * Read a MouseEvent from the given input stream. + * + * @param reader the input supplier + * @return the decoded mouse event + */ + MouseEvent readMouseEvent(IntSupplier reader); + + /** + * Returns true if the terminal has support for focus tracking. + * + * @return whether focus tracking is supported by the terminal + * @see #trackFocus(boolean) + */ + boolean hasFocusSupport(); + + /** + * Enable or disable focus tracking mode. + * When focus tracking has been activated, each time the terminal grabs the focus, + * the string "\33[I" will be sent to the input stream and each time the focus is lost, + * the string "\33[O" will be sent to the input stream. + * + * @param tracking whether the focus tracking mode should be enabled or not + * @return true if focus tracking is supported + */ + boolean trackFocus(boolean tracking); + + /** + * Color support + */ + ColorPalette getPalette(); + + /** + * Types of signals. + */ + enum Signal { + INT, + QUIT, + TSTP, + CONT, + INFO, + WINCH + } + + enum MouseTracking { + /** + * Disable mouse tracking + */ + Off, + /** + * Track button press and release. + */ + Normal, + /** + * Also report button-motion events. Mouse movements are reported if the mouse pointer + * has moved to a different character cell. + */ + Button, + /** + * Report all motions events, even if no mouse button is down. + */ + Any + } + + /** + * The SignalHandler defines the interface used to trap signals and perform specific behaviors. + * + * @see Signal + * @see Terminal#handle(Signal, SignalHandler) + */ + interface SignalHandler { + + /** + * The {@code SIG_DFL} value can be used to specify that the JVM default behavior + * should be used to handle this signal. + */ + SignalHandler SIG_DFL = NativeSignalHandler.SIG_DFL; + + /** + * The {@code SIG_IGN} value can be used to ignore this signal and not perform + * any special processing. + */ + SignalHandler SIG_IGN = NativeSignalHandler.SIG_IGN; + + /** + * Handle the signal. + * + * @param signal the signal + */ + void handle(Signal signal); + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/TerminalBuilder.java b/net-cli/src/main/java/org/jline/terminal/TerminalBuilder.java new file mode 100644 index 0000000..03b1c5c --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/TerminalBuilder.java @@ -0,0 +1,865 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jline.terminal.impl.AbstractPosixTerminal; +import org.jline.terminal.impl.AbstractTerminal; +import org.jline.terminal.impl.DumbTerminal; +import org.jline.terminal.impl.DumbTerminalProvider; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalExt; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.Log; +import org.jline.utils.OSUtils; + +/** + * Builder class to create terminals. + */ +public final class TerminalBuilder { + + // + // System properties + // + + public static final String PROP_ENCODING = "org.jline.terminal.encoding"; + public static final String PROP_CODEPAGE = "org.jline.terminal.codepage"; + public static final String PROP_TYPE = "org.jline.terminal.type"; + public static final String PROP_PROVIDER = "org.jline.terminal.provider"; + public static final String PROP_PROVIDERS = "org.jline.terminal.providers"; + public static final String PROP_PROVIDER_FFM = "ffm"; + public static final String PROP_PROVIDER_JNI = "jni"; + public static final String PROP_PROVIDER_JANSI = "jansi"; + public static final String PROP_PROVIDER_JNA = "jna"; + public static final String PROP_PROVIDER_EXEC = "exec"; + public static final String PROP_PROVIDER_DUMB = "dumb"; + public static final String PROP_PROVIDERS_DEFAULT = String.join( + ",", PROP_PROVIDER_FFM, PROP_PROVIDER_JNI, PROP_PROVIDER_JANSI, PROP_PROVIDER_JNA, PROP_PROVIDER_EXEC); + public static final String PROP_FFM = "org.jline.terminal." + PROP_PROVIDER_FFM; + public static final String PROP_JNI = "org.jline.terminal." + PROP_PROVIDER_JNI; + public static final String PROP_JANSI = "org.jline.terminal." + PROP_PROVIDER_JANSI; + public static final String PROP_JNA = "org.jline.terminal." + PROP_PROVIDER_JNA; + public static final String PROP_EXEC = "org.jline.terminal." + PROP_PROVIDER_EXEC; + public static final String PROP_DUMB = "org.jline.terminal." + PROP_PROVIDER_DUMB; + public static final String PROP_DUMB_COLOR = "org.jline.terminal.dumb.color"; + public static final String PROP_OUTPUT = "org.jline.terminal.output"; + public static final String PROP_OUTPUT_OUT = "out"; + public static final String PROP_OUTPUT_ERR = "err"; + public static final String PROP_OUTPUT_OUT_ERR = "out-err"; + public static final String PROP_OUTPUT_ERR_OUT = "err-out"; + public static final String PROP_OUTPUT_FORCED_OUT = "forced-out"; + public static final String PROP_OUTPUT_FORCED_ERR = "forced-err"; + + // + // Other system properties controlling various jline parts + // + + public static final String PROP_NON_BLOCKING_READS = "org.jline.terminal.pty.nonBlockingReads"; + public static final String PROP_COLOR_DISTANCE = "org.jline.utils.colorDistance"; + public static final String PROP_DISABLE_ALTERNATE_CHARSET = "org.jline.utils.disableAlternateCharset"; + + // + // System properties controlling how FileDescriptor are create. + // The value can be a comma separated list of defined mechanisms. + // + public static final String PROP_FILE_DESCRIPTOR_CREATION_MODE = "org.jline.terminal.pty.fileDescriptorCreationMode"; + public static final String PROP_FILE_DESCRIPTOR_CREATION_MODE_NATIVE = "native"; + public static final String PROP_FILE_DESCRIPTOR_CREATION_MODE_REFLECTION = "reflection"; + public static final String PROP_FILE_DESCRIPTOR_CREATION_MODE_DEFAULT = + String.join(",", PROP_FILE_DESCRIPTOR_CREATION_MODE_REFLECTION, PROP_FILE_DESCRIPTOR_CREATION_MODE_NATIVE); + + // + // System properties controlling how RedirectPipe are created. + // The value can be a comma separated list of defined mechanisms. + // + public static final String PROP_REDIRECT_PIPE_CREATION_MODE = "org.jline.terminal.exec.redirectPipeCreationMode"; + public static final String PROP_REDIRECT_PIPE_CREATION_MODE_NATIVE = "native"; + public static final String PROP_REDIRECT_PIPE_CREATION_MODE_REFLECTION = "reflection"; + public static final String PROP_REDIRECT_PIPE_CREATION_MODE_DEFAULT = + String.join(",", PROP_REDIRECT_PIPE_CREATION_MODE_REFLECTION, PROP_REDIRECT_PIPE_CREATION_MODE_NATIVE); + + public static final Set DEPRECATED_PROVIDERS = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PROP_PROVIDER_JNA, PROP_PROVIDER_JANSI))); + + public static final String PROP_DISABLE_DEPRECATED_PROVIDER_WARNING = + "org.jline.terminal.disableDeprecatedProviderWarning"; + private static final AtomicReference SYSTEM_TERMINAL = new AtomicReference<>(); + private static final AtomicReference TERMINAL_OVERRIDE = new AtomicReference<>(); + private static final int UTF8_CODE_PAGE = 65001; + private String name; + private InputStream in; + private OutputStream out; + private String type; + private Charset encoding; + private int codepage; + private Boolean system; + private SystemOutput systemOutput; + private String provider; + private String providers; + private Boolean jna; + private Boolean jansi; + private Boolean jni; + private Boolean exec; + private Boolean ffm; + private Boolean dumb; + private Boolean color; + private Attributes attributes; + private Size size; + private boolean nativeSignals = true; + private Terminal.SignalHandler signalHandler = Terminal.SignalHandler.SIG_DFL; + private boolean paused = false; + private TerminalBuilder() { + } + + /** + * Returns the default system terminal. + * Terminals should be closed properly using the {@link Terminal#close()} + * method in order to restore the original terminal state. + * + *

+ * This call is equivalent to: + * builder().build() + *

+ * + * @return the default system terminal + * @throws IOException if an error occurs + */ + public static Terminal terminal() throws IOException { + return builder().build(); + } + + /** + * Creates a new terminal builder instance. + * + * @return a builder + */ + public static TerminalBuilder builder() { + return new TerminalBuilder(); + } + + private static SystemStream select(Map system, SystemStream... streams) { + for (SystemStream s : streams) { + if (system.get(s)) { + return s; + } + } + return null; + } + + private static String getParentProcessCommand() { + try { + Class phClass = Class.forName("java.lang.ProcessHandle"); + Object current = phClass.getMethod("current").invoke(null); + Object parent = ((Optional) phClass.getMethod("parent").invoke(current)).orElse(null); + Method infoMethod = phClass.getMethod("info"); + Object info = infoMethod.invoke(parent); + Object command = ((Optional) + infoMethod.getReturnType().getMethod("command").invoke(info)) + .orElse(null); + return (String) command; + } catch (Throwable t) { + return null; + } + } + + private static Boolean getBoolean(String name, Boolean def) { + try { + String str = System.getProperty(name); + if (str != null) { + return Boolean.parseBoolean(str); + } + } catch (IllegalArgumentException | NullPointerException e) { + } + return def; + } + + private static S load(Class clazz) { + return ServiceLoader.load(clazz, clazz.getClassLoader()).iterator().next(); + } + + private static Charset getCodepageCharset(int codepage) { + // http://docs.oracle.com/javase/6/docs/technotes/guides/intl/encoding.doc.html + if (codepage == UTF8_CODE_PAGE) { + return StandardCharsets.UTF_8; + } + String charsetMS = "ms" + codepage; + if (Charset.isSupported(charsetMS)) { + return Charset.forName(charsetMS); + } + String charsetCP = "cp" + codepage; + if (Charset.isSupported(charsetCP)) { + return Charset.forName(charsetCP); + } + return Charset.defaultCharset(); + } + + /** + * Allows an application to override the result of {@link #build()}. The + * intended use case is to allow a container or server application to control + * an embedded application that uses a LineReader that uses Terminal + * constructed with TerminalBuilder.build but provides no public api for setting + * the LineReader of the {@link Terminal}. For example, the sbt + * build tool uses a LineReader to implement an interactive shell. + * One of its supported commands is console which invokes + * the scala REPL. The scala REPL also uses a LineReader and it + * is necessary to override the {@link Terminal} used by the the REPL to + * share the same {@link Terminal} instance used by sbt. + * + *

+ * When this method is called with a non-null {@link Terminal}, all subsequent + * calls to {@link #build()} will return the provided {@link Terminal} regardless + * of how the {@link TerminalBuilder} was constructed. The default behavior + * of {@link TerminalBuilder} can be restored by calling setTerminalOverride + * with a null {@link Terminal} + *

+ * + *

+ * Usage of setTerminalOverride should be restricted to cases where it + * isn't possible to update the api of the nested application to accept + * a {@link Terminal instance}. + *

+ * + * @param terminal the {@link Terminal} to globally override + */ + @Deprecated + public static void setTerminalOverride(final Terminal terminal) { + TERMINAL_OVERRIDE.set(terminal); + } + + public TerminalBuilder name(String name) { + this.name = name; + return this; + } + + public TerminalBuilder streams(InputStream in, OutputStream out) { + this.in = in; + this.out = out; + return this; + } + + public TerminalBuilder system(boolean system) { + this.system = system; + return this; + } + + /** + * Indicates which standard stream should be used when displaying to the terminal. + * The default is to use the system output stream. + * Building a system terminal will fail if one of the stream specified is not linked + * to the controlling terminal. + * + * @param systemOutput The mode to choose the output stream. + * @return The builder. + */ + public TerminalBuilder systemOutput(SystemOutput systemOutput) { + this.systemOutput = systemOutput; + return this; + } + + /** + * Forces the usage of the give terminal provider. + * + * @param provider The {@link TerminalProvider}'s name to use when creating the Terminal. + * @return The builder. + */ + public TerminalBuilder provider(String provider) { + this.provider = provider; + return this; + } + + /** + * Sets the list of providers to try when creating the terminal. + * If not specified, the system property {@link #PROP_PROVIDERS} will be used if set. + * Else, the value {@link #PROP_PROVIDERS_DEFAULT} will be used. + * + * @param providers The list of {@link TerminalProvider}'s names to check when creating the Terminal. + * @return The builder. + */ + public TerminalBuilder providers(String providers) { + this.providers = providers; + return this; + } + + /** + * Enables or disables the {@link #PROP_PROVIDER_JNA}/{@code jna} terminal provider. + * If not specified, the system property {@link #PROP_JNA} will be used if set. + * If not specified, the provider will be checked. + */ + public TerminalBuilder jna(boolean jna) { + this.jna = jna; + return this; + } + + /** + * Enables or disables the {@link #PROP_PROVIDER_JANSI}/{@code jansi} terminal provider. + * If not specified, the system property {@link #PROP_JANSI} will be used if set. + * If not specified, the provider will be checked. + */ + public TerminalBuilder jansi(boolean jansi) { + this.jansi = jansi; + return this; + } + + /** + * Enables or disables the {@link #PROP_PROVIDER_JNI}/{@code jni} terminal provider. + * If not specified, the system property {@link #PROP_JNI} will be used if set. + * If not specified, the provider will be checked. + */ + public TerminalBuilder jni(boolean jni) { + this.jni = jni; + return this; + } + + /** + * Enables or disables the {@link #PROP_PROVIDER_EXEC}/{@code exec} terminal provider. + * If not specified, the system property {@link #PROP_EXEC} will be used if set. + * If not specified, the provider will be checked. + */ + public TerminalBuilder exec(boolean exec) { + this.exec = exec; + return this; + } + + /** + * Enables or disables the {@link #PROP_PROVIDER_FFM}/{@code ffm} terminal provider. + * If not specified, the system property {@link #PROP_FFM} will be used if set. + * If not specified, the provider will be checked. + */ + public TerminalBuilder ffm(boolean ffm) { + this.ffm = ffm; + return this; + } + + /** + * Enables or disables the {@link #PROP_PROVIDER_DUMB}/{@code dumb} terminal provider. + * If not specified, the system property {@link #PROP_DUMB} will be used if set. + * If not specified, the provider will be checked. + */ + public TerminalBuilder dumb(boolean dumb) { + this.dumb = dumb; + return this; + } + + public TerminalBuilder type(String type) { + this.type = type; + return this; + } + + public TerminalBuilder color(boolean color) { + this.color = color; + return this; + } + + /** + * Set the encoding to use for reading/writing from the console. + * If {@code null} (the default value), JLine will automatically select + * a {@link Charset}, usually the default system encoding. However, + * on some platforms (e.g. Windows) it may use a different one depending + * on the {@link Terminal} implementation. + * + *

Use {@link Terminal#encoding()} to get the {@link Charset} that + * should be used for a {@link Terminal}.

+ * + * @param encoding The encoding to use or null to automatically select one + * @return The builder + * @throws UnsupportedCharsetException If the given encoding is not supported + * @see Terminal#encoding() + */ + public TerminalBuilder encoding(String encoding) throws UnsupportedCharsetException { + return encoding(encoding != null ? Charset.forName(encoding) : null); + } + + /** + * Set the {@link Charset} to use for reading/writing from the console. + * If {@code null} (the default value), JLine will automatically select + * a {@link Charset}, usually the default system encoding. However, + * on some platforms (e.g. Windows) it may use a different one depending + * on the {@link Terminal} implementation. + * + *

Use {@link Terminal#encoding()} to get the {@link Charset} that + * should be used to read/write from a {@link Terminal}.

+ * + * @param encoding The encoding to use or null to automatically select one + * @return The builder + * @see Terminal#encoding() + */ + public TerminalBuilder encoding(Charset encoding) { + this.encoding = encoding; + return this; + } + + /** + * @param codepage the codepage + * @return The builder + * @deprecated JLine now writes Unicode output independently from the selected + * code page. Using this option will only make it emulate the selected code + * page for {@link Terminal#input()} and {@link Terminal#output()}. + */ + @Deprecated + public TerminalBuilder codepage(int codepage) { + this.codepage = codepage; + return this; + } + + /** + * Attributes to use when creating a non system terminal, + * i.e. when the builder has been given the input and + * output streams using the {@link #streams(InputStream, OutputStream)} method + * or when {@link #system(boolean)} has been explicitly called with + * false. + * + * @param attributes the attributes to use + * @return The builder + * @see #size(Size) + * @see #system(boolean) + */ + public TerminalBuilder attributes(Attributes attributes) { + this.attributes = attributes; + return this; + } + + /** + * Initial size to use when creating a non system terminal, + * i.e. when the builder has been given the input and + * output streams using the {@link #streams(InputStream, OutputStream)} method + * or when {@link #system(boolean)} has been explicitly called with + * false. + * + * @param size the initial size + * @return The builder + * @see #attributes(Attributes) + * @see #system(boolean) + */ + public TerminalBuilder size(Size size) { + this.size = size; + return this; + } + + public TerminalBuilder nativeSignals(boolean nativeSignals) { + this.nativeSignals = nativeSignals; + return this; + } + + /** + * Determines the default value for signal handlers. + * All signals will be mapped to the given handler. + * + * @param signalHandler the default signal handler + * @return The builder + */ + public TerminalBuilder signalHandler(Terminal.SignalHandler signalHandler) { + this.signalHandler = signalHandler; + return this; + } + + /** + * Initial paused state of the terminal (defaults to false). + * By default, the terminal is started, but in some cases, + * one might want to make sure the input stream is not consumed + * before needed, in which case the terminal needs to be created + * in a paused state. + * + * @param paused the initial paused state + * @return The builder + * @see Terminal#pause() + */ + public TerminalBuilder paused(boolean paused) { + this.paused = paused; + return this; + } + + /** + * Builds the terminal. + * + * @return the newly created terminal, never {@code null} + * @throws IOException if an error occurs + */ + public Terminal build() throws IOException { + Terminal override = TERMINAL_OVERRIDE.get(); + Terminal terminal = override != null ? override : doBuild(); + if (override != null) { + Log.debug(() -> "Overriding terminal with global value set by TerminalBuilder.setTerminalOverride"); + } + Log.debug(() -> "Using terminal " + terminal.getClass().getSimpleName()); + if (terminal instanceof AbstractPosixTerminal) { + Log.debug(() -> "Using pty " + + ((AbstractPosixTerminal) terminal).getPty().getClass().getSimpleName()); + } + return terminal; + } + + private Terminal doBuild() throws IOException { + String name = this.name; + if (name == null) { + name = "JLine terminal"; + } + Charset encoding = computeEncoding(); + String type = computeType(); + + String provider = this.provider; + if (provider == null) { + provider = System.getProperty(PROP_PROVIDER, null); + } + + boolean forceDumb = + (DumbTerminal.TYPE_DUMB.equals(type) || type != null && type.startsWith(DumbTerminal.TYPE_DUMB_COLOR)) + || (provider != null && provider.equals(PROP_PROVIDER_DUMB)); + Boolean dumb = this.dumb; + if (dumb == null) { + dumb = getBoolean(PROP_DUMB, null); + } + IllegalStateException exception = new IllegalStateException("Unable to create a terminal"); + List providers = getProviders(provider, exception); + Terminal terminal = null; + if ((system != null && system) || (system == null && in == null && out == null)) { + if (system != null + && ((in != null && !in.equals(System.in)) + || (out != null && !out.equals(System.out) && !out.equals(System.err)))) { + throw new IllegalArgumentException("Cannot create a system terminal using non System streams"); + } + if (attributes != null || size != null) { + Log.warn("Attributes and size fields are ignored when creating a system terminal"); + } + SystemOutput systemOutput = computeSystemOutput(); + Map system = Stream.of(SystemStream.values()) + .collect(Collectors.toMap( + stream -> stream, stream -> providers.stream().anyMatch(p -> p.isSystemStream(stream)))); + SystemStream systemStream = select(system, systemOutput); + + if (!forceDumb && system.get(SystemStream.Input) && systemStream != null) { + if (attributes != null || size != null) { + Log.warn("Attributes and size fields are ignored when creating a system terminal"); + } + boolean ansiPassThrough = OSUtils.IS_CONEMU; + // Cygwin defaults to XTERM, but actually supports 256 colors, + // so if the value comes from the environment, change it to xterm-256color + if ((OSUtils.IS_CYGWIN || OSUtils.IS_MSYSTEM) + && "xterm".equals(type) + && this.type == null + && System.getProperty(PROP_TYPE) == null) { + type = "xterm-256color"; + } + for (TerminalProvider prov : providers) { + if (terminal == null) { + try { + terminal = prov.sysTerminal( + name, + type, + ansiPassThrough, + encoding, + nativeSignals, + signalHandler, + paused, + systemStream); + } catch (Throwable t) { + Log.debug("Error creating " + prov.name() + " based terminal: ", t.getMessage(), t); + exception.addSuppressed(t); + } + } + } + if (terminal == null && OSUtils.IS_WINDOWS && providers.isEmpty() && (dumb == null || !dumb)) { + throw new IllegalStateException( + "Unable to create a system terminal. On Windows, either JLine's native libraries, JNA " + + "or Jansi library is required. Make sure to add one of those in the classpath.", + exception); + } + } + if (terminal instanceof AbstractTerminal t) { + if (SYSTEM_TERMINAL.compareAndSet(null, t)) { + t.setOnClose(() -> SYSTEM_TERMINAL.compareAndSet(t, null)); + } else { + exception.addSuppressed(new IllegalStateException("A system terminal is already running. " + + "Make sure to use the created system Terminal on the LineReaderBuilder if you're using one " + + "or that previously created system Terminals have been correctly closed.")); + terminal.close(); + terminal = null; + } + } + if (terminal == null && (forceDumb || dumb == null || dumb)) { + if (!forceDumb && dumb == null) { + if (Log.isDebugEnabled()) { + Log.warn("input is tty: " + system.get(SystemStream.Input)); + Log.warn("output is tty: " + system.get(SystemStream.Output)); + Log.warn("error is tty: " + system.get(SystemStream.Error)); + Log.warn("Creating a dumb terminal", exception); + } else { + Log.warn( + "Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)"); + } + } + type = getDumbTerminalType(dumb, systemStream); + terminal = new DumbTerminalProvider() + .sysTerminal(name, type, false, encoding, nativeSignals, signalHandler, paused, systemStream); + if (OSUtils.IS_WINDOWS) { + Attributes attr = terminal.getAttributes(); + attr.setInputFlag(Attributes.InputFlag.IGNCR, true); + terminal.setAttributes(attr); + } + } + } else { + for (TerminalProvider prov : providers) { + if (terminal == null) { + try { + terminal = prov.newTerminal( + name, type, in, out, encoding, signalHandler, paused, attributes, size); + } catch (Throwable t) { + Log.debug("Error creating " + prov.name() + " based terminal: ", t.getMessage(), t); + exception.addSuppressed(t); + } + } + } + } + if (terminal == null) { + throw exception; + } + if (terminal instanceof TerminalExt te) { + if (DEPRECATED_PROVIDERS.contains(te.getProvider().name()) + && !getBoolean(PROP_DISABLE_DEPRECATED_PROVIDER_WARNING, false)) { + Log.warn("The terminal provider " + te.getProvider().name() + + " has been deprecated, check your configuration. This warning can be disabled by setting the system property " + + PROP_DISABLE_DEPRECATED_PROVIDER_WARNING + " to true."); + } + } + return terminal; + } + + private String getDumbTerminalType(Boolean dumb, SystemStream systemStream) { + // forced colored dumb terminal + Boolean color = this.color; + if (color == null) { + color = getBoolean(PROP_DUMB_COLOR, null); + } + if (dumb == null) { + // detect emacs using the env variable + if (color == null) { + String emacs = System.getenv("INSIDE_EMACS"); + if (emacs != null && emacs.contains("comint")) { + color = true; + } + } + // detect Intellij Idea + if (color == null) { + // using the env variable on windows + String ideHome = System.getenv("IDE_HOME"); + if (ideHome != null) { + color = true; + } else { + // using the parent process command on unix/mac + String command = getParentProcessCommand(); + if (command != null && command.endsWith("/idea")) { + color = true; + } + } + } + if (color == null) { + color = systemStream != null && System.getenv("TERM") != null; + } + } else { + if (color == null) { + color = false; + } + } + return color ? Terminal.TYPE_DUMB_COLOR : Terminal.TYPE_DUMB; + } + + public SystemOutput computeSystemOutput() { + SystemOutput systemOutput = null; + if (out != null) { + if (out.equals(System.out)) { + systemOutput = SystemOutput.SysOut; + } else if (out.equals(System.err)) { + systemOutput = SystemOutput.SysErr; + } + } + if (systemOutput == null) { + systemOutput = this.systemOutput; + } + if (systemOutput == null) { + String str = System.getProperty(PROP_OUTPUT); + if (str != null) { + switch (str.trim().toLowerCase(Locale.ROOT)) { + case PROP_OUTPUT_OUT: + systemOutput = SystemOutput.SysOut; + break; + case PROP_OUTPUT_ERR: + systemOutput = SystemOutput.SysErr; + break; + case PROP_OUTPUT_OUT_ERR: + systemOutput = SystemOutput.SysOutOrSysErr; + break; + case PROP_OUTPUT_ERR_OUT: + systemOutput = SystemOutput.SysErrOrSysOut; + break; + case PROP_OUTPUT_FORCED_OUT: + systemOutput = SystemOutput.ForcedSysOut; + break; + case PROP_OUTPUT_FORCED_ERR: + systemOutput = SystemOutput.ForcedSysErr; + break; + default: + Log.debug("Unsupported value for " + PROP_OUTPUT + ": " + str + ". Supported values are: " + + String.join( + ", ", + PROP_OUTPUT_OUT, + PROP_OUTPUT_ERR, + PROP_OUTPUT_OUT_ERR, + PROP_OUTPUT_ERR_OUT) + + "."); + } + } + } + if (systemOutput == null) { + systemOutput = SystemOutput.SysOutOrSysErr; + } + return systemOutput; + } + + public String computeType() { + String type = this.type; + if (type == null) { + type = System.getProperty(PROP_TYPE); + } + if (type == null) { + type = System.getenv("TERM"); + } + return type; + } + + public Charset computeEncoding() { + Charset encoding = this.encoding; + if (encoding == null) { + String charsetName = System.getProperty(PROP_ENCODING); + if (charsetName != null && Charset.isSupported(charsetName)) { + encoding = Charset.forName(charsetName); + } + } + if (encoding == null) { + int codepage = this.codepage; + if (codepage <= 0) { + String str = System.getProperty(PROP_CODEPAGE); + if (str != null) { + codepage = Integer.parseInt(str); + } + } + if (codepage >= 0) { + encoding = getCodepageCharset(codepage); + } else { + encoding = StandardCharsets.UTF_8; + } + } + return encoding; + } + + /** + * Get the list of available terminal providers. + * This list is sorted according to the {@link #PROP_PROVIDERS} system property. + * + * @param provider if not {@code null}, only this provider will be checked + * @param exception if a provider throws an exception, it will be added to this exception as a suppressed exception + * @return a list of terminal providers + */ + public List getProviders(String provider, IllegalStateException exception) { + List providers = new ArrayList<>(); + // Check ffm provider + checkProvider(provider, exception, providers, ffm, PROP_FFM, PROP_PROVIDER_FFM); + // Check jni provider + checkProvider(provider, exception, providers, jni, PROP_JNI, PROP_PROVIDER_JNI); + // Check jansi provider + checkProvider(provider, exception, providers, jansi, PROP_JANSI, PROP_PROVIDER_JANSI); + // Check jna provider + checkProvider(provider, exception, providers, jna, PROP_JNA, PROP_PROVIDER_JNA); + // Check exec provider + checkProvider(provider, exception, providers, exec, PROP_EXEC, PROP_PROVIDER_EXEC); + // Order providers + List order = Arrays.asList( + (this.providers != null ? this.providers : System.getProperty(PROP_PROVIDERS, PROP_PROVIDERS_DEFAULT)) + .split(",")); + providers.sort(Comparator.comparing(l -> { + int idx = order.indexOf(l.name()); + return idx >= 0 ? idx : Integer.MAX_VALUE; + })); + String names = providers.stream().map(TerminalProvider::name).collect(Collectors.joining(", ")); + Log.debug("Available providers: " + names); + return providers; + } + + private void checkProvider( + String provider, + IllegalStateException exception, + List providers, + Boolean load, + String property, + String name) { + Boolean doLoad = provider != null ? (Boolean) name.equals(provider) : load; + if (doLoad == null) { + doLoad = getBoolean(property, true); + } + if (doLoad) { + try { + TerminalProvider prov = TerminalProvider.load(name); + prov.isSystemStream(SystemStream.Output); + providers.add(prov); + } catch (Throwable t) { + Log.debug("Unable to load " + name + " provider: ", t); + exception.addSuppressed(t); + } + } + } + + private SystemStream select(Map system, SystemOutput systemOutput) { + switch (systemOutput) { + case SysOut: + return select(system, SystemStream.Output); + case SysErr: + return select(system, SystemStream.Error); + case SysOutOrSysErr: + return select(system, SystemStream.Output, SystemStream.Error); + case SysErrOrSysOut: + return select(system, SystemStream.Error, SystemStream.Output); + case ForcedSysOut: + return SystemStream.Output; + case ForcedSysErr: + return SystemStream.Error; + } + return null; + } + + // + // Terminal output control + // + public enum SystemOutput { + SysOut, + SysErr, + SysOutOrSysErr, + SysErrOrSysOut, + ForcedSysOut, + ForcedSysErr + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.java b/net-cli/src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.java new file mode 100644 index 0000000..6d8649f --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOError; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Objects; +import java.util.function.IntConsumer; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Cursor; +import org.jline.terminal.Size; +import org.jline.terminal.spi.Pty; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalProvider; + +public abstract class AbstractPosixTerminal extends AbstractTerminal { + + protected final Pty pty; + protected final Attributes originalAttributes; + + public AbstractPosixTerminal(String name, String type, Pty pty) throws IOException { + this(name, type, pty, null, SignalHandler.SIG_DFL); + } + + public AbstractPosixTerminal(String name, String type, Pty pty, Charset encoding, SignalHandler signalHandler) + throws IOException { + super(name, type, encoding, signalHandler); + Objects.requireNonNull(pty); + this.pty = pty; + this.originalAttributes = this.pty.getAttr(); + } + + public Pty getPty() { + return pty; + } + + public Attributes getAttributes() { + try { + return pty.getAttr(); + } catch (IOException e) { + throw new IOError(e); + } + } + + public void setAttributes(Attributes attr) { + try { + pty.setAttr(attr); + } catch (IOException e) { + throw new IOError(e); + } + } + + public Size getSize() { + try { + return pty.getSize(); + } catch (IOException e) { + throw new IOError(e); + } + } + + public void setSize(Size size) { + try { + pty.setSize(size); + } catch (IOException e) { + throw new IOError(e); + } + } + + protected void doClose() throws IOException { + super.doClose(); + pty.setAttr(originalAttributes); + pty.close(); + } + + @Override + public Cursor getCursorPosition(IntConsumer discarded) { + return CursorSupport.getCursorPosition(this, discarded); + } + + @Override + public TerminalProvider getProvider() { + return getPty().getProvider(); + } + + @Override + public SystemStream getSystemStream() { + return getPty().getSystemStream(); + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/AbstractPty.java b/net-cli/src/main/java/org/jline/terminal/impl/AbstractPty.java new file mode 100644 index 0000000..a00ebef --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/AbstractPty.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2002-2019, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.FileDescriptor; +import java.io.FilterInputStream; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.lang.reflect.Field; +import org.jline.terminal.Attributes; +import org.jline.terminal.spi.Pty; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.NonBlockingInputStream; +import static org.jline.terminal.TerminalBuilder.PROP_FILE_DESCRIPTOR_CREATION_MODE; +import static org.jline.terminal.TerminalBuilder.PROP_FILE_DESCRIPTOR_CREATION_MODE_DEFAULT; +import static org.jline.terminal.TerminalBuilder.PROP_FILE_DESCRIPTOR_CREATION_MODE_REFLECTION; +import static org.jline.terminal.TerminalBuilder.PROP_NON_BLOCKING_READS; + +public abstract class AbstractPty implements Pty { + + private static FileDescriptorCreator fileDescriptorCreator; + protected final TerminalProvider provider; + protected final SystemStream systemStream; + private Attributes current; + private boolean skipNextLf; + + public AbstractPty(TerminalProvider provider, SystemStream systemStream) { + this.provider = provider; + this.systemStream = systemStream; + } + + protected static FileDescriptor newDescriptor(int fd) { + if (fileDescriptorCreator == null) { + String str = + System.getProperty(PROP_FILE_DESCRIPTOR_CREATION_MODE, PROP_FILE_DESCRIPTOR_CREATION_MODE_DEFAULT); + String[] modes = str.split(","); + IllegalStateException ise = new IllegalStateException("Unable to create FileDescriptor"); + for (String mode : modes) { + try { + switch (mode) { + case PROP_FILE_DESCRIPTOR_CREATION_MODE_REFLECTION: + fileDescriptorCreator = new ReflectionFileDescriptorCreator(); + break; + } + } catch (Throwable t) { + // ignore + ise.addSuppressed(t); + } + if (fileDescriptorCreator != null) { + break; + } + } + if (fileDescriptorCreator == null) { + throw ise; + } + } + return fileDescriptorCreator.newDescriptor(fd); + } + + @Override + public void setAttr(Attributes attr) throws IOException { + current = new Attributes(attr); + doSetAttr(attr); + } + + @Override + public InputStream getSlaveInput() throws IOException { + InputStream si = doGetSlaveInput(); + InputStream nsi = new FilterInputStream(si) { + @Override + public int read() throws IOException { + for (; ; ) { + int c = super.read(); + if (current.getInputFlag(Attributes.InputFlag.INORMEOL)) { + if (c == '\r') { + skipNextLf = true; + c = '\n'; + } else if (c == '\n') { + if (skipNextLf) { + skipNextLf = false; + continue; + } + } else { + skipNextLf = false; + } + } + return c; + } + } + }; + if (Boolean.parseBoolean(System.getProperty(PROP_NON_BLOCKING_READS, "true"))) { + return new PtyInputStream(nsi); + } else { + return nsi; + } + } + + protected abstract void doSetAttr(Attributes attr) throws IOException; + + protected abstract InputStream doGetSlaveInput() throws IOException; + + protected void checkInterrupted() throws InterruptedIOException { + if (Thread.interrupted()) { + throw new InterruptedIOException(); + } + } + + @Override + public TerminalProvider getProvider() { + return provider; + } + + @Override + public SystemStream getSystemStream() { + return systemStream; + } + + interface FileDescriptorCreator { + FileDescriptor newDescriptor(int fd); + } + + /** + * Reflection based file descriptor creator. + * This requires the following option + * --add-opens java.base/java.io=ALL-UNNAMED + */ + static class ReflectionFileDescriptorCreator implements FileDescriptorCreator { + private final Field fileDescriptorField; + + ReflectionFileDescriptorCreator() throws Exception { + Field field = FileDescriptor.class.getDeclaredField("fd"); + field.setAccessible(true); + fileDescriptorField = field; + } + + @Override + public FileDescriptor newDescriptor(int fd) { + FileDescriptor descriptor = new FileDescriptor(); + try { + fileDescriptorField.set(descriptor, fd); + } catch (IllegalAccessException e) { + // This should not happen as the field has been set accessible + throw new IllegalStateException(e); + } + return descriptor; + } + } + + /* + * Class that could be used on OpenJDK 17. However, it requires the following JVM option + * --add-exports java.base/jdk.internal.access=ALL-UNNAMED + * so the benefit does not seem important enough to warrant the problems caused + * by access the jdk.internal.access package at compile time, which itself requires + * custom compiler options and a different maven module, or at least a different compile + * phase with a JDK 17 compiler. + * So, just keep the ReflectionFileDescriptorCreator for now. + * + static class Jdk17FileDescriptorCreator implements FileDescriptorCreator { + private final jdk.internal.access.JavaIOFileDescriptorAccess fdAccess; + Jdk17FileDescriptorCreator() { + fdAccess = jdk.internal.access.SharedSecrets.getJavaIOFileDescriptorAccess(); + } + + @Override + public FileDescriptor newDescriptor(int fd) { + FileDescriptor descriptor = new FileDescriptor(); + fdAccess.set(descriptor, fd); + return descriptor; + } + } + */ + + class PtyInputStream extends NonBlockingInputStream { + final InputStream in; + int c = 0; + + PtyInputStream(InputStream in) { + this.in = in; + } + + @Override + public int read(long timeout, boolean isPeek) throws IOException { + checkInterrupted(); + if (c != 0) { + int r = c; + if (!isPeek) { + c = 0; + } + return r; + } else { + setNonBlocking(); + long start = System.currentTimeMillis(); + while (true) { + int r = in.read(); + if (r >= 0) { + if (isPeek) { + c = r; + } + return r; + } + checkInterrupted(); + long cur = System.currentTimeMillis(); + if (timeout > 0 && cur - start > timeout) { + return NonBlockingInputStream.READ_EXPIRED; + } + } + } + } + + private void setNonBlocking() { + if (current == null + || current.getControlChar(Attributes.ControlChar.VMIN) != 0 + || current.getControlChar(Attributes.ControlChar.VTIME) != 1) { + try { + Attributes attr = getAttr(); + attr.setControlChar(Attributes.ControlChar.VMIN, 0); + attr.setControlChar(Attributes.ControlChar.VTIME, 1); + setAttr(attr); + } catch (IOException e) { + throw new IOError(e); + } + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/AbstractTerminal.java b/net-cli/src/main/java/org/jline/terminal/impl/AbstractTerminal.java new file mode 100644 index 0000000..4c1293e --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/AbstractTerminal.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.charset.Charset; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Attributes.ControlChar; +import org.jline.terminal.Attributes.InputFlag; +import org.jline.terminal.Attributes.LocalFlag; +import org.jline.terminal.Cursor; +import org.jline.terminal.MouseEvent; +import org.jline.terminal.spi.TerminalExt; +import org.jline.utils.ColorPalette; +import org.jline.utils.Curses; +import org.jline.utils.InfoCmp; +import org.jline.utils.InfoCmp.Capability; +import org.jline.utils.Log; +import org.jline.utils.Status; + +public abstract class AbstractTerminal implements TerminalExt { + + protected final String name; + protected final String type; + protected final Charset encoding; + protected final Map handlers = new ConcurrentHashMap<>(); + protected final Set bools = new HashSet<>(); + protected final Map ints = new HashMap<>(); + protected final Map strings = new HashMap<>(); + protected final ColorPalette palette; + protected Status status; + protected Runnable onClose; + private MouseEvent lastMouseEvent = new MouseEvent( + MouseEvent.Type.Moved, MouseEvent.Button.NoButton, EnumSet.noneOf(MouseEvent.Modifier.class), 0, 0); + + public AbstractTerminal(String name, String type) throws IOException { + this(name, type, null, SignalHandler.SIG_DFL); + } + + @SuppressWarnings("this-escape") + public AbstractTerminal(String name, String type, Charset encoding, SignalHandler signalHandler) + throws IOException { + this.name = name; + this.type = type != null ? type : "ansi"; + this.encoding = encoding != null ? encoding : Charset.defaultCharset(); + this.palette = new ColorPalette(this); + for (Signal signal : Signal.values()) { + handlers.put(signal, signalHandler); + } + } + + public void setOnClose(Runnable onClose) { + this.onClose = onClose; + } + + public Status getStatus() { + return getStatus(true); + } + + public Status getStatus(boolean create) { + if (status == null && create) { + status = new Status(this); + } + return status; + } + + public SignalHandler handle(Signal signal, SignalHandler handler) { + Objects.requireNonNull(signal); + Objects.requireNonNull(handler); + return handlers.put(signal, handler); + } + + public void raise(Signal signal) { + Objects.requireNonNull(signal); + SignalHandler handler = handlers.get(signal); + if (handler == SignalHandler.SIG_DFL) { + if (status != null && signal == Signal.WINCH) { + status.resize(); + } + } else if (handler != SignalHandler.SIG_IGN) { + handler.handle(signal); + } + } + + public final void close() throws IOException { + try { + doClose(); + } finally { + if (onClose != null) { + onClose.run(); + } + } + } + + protected void doClose() throws IOException { + if (status != null) { + status.close(); + } + } + + protected void echoSignal(Signal signal) { + ControlChar cc = null; + switch (signal) { + case INT: + cc = ControlChar.VINTR; + break; + case QUIT: + cc = ControlChar.VQUIT; + break; + case TSTP: + cc = ControlChar.VSUSP; + break; + } + if (cc != null) { + int vcc = getAttributes().getControlChar(cc); + if (vcc > 0 && vcc < 32) { + writer().write(new char[]{'^', (char) (vcc + '@')}, 0, 2); + } + } + } + + public Attributes enterRawMode() { + Attributes prvAttr = getAttributes(); + Attributes newAttr = new Attributes(prvAttr); + newAttr.setLocalFlags(EnumSet.of(LocalFlag.ICANON, LocalFlag.ECHO, LocalFlag.IEXTEN), false); + newAttr.setInputFlags(EnumSet.of(InputFlag.IXON, InputFlag.ICRNL, InputFlag.INLCR), false); + newAttr.setControlChar(ControlChar.VMIN, 0); + newAttr.setControlChar(ControlChar.VTIME, 1); + setAttributes(newAttr); + return prvAttr; + } + + public boolean echo() { + return getAttributes().getLocalFlag(LocalFlag.ECHO); + } + + public boolean echo(boolean echo) { + Attributes attr = getAttributes(); + boolean prev = attr.getLocalFlag(LocalFlag.ECHO); + if (prev != echo) { + attr.setLocalFlag(LocalFlag.ECHO, echo); + setAttributes(attr); + } + return prev; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getKind() { + return getClass().getSimpleName(); + } + + @Override + public Charset encoding() { + return this.encoding; + } + + public void flush() { + writer().flush(); + } + + public boolean puts(Capability capability, Object... params) { + String str = getStringCapability(capability); + if (str == null) { + return false; + } + Curses.tputs(writer(), str, params); + return true; + } + + public boolean getBooleanCapability(Capability capability) { + return bools.contains(capability); + } + + public Integer getNumericCapability(Capability capability) { + return ints.get(capability); + } + + public String getStringCapability(Capability capability) { + return strings.get(capability); + } + + protected void parseInfoCmp() { + String capabilities = null; + try { + capabilities = InfoCmp.getInfoCmp(type); + } catch (Exception e) { + Log.warn("Unable to retrieve infocmp for type " + type, e); + } + if (capabilities == null) { + capabilities = InfoCmp.getLoadedInfoCmp("ansi"); + } + InfoCmp.parseInfoCmp(capabilities, bools, ints, strings); + } + + @Override + public Cursor getCursorPosition(IntConsumer discarded) { + return null; + } + + @Override + public boolean hasMouseSupport() { + return MouseSupport.hasMouseSupport(this); + } + + @Override + public boolean trackMouse(MouseTracking tracking) { + return MouseSupport.trackMouse(this, tracking); + } + + @Override + public MouseEvent readMouseEvent() { + return lastMouseEvent = MouseSupport.readMouse(this, lastMouseEvent); + } + + @Override + public MouseEvent readMouseEvent(IntSupplier reader) { + return lastMouseEvent = MouseSupport.readMouse(reader, lastMouseEvent); + } + + @Override + public boolean hasFocusSupport() { + return type.startsWith("xterm"); + } + + @Override + public boolean trackFocus(boolean tracking) { + if (hasFocusSupport()) { + writer().write(tracking ? "\033[?1004h" : "\033[?1004l"); + writer().flush(); + return true; + } else { + return false; + } + } + + protected void checkInterrupted() throws InterruptedIOException { + if (Thread.interrupted()) { + throw new InterruptedIOException(); + } + } + + @Override + public boolean canPauseResume() { + return false; + } + + @Override + public void pause() { + } + + @Override + public void pause(boolean wait) throws InterruptedException { + } + + @Override + public void resume() { + } + + @Override + public boolean paused() { + return false; + } + + @Override + public ColorPalette getPalette() { + return palette; + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/AbstractWindowsConsoleWriter.java b/net-cli/src/main/java/org/jline/terminal/impl/AbstractWindowsConsoleWriter.java new file mode 100644 index 0000000..f1c9b92 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/AbstractWindowsConsoleWriter.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2002-2017, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOException; +import java.io.Writer; + +public abstract class AbstractWindowsConsoleWriter extends Writer { + + protected abstract void writeConsole(char[] text, int len) throws IOException; + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + char[] text = cbuf; + if (off != 0) { + text = new char[len]; + System.arraycopy(cbuf, off, text, 0, len); + } + + synchronized (this.lock) { + writeConsole(text, len); + } + } + + @Override + public void flush() { + } + + @Override + public void close() { + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java b/net-cli/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java new file mode 100644 index 0000000..544ffe7 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java @@ -0,0 +1,554 @@ +/* + * Copyright (c) 2002-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.Curses; +import org.jline.utils.InfoCmp; +import org.jline.utils.Log; +import org.jline.utils.NonBlocking; +import org.jline.utils.NonBlockingInputStream; +import org.jline.utils.NonBlockingPumpReader; +import org.jline.utils.NonBlockingReader; +import org.jline.utils.ShutdownHooks; +import org.jline.utils.Signals; +import org.jline.utils.WriterOutputStream; + +/** + * The AbstractWindowsTerminal is used as the base class for windows terminal. + * Due to windows limitations, mostly the missing support for ansi sequences, + * the only way to create a correct terminal is to use the windows api to set + * character attributes, move the cursor, erasing, etc... + *

+ * UTF-8 support is also lacking in windows and the code page supposed to + * emulate UTF-8 is a bit broken. In order to work around this broken + * code page, windows api WriteConsoleW is used directly. This means that + * the writer() becomes the primary output, while the output() is bridged + * to the writer() using a WriterOutputStream wrapper. + */ +public abstract class AbstractWindowsTerminal extends AbstractTerminal { + + public static final String TYPE_WINDOWS = "windows"; + public static final String TYPE_WINDOWS_256_COLOR = "windows-256color"; + public static final String TYPE_WINDOWS_CONEMU = "windows-conemu"; + public static final String TYPE_WINDOWS_VTP = "windows-vtp"; + + public static final int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + protected static final int ENABLE_PROCESSED_INPUT = 0x0001; + protected static final int ENABLE_LINE_INPUT = 0x0002; + protected static final int ENABLE_ECHO_INPUT = 0x0004; + protected static final int ENABLE_WINDOW_INPUT = 0x0008; + protected static final int ENABLE_MOUSE_INPUT = 0x0010; + protected static final int ENABLE_INSERT_MODE = 0x0020; + protected static final int ENABLE_QUICK_EDIT_MODE = 0x0040; + protected static final int ENABLE_EXTENDED_FLAGS = 0x0080; + static final int SHIFT_FLAG = 0x01; + static final int ALT_FLAG = 0x02; + static final int CTRL_FLAG = 0x04; + static final int RIGHT_ALT_PRESSED = 0x0001; + static final int LEFT_ALT_PRESSED = 0x0002; + static final int RIGHT_CTRL_PRESSED = 0x0004; + static final int LEFT_CTRL_PRESSED = 0x0008; + static final int SHIFT_PRESSED = 0x0010; + static final int NUMLOCK_ON = 0x0020; + static final int SCROLLLOCK_ON = 0x0040; + static final int CAPSLOCK_ON = 0x0080; + private static final int UTF8_CODE_PAGE = 65001; + protected final Writer slaveInputPipe; + protected final NonBlockingInputStream input; + protected final OutputStream output; + protected final NonBlockingReader reader; + protected final PrintWriter writer; + protected final Map nativeHandlers = new HashMap<>(); + protected final ShutdownHooks.Task closer; + protected final Attributes attributes = new Attributes(); + protected final Console inConsole; + protected final Console outConsole; + protected final int originalInConsoleMode; + protected final int originalOutConsoleMode; + protected final Object lock = new Object(); + private final TerminalProvider provider; + private final SystemStream systemStream; + protected boolean paused = true; + protected Thread pump; + protected MouseTracking tracking = MouseTracking.Off; + protected boolean focusTracking = false; + protected boolean skipNextLf; + private volatile boolean closing; + + @SuppressWarnings("this-escape") + public AbstractWindowsTerminal( + TerminalProvider provider, + SystemStream systemStream, + Writer writer, + String name, + String type, + Charset encoding, + boolean nativeSignals, + SignalHandler signalHandler, + Console inConsole, + int inConsoleMode, + Console outConsole, + int outConsoleMode) + throws IOException { + super(name, type, encoding, signalHandler); + this.provider = provider; + this.systemStream = systemStream; + NonBlockingPumpReader reader = NonBlocking.nonBlockingPumpReader(); + this.slaveInputPipe = reader.getWriter(); + this.reader = reader; + this.input = NonBlocking.nonBlockingStream(reader, encoding()); + this.writer = new PrintWriter(writer); + this.output = new WriterOutputStream(writer, encoding()); + this.inConsole = inConsole; + this.outConsole = outConsole; + parseInfoCmp(); + // Attributes + this.originalInConsoleMode = inConsoleMode; + this.originalOutConsoleMode = outConsoleMode; + attributes.setLocalFlag(Attributes.LocalFlag.ISIG, true); + attributes.setControlChar(Attributes.ControlChar.VINTR, ctrl('C')); + attributes.setControlChar(Attributes.ControlChar.VEOF, ctrl('D')); + attributes.setControlChar(Attributes.ControlChar.VSUSP, ctrl('Z')); + // Handle signals + if (nativeSignals) { + for (final Signal signal : Signal.values()) { + if (signalHandler == SignalHandler.SIG_DFL) { + nativeHandlers.put(signal, Signals.registerDefault(signal.name())); + } else { + nativeHandlers.put(signal, Signals.register(signal.name(), () -> raise(signal))); + } + } + } + closer = this::close; + ShutdownHooks.add(closer); + // ConEMU extended fonts support + if (TYPE_WINDOWS_CONEMU.equals(getType()) + && !Boolean.getBoolean("org.jline.terminal.conemu.disable-activate")) { + writer.write("\u001b[9999E"); + writer.flush(); + } + } + + @Override + public SignalHandler handle(Signal signal, SignalHandler handler) { + SignalHandler prev = super.handle(signal, handler); + if (prev != handler) { + if (handler == SignalHandler.SIG_DFL) { + Signals.registerDefault(signal.name()); + } else { + Signals.register(signal.name(), () -> raise(signal)); + } + } + return prev; + } + + public NonBlockingReader reader() { + return reader; + } + + public PrintWriter writer() { + return writer; + } + + @Override + public InputStream input() { + return input; + } + + @Override + public OutputStream output() { + return output; + } + + public Attributes getAttributes() { + int mode = getConsoleMode(inConsole); + if ((mode & ENABLE_ECHO_INPUT) != 0) { + attributes.setLocalFlag(Attributes.LocalFlag.ECHO, true); + } + if ((mode & ENABLE_LINE_INPUT) != 0) { + attributes.setLocalFlag(Attributes.LocalFlag.ICANON, true); + } + return new Attributes(attributes); + } + + public void setAttributes(Attributes attr) { + attributes.copy(attr); + updateConsoleMode(); + } + + protected void updateConsoleMode() { + int mode = ENABLE_WINDOW_INPUT; + if (attributes.getLocalFlag(Attributes.LocalFlag.ISIG)) { + mode |= ENABLE_PROCESSED_INPUT; + } + if (attributes.getLocalFlag(Attributes.LocalFlag.ECHO)) { + mode |= ENABLE_ECHO_INPUT; + } + if (attributes.getLocalFlag(Attributes.LocalFlag.ICANON)) { + mode |= ENABLE_LINE_INPUT; + } + if (tracking != MouseTracking.Off) { + mode |= ENABLE_MOUSE_INPUT; + // mouse events not send with quick edit mode + // to disable ENABLE_QUICK_EDIT_MODE just set extended flag + mode |= ENABLE_EXTENDED_FLAGS; + } + setConsoleMode(inConsole, mode); + } + + protected int ctrl(char key) { + return (Character.toUpperCase(key) & 0x1f); + } + + public void setSize(Size size) { + throw new UnsupportedOperationException("Can not resize windows terminal"); + } + + protected void doClose() throws IOException { + super.doClose(); + closing = true; + if (pump != null) { + pump.interrupt(); + } + ShutdownHooks.remove(closer); + for (Map.Entry entry : nativeHandlers.entrySet()) { + Signals.unregister(entry.getKey().name(), entry.getValue()); + } + reader.close(); + writer.close(); + setConsoleMode(inConsole, originalInConsoleMode); + setConsoleMode(outConsole, originalOutConsoleMode); + } + + protected void processKeyEvent( + final boolean isKeyDown, final short virtualKeyCode, char ch, final int controlKeyState) + throws IOException { + final boolean isCtrl = (controlKeyState & (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) > 0; + final boolean isAlt = (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED)) > 0; + final boolean isShift = (controlKeyState & SHIFT_PRESSED) > 0; + // key down event + if (isKeyDown && ch != '\3') { + // Pressing "Alt Gr" is translated to Alt-Ctrl, hence it has to be checked that Ctrl is _not_ pressed, + // otherwise inserting of "Alt Gr" codes on non-US keyboards would yield errors + if (ch != 0 + && (controlKeyState + & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) + == (RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED)) { + processInputChar(ch); + } else { + final String keySeq = getEscapeSequence( + virtualKeyCode, (isCtrl ? CTRL_FLAG : 0) + (isAlt ? ALT_FLAG : 0) + (isShift ? SHIFT_FLAG : 0)); + if (keySeq != null) { + for (char c : keySeq.toCharArray()) { + processInputChar(c); + } + return; + } + /* uchar value in Windows when CTRL is pressed: + * 1). Ctrl + <0x41 to 0x5e> : uchar= - 'A' + 1 + * 2). Ctrl + Backspace(0x08) : uchar=0x7f + * 3). Ctrl + Enter(0x0d) : uchar=0x0a + * 4). Ctrl + Space(0x20) : uchar=0x20 + * 5). Ctrl + : uchar=0 + * 6). Ctrl + Alt + : uchar=0 + */ + if (ch > 0) { + if (isAlt) { + processInputChar('\033'); + } + if (isCtrl && ch != ' ' && ch != '\n' && ch != 0x7f) { + processInputChar((char) (ch == '?' ? 0x7f : Character.toUpperCase(ch) & 0x1f)); + } else { + processInputChar(ch); + } + } else if (isCtrl) { // Handles the ctrl key events(uchar=0) + if (virtualKeyCode >= 'A' && virtualKeyCode <= 'Z') { + ch = (char) (virtualKeyCode - 0x40); + } else if (virtualKeyCode == 191) { // ? + ch = 127; + } + if (ch > 0) { + if (isAlt) { + processInputChar('\033'); + } + processInputChar(ch); + } + } + } + } else if (isKeyDown && ch == '\3') { + processInputChar('\3'); + } + // key up event + else { + // support ALT+NumPad input method + if (virtualKeyCode == 0x12 /*VK_MENU ALT key*/ && ch > 0) { + processInputChar(ch); // no such combination in Windows + } + } + } + + protected String getEscapeSequence(short keyCode, int keyState) { + // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx + // TODO: numpad keys, modifiers + String escapeSequence = null; + switch (keyCode) { + case 0x08: // VK_BACK BackSpace + escapeSequence = (keyState & ALT_FLAG) > 0 ? "\\E^H" : getRawSequence(InfoCmp.Capability.key_backspace); + break; + case 0x09: + escapeSequence = (keyState & SHIFT_FLAG) > 0 ? getRawSequence(InfoCmp.Capability.key_btab) : null; + break; + case 0x21: // VK_PRIOR PageUp + escapeSequence = getRawSequence(InfoCmp.Capability.key_ppage); + break; + case 0x22: // VK_NEXT PageDown + escapeSequence = getRawSequence(InfoCmp.Capability.key_npage); + break; + case 0x23: // VK_END + escapeSequence = keyState > 0 ? "\\E[1;%p1%dF" : getRawSequence(InfoCmp.Capability.key_end); + break; + case 0x24: // VK_HOME + escapeSequence = keyState > 0 ? "\\E[1;%p1%dH" : getRawSequence(InfoCmp.Capability.key_home); + break; + case 0x25: // VK_LEFT + escapeSequence = keyState > 0 ? "\\E[1;%p1%dD" : getRawSequence(InfoCmp.Capability.key_left); + break; + case 0x26: // VK_UP + escapeSequence = keyState > 0 ? "\\E[1;%p1%dA" : getRawSequence(InfoCmp.Capability.key_up); + break; + case 0x27: // VK_RIGHT + escapeSequence = keyState > 0 ? "\\E[1;%p1%dC" : getRawSequence(InfoCmp.Capability.key_right); + break; + case 0x28: // VK_DOWN + escapeSequence = keyState > 0 ? "\\E[1;%p1%dB" : getRawSequence(InfoCmp.Capability.key_down); + break; + case 0x2D: // VK_INSERT + escapeSequence = getRawSequence(InfoCmp.Capability.key_ic); + break; + case 0x2E: // VK_DELETE + escapeSequence = getRawSequence(InfoCmp.Capability.key_dc); + break; + case 0x70: // VK_F1 + escapeSequence = keyState > 0 ? "\\E[1;%p1%dP" : getRawSequence(InfoCmp.Capability.key_f1); + break; + case 0x71: // VK_F2 + escapeSequence = keyState > 0 ? "\\E[1;%p1%dQ" : getRawSequence(InfoCmp.Capability.key_f2); + break; + case 0x72: // VK_F3 + escapeSequence = keyState > 0 ? "\\E[1;%p1%dR" : getRawSequence(InfoCmp.Capability.key_f3); + break; + case 0x73: // VK_F4 + escapeSequence = keyState > 0 ? "\\E[1;%p1%dS" : getRawSequence(InfoCmp.Capability.key_f4); + break; + case 0x74: // VK_F5 + escapeSequence = keyState > 0 ? "\\E[15;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f5); + break; + case 0x75: // VK_F6 + escapeSequence = keyState > 0 ? "\\E[17;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f6); + break; + case 0x76: // VK_F7 + escapeSequence = keyState > 0 ? "\\E[18;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f7); + break; + case 0x77: // VK_F8 + escapeSequence = keyState > 0 ? "\\E[19;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f8); + break; + case 0x78: // VK_F9 + escapeSequence = keyState > 0 ? "\\E[20;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f9); + break; + case 0x79: // VK_F10 + escapeSequence = keyState > 0 ? "\\E[21;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f10); + break; + case 0x7A: // VK_F11 + escapeSequence = keyState > 0 ? "\\E[23;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f11); + break; + case 0x7B: // VK_F12 + escapeSequence = keyState > 0 ? "\\E[24;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f12); + break; + case 0x5D: // VK_CLOSE_BRACKET(Menu key) + case 0x5B: // VK_OPEN_BRACKET(Window key) + default: + return null; + } + return Curses.tputs(escapeSequence, keyState + 1); + } + + protected String getRawSequence(InfoCmp.Capability cap) { + return strings.get(cap); + } + + @Override + public boolean hasFocusSupport() { + return true; + } + + @Override + public boolean trackFocus(boolean tracking) { + focusTracking = tracking; + return true; + } + + @Override + public boolean canPauseResume() { + return true; + } + + @Override + public void pause() { + synchronized (lock) { + paused = true; + } + } + + @Override + public void pause(boolean wait) throws InterruptedException { + Thread p; + synchronized (lock) { + paused = true; + p = pump; + } + if (p != null) { + p.interrupt(); + p.join(); + } + } + + @Override + public void resume() { + synchronized (lock) { + paused = false; + if (pump == null) { + pump = new Thread(this::pump, "WindowsStreamPump"); + pump.setDaemon(true); + pump.start(); + } + } + } + + @Override + public boolean paused() { + synchronized (lock) { + return paused; + } + } + + protected void pump() { + try { + while (!closing) { + synchronized (lock) { + if (paused) { + pump = null; + break; + } + } + if (processConsoleInput()) { + slaveInputPipe.flush(); + } + } + } catch (IOException e) { + if (!closing) { + Log.warn("Error in WindowsStreamPump", e); + try { + close(); + } catch (IOException e1) { + Log.warn("Error closing terminal", e); + } + } + } finally { + synchronized (lock) { + pump = null; + } + } + } + + public void processInputChar(char c) throws IOException { + if (attributes.getLocalFlag(Attributes.LocalFlag.ISIG)) { + if (c == attributes.getControlChar(Attributes.ControlChar.VINTR)) { + raise(Signal.INT); + return; + } else if (c == attributes.getControlChar(Attributes.ControlChar.VQUIT)) { + raise(Signal.QUIT); + return; + } else if (c == attributes.getControlChar(Attributes.ControlChar.VSUSP)) { + raise(Signal.TSTP); + return; + } else if (c == attributes.getControlChar(Attributes.ControlChar.VSTATUS)) { + raise(Signal.INFO); + } + } + if (attributes.getInputFlag(Attributes.InputFlag.INORMEOL)) { + if (c == '\r') { + skipNextLf = true; + c = '\n'; + } else if (c == '\n') { + if (skipNextLf) { + skipNextLf = false; + return; + } + } else { + skipNextLf = false; + } + } else if (c == '\r') { + if (attributes.getInputFlag(Attributes.InputFlag.IGNCR)) { + return; + } + if (attributes.getInputFlag(Attributes.InputFlag.ICRNL)) { + c = '\n'; + } + } else if (c == '\n' && attributes.getInputFlag(Attributes.InputFlag.INLCR)) { + c = '\r'; + } + // if (attributes.getLocalFlag(Attributes.LocalFlag.ECHO)) { + // processOutputByte(c); + // masterOutput.flush(); + // } + slaveInputPipe.write(c); + } + + @Override + public boolean trackMouse(MouseTracking tracking) { + this.tracking = tracking; + updateConsoleMode(); + return true; + } + + protected abstract int getConsoleMode(Console console); + + protected abstract void setConsoleMode(Console console, int mode); + + /** + * Read a single input event from the input buffer and process it. + * + * @return true if new input was generated from the event + * @throws IOException if anything wrong happens + */ + protected abstract boolean processConsoleInput() throws IOException; + + @Override + public TerminalProvider getProvider() { + return provider; + } + + @Override + public SystemStream getSystemStream() { + return systemStream; + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/CursorSupport.java b/net-cli/src/main/java/org/jline/terminal/impl/CursorSupport.java new file mode 100644 index 0000000..2e16ef8 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/CursorSupport.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOError; +import java.io.IOException; +import java.util.function.IntConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jline.terminal.Cursor; +import org.jline.terminal.Terminal; +import org.jline.utils.Curses; +import org.jline.utils.InfoCmp; + +public class CursorSupport { + + public static Cursor getCursorPosition(Terminal terminal, IntConsumer discarded) { + try { + String u6 = terminal.getStringCapability(InfoCmp.Capability.user6); + String u7 = terminal.getStringCapability(InfoCmp.Capability.user7); + if (u6 == null || u7 == null) { + return null; + } + // Prepare parser + boolean inc1 = false; + StringBuilder patb = new StringBuilder(); + int index = 0; + while (index < u6.length()) { + char ch; + switch (ch = u6.charAt(index++)) { + case '\\': + switch (u6.charAt(index++)) { + case 'e': + case 'E': + patb.append("\\x1b"); + break; + default: + throw new IllegalArgumentException(); + } + break; + case '%': + ch = u6.charAt(index++); + switch (ch) { + case '%': + patb.append('%'); + break; + case 'i': + inc1 = true; + break; + case 'd': + patb.append("([0-9]+)"); + break; + default: + throw new IllegalArgumentException(); + } + break; + default: + switch (ch) { + case '[': + patb.append('\\'); + break; + } + patb.append(ch); + break; + } + } + Pattern pattern = Pattern.compile(patb.toString()); + // Output cursor position request + Curses.tputs(terminal.writer(), u7); + terminal.flush(); + StringBuilder sb = new StringBuilder(); + int start = 0; + while (true) { + int c = terminal.reader().read(); + if (c < 0) { + return null; + } + sb.append((char) c); + Matcher matcher = pattern.matcher(sb.substring(start)); + if (matcher.matches()) { + int y = Integer.parseInt(matcher.group(1)); + int x = Integer.parseInt(matcher.group(2)); + if (inc1) { + x--; + y--; + } + if (discarded != null) { + for (int i = 0; i < start; i++) { + discarded.accept(sb.charAt(i)); + } + } + return new Cursor(x, y); + } else if (!matcher.hitEnd()) { + start++; + } + } + } catch (IOException e) { + throw new IOError(e); + } + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/Diag.java b/net-cli/src/main/java/org/jline/terminal/impl/Diag.java new file mode 100644 index 0000000..134f06a --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/Diag.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2022, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.TimeUnit; +import org.jline.terminal.Attributes; +import org.jline.terminal.Terminal; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.OSUtils; + +public class Diag { + + private final PrintStream out; + private final boolean verbose; + + public Diag(PrintStream out, boolean verbose) { + this.out = out; + this.verbose = verbose; + } + + public static void main(String[] args) { + diag(System.out, Arrays.asList(args).contains("--verbose")); + } + + public static void diag(PrintStream out) { + diag(out, true); + } + + public static void diag(PrintStream out, boolean verbose) { + new Diag(out, verbose).run(); + } + + public void run() { + out.println("System properties"); + out.println("================="); + out.println("os.name = " + System.getProperty("os.name")); + out.println("OSTYPE = " + System.getenv("OSTYPE")); + out.println("MSYSTEM = " + System.getenv("MSYSTEM")); + out.println("PWD = " + System.getenv("PWD")); + out.println("ConEmuPID = " + System.getenv("ConEmuPID")); + out.println("WSL_DISTRO_NAME = " + System.getenv("WSL_DISTRO_NAME")); + out.println("WSL_INTEROP = " + System.getenv("WSL_INTEROP")); + out.println(); + + out.println("OSUtils"); + out.println("================="); + out.println("IS_WINDOWS = " + OSUtils.IS_WINDOWS); + out.println("IS_CYGWIN = " + OSUtils.IS_CYGWIN); + out.println("IS_MSYSTEM = " + OSUtils.IS_MSYSTEM); + out.println("IS_WSL = " + OSUtils.IS_WSL); + out.println("IS_WSL1 = " + OSUtils.IS_WSL1); + out.println("IS_WSL2 = " + OSUtils.IS_WSL2); + out.println("IS_CONEMU = " + OSUtils.IS_CONEMU); + out.println("IS_OSX = " + OSUtils.IS_OSX); + out.println(); + + // FFM + out.println("FFM Support"); + out.println("================="); + try { + TerminalProvider provider = TerminalProvider.load("ffm"); + testProvider(provider); + } catch (Throwable t) { + error("FFM support not available", t); + } + out.println(); + + out.println("JnaSupport"); + out.println("================="); + try { + TerminalProvider provider = TerminalProvider.load("jna"); + testProvider(provider); + } catch (Throwable t) { + error("JNA support not available", t); + } + out.println(); + + out.println("Jansi2Support"); + out.println("================="); + try { + TerminalProvider provider = TerminalProvider.load("jansi"); + testProvider(provider); + } catch (Throwable t) { + error("Jansi 2 support not available", t); + } + out.println(); + + out.println("JniSupport"); + out.println("================="); + try { + TerminalProvider provider = TerminalProvider.load("jni"); + testProvider(provider); + } catch (Throwable t) { + error("JNI support not available", t); + } + out.println(); + + // Exec + out.println("Exec Support"); + out.println("================="); + try { + TerminalProvider provider = TerminalProvider.load("exec"); + testProvider(provider); + } catch (Throwable t) { + error("Exec support not available", t); + } + + if (!verbose) { + out.println(); + out.println("Run with --verbose argument to print stack traces"); + } + } + + private void testProvider(TerminalProvider provider) { + try { + out.println("StdIn stream = " + provider.isSystemStream(SystemStream.Input)); + out.println("StdOut stream = " + provider.isSystemStream(SystemStream.Output)); + out.println("StdErr stream = " + provider.isSystemStream(SystemStream.Error)); + } catch (Throwable t) { + error("Unable to check stream", t); + } + try { + out.println("StdIn stream name = " + provider.systemStreamName(SystemStream.Input)); + out.println("StdOut stream name = " + provider.systemStreamName(SystemStream.Output)); + out.println("StdErr stream name = " + provider.systemStreamName(SystemStream.Error)); + } catch (Throwable t) { + error("Unable to check stream names", t); + } + try (Terminal terminal = provider.sysTerminal( + "diag", + "xterm", + false, + StandardCharsets.UTF_8, + false, + Terminal.SignalHandler.SIG_DFL, + false, + SystemStream.Output)) { + if (terminal != null) { + Attributes attr = terminal.enterRawMode(); + try { + out.println("Terminal size: " + terminal.getSize()); + ForkJoinPool forkJoinPool = new ForkJoinPool(1); + try { + ForkJoinTask t = + forkJoinPool.submit(() -> terminal.reader().read(1)); + t.get(1000, TimeUnit.MILLISECONDS); + } finally { + forkJoinPool.shutdown(); + } + StringBuilder sb = new StringBuilder(); + sb.append("The terminal seems to work: "); + sb.append("terminal ").append(terminal.getClass().getName()); + if (terminal instanceof AbstractPosixTerminal) { + sb.append(" with pty ") + .append(((AbstractPosixTerminal) terminal) + .getPty() + .getClass() + .getName()); + } + out.println(sb); + } catch (Throwable t2) { + error("Unable to read from terminal", t2); + } finally { + terminal.setAttributes(attr); + } + } else { + out.println("Not supported by provider"); + } + } catch (Throwable t) { + error("Unable to open terminal", t); + } + } + + private void error(String message, Throwable cause) { + if (verbose) { + out.println(message); + cause.printStackTrace(out); + } else { + out.println(message + ": " + cause); + } + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/DumbTerminal.java b/net-cli/src/main/java/org/jline/terminal/impl/DumbTerminal.java new file mode 100644 index 0000000..b5ec71c --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/DumbTerminal.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import org.jline.terminal.Attributes; +import org.jline.terminal.Attributes.ControlChar; +import org.jline.terminal.Size; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.NonBlocking; +import org.jline.utils.NonBlockingInputStream; +import org.jline.utils.NonBlockingReader; + +public class DumbTerminal extends AbstractTerminal { + + private final TerminalProvider provider; + private final SystemStream systemStream; + private final NonBlockingInputStream input; + private final OutputStream output; + private final NonBlockingReader reader; + private final PrintWriter writer; + private final Attributes attributes; + private final Size size; + private boolean skipNextLf; + + public DumbTerminal(InputStream in, OutputStream out) throws IOException { + this(TYPE_DUMB, TYPE_DUMB, in, out, null); + } + + public DumbTerminal(String name, String type, InputStream in, OutputStream out, Charset encoding) + throws IOException { + this(null, null, name, type, in, out, encoding, SignalHandler.SIG_DFL); + } + + @SuppressWarnings("this-escape") + public DumbTerminal( + TerminalProvider provider, + SystemStream systemStream, + String name, + String type, + InputStream in, + OutputStream out, + Charset encoding, + SignalHandler signalHandler) + throws IOException { + super(name, type, encoding, signalHandler); + this.provider = provider; + this.systemStream = systemStream; + NonBlockingInputStream nbis = NonBlocking.nonBlocking(getName(), in); + this.input = new NonBlockingInputStream() { + @Override + public int read(long timeout, boolean isPeek) throws IOException { + for (; ; ) { + int c = nbis.read(timeout, isPeek); + if (attributes.getLocalFlag(Attributes.LocalFlag.ISIG)) { + if (c == attributes.getControlChar(ControlChar.VINTR)) { + raise(Signal.INT); + continue; + } else if (c == attributes.getControlChar(ControlChar.VQUIT)) { + raise(Signal.QUIT); + continue; + } else if (c == attributes.getControlChar(ControlChar.VSUSP)) { + raise(Signal.TSTP); + continue; + } else if (c == attributes.getControlChar(ControlChar.VSTATUS)) { + raise(Signal.INFO); + continue; + } + } + if (attributes.getInputFlag(Attributes.InputFlag.INORMEOL)) { + if (c == '\r') { + skipNextLf = true; + c = '\n'; + } else if (c == '\n') { + if (skipNextLf) { + skipNextLf = false; + continue; + } + } else { + skipNextLf = false; + } + } else if (c == '\r') { + if (attributes.getInputFlag(Attributes.InputFlag.IGNCR)) { + continue; + } + if (attributes.getInputFlag(Attributes.InputFlag.ICRNL)) { + c = '\n'; + } + } else if (c == '\n' && attributes.getInputFlag(Attributes.InputFlag.INLCR)) { + c = '\r'; + } + return c; + } + } + }; + this.output = out; + this.reader = NonBlocking.nonBlocking(getName(), input, encoding()); + this.writer = new PrintWriter(new OutputStreamWriter(output, encoding())); + this.attributes = new Attributes(); + this.attributes.setControlChar(ControlChar.VERASE, (char) 127); + this.attributes.setControlChar(ControlChar.VWERASE, (char) 23); + this.attributes.setControlChar(ControlChar.VKILL, (char) 21); + this.attributes.setControlChar(ControlChar.VLNEXT, (char) 22); + this.size = new Size(); + parseInfoCmp(); + } + + public NonBlockingReader reader() { + return reader; + } + + public PrintWriter writer() { + return writer; + } + + @Override + public InputStream input() { + return input; + } + + @Override + public OutputStream output() { + return output; + } + + public Attributes getAttributes() { + return new Attributes(attributes); + } + + public void setAttributes(Attributes attr) { + attributes.copy(attr); + } + + public Size getSize() { + Size sz = new Size(); + sz.copy(size); + return sz; + } + + public void setSize(Size sz) { + size.copy(sz); + } + + @Override + public TerminalProvider getProvider() { + return provider; + } + + @Override + public SystemStream getSystemStream() { + return systemStream; + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/DumbTerminalProvider.java b/net-cli/src/main/java/org/jline/terminal/impl/DumbTerminalProvider.java new file mode 100644 index 0000000..f505c77 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/DumbTerminalProvider.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalProvider; + +public class DumbTerminalProvider implements TerminalProvider { + + @Override + public String name() { + return TerminalBuilder.PROP_PROVIDER_DUMB; + } + + @Override + public Terminal sysTerminal( + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + boolean nativeSignals, + Terminal.SignalHandler signalHandler, + boolean paused, + SystemStream systemStream) + throws IOException { + return new DumbTerminal( + this, + systemStream, + name, + type, + new FileInputStream(FileDescriptor.in), + new FileOutputStream(systemStream == SystemStream.Error ? FileDescriptor.err : FileDescriptor.out), + encoding, + signalHandler); + } + + @Override + public Terminal newTerminal( + String name, + String type, + InputStream masterInput, + OutputStream masterOutput, + Charset encoding, + Terminal.SignalHandler signalHandler, + boolean paused, + Attributes attributes, + Size size) + throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSystemStream(SystemStream stream) { + return false; + } + + @Override + public String systemStreamName(SystemStream stream) { + return null; + } + + @Override + public int systemStreamWidth(SystemStream stream) { + return 0; + } + + @Override + public String toString() { + return "TerminalProvider[" + name() + "]"; + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/ExternalTerminal.java b/net-cli/src/main/java/org/jline/terminal/impl/ExternalTerminal.java new file mode 100644 index 0000000..19cd727 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/ExternalTerminal.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.IntConsumer; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Cursor; +import org.jline.terminal.Size; +import org.jline.terminal.spi.TerminalProvider; + +/** + * Console implementation with embedded line disciplined. + *

+ * This terminal is well-suited for supporting incoming external + * connections, such as from the network (through telnet, ssh, + * or any kind of protocol). + * The terminal will start consuming the input in a separate thread + * to generate interruption events. + * + * @see LineDisciplineTerminal + */ +public class ExternalTerminal extends LineDisciplineTerminal { + + protected final AtomicBoolean closed = new AtomicBoolean(); + protected final InputStream masterInput; + protected final Object lock = new Object(); + private final TerminalProvider provider; + protected boolean paused = true; + protected Thread pumpThread; + + public ExternalTerminal( + String name, String type, InputStream masterInput, OutputStream masterOutput, Charset encoding) + throws IOException { + this(null, name, type, masterInput, masterOutput, encoding, SignalHandler.SIG_DFL); + } + + public ExternalTerminal( + TerminalProvider provider, + String name, + String type, + InputStream masterInput, + OutputStream masterOutput, + Charset encoding, + SignalHandler signalHandler) + throws IOException { + this(provider, name, type, masterInput, masterOutput, encoding, signalHandler, false); + } + + public ExternalTerminal( + TerminalProvider provider, + String name, + String type, + InputStream masterInput, + OutputStream masterOutput, + Charset encoding, + SignalHandler signalHandler, + boolean paused) + throws IOException { + this(provider, name, type, masterInput, masterOutput, encoding, signalHandler, paused, null, null); + } + + @SuppressWarnings("this-escape") + public ExternalTerminal( + TerminalProvider provider, + String name, + String type, + InputStream masterInput, + OutputStream masterOutput, + Charset encoding, + SignalHandler signalHandler, + boolean paused, + Attributes attributes, + Size size) + throws IOException { + super(name, type, masterOutput, encoding, signalHandler); + this.provider = provider; + this.masterInput = masterInput; + if (attributes != null) { + setAttributes(attributes); + } + if (size != null) { + setSize(size); + } + if (!paused) { + resume(); + } + } + + protected void doClose() throws IOException { + if (closed.compareAndSet(false, true)) { + pause(); + super.doClose(); + } + } + + @Override + public boolean canPauseResume() { + return true; + } + + @Override + public void pause() { + synchronized (lock) { + paused = true; + } + } + + @Override + public void pause(boolean wait) throws InterruptedException { + Thread p; + synchronized (lock) { + paused = true; + p = pumpThread; + } + if (p != null) { + p.interrupt(); + p.join(); + } + } + + @Override + public void resume() { + synchronized (lock) { + paused = false; + if (pumpThread == null) { + pumpThread = new Thread(this::pump, this + " input pump thread"); + pumpThread.setDaemon(true); + pumpThread.start(); + } + } + } + + @Override + public boolean paused() { + synchronized (lock) { + return paused; + } + } + + public void pump() { + try { + byte[] buf = new byte[1024]; + while (true) { + int c = masterInput.read(buf); + if (c >= 0) { + processInputBytes(buf, 0, c); + } + if (c < 0 || closed.get()) { + break; + } + synchronized (lock) { + if (paused) { + pumpThread = null; + return; + } + } + } + } catch (IOException e) { + processIOException(e); + } finally { + synchronized (lock) { + pumpThread = null; + } + } + try { + slaveInput.close(); + } catch (IOException e) { + // ignore + } + } + + @Override + public Cursor getCursorPosition(IntConsumer discarded) { + return CursorSupport.getCursorPosition(this, discarded); + } + + @Override + public TerminalProvider getProvider() { + return provider; + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/LineDisciplineTerminal.java b/net-cli/src/main/java/org/jline/terminal/impl/LineDisciplineTerminal.java new file mode 100644 index 0000000..3a36276 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/LineDisciplineTerminal.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.EnumSet; +import java.util.Objects; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Attributes.ControlChar; +import org.jline.terminal.Attributes.InputFlag; +import org.jline.terminal.Attributes.LocalFlag; +import org.jline.terminal.Attributes.OutputFlag; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.NonBlocking; +import org.jline.utils.NonBlockingPumpInputStream; +import org.jline.utils.NonBlockingReader; + +/** + * Abstract terminal with support for line discipline. + * The {@link Terminal} interface represents the slave + * side of a PTY, but implementations derived from this class + * will handle both the slave and master side of things. + *

+ * In order to correctly handle line discipline, the terminal + * needs to read the input in advance in order to raise the + * signals as fast as possible. + * For example, when the user hits Ctrl+C, we can't wait until + * the application consumes all the read events. + * The same applies to echoing, when enabled, as the echoing + * has to happen as soon as the user hit the keyboard, and not + * only when the application running in the terminal processes + * the input. + */ +public class LineDisciplineTerminal extends AbstractTerminal { + + private static final int PIPE_SIZE = 1024; + + /* + * Master output stream + */ + protected final OutputStream masterOutput; + + /* + * Slave input pipe write side + */ + protected final OutputStream slaveInputPipe; + + /* + * Slave streams + */ + protected final NonBlockingPumpInputStream slaveInput; + protected final NonBlockingReader slaveReader; + protected final PrintWriter slaveWriter; + protected final OutputStream slaveOutput; + + /** + * Console data + */ + protected final Attributes attributes; + + protected final Size size; + + protected boolean skipNextLf; + + public LineDisciplineTerminal(String name, String type, OutputStream masterOutput, Charset encoding) + throws IOException { + this(name, type, masterOutput, encoding, SignalHandler.SIG_DFL); + } + + @SuppressWarnings("this-escape") + public LineDisciplineTerminal( + String name, String type, OutputStream masterOutput, Charset encoding, SignalHandler signalHandler) + throws IOException { + super(name, type, encoding, signalHandler); + NonBlockingPumpInputStream input = NonBlocking.nonBlockingPumpInputStream(PIPE_SIZE); + this.slaveInputPipe = input.getOutputStream(); + this.slaveInput = input; + this.slaveReader = NonBlocking.nonBlocking(getName(), slaveInput, encoding()); + this.slaveOutput = new FilteringOutputStream(); + this.slaveWriter = new PrintWriter(new OutputStreamWriter(slaveOutput, encoding())); + this.masterOutput = masterOutput; + this.attributes = getDefaultTerminalAttributes(); + this.size = new Size(160, 50); + parseInfoCmp(); + } + + private static Attributes getDefaultTerminalAttributes() { + // speed 9600 baud; 24 rows; 80 columns; + // lflags: icanon isig iexten echo echoe -echok echoke -echonl echoctl + // -echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo + // -extproc + // iflags: -istrip icrnl -inlcr -igncr ixon -ixoff ixany imaxbel iutf8 + // -ignbrk brkint -inpck -ignpar -parmrk + // oflags: opost onlcr -oxtabs -onocr -onlret + // cflags: cread cs8 -parenb -parodd hupcl -clocal -cstopb -crtscts -dsrflow + // -dtrflow -mdmbuf + // cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = ; + // eol2 = ; erase = ^?; intr = ^C; kill = ^U; lnext = ^V; + // min = 1; quit = ^\\; reprint = ^R; start = ^Q; status = ^T; + // stop = ^S; susp = ^Z; time = 0; werase = ^W; + Attributes attr = new Attributes(); + attr.setLocalFlags(EnumSet.of( + LocalFlag.ICANON, + LocalFlag.ISIG, + LocalFlag.IEXTEN, + LocalFlag.ECHO, + LocalFlag.ECHOE, + LocalFlag.ECHOKE, + LocalFlag.ECHOCTL, + LocalFlag.PENDIN)); + attr.setInputFlags(EnumSet.of( + InputFlag.ICRNL, + InputFlag.IXON, + InputFlag.IXANY, + InputFlag.IMAXBEL, + InputFlag.IUTF8, + InputFlag.BRKINT)); + attr.setOutputFlags(EnumSet.of(OutputFlag.OPOST, OutputFlag.ONLCR)); + attr.setControlChar(ControlChar.VDISCARD, ctrl('O')); + attr.setControlChar(ControlChar.VDSUSP, ctrl('Y')); + attr.setControlChar(ControlChar.VEOF, ctrl('D')); + attr.setControlChar(ControlChar.VERASE, ctrl('?')); + attr.setControlChar(ControlChar.VINTR, ctrl('C')); + attr.setControlChar(ControlChar.VKILL, ctrl('U')); + attr.setControlChar(ControlChar.VLNEXT, ctrl('V')); + attr.setControlChar(ControlChar.VMIN, 1); + attr.setControlChar(ControlChar.VQUIT, ctrl('\\')); + attr.setControlChar(ControlChar.VREPRINT, ctrl('R')); + attr.setControlChar(ControlChar.VSTART, ctrl('Q')); + attr.setControlChar(ControlChar.VSTATUS, ctrl('T')); + attr.setControlChar(ControlChar.VSTOP, ctrl('S')); + attr.setControlChar(ControlChar.VSUSP, ctrl('Z')); + attr.setControlChar(ControlChar.VTIME, 0); + attr.setControlChar(ControlChar.VWERASE, ctrl('W')); + return attr; + } + + private static int ctrl(char c) { + return c == '?' ? 177 : c - 64; + } + + public NonBlockingReader reader() { + return slaveReader; + } + + public PrintWriter writer() { + return slaveWriter; + } + + @Override + public InputStream input() { + return slaveInput; + } + + @Override + public OutputStream output() { + return slaveOutput; + } + + public Attributes getAttributes() { + return new Attributes(attributes); + } + + public void setAttributes(Attributes attr) { + attributes.copy(attr); + } + + public Size getSize() { + Size sz = new Size(); + sz.copy(size); + return sz; + } + + public void setSize(Size sz) { + size.copy(sz); + } + + @Override + public void raise(Signal signal) { + Objects.requireNonNull(signal); + // Do not call clear() atm as this can cause + // deadlock between reading / writing threads + // TODO: any way to fix that ? + /* + if (!attributes.getLocalFlag(LocalFlag.NOFLSH)) { + try { + slaveReader.clear(); + } catch (IOException e) { + // Ignore + } + } + */ + echoSignal(signal); + super.raise(signal); + } + + /** + * Master input processing. + * All data coming to the terminal should be provided + * using this method. + * + * @param c the input byte + * @throws IOException if anything wrong happens + */ + public void processInputByte(int c) throws IOException { + boolean flushOut = doProcessInputByte(c); + slaveInputPipe.flush(); + if (flushOut) { + masterOutput.flush(); + } + } + + public void processInputBytes(byte[] input) throws IOException { + processInputBytes(input, 0, input.length); + } + + public void processInputBytes(byte[] input, int offset, int length) throws IOException { + boolean flushOut = false; + for (int i = 0; i < length; i++) { + flushOut |= doProcessInputByte(input[offset + i]); + } + slaveInputPipe.flush(); + if (flushOut) { + masterOutput.flush(); + } + } + + protected boolean doProcessInputByte(int c) throws IOException { + if (attributes.getLocalFlag(LocalFlag.ISIG)) { + if (c == attributes.getControlChar(ControlChar.VINTR)) { + raise(Signal.INT); + return false; + } else if (c == attributes.getControlChar(ControlChar.VQUIT)) { + raise(Signal.QUIT); + return false; + } else if (c == attributes.getControlChar(ControlChar.VSUSP)) { + raise(Signal.TSTP); + return false; + } else if (c == attributes.getControlChar(ControlChar.VSTATUS)) { + raise(Signal.INFO); + } + } + if (attributes.getInputFlag(InputFlag.INORMEOL)) { + if (c == '\r') { + skipNextLf = true; + c = '\n'; + } else if (c == '\n') { + if (skipNextLf) { + skipNextLf = false; + return false; + } + } else { + skipNextLf = false; + } + } else if (c == '\r') { + if (attributes.getInputFlag(InputFlag.IGNCR)) { + return false; + } + if (attributes.getInputFlag(InputFlag.ICRNL)) { + c = '\n'; + } + } else if (c == '\n' && attributes.getInputFlag(InputFlag.INLCR)) { + c = '\r'; + } + boolean flushOut = false; + if (attributes.getLocalFlag(LocalFlag.ECHO)) { + processOutputByte(c); + flushOut = true; + } + slaveInputPipe.write(c); + return flushOut; + } + + /** + * Master output processing. + * All data going to the master should be provided by this method. + * + * @param c the output byte + * @throws IOException if anything wrong happens + */ + protected void processOutputByte(int c) throws IOException { + if (attributes.getOutputFlag(OutputFlag.OPOST)) { + if (c == '\n') { + if (attributes.getOutputFlag(OutputFlag.ONLCR)) { + masterOutput.write('\r'); + masterOutput.write('\n'); + return; + } + } + } + masterOutput.write(c); + } + + protected void processIOException(IOException ioException) { + this.slaveInput.setIoException(ioException); + } + + protected void doClose() throws IOException { + super.doClose(); + try { + slaveReader.close(); + } finally { + try { + slaveInputPipe.close(); + } finally { + try { + } finally { + slaveWriter.close(); + } + } + } + } + + @Override + public TerminalProvider getProvider() { + return null; + } + + @Override + public SystemStream getSystemStream() { + return null; + } + + private class FilteringOutputStream extends OutputStream { + @Override + public void write(int b) throws IOException { + processOutputByte(b); + flush(); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + for (int i = 0; i < len; i++) { + processOutputByte(b[off + i]); + } + flush(); + } + + @Override + public void flush() throws IOException { + masterOutput.flush(); + } + + @Override + public void close() throws IOException { + masterOutput.close(); + } + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/MouseSupport.java b/net-cli/src/main/java/org/jline/terminal/impl/MouseSupport.java new file mode 100644 index 0000000..722dafe --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/MouseSupport.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.EOFException; +import java.io.IOError; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.function.IntSupplier; + +import org.jline.terminal.MouseEvent; +import org.jline.terminal.Terminal; +import org.jline.utils.InfoCmp; +import org.jline.utils.InputStreamReader; + +public class MouseSupport { + + public static boolean hasMouseSupport(Terminal terminal) { + return terminal.getStringCapability(InfoCmp.Capability.key_mouse) != null; + } + + public static boolean trackMouse(Terminal terminal, Terminal.MouseTracking tracking) { + if (hasMouseSupport(terminal)) { + switch (tracking) { + case Off: + terminal.writer().write("\033[?1000l"); + break; + case Normal: + terminal.writer().write("\033[?1005h\033[?1000h"); + break; + case Button: + terminal.writer().write("\033[?1005h\033[?1002h"); + break; + case Any: + terminal.writer().write("\033[?1005h\033[?1003h"); + break; + } + terminal.flush(); + return true; + } else { + return false; + } + } + + public static MouseEvent readMouse(Terminal terminal, MouseEvent last) { + return readMouse(() -> readExt(terminal), last); + } + + public static MouseEvent readMouse(IntSupplier reader, MouseEvent last) { + int cb = reader.getAsInt() - ' '; + int cx = reader.getAsInt() - ' ' - 1; + int cy = reader.getAsInt() - ' ' - 1; + MouseEvent.Type type; + MouseEvent.Button button; + EnumSet modifiers = EnumSet.noneOf(MouseEvent.Modifier.class); + if ((cb & 4) == 4) { + modifiers.add(MouseEvent.Modifier.Shift); + } + if ((cb & 8) == 8) { + modifiers.add(MouseEvent.Modifier.Alt); + } + if ((cb & 16) == 16) { + modifiers.add(MouseEvent.Modifier.Control); + } + if ((cb & 64) == 64) { + type = MouseEvent.Type.Wheel; + button = (cb & 1) == 1 ? MouseEvent.Button.WheelDown : MouseEvent.Button.WheelUp; + } else { + int b = (cb & 3); + switch (b) { + case 0: + button = MouseEvent.Button.Button1; + if (last.getButton() == button + && (last.getType() == MouseEvent.Type.Pressed + || last.getType() == MouseEvent.Type.Dragged)) { + type = MouseEvent.Type.Dragged; + } else { + type = MouseEvent.Type.Pressed; + } + break; + case 1: + button = MouseEvent.Button.Button2; + if (last.getButton() == button + && (last.getType() == MouseEvent.Type.Pressed + || last.getType() == MouseEvent.Type.Dragged)) { + type = MouseEvent.Type.Dragged; + } else { + type = MouseEvent.Type.Pressed; + } + break; + case 2: + button = MouseEvent.Button.Button3; + if (last.getButton() == button + && (last.getType() == MouseEvent.Type.Pressed + || last.getType() == MouseEvent.Type.Dragged)) { + type = MouseEvent.Type.Dragged; + } else { + type = MouseEvent.Type.Pressed; + } + break; + default: + if (last.getType() == MouseEvent.Type.Pressed || last.getType() == MouseEvent.Type.Dragged) { + button = last.getButton(); + type = MouseEvent.Type.Released; + } else { + button = MouseEvent.Button.NoButton; + type = MouseEvent.Type.Moved; + } + break; + } + } + return new MouseEvent(type, button, modifiers, cx, cy); + } + + private static int readExt(Terminal terminal) { + try { + // The coordinates are encoded in UTF-8, so if that's not the input encoding, + // we need to get around + int c; + if (terminal.encoding() != StandardCharsets.UTF_8) { + c = new InputStreamReader(terminal.input(), StandardCharsets.UTF_8).read(); + } else { + c = terminal.reader().read(); + } + if (c < 0) { + throw new EOFException(); + } + return c; + } catch (IOException e) { + throw new IOError(e); + } + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/NativeSignalHandler.java b/net-cli/src/main/java/org/jline/terminal/impl/NativeSignalHandler.java new file mode 100644 index 0000000..0312ca3 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/NativeSignalHandler.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import org.jline.terminal.Terminal.Signal; +import org.jline.terminal.Terminal.SignalHandler; + +public final class NativeSignalHandler implements SignalHandler { + + public static final NativeSignalHandler SIG_DFL = new NativeSignalHandler(); + + public static final NativeSignalHandler SIG_IGN = new NativeSignalHandler(); + + private NativeSignalHandler() { + } + + public void handle(Signal signal) { + throw new UnsupportedOperationException(); + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/PosixPtyTerminal.java b/net-cli/src/main/java/org/jline/terminal/impl/PosixPtyTerminal.java new file mode 100644 index 0000000..849df30 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/PosixPtyTerminal.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.Objects; + +import org.jline.terminal.spi.Pty; +import org.jline.utils.ClosedException; +import org.jline.utils.NonBlocking; +import org.jline.utils.NonBlockingInputStream; +import org.jline.utils.NonBlockingReader; + +public class PosixPtyTerminal extends AbstractPosixTerminal { + + private final InputStream in; + private final OutputStream out; + private final InputStream masterInput; + private final OutputStream masterOutput; + private final NonBlockingInputStream input; + private final OutputStream output; + private final NonBlockingReader reader; + private final PrintWriter writer; + + private final Object lock = new Object(); + private Thread inputPumpThread; + private Thread outputPumpThread; + private boolean paused = true; + + public PosixPtyTerminal(String name, String type, Pty pty, InputStream in, OutputStream out, Charset encoding) + throws IOException { + this(name, type, pty, in, out, encoding, SignalHandler.SIG_DFL); + } + + public PosixPtyTerminal( + String name, + String type, + Pty pty, + InputStream in, + OutputStream out, + Charset encoding, + SignalHandler signalHandler) + throws IOException { + this(name, type, pty, in, out, encoding, signalHandler, false); + } + + @SuppressWarnings("this-escape") + public PosixPtyTerminal( + String name, + String type, + Pty pty, + InputStream in, + OutputStream out, + Charset encoding, + SignalHandler signalHandler, + boolean paused) + throws IOException { + super(name, type, pty, encoding, signalHandler); + this.in = Objects.requireNonNull(in); + this.out = Objects.requireNonNull(out); + this.masterInput = pty.getMasterInput(); + this.masterOutput = pty.getMasterOutput(); + this.input = new InputStreamWrapper(NonBlocking.nonBlocking(name, pty.getSlaveInput())); + this.output = pty.getSlaveOutput(); + this.reader = NonBlocking.nonBlocking(name, input, encoding()); + this.writer = new PrintWriter(new OutputStreamWriter(output, encoding())); + parseInfoCmp(); + if (!paused) { + resume(); + } + } + + public InputStream input() { + return input; + } + + public NonBlockingReader reader() { + return reader; + } + + public OutputStream output() { + return output; + } + + public PrintWriter writer() { + return writer; + } + + @Override + protected void doClose() throws IOException { + super.doClose(); + reader.close(); + } + + @Override + public boolean canPauseResume() { + return true; + } + + @Override + public void pause() { + synchronized (lock) { + paused = true; + } + } + + @Override + public void pause(boolean wait) throws InterruptedException { + Thread p1, p2; + synchronized (lock) { + paused = true; + p1 = inputPumpThread; + p2 = outputPumpThread; + } + if (p1 != null) { + p1.interrupt(); + } + if (p2 != null) { + p2.interrupt(); + } + if (p1 != null) { + p1.join(); + } + if (p2 != null) { + p2.join(); + } + } + + @Override + public void resume() { + synchronized (lock) { + paused = false; + if (inputPumpThread == null) { + inputPumpThread = new Thread(this::pumpIn, this + " input pump thread"); + inputPumpThread.setDaemon(true); + inputPumpThread.start(); + } + if (outputPumpThread == null) { + outputPumpThread = new Thread(this::pumpOut, this + " output pump thread"); + outputPumpThread.setDaemon(true); + outputPumpThread.start(); + } + } + } + + @Override + public boolean paused() { + synchronized (lock) { + return paused; + } + } + + private void pumpIn() { + try { + for (; ; ) { + synchronized (lock) { + if (paused) { + inputPumpThread = null; + return; + } + } + int b = in.read(); + if (b < 0) { + input.close(); + break; + } + masterOutput.write(b); + masterOutput.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + synchronized (lock) { + inputPumpThread = null; + } + } + } + + private void pumpOut() { + try { + for (; ; ) { + synchronized (lock) { + if (paused) { + outputPumpThread = null; + return; + } + } + int b = masterInput.read(); + if (b < 0) { + input.close(); + break; + } + out.write(b); + out.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + synchronized (lock) { + outputPumpThread = null; + } + } + try { + close(); + } catch (Throwable t) { + // Ignore + } + } + + private static class InputStreamWrapper extends NonBlockingInputStream { + + private final NonBlockingInputStream in; + private volatile boolean closed; + + protected InputStreamWrapper(NonBlockingInputStream in) { + this.in = in; + } + + @Override + public int read(long timeout, boolean isPeek) throws IOException { + if (closed) { + throw new ClosedException(); + } + return in.read(timeout, isPeek); + } + + @Override + public void close() throws IOException { + closed = true; + } + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/PosixSysTerminal.java b/net-cli/src/main/java/org/jline/terminal/impl/PosixSysTerminal.java new file mode 100644 index 0000000..5b3be98 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/PosixSysTerminal.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import org.jline.terminal.spi.Pty; +import org.jline.utils.FastBufferedOutputStream; +import org.jline.utils.NonBlocking; +import org.jline.utils.NonBlockingInputStream; +import org.jline.utils.NonBlockingReader; +import org.jline.utils.ShutdownHooks; +import org.jline.utils.ShutdownHooks.Task; +import org.jline.utils.Signals; + +public class PosixSysTerminal extends AbstractPosixTerminal { + + protected final NonBlockingInputStream input; + protected final OutputStream output; + protected final NonBlockingReader reader; + protected final PrintWriter writer; + protected final Map nativeHandlers = new HashMap<>(); + protected final Task closer; + + @SuppressWarnings("this-escape") + public PosixSysTerminal( + String name, String type, Pty pty, Charset encoding, boolean nativeSignals, SignalHandler signalHandler) + throws IOException { + super(name, type, pty, encoding, signalHandler); + this.input = NonBlocking.nonBlocking(getName(), pty.getSlaveInput()); + this.output = new FastBufferedOutputStream(pty.getSlaveOutput()); + this.reader = NonBlocking.nonBlocking(getName(), input, encoding()); + this.writer = new PrintWriter(new OutputStreamWriter(output, encoding())); + parseInfoCmp(); + if (nativeSignals) { + for (final Signal signal : Signal.values()) { + if (signalHandler == SignalHandler.SIG_DFL) { + nativeHandlers.put(signal, Signals.registerDefault(signal.name())); + } else { + nativeHandlers.put(signal, Signals.register(signal.name(), () -> raise(signal))); + } + } + } + closer = PosixSysTerminal.this::close; + ShutdownHooks.add(closer); + } + + @Override + public SignalHandler handle(Signal signal, SignalHandler handler) { + SignalHandler prev = super.handle(signal, handler); + if (prev != handler) { + if (handler == SignalHandler.SIG_DFL) { + Signals.registerDefault(signal.name()); + } else { + Signals.register(signal.name(), () -> raise(signal)); + } + } + return prev; + } + + public NonBlockingReader reader() { + return reader; + } + + public PrintWriter writer() { + return writer; + } + + @Override + public InputStream input() { + return input; + } + + @Override + public OutputStream output() { + return output; + } + + @Override + protected void doClose() throws IOException { + ShutdownHooks.remove(closer); + for (Map.Entry entry : nativeHandlers.entrySet()) { + Signals.unregister(entry.getKey().name(), entry.getValue()); + } + super.doClose(); + // Do not call reader.close() + reader.shutdown(); + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/exec/ExecPty.java b/net-cli/src/main/java/org/jline/terminal/impl/exec/ExecPty.java new file mode 100644 index 0000000..36a1c5b --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/exec/ExecPty.java @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.exec; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jline.terminal.Attributes; +import org.jline.terminal.Attributes.ControlChar; +import org.jline.terminal.Attributes.ControlFlag; +import org.jline.terminal.Attributes.InputFlag; +import org.jline.terminal.Attributes.LocalFlag; +import org.jline.terminal.Attributes.OutputFlag; +import org.jline.terminal.Size; +import org.jline.terminal.impl.AbstractPty; +import org.jline.terminal.spi.Pty; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.OSUtils; +import static org.jline.utils.ExecHelper.exec; + +public class ExecPty extends AbstractPty implements Pty { + + private final String name; + + protected ExecPty(TerminalProvider provider, SystemStream systemStream, String name) { + super(provider, systemStream); + this.name = name; + } + + public static Pty current(TerminalProvider provider, SystemStream systemStream) throws IOException { + try { + String result = exec(true, OSUtils.TTY_COMMAND); + if (systemStream != SystemStream.Output && systemStream != SystemStream.Error) { + throw new IllegalArgumentException("systemStream should be Output or Error: " + systemStream); + } + return new ExecPty(provider, systemStream, result.trim()); + } catch (IOException e) { + throw new IOException("Not a tty", e); + } + } + + public static Attributes doGetAttr(String cfg) throws IOException { + Attributes attributes = new Attributes(); + for (InputFlag flag : InputFlag.values()) { + Boolean value = doGetFlag(cfg, flag); + if (value != null) { + attributes.setInputFlag(flag, value); + } + } + for (OutputFlag flag : OutputFlag.values()) { + Boolean value = doGetFlag(cfg, flag); + if (value != null) { + attributes.setOutputFlag(flag, value); + } + } + for (ControlFlag flag : ControlFlag.values()) { + Boolean value = doGetFlag(cfg, flag); + if (value != null) { + attributes.setControlFlag(flag, value); + } + } + for (LocalFlag flag : LocalFlag.values()) { + Boolean value = doGetFlag(cfg, flag); + if (value != null) { + attributes.setLocalFlag(flag, value); + } + } + for (ControlChar cchar : ControlChar.values()) { + String name = cchar.name().toLowerCase().substring(1); + if ("reprint".endsWith(name)) { + name = "(?:reprint|rprnt)"; + } + Matcher matcher = + Pattern.compile("[\\s;]" + name + "\\s*=\\s*(.+?)[\\s;]").matcher(cfg); + if (matcher.find()) { + attributes.setControlChar( + cchar, parseControlChar(matcher.group(1).toUpperCase())); + } + } + return attributes; + } + + private static Boolean doGetFlag(String cfg, Enum flag) { + Matcher matcher = Pattern.compile("(?:^|[\\s;])(\\-?" + flag.name().toLowerCase() + ")(?:[\\s;]|$)") + .matcher(cfg); + return matcher.find() ? !matcher.group(1).startsWith("-") : null; + } + + static int parseControlChar(String str) { + // undef + if ("".equals(str)) { + return -1; + } + // del + if ("DEL".equalsIgnoreCase(str)) { + return 127; + } + // octal + if (str.charAt(0) == '0') { + return Integer.parseInt(str, 8); + } + // decimal + if (str.charAt(0) >= '1' && str.charAt(0) <= '9') { + return Integer.parseInt(str, 10); + } + // control char + if (str.charAt(0) == '^') { + if (str.charAt(1) == '?') { + return 127; + } else { + return str.charAt(1) - 64; + } + } else if (str.charAt(0) == 'M' && str.charAt(1) == '-') { + if (str.charAt(2) == '^') { + if (str.charAt(3) == '?') { + return 127 + 128; + } else { + return str.charAt(3) - 64 + 128; + } + } else { + return str.charAt(2) + 128; + } + } else { + return str.charAt(0); + } + } + + static Size doGetSize(String cfg) throws IOException { + return new Size(doGetInt("columns", cfg), doGetInt("rows", cfg)); + } + + static int doGetInt(String name, String cfg) throws IOException { + String[] patterns = new String[]{ + "\\b([0-9]+)\\s+" + name + "\\b", "\\b" + name + "\\s+([0-9]+)\\b", "\\b" + name + "\\s*=\\s*([0-9]+)\\b" + }; + for (String pattern : patterns) { + Matcher matcher = Pattern.compile(pattern).matcher(cfg); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + } + return 0; + } + + @Override + public void close() throws IOException { + } + + public String getName() { + return name; + } + + @Override + public InputStream getMasterInput() { + throw new UnsupportedOperationException(); + } + + @Override + public OutputStream getMasterOutput() { + throw new UnsupportedOperationException(); + } + + @Override + protected InputStream doGetSlaveInput() throws IOException { + return systemStream != null ? new FileInputStream(FileDescriptor.in) : new FileInputStream(getName()); + } + + @Override + public OutputStream getSlaveOutput() throws IOException { + return systemStream == SystemStream.Output + ? new FileOutputStream(FileDescriptor.out) + : systemStream == SystemStream.Error + ? new FileOutputStream(FileDescriptor.err) + : new FileOutputStream(getName()); + } + + @Override + public Attributes getAttr() throws IOException { + String cfg = doGetConfig(); + return doGetAttr(cfg); + } + + @Override + protected void doSetAttr(Attributes attr) throws IOException { + List commands = getFlagsToSet(attr, getAttr()); + if (!commands.isEmpty()) { + commands.add(0, OSUtils.STTY_COMMAND); + if (systemStream == null) { + commands.add(1, OSUtils.STTY_F_OPTION); + commands.add(2, getName()); + } + try { + exec(systemStream != null, commands.toArray(new String[0])); + } catch (IOException e) { + // Handle partial failures with GNU stty, see #97 + if (e.toString().contains("unable to perform all requested operations")) { + commands = getFlagsToSet(attr, getAttr()); + if (!commands.isEmpty()) { + throw new IOException("Could not set the following flags: " + String.join(", ", commands), e); + } + } else { + throw e; + } + } + } + } + + protected List getFlagsToSet(Attributes attr, Attributes current) { + List commands = new ArrayList<>(); + for (InputFlag flag : InputFlag.values()) { + if (attr.getInputFlag(flag) != current.getInputFlag(flag) && flag != InputFlag.INORMEOL) { + commands.add((attr.getInputFlag(flag) ? flag.name() : "-" + flag.name()).toLowerCase()); + } + } + for (OutputFlag flag : OutputFlag.values()) { + if (attr.getOutputFlag(flag) != current.getOutputFlag(flag)) { + commands.add((attr.getOutputFlag(flag) ? flag.name() : "-" + flag.name()).toLowerCase()); + } + } + for (ControlFlag flag : ControlFlag.values()) { + if (attr.getControlFlag(flag) != current.getControlFlag(flag)) { + commands.add((attr.getControlFlag(flag) ? flag.name() : "-" + flag.name()).toLowerCase()); + } + } + for (LocalFlag flag : LocalFlag.values()) { + if (attr.getLocalFlag(flag) != current.getLocalFlag(flag)) { + commands.add((attr.getLocalFlag(flag) ? flag.name() : "-" + flag.name()).toLowerCase()); + } + } + String undef = System.getProperty("os.name").toLowerCase().startsWith("hp") ? "^-" : "undef"; + for (ControlChar cchar : ControlChar.values()) { + int v = attr.getControlChar(cchar); + if (v >= 0 && v != current.getControlChar(cchar)) { + String str = ""; + commands.add(cchar.name().toLowerCase().substring(1)); + if (cchar == ControlChar.VMIN || cchar == ControlChar.VTIME) { + commands.add(Integer.toString(v)); + } else if (v == 0) { + commands.add(undef); + } else { + if (v >= 128) { + v -= 128; + str += "M-"; + } + if (v < 32 || v == 127) { + v ^= 0x40; + str += "^"; + } + str += (char) v; + commands.add(str); + } + } + } + return commands; + } + + @Override + public Size getSize() throws IOException { + String cfg = doGetConfig(); + return doGetSize(cfg); + } + + @Override + public void setSize(Size size) throws IOException { + if (systemStream != null) { + exec( + true, + OSUtils.STTY_COMMAND, + "columns", + Integer.toString(size.getColumns()), + "rows", + Integer.toString(size.getRows())); + } else { + exec( + false, + OSUtils.STTY_COMMAND, + OSUtils.STTY_F_OPTION, + getName(), + "columns", + Integer.toString(size.getColumns()), + "rows", + Integer.toString(size.getRows())); + } + } + + protected String doGetConfig() throws IOException { + return systemStream != null + ? exec(true, OSUtils.STTY_COMMAND, "-a") + : exec(false, OSUtils.STTY_COMMAND, OSUtils.STTY_F_OPTION, getName(), "-a"); + } + + @Override + public String toString() { + return "ExecPty[" + getName() + (systemStream != null ? ", system]" : "]"); + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java b/net-cli/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java new file mode 100644 index 0000000..d4d8cbb --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/exec/ExecTerminalProvider.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2022, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.exec; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.nio.charset.Charset; +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.terminal.impl.ExternalTerminal; +import org.jline.terminal.impl.PosixSysTerminal; +import org.jline.terminal.spi.Pty; +import org.jline.terminal.spi.SystemStream; +import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.ExecHelper; +import org.jline.utils.Log; +import org.jline.utils.OSUtils; +import static org.jline.terminal.TerminalBuilder.PROP_REDIRECT_PIPE_CREATION_MODE; +import static org.jline.terminal.TerminalBuilder.PROP_REDIRECT_PIPE_CREATION_MODE_DEFAULT; +import static org.jline.terminal.TerminalBuilder.PROP_REDIRECT_PIPE_CREATION_MODE_REFLECTION; + +public class ExecTerminalProvider implements TerminalProvider { + + private static boolean warned; + private static RedirectPipeCreator redirectPipeCreator; + + protected static ProcessBuilder.Redirect newDescriptor(FileDescriptor fd) { + if (redirectPipeCreator == null) { + String str = System.getProperty(PROP_REDIRECT_PIPE_CREATION_MODE, PROP_REDIRECT_PIPE_CREATION_MODE_DEFAULT); + String[] modes = str.split(","); + IllegalStateException ise = new IllegalStateException("Unable to create RedirectPipe"); + for (String mode : modes) { + try { + switch (mode) { + case PROP_REDIRECT_PIPE_CREATION_MODE_REFLECTION: + redirectPipeCreator = new ReflectionRedirectPipeCreator(); + break; + } + } catch (Throwable t) { + // ignore + ise.addSuppressed(t); + } + if (redirectPipeCreator != null) { + break; + } + } + if (redirectPipeCreator == null) { + throw ise; + } + } + return redirectPipeCreator.newRedirectPipe(fd); + } + + public String name() { + return TerminalBuilder.PROP_PROVIDER_EXEC; + } + + public Pty current(SystemStream systemStream) throws IOException { + return ExecPty.current(this, systemStream); + } + + @Override + public Terminal sysTerminal( + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + boolean nativeSignals, + Terminal.SignalHandler signalHandler, + boolean paused, + SystemStream systemStream) + throws IOException { + if (OSUtils.IS_WINDOWS) { + return winSysTerminal( + name, type, ansiPassThrough, encoding, nativeSignals, signalHandler, paused, systemStream); + } else { + return posixSysTerminal( + name, type, ansiPassThrough, encoding, nativeSignals, signalHandler, paused, systemStream); + } + } + + public Terminal winSysTerminal( + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + boolean nativeSignals, + Terminal.SignalHandler signalHandler, + boolean paused, + SystemStream systemStream) + throws IOException { + if (OSUtils.IS_CYGWIN || OSUtils.IS_MSYSTEM) { + Pty pty = current(systemStream); + return new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler); + } else { + return null; + } + } + + public Terminal posixSysTerminal( + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + boolean nativeSignals, + Terminal.SignalHandler signalHandler, + boolean paused, + SystemStream systemStream) + throws IOException { + Pty pty = current(systemStream); + return new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler); + } + + @Override + public Terminal newTerminal( + String name, + String type, + InputStream in, + OutputStream out, + Charset encoding, + Terminal.SignalHandler signalHandler, + boolean paused, + Attributes attributes, + Size size) + throws IOException { + return new ExternalTerminal(this, name, type, in, out, encoding, signalHandler, paused, attributes, size); + } + + @Override + public boolean isSystemStream(SystemStream stream) { + try { + return isPosixSystemStream(stream) || isWindowsSystemStream(stream); + } catch (Throwable t) { + return false; + } + } + + public boolean isWindowsSystemStream(SystemStream stream) { + return systemStreamName(stream) != null; + } + + public boolean isPosixSystemStream(SystemStream stream) { + try { + Process p = new ProcessBuilder(OSUtils.TEST_COMMAND, "-t", Integer.toString(stream.ordinal())) + .inheritIO() + .start(); + return p.waitFor() == 0; + } catch (Throwable t) { + Log.debug("ExecTerminalProvider failed 'test -t' for " + stream, t); + // ignore + } + return false; + } + + @Override + public String systemStreamName(SystemStream stream) { + try { + ProcessBuilder.Redirect input = stream == SystemStream.Input + ? ProcessBuilder.Redirect.INHERIT + : newDescriptor(stream == SystemStream.Output ? FileDescriptor.out : FileDescriptor.err); + Process p = + new ProcessBuilder(OSUtils.TTY_COMMAND).redirectInput(input).start(); + String result = ExecHelper.waitAndCapture(p); + if (p.exitValue() == 0) { + return result.trim(); + } + } catch (Throwable t) { + if ("java.lang.reflect.InaccessibleObjectException" + .equals(t.getClass().getName()) + && !warned) { + Log.warn( + "The ExecTerminalProvider requires the JVM options: '--add-opens java.base/java.lang=ALL-UNNAMED'"); + warned = true; + } + // ignore + } + return null; + } + + @Override + public int systemStreamWidth(SystemStream stream) { + try (ExecPty pty = new ExecPty(this, stream, null)) { + return pty.getSize().getColumns(); + } catch (Throwable t) { + return -1; + } + } + + @Override + public String toString() { + return "TerminalProvider[" + name() + "]"; + } + + interface RedirectPipeCreator { + ProcessBuilder.Redirect newRedirectPipe(FileDescriptor fd); + } + + /** + * Reflection based file descriptor creator. + * This requires the following option + * --add-opens java.base/java.lang=ALL-UNNAMED + */ + static class ReflectionRedirectPipeCreator implements RedirectPipeCreator { + private final Constructor constructor; + private final Field fdField; + + @SuppressWarnings("unchecked") + ReflectionRedirectPipeCreator() throws Exception { + Class rpi = Class.forName("java.lang.ProcessBuilder$RedirectPipeImpl"); + constructor = (Constructor) rpi.getDeclaredConstructor(); + constructor.setAccessible(true); + fdField = rpi.getDeclaredField("fd"); + fdField.setAccessible(true); + } + + @Override + public ProcessBuilder.Redirect newRedirectPipe(FileDescriptor fd) { + try { + ProcessBuilder.Redirect input = constructor.newInstance(); + fdField.set(input, fd); + return input; + } catch (ReflectiveOperationException e) { + // This should not happen as the field has been set accessible + throw new IllegalStateException(e); + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/terminal/impl/package-info.java b/net-cli/src/main/java/org/jline/terminal/impl/package-info.java new file mode 100644 index 0000000..d2b2154 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/impl/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2002-2016, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +/** + * JLine 3. + * + * @since 3.0 + */ +package org.jline.terminal.impl; diff --git a/net-cli/src/main/java/org/jline/terminal/spi/Pty.java b/net-cli/src/main/java/org/jline/terminal/spi/Pty.java new file mode 100644 index 0000000..244c6f9 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/spi/Pty.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.spi; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; + +public interface Pty extends Closeable { + + InputStream getMasterInput() throws IOException; + + OutputStream getMasterOutput() throws IOException; + + InputStream getSlaveInput() throws IOException; + + OutputStream getSlaveOutput() throws IOException; + + Attributes getAttr() throws IOException; + + void setAttr(Attributes attr) throws IOException; + + Size getSize() throws IOException; + + void setSize(Size size) throws IOException; + + SystemStream getSystemStream(); + + TerminalProvider getProvider(); +} diff --git a/net-cli/src/main/java/org/jline/terminal/spi/SystemStream.java b/net-cli/src/main/java/org/jline/terminal/spi/SystemStream.java new file mode 100644 index 0000000..51b284c --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/spi/SystemStream.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.spi; + +public enum SystemStream { + Input, + Output, + Error +} diff --git a/net-cli/src/main/java/org/jline/terminal/spi/TerminalExt.java b/net-cli/src/main/java/org/jline/terminal/spi/TerminalExt.java new file mode 100644 index 0000000..8c79938 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/spi/TerminalExt.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.spi; + +import org.jline.terminal.Terminal; + +/** + * The {@code TerminalExt} interface is implemented by {@code Terminal}s + * and provides access to the Terminal's internals. + */ +public interface TerminalExt extends Terminal { + + /** + * Returns the {@code TerminalProvider} that created this terminal + * or {@code null} if the terminal was created with no provider. + */ + TerminalProvider getProvider(); + + /** + * The underlying system stream, may be {@link SystemStream#Output}, + * {@link SystemStream#Error}, or {@code null} if this terminal is not bound + * to a system stream. + */ + SystemStream getSystemStream(); +} diff --git a/net-cli/src/main/java/org/jline/terminal/spi/TerminalProvider.java b/net-cli/src/main/java/org/jline/terminal/spi/TerminalProvider.java new file mode 100644 index 0000000..b80f0d0 --- /dev/null +++ b/net-cli/src/main/java/org/jline/terminal/spi/TerminalProvider.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.spi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Properties; +import org.jline.terminal.Attributes; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; + +public interface TerminalProvider { + + static TerminalProvider load(String name) throws IOException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = ClassLoader.getSystemClassLoader(); + } + InputStream is = cl.getResourceAsStream("META-INF/services/org/jline/terminal/provider/" + name); + if (is != null) { + Properties props = new Properties(); + try { + props.load(is); + String className = props.getProperty("class"); + if (className == null) { + throw new IOException("No class defined in terminal provider file " + name); + } + Class clazz = cl.loadClass(className); + return (TerminalProvider) clazz.getConstructor().newInstance(); + } catch (Exception e) { + throw new IOException("Unable to load terminal provider " + name + ": " + e.getMessage(), e); + } + } else { + throw new IOException("Unable to find terminal provider " + name); + } + } + + String name(); + + Terminal sysTerminal( + String name, + String type, + boolean ansiPassThrough, + Charset encoding, + boolean nativeSignals, + Terminal.SignalHandler signalHandler, + boolean paused, + SystemStream systemStream) + throws IOException; + + Terminal newTerminal( + String name, + String type, + InputStream masterInput, + OutputStream masterOutput, + Charset encoding, + Terminal.SignalHandler signalHandler, + boolean paused, + Attributes attributes, + Size size) + throws IOException; + + boolean isSystemStream(SystemStream stream); + + String systemStreamName(SystemStream stream); + + int systemStreamWidth(SystemStream stream); +} diff --git a/net-cli/src/main/java/org/jline/utils/AnsiWriter.java b/net-cli/src/main/java/org/jline/utils/AnsiWriter.java new file mode 100644 index 0000000..bdd8205 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/AnsiWriter.java @@ -0,0 +1,834 @@ +package org.jline.utils; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * A ANSI writer extracts ANSI escape codes written to + * a {@link Writer} and calls corresponding process* methods. + *

+ * For more information about ANSI escape codes, see: + * http://en.wikipedia.org/wiki/ANSI_escape_code + *

+ * This class just filters out the escape codes so that they are not + * sent out to the underlying {@link Writer}: process* methods + * are empty. Subclasses should actually perform the ANSI escape behaviors + * by implementing active code in process* methods. + * + * @author Hiram Chirino + * @author Joris Kuipers + * @since 1.0 + */ +public class AnsiWriter extends FilterWriter { + + protected static final int ERASE_SCREEN_TO_END = 0; + protected static final int ERASE_SCREEN_TO_BEGINING = 1; + protected static final int ERASE_SCREEN = 2; + protected static final int ERASE_LINE_TO_END = 0; + protected static final int ERASE_LINE_TO_BEGINING = 1; + protected static final int ERASE_LINE = 2; + protected static final int ATTRIBUTE_INTENSITY_BOLD = 1; // Intensity: Bold + protected static final int ATTRIBUTE_INTENSITY_FAINT = 2; // Intensity; Faint not widely supported + protected static final int ATTRIBUTE_ITALIC = 3; // Italic; on not widely supported. Sometimes treated as inverse. + protected static final int ATTRIBUTE_UNDERLINE = 4; // Underline; Single + protected static final int ATTRIBUTE_BLINK_SLOW = 5; // Blink; Slow less than 150 per minute + protected static final int ATTRIBUTE_BLINK_FAST = 6; // Blink; Rapid MS-DOS ANSI.SYS; 150 per minute or more + protected static final int ATTRIBUTE_NEGATIVE_ON = + 7; // Image; Negative inverse or reverse; swap foreground and background + protected static final int ATTRIBUTE_CONCEAL_ON = 8; // Conceal on + protected static final int ATTRIBUTE_UNDERLINE_DOUBLE = 21; // Underline; Double not widely supported + protected static final int ATTRIBUTE_INTENSITY_NORMAL = 22; // Intensity; Normal not bold and not faint + protected static final int ATTRIBUTE_UNDERLINE_OFF = 24; // Underline; None + protected static final int ATTRIBUTE_BLINK_OFF = 25; // Blink; off + @Deprecated + protected static final int ATTRIBUTE_NEGATIVE_Off = 27; // Image; Positive + protected static final int ATTRIBUTE_NEGATIVE_OFF = 27; // Image; Positive + protected static final int ATTRIBUTE_CONCEAL_OFF = 28; // Reveal conceal off + protected static final int BLACK = 0; + protected static final int RED = 1; + protected static final int GREEN = 2; + protected static final int YELLOW = 3; + protected static final int BLUE = 4; + protected static final int MAGENTA = 5; + protected static final int CYAN = 6; + protected static final int WHITE = 7; + private static final char[] RESET_CODE = "\033[0m".toCharArray(); + private static final int MAX_ESCAPE_SEQUENCE_LENGTH = 100; + private static final int LOOKING_FOR_FIRST_ESC_CHAR = 0; + private static final int LOOKING_FOR_SECOND_ESC_CHAR = 1; + private static final int LOOKING_FOR_NEXT_ARG = 2; + private static final int LOOKING_FOR_STR_ARG_END = 3; + private static final int LOOKING_FOR_INT_ARG_END = 4; + private static final int LOOKING_FOR_OSC_COMMAND = 5; + private static final int LOOKING_FOR_OSC_COMMAND_END = 6; + private static final int LOOKING_FOR_OSC_PARAM = 7; + private static final int LOOKING_FOR_ST = 8; + private static final int LOOKING_FOR_CHARSET = 9; + private static final int FIRST_ESC_CHAR = 27; + private static final int SECOND_ESC_CHAR = '['; + private static final int SECOND_OSC_CHAR = ']'; + private static final int BEL = 7; + private static final int SECOND_ST_CHAR = '\\'; + private static final int SECOND_CHARSET0_CHAR = '('; + private static final int SECOND_CHARSET1_CHAR = ')'; + private final char[] buffer = new char[MAX_ESCAPE_SEQUENCE_LENGTH]; + private final ArrayList options = new ArrayList<>(); + int state = LOOKING_FOR_FIRST_ESC_CHAR; + private int pos = 0; + private int startOfValue; + public AnsiWriter(Writer out) { + super(out); + } + + @Override + public synchronized void write(int data) throws IOException { + switch (state) { + case LOOKING_FOR_FIRST_ESC_CHAR: + if (data == FIRST_ESC_CHAR) { + buffer[pos++] = (char) data; + state = LOOKING_FOR_SECOND_ESC_CHAR; + } else { + out.write(data); + } + break; + + case LOOKING_FOR_SECOND_ESC_CHAR: + buffer[pos++] = (char) data; + if (data == SECOND_ESC_CHAR) { + state = LOOKING_FOR_NEXT_ARG; + } else if (data == SECOND_OSC_CHAR) { + state = LOOKING_FOR_OSC_COMMAND; + } else if (data == SECOND_CHARSET0_CHAR) { + options.add((int) '0'); + state = LOOKING_FOR_CHARSET; + } else if (data == SECOND_CHARSET1_CHAR) { + options.add((int) '1'); + state = LOOKING_FOR_CHARSET; + } else { + reset(false); + } + break; + + case LOOKING_FOR_NEXT_ARG: + buffer[pos++] = (char) data; + if ('"' == data) { + startOfValue = pos - 1; + state = LOOKING_FOR_STR_ARG_END; + } else if ('0' <= data && data <= '9') { + startOfValue = pos - 1; + state = LOOKING_FOR_INT_ARG_END; + } else if (';' == data) { + options.add(null); + } else if ('?' == data) { + options.add('?'); + } else if ('=' == data) { + options.add('='); + } else { + boolean skip = true; + try { + skip = processEscapeCommand(options, data); + } finally { + reset(skip); + } + } + break; + case LOOKING_FOR_INT_ARG_END: + buffer[pos++] = (char) data; + if (!('0' <= data && data <= '9')) { + String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue); + Integer value = Integer.valueOf(strValue); + options.add(value); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + boolean skip = true; + try { + skip = processEscapeCommand(options, data); + } finally { + reset(skip); + } + } + } + break; + + case LOOKING_FOR_STR_ARG_END: + buffer[pos++] = (char) data; + if ('"' != data) { + String value = new String(buffer, startOfValue, (pos - 1) - startOfValue); + options.add(value); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + reset(processEscapeCommand(options, data)); + } + } + break; + + case LOOKING_FOR_OSC_COMMAND: + buffer[pos++] = (char) data; + if ('0' <= data && data <= '9') { + startOfValue = pos - 1; + state = LOOKING_FOR_OSC_COMMAND_END; + } else { + reset(false); + } + break; + + case LOOKING_FOR_OSC_COMMAND_END: + buffer[pos++] = (char) data; + if (';' == data) { + String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue); + Integer value = Integer.valueOf(strValue); + options.add(value); + startOfValue = pos; + state = LOOKING_FOR_OSC_PARAM; + } else if ('0' <= data && data <= '9') { + // already pushed digit to buffer, just keep looking + } else { + // oops, did not expect this + reset(false); + } + break; + + case LOOKING_FOR_OSC_PARAM: + buffer[pos++] = (char) data; + if (BEL == data) { + String value = new String(buffer, startOfValue, (pos - 1) - startOfValue); + options.add(value); + boolean skip = true; + try { + skip = processOperatingSystemCommand(options); + } finally { + reset(skip); + } + } else if (FIRST_ESC_CHAR == data) { + state = LOOKING_FOR_ST; + } else { + // just keep looking while adding text + } + break; + + case LOOKING_FOR_ST: + buffer[pos++] = (char) data; + if (SECOND_ST_CHAR == data) { + String value = new String(buffer, startOfValue, (pos - 2) - startOfValue); + options.add(value); + boolean skip = true; + try { + skip = processOperatingSystemCommand(options); + } finally { + reset(skip); + } + } else { + state = LOOKING_FOR_OSC_PARAM; + } + break; + + case LOOKING_FOR_CHARSET: + options.add((char) data); + reset(processCharsetSelect(options)); + break; + default: + break; + + } + + // Is it just too long? + if (pos >= buffer.length) { + reset(false); + } + } + + /** + * Resets all state to continue with regular parsing + * + * @param skipBuffer if current buffer should be skipped or written to out + * @throws IOException if an error occurs + */ + private void reset(boolean skipBuffer) throws IOException { + if (!skipBuffer) { + out.write(buffer, 0, pos); + } + pos = 0; + startOfValue = 0; + options.clear(); + state = LOOKING_FOR_FIRST_ESC_CHAR; + } + + /** + * Helper for processEscapeCommand() to iterate over integer options + * + * @param optionsIterator the underlying iterator + * @throws IOException if no more non-null values left + */ + private int getNextOptionInt(Iterator optionsIterator) throws IOException { + for (; ; ) { + if (!optionsIterator.hasNext()) throw new IllegalArgumentException(); + Object arg = optionsIterator.next(); + if (arg != null) return (Integer) arg; + } + } + + /** + * Process escape command + * + * @param options the list of options + * @param command the command + * @return true if the escape command was processed. + * @throws IOException if an error occurs + */ + private boolean processEscapeCommand(ArrayList options, int command) throws IOException { + try { + switch (command) { + case 'A': + processCursorUp(optionInt(options, 0, 1)); + return true; + case 'B': + processCursorDown(optionInt(options, 0, 1)); + return true; + case 'C': + processCursorRight(optionInt(options, 0, 1)); + return true; + case 'D': + processCursorLeft(optionInt(options, 0, 1)); + return true; + case 'E': + processCursorDownLine(optionInt(options, 0, 1)); + return true; + case 'F': + processCursorUpLine(optionInt(options, 0, 1)); + return true; + case 'G': + processCursorToColumn(optionInt(options, 0)); + return true; + case 'H': + case 'f': + processCursorTo(optionInt(options, 0, 1), optionInt(options, 1, 1)); + return true; + case 'J': + processEraseScreen(optionInt(options, 0, 0)); + return true; + case 'K': + processEraseLine(optionInt(options, 0, 0)); + return true; + case 'L': + processInsertLine(optionInt(options, 0, 1)); + return true; + case 'M': + processDeleteLine(optionInt(options, 0, 1)); + return true; + case 'S': + processScrollUp(optionInt(options, 0, 1)); + return true; + case 'T': + processScrollDown(optionInt(options, 0, 1)); + return true; + case 'm': + // Validate all options are ints... + for (Object next : options) { + if (next != null && next.getClass() != Integer.class) { + throw new IllegalArgumentException(); + } + } + + int count = 0; + Iterator optionsIterator = options.iterator(); + while (optionsIterator.hasNext()) { + Object next = optionsIterator.next(); + if (next != null) { + count++; + int value = (Integer) next; + if (30 <= value && value <= 37) { + processSetForegroundColor(value - 30); + } else if (40 <= value && value <= 47) { + processSetBackgroundColor(value - 40); + } else if (90 <= value && value <= 97) { + processSetForegroundColor(value - 90, true); + } else if (100 <= value && value <= 107) { + processSetBackgroundColor(value - 100, true); + } else if (value == 38 || value == 48) { + // extended color like `esc[38;5;m` or `esc[38;2;;;m` + int arg2or5 = getNextOptionInt(optionsIterator); + if (arg2or5 == 2) { + // 24 bit color style like `esc[38;2;;;m` + int r = getNextOptionInt(optionsIterator); + int g = getNextOptionInt(optionsIterator); + int b = getNextOptionInt(optionsIterator); + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { + if (value == 38) processSetForegroundColorExt(r, g, b); + else processSetBackgroundColorExt(r, g, b); + } else { + throw new IllegalArgumentException(); + } + } else if (arg2or5 == 5) { + // 256 color style like `esc[38;5;m` + int paletteIndex = getNextOptionInt(optionsIterator); + if (paletteIndex >= 0 && paletteIndex <= 255) { + if (value == 38) processSetForegroundColorExt(paletteIndex); + else processSetBackgroundColorExt(paletteIndex); + } else { + throw new IllegalArgumentException(); + } + } else { + throw new IllegalArgumentException(); + } + } else { + switch (value) { + case 39: + processDefaultTextColor(); + break; + case 49: + processDefaultBackgroundColor(); + break; + case 0: + processAttributeRest(); + break; + default: + processSetAttribute(value); + } + } + } + } + if (count == 0) { + processAttributeRest(); + } + return true; + case 's': + processSaveCursorPosition(); + return true; + case 'u': + processRestoreCursorPosition(); + return true; + + default: + if ('a' <= command && 'z' <= command) { + processUnknownExtension(options, command); + return true; + } + if ('A' <= command && 'Z' <= command) { + processUnknownExtension(options, command); + return true; + } + return false; + } + } catch (IllegalArgumentException ignore) { + } + return false; + } + + /** + * Process operating system command. + * + * @param options the options list + * @return true if the operating system command was processed. + */ + private boolean processOperatingSystemCommand(ArrayList options) throws IOException { + int command = optionInt(options, 0); + String label = (String) options.get(1); + // for command > 2 label could be composed (i.e. contain ';'), but we'll leave + // it to processUnknownOperatingSystemCommand implementations to handle that + try { + switch (command) { + case 0: + processChangeIconNameAndWindowTitle(label); + return true; + case 1: + processChangeIconName(label); + return true; + case 2: + processChangeWindowTitle(label); + return true; + + default: + // not exactly unknown, but not supported through dedicated process methods: + processUnknownOperatingSystemCommand(command, label); + return true; + } + } catch (IllegalArgumentException ignore) { + } + return false; + } + + /** + * Process CSI u ANSI code, corresponding to RCP – Restore Cursor Position + * + * @throws IOException if an error occurs + */ + protected void processRestoreCursorPosition() throws IOException { + } + + /** + * Process CSI s ANSI code, corresponding to SCP – Save Cursor Position + * + * @throws IOException if an error occurs + */ + protected void processSaveCursorPosition() throws IOException { + } + + /** + * Process CSI s ANSI code, corresponding to IL – Insert Line + * + * @param optionInt the option + * @throws IOException if an error occurs + */ + protected void processInsertLine(int optionInt) throws IOException { + } + + /** + * Process CSI s ANSI code, corresponding to DL – Delete Line + * + * @param optionInt the option + * @throws IOException if an error occurs + */ + protected void processDeleteLine(int optionInt) throws IOException { + } + + /** + * Process CSI n T ANSI code, corresponding to SD – Scroll Down + * + * @param optionInt the option + * @throws IOException if an error occurs + */ + protected void processScrollDown(int optionInt) throws IOException { + } + + /** + * Process CSI n U ANSI code, corresponding to SU – Scroll Up + * + * @param optionInt the option + * @throws IOException if an error occurs + */ + protected void processScrollUp(int optionInt) throws IOException { + } + + /** + * Process CSI n J ANSI code, corresponding to ED – Erase in Display + * + * @param eraseOption the erase option + * @throws IOException if an error occurs + */ + protected void processEraseScreen(int eraseOption) throws IOException { + } + + /** + * Process CSI n K ANSI code, corresponding to ED – Erase in Line + * + * @param eraseOption the erase option + * @throws IOException if an error occurs + */ + protected void processEraseLine(int eraseOption) throws IOException { + } + + /** + * process SGR other than 0 (reset), 30-39 (foreground), + * 40-49 (background), 90-97 (foreground high intensity) or + * 100-107 (background high intensity) + * + * @param attribute the attribute to set + * @throws IOException if an error occurs + * @see #processAttributeRest() + * @see #processSetForegroundColor(int) + * @see #processSetForegroundColor(int, boolean) + * @see #processSetForegroundColorExt(int) + * @see #processSetForegroundColorExt(int, int, int) + * @see #processDefaultTextColor() + * @see #processDefaultBackgroundColor() + */ + protected void processSetAttribute(int attribute) throws IOException { + } + + /** + * process SGR 30-37 corresponding to Set text color (foreground). + * + * @param color the text color + * @throws IOException if an error occurs + */ + protected void processSetForegroundColor(int color) throws IOException { + processSetForegroundColor(color, false); + } + + /** + * process SGR 30-37 or SGR 90-97 corresponding to + * Set text color (foreground) either in normal mode or high intensity. + * + * @param color the text color + * @param bright is high intensity? + * @throws IOException if an error occurs + */ + protected void processSetForegroundColor(int color, boolean bright) throws IOException { + processSetForegroundColorExt(bright ? color + 8 : color); + } + + /** + * process SGR 38 corresponding to extended set text color (foreground) + * with a palette of 255 colors. + * + * @param paletteIndex the text color in the palette + * @throws IOException if an error occurs + */ + protected void processSetForegroundColorExt(int paletteIndex) throws IOException { + } + + /** + * process SGR 38 corresponding to extended set text color (foreground) + * with a 24 bits RGB definition of the color. + * + * @param r red + * @param g green + * @param b blue + * @throws IOException if an error occurs + */ + protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { + processSetForegroundColorExt(Colors.roundRgbColor(r, g, b, 16)); + } + + /** + * process SGR 40-47 corresponding to Set background color. + * + * @param color the background color + * @throws IOException if an error occurs + */ + protected void processSetBackgroundColor(int color) throws IOException { + processSetBackgroundColor(color, false); + } + + /** + * process SGR 40-47 or SGR 100-107 corresponding to + * Set background color either in normal mode or high intensity. + * + * @param color the background color + * @param bright is high intensity? + * @throws IOException if an error occurs + */ + protected void processSetBackgroundColor(int color, boolean bright) throws IOException { + processSetBackgroundColorExt(bright ? color + 8 : color); + } + + /** + * process SGR 48 corresponding to extended set background color + * with a palette of 255 colors. + * + * @param paletteIndex the background color in the palette + * @throws IOException if an error occurs + */ + protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { + } + + /** + * process SGR 48 corresponding to extended set background color + * with a 24 bits RGB definition of the color. + * + * @param r red + * @param g green + * @param b blue + * @throws IOException if an error occurs + */ + protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { + processSetBackgroundColorExt(Colors.roundRgbColor(r, g, b, 16)); + } + + /** + * process SGR 39 corresponding to Default text color (foreground) + * + * @throws IOException if an error occurs + */ + protected void processDefaultTextColor() throws IOException { + } + + /** + * process SGR 49 corresponding to Default background color + * + * @throws IOException if an error occurs + */ + protected void processDefaultBackgroundColor() throws IOException { + } + + /** + * process SGR 0 corresponding to Reset / Normal + * + * @throws IOException if an error occurs + */ + protected void processAttributeRest() throws IOException { + } + + /** + * process CSI n ; m H corresponding to CUP – Cursor Position or + * CSI n ; m f corresponding to HVP – Horizontal and Vertical Position + * + * @param row the row + * @param col the column + * @throws IOException if an error occurs + */ + protected void processCursorTo(int row, int col) throws IOException { + } + + /** + * process CSI n G corresponding to CHA – Cursor Horizontal Absolute + * + * @param x the column + * @throws IOException if an error occurs + */ + protected void processCursorToColumn(int x) throws IOException { + } + + /** + * process CSI n F corresponding to CPL – Cursor Previous Line + * + * @param count line count + * @throws IOException if an error occurs + */ + protected void processCursorUpLine(int count) throws IOException { + } + + /** + * process CSI n E corresponding to CNL – Cursor Next Line + * + * @param count line count + * @throws IOException if an error occurs + */ + protected void processCursorDownLine(int count) throws IOException { + // Poor mans impl.. + for (int i = 0; i < count; i++) { + out.write('\n'); + } + } + + /** + * process CSI n D corresponding to CUB – Cursor Back + * + * @param count the count + * @throws IOException if an error occurs + */ + protected void processCursorLeft(int count) throws IOException { + } + + /** + * process CSI n C corresponding to CUF – Cursor Forward + * + * @param count the count + * @throws IOException if an error occurs + */ + protected void processCursorRight(int count) throws IOException { + // Poor mans impl.. + for (int i = 0; i < count; i++) { + out.write(' '); + } + } + + /** + * process CSI n B corresponding to CUD – Cursor Down + * + * @param count the count + * @throws IOException if an error occurs + */ + protected void processCursorDown(int count) throws IOException { + } + + /** + * process CSI n A corresponding to CUU – Cursor Up + * + * @param count the count + * @throws IOException if an error occurs + */ + protected void processCursorUp(int count) throws IOException { + } + + protected void processUnknownExtension(ArrayList options, int command) { + } + + /** + * process OSC 0;text BEL corresponding to Change Window and Icon label + * + * @param label the label + */ + protected void processChangeIconNameAndWindowTitle(String label) { + processChangeIconName(label); + processChangeWindowTitle(label); + } + + /** + * process OSC 1;text BEL corresponding to Change Icon label + * + * @param name the icon name + */ + protected void processChangeIconName(String name) { + } + + /** + * process OSC 2;text BEL corresponding to Change Window title + * + * @param title the title + */ + protected void processChangeWindowTitle(String title) { + } + + /** + * Process unknown OSC command. + * + * @param command the command + * @param param the param + */ + protected void processUnknownOperatingSystemCommand(int command, String param) { + } + + /** + * Process character set sequence. + * + * @param options + * @return true if the charcter set select command was processed. + */ + private boolean processCharsetSelect(ArrayList options) throws IOException { + int set = optionInt(options, 0); + char seq = (Character) options.get(1); + processCharsetSelect(set, seq); + return true; + } + + protected void processCharsetSelect(int set, char seq) { + } + + private int optionInt(ArrayList options, int index) { + if (options.size() <= index) throw new IllegalArgumentException(); + Object value = options.get(index); + if (value == null) throw new IllegalArgumentException(); + if (!value.getClass().equals(Integer.class)) throw new IllegalArgumentException(); + return (Integer) value; + } + + private int optionInt(ArrayList options, int index, int defaultValue) { + if (options.size() > index) { + Object value = options.get(index); + if (value == null) { + return defaultValue; + } + return (Integer) value; + } + return defaultValue; + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + // TODO: Optimize this + for (int i = 0; i < len; i++) { + write(cbuf[off + i]); + } + } + + @Override + public void write(String str, int off, int len) throws IOException { + // TODO: Optimize this + for (int i = 0; i < len; i++) { + write(str.charAt(off + i)); + } + } + + @Override + public void close() throws IOException { + write(RESET_CODE); + flush(); + super.close(); + } +} diff --git a/net-cli/src/main/java/org/jline/utils/AttributedCharSequence.java b/net-cli/src/main/java/org/jline/utils/AttributedCharSequence.java new file mode 100644 index 0000000..f43e8f3 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/AttributedCharSequence.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.util.ArrayList; +import java.util.List; +import org.jline.terminal.Terminal; +import org.jline.terminal.impl.AbstractWindowsTerminal; +import org.jline.utils.InfoCmp.Capability; +import static org.jline.terminal.TerminalBuilder.PROP_DISABLE_ALTERNATE_CHARSET; +import static org.jline.utils.AttributedStyle.BG_COLOR; +import static org.jline.utils.AttributedStyle.BG_COLOR_EXP; +import static org.jline.utils.AttributedStyle.FG_COLOR; +import static org.jline.utils.AttributedStyle.FG_COLOR_EXP; +import static org.jline.utils.AttributedStyle.F_BACKGROUND; +import static org.jline.utils.AttributedStyle.F_BACKGROUND_IND; +import static org.jline.utils.AttributedStyle.F_BACKGROUND_RGB; +import static org.jline.utils.AttributedStyle.F_BLINK; +import static org.jline.utils.AttributedStyle.F_BOLD; +import static org.jline.utils.AttributedStyle.F_CONCEAL; +import static org.jline.utils.AttributedStyle.F_CROSSED_OUT; +import static org.jline.utils.AttributedStyle.F_FAINT; +import static org.jline.utils.AttributedStyle.F_FOREGROUND; +import static org.jline.utils.AttributedStyle.F_FOREGROUND_IND; +import static org.jline.utils.AttributedStyle.F_FOREGROUND_RGB; +import static org.jline.utils.AttributedStyle.F_HIDDEN; +import static org.jline.utils.AttributedStyle.F_INVERSE; +import static org.jline.utils.AttributedStyle.F_ITALIC; +import static org.jline.utils.AttributedStyle.F_UNDERLINE; +import static org.jline.utils.AttributedStyle.MASK; + +public abstract class AttributedCharSequence implements CharSequence { + + public static final int TRUE_COLORS = 0x1000000; + // cache the value here as we can't afford to get it each time + static final boolean DISABLE_ALTERNATE_CHARSET = Boolean.getBoolean(PROP_DISABLE_ALTERNATE_CHARSET); + private static final int HIGH_COLORS = 0x7FFF; + + @Deprecated + public static int rgbColor(int col) { + return Colors.rgbColor(col); + } + + @Deprecated + public static int roundColor(int col, int max) { + return Colors.roundColor(col, max); + } + + @Deprecated + public static int roundRgbColor(int r, int g, int b, int max) { + return Colors.roundRgbColor(r, g, b, max); + } + + private static boolean attr(StringBuilder sb, String s, boolean first) { + if (!first) { + sb.append(";"); + } + sb.append(s); + return false; + } + + public void print(Terminal terminal) { + terminal.writer().print(toAnsi(terminal)); + } + + public void println(Terminal terminal) { + terminal.writer().println(toAnsi(terminal)); + } + + public String toAnsi() { + return toAnsi(null); + } + + public String toAnsi(Terminal terminal) { + if (terminal != null && Terminal.TYPE_DUMB.equals(terminal.getType())) { + return toString(); + } + int colors = 256; + ForceMode forceMode = ForceMode.None; + ColorPalette palette = null; + String alternateIn = null, alternateOut = null; + if (terminal != null) { + Integer max_colors = terminal.getNumericCapability(Capability.max_colors); + if (max_colors != null) { + colors = max_colors; + } + if (AbstractWindowsTerminal.TYPE_WINDOWS_256_COLOR.equals(terminal.getType()) + || AbstractWindowsTerminal.TYPE_WINDOWS_CONEMU.equals(terminal.getType())) { + forceMode = ForceMode.Force256Colors; + } + palette = terminal.getPalette(); + if (!DISABLE_ALTERNATE_CHARSET) { + alternateIn = Curses.tputs(terminal.getStringCapability(Capability.enter_alt_charset_mode)); + alternateOut = Curses.tputs(terminal.getStringCapability(Capability.exit_alt_charset_mode)); + } + } + return toAnsi(colors, forceMode, palette, alternateIn, alternateOut); + } + + @Deprecated + public String toAnsi(int colors, boolean force256colors) { + return toAnsi(colors, force256colors, null, null); + } + + @Deprecated + public String toAnsi(int colors, boolean force256colors, String altIn, String altOut) { + return toAnsi(colors, force256colors ? ForceMode.Force256Colors : ForceMode.None, null, altIn, altOut); + } + + public String toAnsi(int colors, ForceMode force) { + return toAnsi(colors, force, null, null, null); + } + + public String toAnsi(int colors, ForceMode force, ColorPalette palette) { + return toAnsi(colors, force, palette, null, null); + } + + public String toAnsi(int colors, ForceMode force, ColorPalette palette, String altIn, String altOut) { + StringBuilder sb = new StringBuilder(); + long style = 0; + long foreground = 0; + long background = 0; + boolean alt = false; + if (palette == null) { + palette = ColorPalette.DEFAULT; + } + for (int i = 0; i < length(); i++) { + char c = charAt(i); + if (altIn != null && altOut != null) { + char pc = c; + // @spotless:off + switch (c) { + case '┘': + c = 'j'; + break; + case '┐': + c = 'k'; + break; + case '┌': + c = 'l'; + break; + case '└': + c = 'm'; + break; + case '┼': + c = 'n'; + break; + case '─': + c = 'q'; + break; + case '├': + c = 't'; + break; + case '┤': + c = 'u'; + break; + case '┴': + c = 'v'; + break; + case '┬': + c = 'w'; + break; + case '│': + c = 'x'; + break; + } + // @spotless:on + boolean oldalt = alt; + alt = c != pc; + if (oldalt ^ alt) { + sb.append(alt ? altIn : altOut); + } + } + long s = styleCodeAt(i) & ~F_HIDDEN; // The hidden flag does not change the ansi styles + if (style != s) { + long d = (style ^ s) & MASK; + long fg = (s & F_FOREGROUND) != 0 ? s & (FG_COLOR | F_FOREGROUND) : 0; + long bg = (s & F_BACKGROUND) != 0 ? s & (BG_COLOR | F_BACKGROUND) : 0; + if (s == 0) { + sb.append("\033[0m"); + foreground = background = 0; + } else { + sb.append("\033["); + boolean first = true; + if ((d & F_ITALIC) != 0) { + first = attr(sb, (s & F_ITALIC) != 0 ? "3" : "23", first); + } + if ((d & F_UNDERLINE) != 0) { + first = attr(sb, (s & F_UNDERLINE) != 0 ? "4" : "24", first); + } + if ((d & F_BLINK) != 0) { + first = attr(sb, (s & F_BLINK) != 0 ? "5" : "25", first); + } + if ((d & F_INVERSE) != 0) { + first = attr(sb, (s & F_INVERSE) != 0 ? "7" : "27", first); + } + if ((d & F_CONCEAL) != 0) { + first = attr(sb, (s & F_CONCEAL) != 0 ? "8" : "28", first); + } + if ((d & F_CROSSED_OUT) != 0) { + first = attr(sb, (s & F_CROSSED_OUT) != 0 ? "9" : "29", first); + } + if (foreground != fg) { + if (fg > 0) { + int rounded = -1; + if ((fg & F_FOREGROUND_RGB) != 0) { + int r = (int) (fg >> (FG_COLOR_EXP + 16)) & 0xFF; + int g = (int) (fg >> (FG_COLOR_EXP + 8)) & 0xFF; + int b = (int) (fg >> FG_COLOR_EXP) & 0xFF; + if (colors >= HIGH_COLORS) { + first = attr(sb, "38;2;" + r + ";" + g + ";" + b, first); + } else { + rounded = palette.round(r, g, b); + } + } else if ((fg & F_FOREGROUND_IND) != 0) { + rounded = palette.round((int) (fg >> FG_COLOR_EXP) & 0xFF); + } + if (rounded >= 0) { + if (colors >= HIGH_COLORS && force == ForceMode.ForceTrueColors) { + int col = palette.getColor(rounded); + int r = (col >> 16) & 0xFF; + int g = (col >> 8) & 0xFF; + int b = col & 0xFF; + first = attr(sb, "38;2;" + r + ";" + g + ";" + b, first); + } else if (force == ForceMode.Force256Colors || rounded >= 16) { + first = attr(sb, "38;5;" + rounded, first); + } else if (rounded >= 8) { + first = attr(sb, "9" + (rounded - 8), first); + // small hack to force setting bold again after a foreground color change + d |= (s & F_BOLD); + } else { + first = attr(sb, "3" + rounded, first); + // small hack to force setting bold again after a foreground color change + d |= (s & F_BOLD); + } + } + } else { + first = attr(sb, "39", first); + } + foreground = fg; + } + if (background != bg) { + if (bg > 0) { + int rounded = -1; + if ((bg & F_BACKGROUND_RGB) != 0) { + int r = (int) (bg >> (BG_COLOR_EXP + 16)) & 0xFF; + int g = (int) (bg >> (BG_COLOR_EXP + 8)) & 0xFF; + int b = (int) (bg >> BG_COLOR_EXP) & 0xFF; + if (colors >= HIGH_COLORS) { + first = attr(sb, "48;2;" + r + ";" + g + ";" + b, first); + } else { + rounded = palette.round(r, g, b); + } + } else if ((bg & F_BACKGROUND_IND) != 0) { + rounded = palette.round((int) (bg >> BG_COLOR_EXP) & 0xFF); + } + if (rounded >= 0) { + if (colors >= HIGH_COLORS && force == ForceMode.ForceTrueColors) { + int col = palette.getColor(rounded); + int r = (col >> 16) & 0xFF; + int g = (col >> 8) & 0xFF; + int b = col & 0xFF; + first = attr(sb, "48;2;" + r + ";" + g + ";" + b, first); + } else if (force == ForceMode.Force256Colors || rounded >= 16) { + first = attr(sb, "48;5;" + rounded, first); + } else if (rounded >= 8) { + first = attr(sb, "10" + (rounded - 8), first); + } else { + first = attr(sb, "4" + rounded, first); + } + } + } else { + first = attr(sb, "49", first); + } + background = bg; + } + if ((d & (F_BOLD | F_FAINT)) != 0) { + if ((d & F_BOLD) != 0 && (s & F_BOLD) == 0 || (d & F_FAINT) != 0 && (s & F_FAINT) == 0) { + first = attr(sb, "22", first); + } + if ((d & F_BOLD) != 0 && (s & F_BOLD) != 0) { + first = attr(sb, "1", first); + } + if ((d & F_FAINT) != 0 && (s & F_FAINT) != 0) { + first = attr(sb, "2", first); + } + } + sb.append("m"); + } + style = s; + } + sb.append(c); + } + if (alt) { + sb.append(altOut); + } + if (style != 0) { + sb.append("\033[0m"); + } + return sb.toString(); + } + + public abstract AttributedStyle styleAt(int index); + + long styleCodeAt(int index) { + return styleAt(index).getStyle(); + } + + public boolean isHidden(int index) { + return (styleCodeAt(index) & F_HIDDEN) != 0; + } + + public int runStart(int index) { + AttributedStyle style = styleAt(index); + while (index > 0 && styleAt(index - 1).equals(style)) { + index--; + } + return index; + } + + public int runLimit(int index) { + AttributedStyle style = styleAt(index); + while (index < length() - 1 && styleAt(index + 1).equals(style)) { + index++; + } + return index + 1; + } + + @Override + public abstract AttributedString subSequence(int start, int end); + + public AttributedString substring(int start, int end) { + return subSequence(start, end); + } + + protected abstract char[] buffer(); + + protected abstract int offset(); + + @Override + public char charAt(int index) { + return buffer()[offset() + index]; + } + + public int codePointAt(int index) { + return Character.codePointAt(buffer(), index + offset()); + } + + public boolean contains(char c) { + for (int i = 0; i < length(); i++) { + if (charAt(i) == c) { + return true; + } + } + return false; + } + + public int codePointBefore(int index) { + return Character.codePointBefore(buffer(), index + offset()); + } + + public int codePointCount(int index, int length) { + return Character.codePointCount(buffer(), index + offset(), length); + } + + public int columnLength() { + int cols = 0; + int len = length(); + for (int cur = 0; cur < len; ) { + int cp = codePointAt(cur); + if (!isHidden(cur)) cols += WCWidth.wcwidth(cp); + cur += Character.charCount(cp); + } + return cols; + } + + public AttributedString columnSubSequence(int start, int stop) { + int begin = 0; + int col = 0; + while (begin < this.length()) { + int cp = codePointAt(begin); + int w = isHidden(begin) ? 0 : WCWidth.wcwidth(cp); + if (col + w > start) { + break; + } + begin += Character.charCount(cp); + col += w; + } + int end = begin; + while (end < this.length()) { + int cp = codePointAt(end); + if (cp == '\n') break; + int w = isHidden(end) ? 0 : WCWidth.wcwidth(cp); + if (col + w > stop) { + break; + } + end += Character.charCount(cp); + col += w; + } + return subSequence(begin, end); + } + + public List columnSplitLength(int columns) { + return columnSplitLength(columns, false, true); + } + + public List columnSplitLength(int columns, boolean includeNewlines, boolean delayLineWrap) { + List strings = new ArrayList<>(); + int cur = 0; + int beg = cur; + int col = 0; + while (cur < length()) { + int cp = codePointAt(cur); + int w = isHidden(cur) ? 0 : WCWidth.wcwidth(cp); + if (cp == '\n') { + strings.add(subSequence(beg, includeNewlines ? cur + 1 : cur)); + beg = cur + 1; + col = 0; + } else if ((col += w) > columns) { + strings.add(subSequence(beg, cur)); + beg = cur; + col = w; + } + cur += Character.charCount(cp); + } + strings.add(subSequence(beg, cur)); + return strings; + } + + @Override + public String toString() { + return new String(buffer(), offset(), length()); + } + + public AttributedString toAttributedString() { + return substring(0, length()); + } + + public enum ForceMode { + None, + Force256Colors, + ForceTrueColors + } +} diff --git a/net-cli/src/main/java/org/jline/utils/AttributedString.java b/net-cli/src/main/java/org/jline/utils/AttributedString.java new file mode 100644 index 0000000..c26a44f --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/AttributedString.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.security.InvalidParameterException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jline.terminal.Terminal; + +/** + * Attributed string. + * Instances of this class are immutables. + * Substrings are created without any memory copy. + * + * @author Guillaume Nodet + */ +public class AttributedString extends AttributedCharSequence { + + public static final AttributedString EMPTY = new AttributedString(""); + public static final AttributedString NEWLINE = new AttributedString("\n"); + final char[] buffer; + final long[] style; + final int start; + final int end; + + public AttributedString(CharSequence str) { + this(str, 0, str.length(), null); + } + + public AttributedString(CharSequence str, int start, int end) { + this(str, start, end, null); + } + + public AttributedString(CharSequence str, AttributedStyle s) { + this(str, 0, str.length(), s); + } + + public AttributedString(CharSequence str, int start, int end, AttributedStyle s) { + if (end < start) { + throw new InvalidParameterException(); + } + if (str instanceof AttributedString as) { + this.buffer = as.buffer; + if (s != null) { + this.style = as.style.clone(); + for (int i = 0; i < style.length; i++) { + this.style[i] = (this.style[i] & ~s.getMask()) | s.getStyle(); + } + } else { + this.style = as.style; + } + this.start = as.start + start; + this.end = as.start + end; + } else if (str instanceof AttributedStringBuilder asb) { + AttributedString as = asb.subSequence(start, end); + this.buffer = as.buffer; + this.style = as.style; + if (s != null) { + for (int i = 0; i < style.length; i++) { + this.style[i] = (this.style[i] & ~s.getMask()) | s.getStyle(); + } + } + this.start = as.start; + this.end = as.end; + } else { + int l = end - start; + buffer = new char[l]; + for (int i = 0; i < l; i++) { + buffer[i] = str.charAt(start + i); + } + style = new long[l]; + if (s != null) { + Arrays.fill(style, s.getStyle()); + } + this.start = 0; + this.end = l; + } + } + + AttributedString(char[] buffer, long[] style, int start, int end) { + this.buffer = buffer; + this.style = style; + this.start = start; + this.end = end; + } + + public static AttributedString fromAnsi(String ansi) { + return fromAnsi(ansi, 0); + } + + public static AttributedString fromAnsi(String ansi, int tabs) { + return fromAnsi(ansi, List.of(tabs)); + } + + public static AttributedString fromAnsi(String ansi, List tabs) { + return fromAnsi(ansi, tabs, null, null); + } + + public static AttributedString fromAnsi(String ansi, Terminal terminal) { + String alternateIn, alternateOut; + if (!DISABLE_ALTERNATE_CHARSET) { + alternateIn = Curses.tputs(terminal.getStringCapability(InfoCmp.Capability.enter_alt_charset_mode)); + alternateOut = Curses.tputs(terminal.getStringCapability(InfoCmp.Capability.exit_alt_charset_mode)); + } else { + alternateIn = null; + alternateOut = null; + } + return fromAnsi(ansi, List.of(0), alternateIn, alternateOut); + } + + public static AttributedString fromAnsi(String ansi, List tabs, String altIn, String altOut) { + if (ansi == null) { + return null; + } + return new AttributedStringBuilder(ansi.length()) + .tabs(tabs) + .altCharset(altIn, altOut) + .ansiAppend(ansi) + .toAttributedString(); + } + + public static String stripAnsi(String ansi) { + if (ansi == null) { + return null; + } + return new AttributedStringBuilder(ansi.length()).ansiAppend(ansi).toString(); + } + + public static AttributedString join(AttributedString delimiter, AttributedString... elements) { + Objects.requireNonNull(delimiter); + Objects.requireNonNull(elements); + return join(delimiter, Arrays.asList(elements)); + } + + public static AttributedString join(AttributedString delimiter, Iterable elements) { + Objects.requireNonNull(elements); + AttributedStringBuilder sb = new AttributedStringBuilder(); + int i = 0; + for (AttributedString str : elements) { + if (i++ > 0 && delimiter != null) { + sb.append(delimiter); + } + sb.append(str); + } + return sb.toAttributedString(); + } + + @Override + protected char[] buffer() { + return buffer; + } + + @Override + protected int offset() { + return start; + } + + @Override + public int length() { + return end - start; + } + + @Override + public AttributedStyle styleAt(int index) { + return new AttributedStyle(style[start + index], style[start + index]); + } + + @Override + long styleCodeAt(int index) { + return style[start + index]; + } + + @Override + public AttributedString subSequence(int start, int end) { + return new AttributedString(this, start, end); + } + + public AttributedString styleMatches(Pattern pattern, AttributedStyle style) { + Matcher matcher = pattern.matcher(this); + boolean result = matcher.find(); + if (result) { + long[] newstyle = this.style.clone(); + do { + for (int i = matcher.start(); i < matcher.end(); i++) { + newstyle[this.start + i] = (newstyle[this.start + i] & ~style.getMask()) | style.getStyle(); + } + result = matcher.find(); + } while (result); + return new AttributedString(buffer, newstyle, start, end); + } + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AttributedString that = (AttributedString) o; + return end - start == that.end - that.start + && arrEq(buffer, that.buffer, start, that.start, end - start) + && arrEq(style, that.style, start, that.start, end - start); + } + + private boolean arrEq(char[] a1, char[] a2, int s1, int s2, int l) { + for (int i = 0; i < l; i++) { + if (a1[s1 + i] != a2[s2 + i]) { + return false; + } + } + return true; + } + + private boolean arrEq(long[] a1, long[] a2, int s1, int s2, int l) { + for (int i = 0; i < l; i++) { + if (a1[s1 + i] != a2[s2 + i]) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(buffer); + result = 31 * result + Arrays.hashCode(style); + result = 31 * result + start; + result = 31 * result + end; + return result; + } +} diff --git a/net-cli/src/main/java/org/jline/utils/AttributedStringBuilder.java b/net-cli/src/main/java/org/jline/utils/AttributedStringBuilder.java new file mode 100644 index 0000000..657ec74 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/AttributedStringBuilder.java @@ -0,0 +1,546 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Attributed string builder + * + * @author Guillaume Nodet + */ +public class AttributedStringBuilder extends AttributedCharSequence implements Appendable { + + private char[] buffer; + private long[] style; + private int length; + private TabStops tabs = new TabStops(0); + private char[] altIn; + private char[] altOut; + private boolean inAltCharset; + private int lastLineLength = 0; + private AttributedStyle current = AttributedStyle.DEFAULT; + + public AttributedStringBuilder() { + this(64); + } + + public AttributedStringBuilder(int capacity) { + buffer = new char[capacity]; + style = new long[capacity]; + length = 0; + } + + public static AttributedString append(CharSequence... strings) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + for (CharSequence s : strings) { + sb.append(s); + } + return sb.toAttributedString(); + } + + private static boolean equals(char[] a, int aFromIndex, char[] b, int bFromIndex, int length) { + if (aFromIndex < 0 || bFromIndex < 0 || aFromIndex + length > a.length || bFromIndex + length > b.length) { + return false; + } + for (int i = 0; i < length; i++) { + if (a[aFromIndex + i] != b[bFromIndex + i]) { + return false; + } + } + return true; + } + + @Override + public int length() { + return length; + } + + @Override + public char charAt(int index) { + return buffer[index]; + } + + @Override + public AttributedStyle styleAt(int index) { + return new AttributedStyle(style[index], style[index]); + } + + @Override + long styleCodeAt(int index) { + return style[index]; + } + + @Override + protected char[] buffer() { + return buffer; + } + + @Override + protected int offset() { + return 0; + } + + @Override + public AttributedString subSequence(int start, int end) { + return new AttributedString( + Arrays.copyOfRange(buffer, start, end), Arrays.copyOfRange(style, start, end), 0, end - start); + } + + @Override + public AttributedStringBuilder append(CharSequence csq) { + if (csq == null) { + csq = "null"; // Required by Appendable.append + } + return append(new AttributedString(csq, current)); + } + + @Override + public AttributedStringBuilder append(CharSequence csq, int start, int end) { + if (csq == null) { + csq = "null"; // Required by Appendable.append + } + return append(csq.subSequence(start, end)); + } + + @Override + public AttributedStringBuilder append(char c) { + return append(Character.toString(c)); + } + + public AttributedStringBuilder append(char c, int repeat) { + AttributedString s = new AttributedString(Character.toString(c), current); + while (repeat-- > 0) { + append(s); + } + return this; + } + + public AttributedStringBuilder append(CharSequence csq, AttributedStyle style) { + return append(new AttributedString(csq, style)); + } + + public AttributedStringBuilder style(AttributedStyle style) { + current = style; + return this; + } + + public AttributedStringBuilder style(Function style) { + current = style.apply(current); + return this; + } + + public AttributedStringBuilder styled(Function style, CharSequence cs) { + return styled(style, sb -> sb.append(cs)); + } + + public AttributedStringBuilder styled(AttributedStyle style, CharSequence cs) { + return styled(s -> style, sb -> sb.append(cs)); + } + + public AttributedStringBuilder styled( + Function style, Consumer consumer) { + AttributedStyle prev = current; + current = style.apply(prev); + consumer.accept(this); + current = prev; + return this; + } + + public AttributedStyle style() { + return current; + } + + public AttributedStringBuilder append(AttributedString str) { + return append((AttributedCharSequence) str, 0, str.length()); + } + + public AttributedStringBuilder append(AttributedString str, int start, int end) { + return append((AttributedCharSequence) str, start, end); + } + + public AttributedStringBuilder append(AttributedCharSequence str) { + return append(str, 0, str.length()); + } + + public AttributedStringBuilder append(AttributedCharSequence str, int start, int end) { + ensureCapacity(length + end - start); + for (int i = start; i < end; i++) { + char c = str.charAt(i); + long s = str.styleCodeAt(i) & ~current.getMask() | current.getStyle(); + if (tabs.defined() && c == '\t') { + insertTab(new AttributedStyle(s, 0)); + } else { + ensureCapacity(length + 1); + buffer[length] = c; + style[length] = s; + if (c == '\n') { + lastLineLength = 0; + } else { + lastLineLength++; + } + length++; + } + } + return this; + } + + protected void ensureCapacity(int nl) { + if (nl > buffer.length) { + int s = Math.max(buffer.length, 1); + while (s <= nl) { + s *= 2; + } + buffer = Arrays.copyOf(buffer, s); + style = Arrays.copyOf(style, s); + } + } + + public void appendAnsi(String ansi) { + ansiAppend(ansi); + } + + public AttributedStringBuilder ansiAppend(String ansi) { + int ansiStart = 0; + int ansiState = 0; + ensureCapacity(length + ansi.length()); + for (int i = 0; i < ansi.length(); i++) { + char c = ansi.charAt(i); + if (ansiState == 0 && c == 27) { + ansiState++; + } else if (ansiState == 1 && c == '[') { + ansiState++; + ansiStart = i + 1; + } else if (ansiState == 2) { + if (c == 'm') { + String[] params = ansi.substring(ansiStart, i).split(";"); + int j = 0; + while (j < params.length) { + int ansiParam = params[j].isEmpty() ? 0 : Integer.parseInt(params[j]); + switch (ansiParam) { + case 0: + current = AttributedStyle.DEFAULT; + break; + case 1: + current = current.bold(); + break; + case 2: + current = current.faint(); + break; + case 3: + current = current.italic(); + break; + case 4: + current = current.underline(); + break; + case 5: + current = current.blink(); + break; + case 7: + current = current.inverse(); + break; + case 8: + current = current.conceal(); + break; + case 9: + current = current.crossedOut(); + break; + case 22: + current = current.boldOff().faintOff(); + break; + case 23: + current = current.italicOff(); + break; + case 24: + current = current.underlineOff(); + break; + case 25: + current = current.blinkOff(); + break; + case 27: + current = current.inverseOff(); + break; + case 28: + current = current.concealOff(); + break; + case 29: + current = current.crossedOutOff(); + break; + case 30: + case 31: + case 32: + case 33: + case 34: + case 35: + case 36: + case 37: + current = current.foreground(ansiParam - 30); + break; + case 39: + current = current.foregroundOff(); + break; + case 40: + case 41: + case 42: + case 43: + case 44: + case 45: + case 46: + case 47: + current = current.background(ansiParam - 40); + break; + case 49: + current = current.backgroundOff(); + break; + case 38: + case 48: + if (j + 1 < params.length) { + int ansiParam2 = Integer.parseInt(params[++j]); + if (ansiParam2 == 2) { + if (j + 3 < params.length) { + int r = Integer.parseInt(params[++j]); + int g = Integer.parseInt(params[++j]); + int b = Integer.parseInt(params[++j]); + if (ansiParam == 38) { + current = current.foreground(r, g, b); + } else { + current = current.background(r, g, b); + } + } + } else if (ansiParam2 == 5) { + if (j + 1 < params.length) { + int col = Integer.parseInt(params[++j]); + if (ansiParam == 38) { + current = current.foreground(col); + } else { + current = current.background(col); + } + } + } + } + break; + case 90: + case 91: + case 92: + case 93: + case 94: + case 95: + case 96: + case 97: + current = current.foreground(ansiParam - 90 + 8); + break; + case 100: + case 101: + case 102: + case 103: + case 104: + case 105: + case 106: + case 107: + current = current.background(ansiParam - 100 + 8); + break; + } + j++; + } + ansiState = 0; + } else if (!(c >= '0' && c <= '9' || c == ';')) { + // This is not a SGR code, so ignore + ansiState = 0; + } + } else { + if (ansiState >= 1) { + ensureCapacity(length + 1); + buffer[length++] = 27; + if (ansiState >= 2) { + ensureCapacity(length + 1); + buffer[length++] = '['; + } + ansiState = 0; + } + if (c == '\t' && tabs.defined()) { + insertTab(current); + } else { + ensureCapacity(length + 1); + if (inAltCharset) { + switch (c) { + case 'j': + c = '┘'; + break; + case 'k': + c = '┐'; + break; + case 'l': + c = '┌'; + break; + case 'm': + c = '└'; + break; + case 'n': + c = '┼'; + break; + case 'q': + c = '─'; + break; + case 't': + c = '├'; + break; + case 'u': + c = '┤'; + break; + case 'v': + c = '┴'; + break; + case 'w': + c = '┬'; + break; + case 'x': + c = '│'; + break; + } + } + buffer[length] = c; + style[length] = this.current.getStyle(); + if (c == '\n') { + lastLineLength = 0; + } else { + lastLineLength++; + } + length++; + if (altIn != null && altOut != null) { + char[] alt = inAltCharset ? altOut : altIn; + if (equals(buffer, length - alt.length, alt, 0, alt.length)) { + inAltCharset = !inAltCharset; + length -= alt.length; + } + } + } + } + } + return this; + } + + protected void insertTab(AttributedStyle s) { + int nb = tabs.spaces(lastLineLength); + ensureCapacity(length + nb); + for (int i = 0; i < nb; i++) { + buffer[length] = ' '; + style[length] = s.getStyle(); + length++; + } + lastLineLength += nb; + } + + public void setLength(int l) { + length = l; + } + + /** + * Set the number of spaces a tab is expanded to. Tab size cannot be changed + * after text has been added to prevent inconsistent indentation. + *

+ * If tab size is set to 0, tabs are not expanded (the default). + * + * @param tabsize Spaces per tab or 0 for no tab expansion. Must be non-negative + * @return this + */ + public AttributedStringBuilder tabs(int tabsize) { + if (tabsize < 0) { + throw new IllegalArgumentException("Tab size must be non negative"); + } + return tabs(List.of(tabsize)); + } + + public AttributedStringBuilder tabs(List tabs) { + if (length > 0) { + throw new IllegalStateException("Cannot change tab size after appending text"); + } + this.tabs = new TabStops(tabs); + return this; + } + + public AttributedStringBuilder altCharset(String altIn, String altOut) { + if (length > 0) { + throw new IllegalStateException("Cannot change alternative charset after appending text"); + } + this.altIn = altIn != null ? altIn.toCharArray() : null; + this.altOut = altOut != null ? altOut.toCharArray() : null; + return this; + } + + public AttributedStringBuilder styleMatches(Pattern pattern, AttributedStyle s) { + Matcher matcher = pattern.matcher(this); + while (matcher.find()) { + for (int i = matcher.start(); i < matcher.end(); i++) { + style[i] = (style[i] & ~s.getMask()) | s.getStyle(); + } + } + return this; + } + + public AttributedStringBuilder styleMatches(Pattern pattern, List styles) { + Matcher matcher = pattern.matcher(this); + while (matcher.find()) { + for (int group = 0; group < matcher.groupCount(); group++) { + AttributedStyle s = styles.get(group); + for (int i = matcher.start(group + 1); i < matcher.end(group + 1); i++) { + style[i] = (style[i] & ~s.getMask()) | s.getStyle(); + } + } + } + return this; + } + + private static class TabStops { + private List tabs = new ArrayList<>(); + private int lastStop = 0; + private int lastSize = 0; + + public TabStops(int tabs) { + this.lastSize = tabs; + } + + public TabStops(List tabs) { + this.tabs = tabs; + int p = 0; + for (int s : tabs) { + if (s <= p) { + continue; + } + lastStop = s; + lastSize = s - p; + p = s; + } + } + + boolean defined() { + return lastSize > 0; + } + + int spaces(int lastLineLength) { + int out = 0; + if (lastLineLength >= lastStop) { + out = lastSize - (lastLineLength - lastStop) % lastSize; + } else { + for (int s : tabs) { + if (s > lastLineLength) { + out = s - lastLineLength; + break; + } + } + } + return out; + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/AttributedStyle.java b/net-cli/src/main/java/org/jline/utils/AttributedStyle.java new file mode 100644 index 0000000..5f76142 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/AttributedStyle.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +/** + * Text styling. + * + * @author Guillaume Nodet + */ +public class AttributedStyle { + + public static final int BLACK = 0; + public static final int RED = 1; + public static final int GREEN = 2; + public static final int YELLOW = 3; + public static final int BLUE = 4; + public static final int MAGENTA = 5; + public static final int CYAN = 6; + public static final int WHITE = 7; + + public static final int BRIGHT = 8; + public static final AttributedStyle DEFAULT = new AttributedStyle(); + static final long F_BOLD = 0x00000001; + public static final AttributedStyle BOLD = DEFAULT.bold(); + public static final AttributedStyle BOLD_OFF = DEFAULT.boldOff(); + static final long F_FAINT = 0x00000002; + static final long F_ITALIC = 0x00000004; + static final long F_UNDERLINE = 0x00000008; + static final long F_BLINK = 0x00000010; + static final long F_INVERSE = 0x00000020; + public static final AttributedStyle INVERSE = DEFAULT.inverse(); + public static final AttributedStyle INVERSE_OFF = DEFAULT.inverseOff(); + static final long F_CONCEAL = 0x00000040; + static final long F_CROSSED_OUT = 0x00000080; + static final long F_FOREGROUND_IND = 0x00000100; + static final long F_FOREGROUND_RGB = 0x00000200; + static final long F_FOREGROUND = F_FOREGROUND_IND | F_FOREGROUND_RGB; + static final long F_BACKGROUND_IND = 0x00000400; + static final long F_BACKGROUND_RGB = 0x00000800; + static final long F_BACKGROUND = F_BACKGROUND_IND | F_BACKGROUND_RGB; + static final long F_HIDDEN = 0x00001000; + public static final AttributedStyle HIDDEN = DEFAULT.hidden(); + public static final AttributedStyle HIDDEN_OFF = DEFAULT.hiddenOff(); + static final long MASK = 0x00001FFF; + static final int FG_COLOR_EXP = 15; + static final int BG_COLOR_EXP = 39; + static final long FG_COLOR = 0xFFFFFFL << FG_COLOR_EXP; + static final long BG_COLOR = 0xFFFFFFL << BG_COLOR_EXP; + final long style; + final long mask; + + public AttributedStyle() { + this(0, 0); + } + + public AttributedStyle(AttributedStyle s) { + this(s.style, s.mask); + } + + public AttributedStyle(long style, long mask) { + this.style = style; + this.mask = mask & MASK + | ((style & F_FOREGROUND) != 0 ? FG_COLOR : 0) + | ((style & F_BACKGROUND) != 0 ? BG_COLOR : 0); + } + + public AttributedStyle bold() { + return new AttributedStyle(style | F_BOLD, mask | F_BOLD); + } + + public AttributedStyle boldOff() { + return new AttributedStyle(style & ~F_BOLD, mask | F_BOLD); + } + + public AttributedStyle boldDefault() { + return new AttributedStyle(style & ~F_BOLD, mask & ~F_BOLD); + } + + public AttributedStyle faint() { + return new AttributedStyle(style | F_FAINT, mask | F_FAINT); + } + + public AttributedStyle faintOff() { + return new AttributedStyle(style & ~F_FAINT, mask | F_FAINT); + } + + public AttributedStyle faintDefault() { + return new AttributedStyle(style & ~F_FAINT, mask & ~F_FAINT); + } + + public AttributedStyle italic() { + return new AttributedStyle(style | F_ITALIC, mask | F_ITALIC); + } + + public AttributedStyle italicOff() { + return new AttributedStyle(style & ~F_ITALIC, mask | F_ITALIC); + } + + public AttributedStyle italicDefault() { + return new AttributedStyle(style & ~F_ITALIC, mask & ~F_ITALIC); + } + + public AttributedStyle underline() { + return new AttributedStyle(style | F_UNDERLINE, mask | F_UNDERLINE); + } + + public AttributedStyle underlineOff() { + return new AttributedStyle(style & ~F_UNDERLINE, mask | F_UNDERLINE); + } + + public AttributedStyle underlineDefault() { + return new AttributedStyle(style & ~F_UNDERLINE, mask & ~F_UNDERLINE); + } + + public AttributedStyle blink() { + return new AttributedStyle(style | F_BLINK, mask | F_BLINK); + } + + public AttributedStyle blinkOff() { + return new AttributedStyle(style & ~F_BLINK, mask | F_BLINK); + } + + public AttributedStyle blinkDefault() { + return new AttributedStyle(style & ~F_BLINK, mask & ~F_BLINK); + } + + public AttributedStyle inverse() { + return new AttributedStyle(style | F_INVERSE, mask | F_INVERSE); + } + + public AttributedStyle inverseNeg() { + long s = (style & F_INVERSE) != 0 ? style & ~F_INVERSE : style | F_INVERSE; + return new AttributedStyle(s, mask | F_INVERSE); + } + + public AttributedStyle inverseOff() { + return new AttributedStyle(style & ~F_INVERSE, mask | F_INVERSE); + } + + public AttributedStyle inverseDefault() { + return new AttributedStyle(style & ~F_INVERSE, mask & ~F_INVERSE); + } + + public AttributedStyle conceal() { + return new AttributedStyle(style | F_CONCEAL, mask | F_CONCEAL); + } + + public AttributedStyle concealOff() { + return new AttributedStyle(style & ~F_CONCEAL, mask | F_CONCEAL); + } + + public AttributedStyle concealDefault() { + return new AttributedStyle(style & ~F_CONCEAL, mask & ~F_CONCEAL); + } + + public AttributedStyle crossedOut() { + return new AttributedStyle(style | F_CROSSED_OUT, mask | F_CROSSED_OUT); + } + + public AttributedStyle crossedOutOff() { + return new AttributedStyle(style & ~F_CROSSED_OUT, mask | F_CROSSED_OUT); + } + + public AttributedStyle crossedOutDefault() { + return new AttributedStyle(style & ~F_CROSSED_OUT, mask & ~F_CROSSED_OUT); + } + + public AttributedStyle foreground(int color) { + return new AttributedStyle( + style & ~FG_COLOR | F_FOREGROUND_IND | (((long) color << FG_COLOR_EXP) & FG_COLOR), + mask | F_FOREGROUND_IND); + } + + public AttributedStyle foreground(int r, int g, int b) { + return foregroundRgb(r << 16 | g << 8 | b); + } + + public AttributedStyle foregroundRgb(int color) { + return new AttributedStyle( + style & ~FG_COLOR | F_FOREGROUND_RGB | ((((long) color & 0xFFFFFF) << FG_COLOR_EXP) & FG_COLOR), + mask | F_FOREGROUND_RGB); + } + + public AttributedStyle foregroundOff() { + return new AttributedStyle(style & ~FG_COLOR & ~F_FOREGROUND, mask | F_FOREGROUND); + } + + public AttributedStyle foregroundDefault() { + return new AttributedStyle(style & ~FG_COLOR & ~F_FOREGROUND, mask & ~(F_FOREGROUND | FG_COLOR)); + } + + public AttributedStyle background(int color) { + return new AttributedStyle( + style & ~BG_COLOR | F_BACKGROUND_IND | (((long) color << BG_COLOR_EXP) & BG_COLOR), + mask | F_BACKGROUND_IND); + } + + public AttributedStyle background(int r, int g, int b) { + return backgroundRgb(r << 16 | g << 8 | b); + } + + public AttributedStyle backgroundRgb(int color) { + return new AttributedStyle( + style & ~BG_COLOR | F_BACKGROUND_RGB | ((((long) color & 0xFFFFFF) << BG_COLOR_EXP) & BG_COLOR), + mask | F_BACKGROUND_RGB); + } + + public AttributedStyle backgroundOff() { + return new AttributedStyle(style & ~BG_COLOR & ~F_BACKGROUND, mask | F_BACKGROUND); + } + + public AttributedStyle backgroundDefault() { + return new AttributedStyle(style & ~BG_COLOR & ~F_BACKGROUND, mask & ~(F_BACKGROUND | BG_COLOR)); + } + + /** + * The hidden flag can be used to embed custom escape sequences. + * The characters are considered being 0-column long and will be printed as-is. + * The user is responsible for ensuring that those sequences do not move the cursor. + * + * @return the new style + */ + public AttributedStyle hidden() { + return new AttributedStyle(style | F_HIDDEN, mask | F_HIDDEN); + } + + public AttributedStyle hiddenOff() { + return new AttributedStyle(style & ~F_HIDDEN, mask | F_HIDDEN); + } + + public AttributedStyle hiddenDefault() { + return new AttributedStyle(style & ~F_HIDDEN, mask & ~F_HIDDEN); + } + + public long getStyle() { + return style; + } + + public long getMask() { + return mask; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AttributedStyle that = (AttributedStyle) o; + if (style != that.style) return false; + return mask == that.mask; + } + + @Override + public int hashCode() { + return 31 * Long.hashCode(style) + Long.hashCode(mask); + } + + public String toAnsi() { + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.styled(this, " "); + String s = sb.toAnsi(AttributedCharSequence.TRUE_COLORS, AttributedCharSequence.ForceMode.None); + return s.length() > 1 ? s.substring(2, s.indexOf('m')) : s; + } + + @Override + public String toString() { + return "AttributedStyle{" + "style=" + style + ", mask=" + mask + ", ansi=" + toAnsi() + '}'; + } +} diff --git a/net-cli/src/main/java/org/jline/utils/ClosedException.java b/net-cli/src/main/java/org/jline/utils/ClosedException.java new file mode 100644 index 0000000..73920d2 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/ClosedException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; + +public class ClosedException extends IOException { + + private static final long serialVersionUID = 3085420657077696L; + + public ClosedException() { + } + + public ClosedException(String message) { + super(message); + } + + public ClosedException(String message, Throwable cause) { + super(message, cause); + } + + public ClosedException(Throwable cause) { + super(cause); + } +} diff --git a/net-cli/src/main/java/org/jline/utils/ColorPalette.java b/net-cli/src/main/java/org/jline/utils/ColorPalette.java new file mode 100644 index 0000000..6ad623b --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/ColorPalette.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.jline.terminal.Terminal; + +/** + * Color palette + */ +public class ColorPalette { + + public static final String XTERM_INITC = + "\\E]4;%p1%d;rgb\\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\\E\\\\"; + + public static final ColorPalette DEFAULT = new ColorPalette(); + + private final Terminal terminal; + private String distanceName; + private Colors.Distance distance; + private boolean osc4; + private int[] palette; + + public ColorPalette() { + this.terminal = null; + this.distanceName = null; + this.palette = Colors.DEFAULT_COLORS_256; + } + + public ColorPalette(Terminal terminal) throws IOException { + this(terminal, null); + } + + @SuppressWarnings("this-escape") + public ColorPalette(Terminal terminal, String distance) throws IOException { + this.terminal = terminal; + this.distanceName = distance; + loadPalette(false); + } + + private static int[] doLoad(Terminal terminal) throws IOException { + PrintWriter writer = terminal.writer(); + NonBlockingReader reader = terminal.reader(); + + int[] palette = new int[256]; + for (int i = 0; i < 16; i++) { + StringBuilder req = new StringBuilder(1024); + req.append("\033]4"); + for (int j = 0; j < 16; j++) { + req.append(';').append(i * 16 + j).append(";?"); + } + req.append("\033\\"); + writer.write(req.toString()); + writer.flush(); + + boolean black = true; + for (int j = 0; j < 16; j++) { + if (reader.peek(50) < 0) { + break; + } + if (reader.read(10) != '\033' + || reader.read(10) != ']' + || reader.read(10) != '4' + || reader.read(10) != ';') { + return null; + } + int idx = 0; + int c; + while (true) { + c = reader.read(10); + if (c >= '0' && c <= '9') { + idx = idx * 10 + (c - '0'); + } else if (c == ';') { + break; + } else { + return null; + } + } + if (idx > 255) { + return null; + } + if (reader.read(10) != 'r' + || reader.read(10) != 'g' + || reader.read(10) != 'b' + || reader.read(10) != ':') { + return null; + } + StringBuilder sb = new StringBuilder(16); + List rgb = new ArrayList<>(); + while (true) { + c = reader.read(10); + if (c == '\007') { + rgb.add(sb.toString()); + break; + } else if (c == '\033') { + c = reader.read(10); + if (c == '\\') { + rgb.add(sb.toString()); + break; + } else { + return null; + } + } else if (c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { + sb.append((char) c); + } else if (c == '/') { + rgb.add(sb.toString()); + sb.setLength(0); + } + } + if (rgb.size() != 3) { + return null; + } + double r = Integer.parseInt(rgb.get(0), 16) + / ((1 << (4 * rgb.get(0).length())) - 1.0); + double g = Integer.parseInt(rgb.get(1), 16) + / ((1 << (4 * rgb.get(1).length())) - 1.0); + double b = Integer.parseInt(rgb.get(2), 16) + / ((1 << (4 * rgb.get(2).length())) - 1.0); + palette[idx] = (int) ((Math.round(r * 255) << 16) + (Math.round(g * 255) << 8) + Math.round(b * 255)); + black &= palette[idx] == 0; + } + if (black) { + break; + } + } + int max = 256; + while (max > 0 && palette[--max] == 0) + ; + return Arrays.copyOfRange(palette, 0, max + 1); + } + + /** + * Get the name of the distance to use for rounding colors. + * + * @return the name of the color distance + */ + public String getDistanceName() { + return distanceName; + } + + /** + * Set the name of the color distance to use when rounding RGB colors to the palette. + * + * @param name the name of the color distance + */ + public void setDistance(String name) { + this.distanceName = name; + } + + /** + * Check if the terminal has the capability to change colors. + * + * @return true if the terminal can change colors + */ + public boolean canChange() { + return terminal != null && terminal.getBooleanCapability(InfoCmp.Capability.can_change); + } + + /** + * Load the palette from the terminal. + * If the palette has already been loaded, subsequent calls will simply return true. + * + * @return true if the palette has been successfully loaded. + * @throws IOException + */ + public boolean loadPalette() throws IOException { + if (!osc4) { + loadPalette(true); + } + return osc4; + } + + protected void loadPalette(boolean doLoad) throws IOException { + if (terminal != null) { + int[] pal = doLoad ? doLoad(terminal) : null; + if (pal != null) { + this.palette = pal; + this.osc4 = true; + } else { + Integer cols = terminal.getNumericCapability(InfoCmp.Capability.max_colors); + if (cols != null) { + if (cols == Colors.DEFAULT_COLORS_88.length) { + this.palette = Colors.DEFAULT_COLORS_88; + } else { + this.palette = Arrays.copyOf(Colors.DEFAULT_COLORS_256, Math.min(cols, 256)); + } + } else { + this.palette = Arrays.copyOf(Colors.DEFAULT_COLORS_256, 256); + } + this.osc4 = false; + } + } else { + this.palette = Colors.DEFAULT_COLORS_256; + this.osc4 = false; + } + } + + /** + * Get the palette length + * + * @return the palette length + */ + public int getLength() { + return this.palette.length; + } + + /** + * Get a specific color in the palette + * + * @param index the index of the color + * @return the color at the given index + */ + public int getColor(int index) { + return palette[index]; + } + + /** + * Change the color of the palette + * + * @param index the index of the color + * @param color the new color value + */ + public void setColor(int index, int color) { + palette[index] = color; + if (canChange()) { + String initc = terminal.getStringCapability(InfoCmp.Capability.initialize_color); + if (initc != null || osc4) { + // initc expects color in 0..1000 range + int r = (((color >> 16) & 0xFF) * 1000) / 255 + 1; + int g = (((color >> 8) & 0xFF) * 1000) / 255 + 1; + int b = ((color & 0xFF) * 1000) / 255 + 1; + if (initc == null) { + // This is the xterm version + initc = XTERM_INITC; + } + Curses.tputs(terminal.writer(), initc, index, r, g, b); + terminal.writer().flush(); + } + } + } + + public boolean isReal() { + return osc4; + } + + public int round(int r, int g, int b) { + return Colors.roundColor((r << 16) + (g << 8) + b, palette, palette.length, getDist()); + } + + public int round(int col) { + if (col >= palette.length) { + col = Colors.roundColor(DEFAULT.getColor(col), palette, palette.length, getDist()); + } + return col; + } + + protected Colors.Distance getDist() { + if (distance == null) { + distance = Colors.getDistance(distanceName); + } + return distance; + } + + @Override + public String toString() { + return "ColorPalette[" + "length=" + getLength() + ", " + "distance='" + getDist() + "']"; + } +} diff --git a/net-cli/src/main/java/org/jline/utils/Colors.java b/net-cli/src/main/java/org/jline/utils/Colors.java new file mode 100644 index 0000000..88bfb13 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/Colors.java @@ -0,0 +1,737 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.BufferedReader; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; +import static org.jline.terminal.TerminalBuilder.PROP_COLOR_DISTANCE; + +public class Colors { + + // @spotless:off + + /** + * Default 256 colors palette + */ + public static final int[] DEFAULT_COLORS_256 = { + // 16 ansi + 0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0, + 0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff, + + // 6x6x6 color cube + 0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, + 0x005f00, 0x005f5f, 0x005f87, 0x005faf, 0x005fd7, 0x005fff, + 0x008700, 0x00875f, 0x008787, 0x0087af, 0x0087d7, 0x0087ff, + 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, + 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, + 0x00ff00, 0x00ff5f, 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, + + 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, 0x5f00d7, 0x5f00ff, + 0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, + 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, + 0x5faf00, 0x5faf5f, 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, + 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, 0x5fd7d7, 0x5fd7ff, + 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, + + 0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff, + 0x875f00, 0x875f5f, 0x875f87, 0x875faf, 0x875fd7, 0x875fff, + 0x878700, 0x87875f, 0x878787, 0x8787af, 0x8787d7, 0x8787ff, + 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, + 0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, + 0x87ff00, 0x87ff5f, 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, + + 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, 0xaf00d7, 0xaf00ff, + 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff, + 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, + 0xafaf00, 0xafaf5f, 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, + 0xafd700, 0xafd75f, 0xafd787, 0xafd7af, 0xafd7d7, 0xafd7ff, + 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, + + 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, + 0xd75f00, 0xd75f5f, 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, + 0xd78700, 0xd7875f, 0xd78787, 0xd787af, 0xd787d7, 0xd787ff, + 0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, + 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, + 0xd7ff00, 0xd7ff5f, 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, + + 0xff0000, 0xff005f, 0xff0087, 0xff00af, 0xff00d7, 0xff00ff, + 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, + 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, + 0xffaf00, 0xffaf5f, 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, + 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, 0xffd7d7, 0xffd7ff, + 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, + + // 24 grey ramp + 0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, + 0x585858, 0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, + 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee, + }; + + /** + * Default 88 colors palette + */ + public static final int[] DEFAULT_COLORS_88 = { + // 16 ansi + 0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0, + 0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff, + + // 4x4x4 color cube + 0x000000, 0x00008b, 0x0000cd, 0x0000ff, + 0x008b00, 0x008b8b, 0x008bcd, 0x008bff, + 0x00cd00, 0x00cd8b, 0x00cdcd, 0x00cdff, + 0x00ff00, 0x00ff8b, 0x00ffcd, 0x00ffff, + + 0x8b0000, 0x8b008b, 0x8b00cd, 0x8b00ff, + 0x8b8b00, 0x8b8b8b, 0x8b8bcd, 0x8b8bff, + 0x8bcd00, 0x8bcd8b, 0x8bcdcd, 0x8bcdff, + 0x8bff00, 0x8bff8b, 0x8bffcd, 0x8bffff, + + 0xcd0000, 0xcd008b, 0xcd00cd, 0xcd00ff, + 0xcd8b00, 0xcd8b8b, 0xcd8bcd, 0xcd8bff, + 0xcdcd00, 0xcdcd8b, 0xcdcdcd, 0xcdcdff, + 0xcdff00, 0xcdff8b, 0xcdffcd, 0xcdffff, + + 0xff0000, 0xff008b, 0xff00cd, 0xff00ff, + 0xff8b00, 0xff8b8b, 0xff8bcd, 0xff8bff, + 0xffcd00, 0xffcd8b, 0xffcdcd, 0xffcdff, + 0xffff00, 0xffff8b, 0xffffcd, 0xffffff, + + // 8 grey ramp + 0x2e2e2e, 0x5c5c5c, 0x737373, 0x8b8b8b, 0xa2a2a2, 0xb9b9b9, 0xd0d0d0, 0xe7e7e7, + }; + + // @spotless:on + + /** + * D50 illuminant for CAM color spaces + */ + public static final double[] D50 = new double[]{96.422f, 100.0f, 82.521f}; + /** + * D65 illuminant for CAM color spaces + */ + public static final double[] D65 = new double[]{95.047, 100.0, 108.883}; + + /** + * Average surrounding for CAM color spaces + */ + public static final double[] averageSurrounding = new double[]{1.0, 0.690, 1.0}; + /** + * Dim surrounding for CAM color spaces + */ + public static final double[] dimSurrounding = new double[]{0.9, 0.590, 0.9}; + /** + * Dark surrounding for CAM color spaces + */ + public static final double[] darkSurrounding = new double[]{0.8, 0.525, 0.8}; + /** + * Lightness + */ + public static final int J = 0; + /** + * Brightness + */ + public static final int Q = 1; + /** + * Chroma + */ + public static final int C = 2; + /** + * Colorfulness + */ + public static final int M = 3; + /** + * Saturation + */ + public static final int s = 4; + /** + * Hue Composition / Hue Quadrature + */ + public static final int H = 5; + /** + * Hue + */ + public static final int h = 6; + static final int SUR_F = 0; + static final int SUR_C = 1; + static final int SUR_N_C = 2; + static final int VC_X_W = 0; + static final int VC_Y_W = 1; + static final int VC_Z_W = 2; + static final int VC_L_A = 3; + static final int VC_Y_B = 4; + static final int VC_F = 5; + static final int VC_C = 6; + static final int VC_N_C = 7; + static final int VC_Z = 8; + static final int VC_N = 9; + static final int VC_N_BB = 10; + static final int VC_N_CB = 11; + static final int VC_A_W = 12; + static final int VC_F_L = 13; + static final int VC_D_RGB_R = 14; + static final int VC_D_RGB_G = 15; + static final int VC_D_RGB_B = 16; + /** + * sRGB encoding environment + */ + public static final double[] sRGB_encoding_environment = vc(D50, 64.0, 64.0 / 5, dimSurrounding); + /** + * sRGB typical environment + */ + public static final double[] sRGB_typical_environment = vc(D50, 200.0, 200.0 / 5, averageSurrounding); + /** + * Adobe RGB environment + */ + public static final double[] AdobeRGB_environment = vc(D65, 160.0, 160.0 / 5, averageSurrounding); + private static final int L = 0; + private static final int A = 1; + private static final int B = 2; + private static final int X = 0; + private static final int Y = 1; + private static final int Z = 2; + private static final double kl = 2.0; + private static final double kc = 1.0; + private static final double kh = 1.0; + private static final double k1 = 0.045; + private static final double k2 = 0.015; + private static final double epsilon = 216.0 / 24389.0; + private static final double kappa = 24389.0 / 27.0; + private static int[] COLORS_256 = DEFAULT_COLORS_256; + private static Map COLOR_NAMES; + + public static void setRgbColors(int[] colors) { + if (colors == null || colors.length != 256) { + throw new IllegalArgumentException(); + } + COLORS_256 = colors; + } + + public static int rgbColor(int col) { + return COLORS_256[col]; + } + + public static Integer rgbColor(String name) { + if (COLOR_NAMES == null) { + Map colors = new LinkedHashMap<>(); + try (InputStream is = InfoCmp.class.getResourceAsStream("colors.txt"); + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + br.lines() + .map(String::trim) + .filter(s -> !s.startsWith("#")) + .filter(s -> !s.isEmpty()) + .forEachOrdered(s -> { + colors.put(s, colors.size()); + }); + COLOR_NAMES = colors; + } catch (IOException e) { + throw new IOError(e); + } + } + return COLOR_NAMES.get(name); + } + + public static int roundColor(int col, int max) { + return roundColor(col, max, null); + } + + public static int roundColor(int col, int max, String dist) { + if (col >= max) { + int c = COLORS_256[col]; + col = roundColor(c, COLORS_256, max, dist); + } + return col; + } + + public static int roundRgbColor(int r, int g, int b, int max) { + return roundColor((r << 16) + (g << 8) + b, COLORS_256, max, (String) null); + } + + static int roundColor(int color, int[] colors, int max, String dist) { + return roundColor(color, colors, max, getDistance(dist)); + } + + static int roundColor(int color, int[] colors, int max, Distance distance) { + double best_distance = Integer.MAX_VALUE; + int best_index = Integer.MAX_VALUE; + for (int idx = 0; idx < max; idx++) { + double d = distance.compute(color, colors[idx]); + if (d <= best_distance) { + best_index = idx; + best_distance = d; + } + } + return best_index; + } + + static Distance getDistance(String dist) { + if (dist == null) { + dist = System.getProperty(PROP_COLOR_DISTANCE, "cie76"); + } + return new NamedDistance(dist, doGetDistance(dist)); + } + + private static Distance doGetDistance(String dist) { + if (dist.equals("rgb")) { + return (p1, p2) -> { + // rgb: see https://www.compuphase.com/cmetric.htm + double[] c1 = rgb(p1); + double[] c2 = rgb(p2); + double rmean = (c1[0] + c2[0]) / 2.0; + double[] w = {2.0 + rmean, 4.0, 3.0 - rmean}; + return scalar(c1, c2, w); + }; + } + if (dist.matches("rgb\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) { + return (p1, p2) -> scalar(rgb(p1), rgb(p2), getWeights(dist)); + } + if (dist.equals("lab") || dist.equals("cie76")) { + return (p1, p2) -> scalar(rgb2cielab(p1), rgb2cielab(p2)); + } + if (dist.matches("lab\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) { + double[] w = getWeights(dist); + return (p1, p2) -> scalar(rgb2cielab(p1), rgb2cielab(p2), new double[]{w[0], w[1], w[1]}); + } + if (dist.equals("cie94")) { + return (p1, p2) -> cie94(rgb2cielab(p1), rgb2cielab(p2)); + } + if (dist.equals("cie00") || dist.equals("cie2000")) { + return (p1, p2) -> cie00(rgb2cielab(p1), rgb2cielab(p2)); + } + if (dist.equals("cam02")) { + return (p1, p2) -> cam02(p1, p2, sRGB_typical_environment); + } + if (dist.equals("camlab")) { + return (p1, p2) -> { + double[] c1 = camlab(p1, sRGB_typical_environment); + double[] c2 = camlab(p2, sRGB_typical_environment); + return scalar(c1, c2); + }; + } + if (dist.matches("camlab\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) { + return (p1, p2) -> { + double[] c1 = camlab(p1, sRGB_typical_environment); + double[] c2 = camlab(p2, sRGB_typical_environment); + double[] w = getWeights(dist); + return scalar(c1, c2, new double[]{w[0], w[1], w[1]}); + }; + } + if (dist.matches("camlch")) { + return (p1, p2) -> { + double[] c1 = camlch(p1, sRGB_typical_environment); + double[] c2 = camlch(p2, sRGB_typical_environment); + return camlch(c1, c2); + }; + } + if (dist.matches("camlch\\(([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?),([0-9]+(\\.[0-9]+)?)\\)")) { + return (p1, p2) -> { + double[] c1 = camlch(p1, sRGB_typical_environment); + double[] c2 = camlch(p2, sRGB_typical_environment); + double[] w = getWeights(dist); + return camlch(c1, c2, w); + }; + } + throw new IllegalArgumentException("Unsupported distance function: " + dist); + } + + private static double[] getWeights(String dist) { + String[] weights = + dist.substring(dist.indexOf('(') + 1, dist.length() - 1).split(","); + return Stream.of(weights).mapToDouble(Double::parseDouble).toArray(); + } + + private static double scalar(double[] c1, double[] c2, double[] w) { + return sqr((c1[0] - c2[0]) * w[0]) + sqr((c1[1] - c2[1]) * w[1]) + sqr((c1[2] - c2[2]) * w[2]); + } + + private static double scalar(double[] c1, double[] c2) { + return sqr(c1[0] - c2[0]) + sqr(c1[1] - c2[1]) + sqr(c1[2] - c2[2]); + } + + private static double cie94(double[] lab1, double[] lab2) { + double dl = lab1[L] - lab2[L]; + double da = lab1[A] - lab2[A]; + double db = lab1[B] - lab2[B]; + double c1 = Math.sqrt(lab1[A] * lab1[A] + lab1[B] * lab1[B]); + double c2 = Math.sqrt(lab2[A] * lab2[A] + lab2[B] * lab2[B]); + double dc = c1 - c2; + double dh = da * da + db * db - dc * dc; + dh = dh < 0.0 ? 0.0 : Math.sqrt(dh); + double sl = 1.0; + double sc = 1.0 + k1 * c1; + double sh = 1.0 + k2 * c1; + double dLKlsl = dl / (kl * sl); + double dCkcsc = dc / (kc * sc); + double dHkhsh = dh / (kh * sh); + return dLKlsl * dLKlsl + dCkcsc * dCkcsc + dHkhsh * dHkhsh; + } + + private static double cie00(double[] lab1, double[] lab2) { + double c_star_1_ab = Math.sqrt(lab1[A] * lab1[A] + lab1[B] * lab1[B]); + double c_star_2_ab = Math.sqrt(lab2[A] * lab2[A] + lab2[B] * lab2[B]); + double c_star_average_ab = (c_star_1_ab + c_star_2_ab) / 2.0; + double c_star_average_ab_pot_3 = c_star_average_ab * c_star_average_ab * c_star_average_ab; + double c_star_average_ab_pot_7 = c_star_average_ab_pot_3 * c_star_average_ab_pot_3 * c_star_average_ab; + double G = 0.5 * (1.0 - Math.sqrt(c_star_average_ab_pot_7 / (c_star_average_ab_pot_7 + 6103515625.0))); // 25^7 + double a1_prime = (1.0 + G) * lab1[A]; + double a2_prime = (1.0 + G) * lab2[A]; + double C_prime_1 = Math.sqrt(a1_prime * a1_prime + lab1[B] * lab1[B]); + double C_prime_2 = Math.sqrt(a2_prime * a2_prime + lab2[B] * lab2[B]); + double h_prime_1 = (Math.toDegrees(Math.atan2(lab1[B], a1_prime)) + 360.0) % 360.0; + double h_prime_2 = (Math.toDegrees(Math.atan2(lab2[B], a2_prime)) + 360.0) % 360.0; + double delta_L_prime = lab2[L] - lab1[L]; + double delta_C_prime = C_prime_2 - C_prime_1; + double h_bar = Math.abs(h_prime_1 - h_prime_2); + double delta_h_prime; + if (C_prime_1 * C_prime_2 == 0.0) { + delta_h_prime = 0.0; + } else if (h_bar <= 180.0) { + delta_h_prime = h_prime_2 - h_prime_1; + } else if (h_prime_2 <= h_prime_1) { + delta_h_prime = h_prime_2 - h_prime_1 + 360.0; + } else { + delta_h_prime = h_prime_2 - h_prime_1 - 360.0; + } + double delta_H_prime = 2.0 * Math.sqrt(C_prime_1 * C_prime_2) * Math.sin(Math.toRadians(delta_h_prime / 2.0)); + double L_prime_average = (lab1[L] + lab2[L]) / 2.0; + double C_prime_average = (C_prime_1 + C_prime_2) / 2.0; + double h_prime_average; + if (C_prime_1 * C_prime_2 == 0.0) { + h_prime_average = 0.0; + } else if (h_bar <= 180.0) { + h_prime_average = (h_prime_1 + h_prime_2) / 2.0; + } else if ((h_prime_1 + h_prime_2) < 360.0) { + h_prime_average = (h_prime_1 + h_prime_2 + 360.0) / 2.0; + } else { + h_prime_average = (h_prime_1 + h_prime_2 - 360.0) / 2.0; + } + double L_prime_average_minus_50 = L_prime_average - 50.0; + double L_prime_average_minus_50_square = L_prime_average_minus_50 * L_prime_average_minus_50; + double T = 1.0 + - 0.17 * Math.cos(Math.toRadians(h_prime_average - 30.0)) + + 0.24 * Math.cos(Math.toRadians(h_prime_average * 2.0)) + + 0.32 * Math.cos(Math.toRadians(h_prime_average * 3.0 + 6.0)) + - 0.20 * Math.cos(Math.toRadians(h_prime_average * 4.0 - 63.0)); + double S_L = + 1.0 + ((0.015 * L_prime_average_minus_50_square) / Math.sqrt(20.0 + L_prime_average_minus_50_square)); + double S_C = 1.0 + 0.045 * C_prime_average; + double S_H = 1.0 + 0.015 * T * C_prime_average; + double h_prime_average_minus_275_div_25 = (h_prime_average - 275.0) / (25.0); + double h_prime_average_minus_275_div_25_square = + h_prime_average_minus_275_div_25 * h_prime_average_minus_275_div_25; + double delta_theta = 30.0 * Math.exp(-h_prime_average_minus_275_div_25_square); + double C_prime_average_pot_3 = C_prime_average * C_prime_average * C_prime_average; + double C_prime_average_pot_7 = C_prime_average_pot_3 * C_prime_average_pot_3 * C_prime_average; + double R_C = 2.0 * Math.sqrt(C_prime_average_pot_7 / (C_prime_average_pot_7 + 6103515625.0)); // 25^7 + double R_T = -Math.sin(Math.toRadians(2.0 * delta_theta)) * R_C; + double dLKlsl = delta_L_prime / (kl * S_L); + double dCkcsc = delta_C_prime / (kc * S_C); + double dHkhsh = delta_H_prime / (kh * S_H); + return dLKlsl * dLKlsl + dCkcsc * dCkcsc + dHkhsh * dHkhsh + R_T * dCkcsc * dHkhsh; + } + + private static double cam02(int p1, int p2, double[] vc) { + double[] c1 = jmh2ucs(camlch(p1, vc)); + double[] c2 = jmh2ucs(camlch(p2, vc)); + return scalar(c1, c2); + } + + private static double[] jmh2ucs(double[] lch) { + double sJ = ((1.0 + 100 * 0.007) * lch[0]) / (1.0 + 0.007 * lch[0]); + double sM = ((1.0 / 0.0228) * Math.log(1.0 + 0.0228 * lch[1])); + double a = sM * Math.cos(Math.toRadians(lch[2])); + double b = sM * Math.sin(Math.toRadians(lch[2])); + return new double[]{sJ, a, b}; + } + + static double camlch(double[] c1, double[] c2) { + return camlch(c1, c2, new double[]{1.0, 1.0, 1.0}); + } + + static double camlch(double[] c1, double[] c2, double[] w) { + // normalize weights to correlate range + double lightnessWeight = w[0] / 100.0; + double colorfulnessWeight = w[1] / 120.0; + double hueWeight = w[2] / 360.0; + // calculate sort-of polar distance + double dl = (c1[0] - c2[0]) * lightnessWeight; + double dc = (c1[1] - c2[1]) * colorfulnessWeight; + double dh = hueDifference(c1[2], c2[2], 360.0) * hueWeight; + return dl * dl + dc * dc + dh * dh; + } + + private static double hueDifference(double hue1, double hue2, double c) { + double difference = (hue2 - hue1) % c; + double ch = c / 2; + if (difference > ch) difference -= c; + if (difference < -ch) difference += c; + return difference; + } + + private static double[] rgb(int color) { + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = (color >> 0) & 0xFF; + return new double[]{r / 255.0, g / 255.0, b / 255.0}; + } + + static double[] rgb2xyz(int color) { + return rgb2xyz(rgb(color)); + } + + static double[] rgb2cielab(int color) { + return rgb2cielab(rgb(color)); + } + + static double[] camlch(int color) { + return camlch(color, sRGB_typical_environment); + } + + static double[] camlch(int color, double[] vc) { + return xyz2camlch(rgb2xyz(color), vc); + } + + static double[] camlab(int color) { + return camlab(color, sRGB_typical_environment); + } + + static double[] camlab(int color, double[] vc) { + return lch2lab(camlch(color, vc)); + } + + static double[] lch2lab(double[] lch) { + double toRad = Math.PI / 180; + return new double[]{lch[0], lch[1] * Math.cos(lch[2] * toRad), lch[1] * Math.sin(lch[2] * toRad)}; + } + + private static double[] xyz2camlch(double[] xyz, double[] vc) { + double[] XYZ = new double[]{xyz[0] * 100.0, xyz[1] * 100.0, xyz[2] * 100.0}; + double[] cam = forwardTransform(XYZ, vc); + return new double[]{cam[J], cam[M], cam[h]}; + } + + /** + * CIECAM02 appearance correlates + */ + private static double[] forwardTransform(double[] XYZ, double[] vc) { + // calculate sharpened cone response + double[] RGB = forwardPreAdaptationConeResponse(XYZ); + // calculate corresponding (sharpened) cone response considering various luminance level and surround conditions + // in D + double[] RGB_c = forwardPostAdaptationConeResponse(RGB, vc); + // calculate HPE equal area cone fundamentals + double[] RGBPrime = CAT02toHPE(RGB_c); + // calculate response-compressed postadaptation cone response + double[] RGBPrime_a = forwardResponseCompression(RGBPrime, vc); + // calculate achromatic response + double A = (2.0 * RGBPrime_a[0] + RGBPrime_a[1] + RGBPrime_a[2] / 20.0 - 0.305) * vc[VC_N_BB]; + // calculate lightness + double J = 100.0 * Math.pow(A / vc[VC_A_W], vc[VC_Z] * vc[VC_C]); + // calculate redness-greenness and yellowness-blueness color opponent values + double a = RGBPrime_a[0] + (-12.0 * RGBPrime_a[1] + RGBPrime_a[2]) / 11.0; + double b = (RGBPrime_a[0] + RGBPrime_a[1] - 2.0 * RGBPrime_a[2]) / 9.0; + // calculate hue angle + double h = (Math.toDegrees(Math.atan2(b, a)) + 360.0) % 360.0; + // calculate eccentricity + double e = ((12500.0 / 13.0) * vc[VC_N_C] * vc[VC_N_CB]) * (Math.cos(Math.toRadians(h) + 2.0) + 3.8); + // get t + double t = e + * Math.sqrt(Math.pow(a, 2.0) + Math.pow(b, 2.0)) + / (RGBPrime_a[0] + RGBPrime_a[1] + 1.05 * RGBPrime_a[2]); + // calculate brightness + double Q = (4.0 / vc[VC_C]) * Math.sqrt(J / 100.0) * (vc[VC_A_W] + 4.0) * Math.pow(vc[VC_F_L], 0.25); + // calculate the correlates of chroma, colorfulness, and saturation + double C = Math.signum(t) + * Math.pow(Math.abs(t), 0.9) + * Math.sqrt(J / 100.0) + * Math.pow(1.64 - Math.pow(0.29, vc[VC_N]), 0.73); + double M = C * Math.pow(vc[VC_F_L], 0.25); + double s = 100.0 * Math.sqrt(M / Q); + // calculate hue composition + double H = calculateH(h); + return new double[]{J, Q, C, M, s, H, h}; + } + + private static double calculateH(double h) { + if (h < 20.14) h = h + 360; + double i; + if (h >= 20.14 && h < 90.0) { // index i = 1 + i = (h - 20.14) / 0.8; + return 100.0 * i / (i + (90 - h) / 0.7); + } else if (h < 164.25) { // index i = 2 + i = (h - 90) / 0.7; + return 100.0 + 100.0 * i / (i + (164.25 - h)); + } else if (h < 237.53) { // index i = 3 + i = (h - 164.25); + return 200.0 + 100.0 * i / (i + (237.53 - h) / 1.2); + } else if (h <= 380.14) { // index i = 4 + i = (h - 237.53) / 1.2; + double H = 300.0 + 100.0 * i / (i + (380.14 - h) / 0.8); + // don't use 400 if we can use 0 + if (H <= 400.0 && H >= 399.999) H = 0; + return H; + } else { + throw new IllegalArgumentException("h outside assumed range 0..360: " + h); + } + } + + private static double[] forwardResponseCompression(double[] RGB, double[] vc) { + double[] result = new double[3]; + for (int channel = 0; channel < RGB.length; channel++) { + if (RGB[channel] >= 0) { + double n = Math.pow(vc[VC_F_L] * RGB[channel] / 100.0, 0.42); + result[channel] = 400.0 * n / (n + 27.13) + 0.1; + } else { + double n = Math.pow(-1.0 * vc[VC_F_L] * RGB[channel] / 100.0, 0.42); + result[channel] = -400.0 * n / (n + 27.13) + 0.1; + } + } + return result; + } + + private static double[] forwardPostAdaptationConeResponse(double[] RGB, double[] vc) { + return new double[]{vc[VC_D_RGB_R] * RGB[0], vc[VC_D_RGB_G] * RGB[1], vc[VC_D_RGB_B] * RGB[2]}; + } + + public static double[] CAT02toHPE(double[] RGB) { + double[] RGBPrime = new double[3]; + RGBPrime[0] = 0.7409792 * RGB[0] + 0.2180250 * RGB[1] + 0.0410058 * RGB[2]; + RGBPrime[1] = 0.2853532 * RGB[0] + 0.6242014 * RGB[1] + 0.0904454 * RGB[2]; + RGBPrime[2] = -0.0096280 * RGB[0] - 0.0056980 * RGB[1] + 1.0153260 * RGB[2]; + return RGBPrime; + } + + private static double[] forwardPreAdaptationConeResponse(double[] XYZ) { + double[] RGB = new double[3]; + RGB[0] = 0.7328 * XYZ[0] + 0.4296 * XYZ[1] - 0.1624 * XYZ[2]; + RGB[1] = -0.7036 * XYZ[0] + 1.6975 * XYZ[1] + 0.0061 * XYZ[2]; + RGB[2] = 0.0030 * XYZ[0] + 0.0136 * XYZ[1] + 0.9834 * XYZ[2]; + return RGB; + } + + static double[] vc(double[] xyz_w, double L_A, double Y_b, double[] surrounding) { + double[] vc = new double[17]; + vc[VC_X_W] = xyz_w[0]; + vc[VC_Y_W] = xyz_w[1]; + vc[VC_Z_W] = xyz_w[2]; + vc[VC_L_A] = L_A; + vc[VC_Y_B] = Y_b; + vc[VC_F] = surrounding[SUR_F]; + vc[VC_C] = surrounding[SUR_C]; + vc[VC_N_C] = surrounding[SUR_N_C]; + + double[] RGB_w = forwardPreAdaptationConeResponse(xyz_w); + double D = + Math.max(0.0, Math.min(1.0, vc[VC_F] * (1.0 - (1.0 / 3.6) * Math.pow(Math.E, (-L_A - 42.0) / 92.0)))); + double Yw = xyz_w[1]; + double[] RGB_c = new double[]{ + (D * Yw / RGB_w[0]) + (1.0 - D), (D * Yw / RGB_w[1]) + (1.0 - D), (D * Yw / RGB_w[2]) + (1.0 - D), + }; + + // calculate increase in brightness and colorfulness caused by brighter viewing environments + double L_Ax5 = 5.0 * L_A; + double k = 1.0 / (L_Ax5 + 1.0); + double kpow4 = Math.pow(k, 4.0); + vc[VC_F_L] = 0.2 * kpow4 * (L_Ax5) + 0.1 * Math.pow(1.0 - kpow4, 2.0) * Math.pow(L_Ax5, 1.0 / 3.0); + + // calculate response compression on J and C caused by background lightness. + vc[VC_N] = Y_b / Yw; + vc[VC_Z] = 1.48 + Math.sqrt(vc[VC_N]); + + vc[VC_N_BB] = 0.725 * Math.pow(1.0 / vc[VC_N], 0.2); + vc[VC_N_CB] = vc[ + VC_N_BB]; // chromatic contrast factors (calculate increase in J, Q, and C caused by dark backgrounds) + + // calculate achromatic response to white + double[] RGB_wc = new double[]{RGB_c[0] * RGB_w[0], RGB_c[1] * RGB_w[1], RGB_c[2] * RGB_w[2]}; + double[] RGBPrime_w = CAT02toHPE(RGB_wc); + double[] RGBPrime_aw = new double[3]; + for (int channel = 0; channel < RGBPrime_w.length; channel++) { + if (RGBPrime_w[channel] >= 0) { + double n = Math.pow(vc[VC_F_L] * RGBPrime_w[channel] / 100.0, 0.42); + RGBPrime_aw[channel] = 400.0 * n / (n + 27.13) + 0.1; + } else { + double n = Math.pow(-1.0 * vc[VC_F_L] * RGBPrime_w[channel] / 100.0, 0.42); + RGBPrime_aw[channel] = -400.0 * n / (n + 27.13) + 0.1; + } + } + vc[VC_A_W] = (2.0 * RGBPrime_aw[0] + RGBPrime_aw[1] + RGBPrime_aw[2] / 20.0 - 0.305) * vc[VC_N_BB]; + vc[VC_D_RGB_R] = RGB_c[0]; + vc[VC_D_RGB_G] = RGB_c[1]; + vc[VC_D_RGB_B] = RGB_c[2]; + return vc; + } + + public static double[] rgb2cielab(double[] rgb) { + return xyz2lab(rgb2xyz(rgb)); + } + + private static double[] rgb2xyz(double[] rgb) { + double vr = pivotRgb(rgb[0]); + double vg = pivotRgb(rgb[1]); + double vb = pivotRgb(rgb[2]); + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + double x = vr * 0.4124564 + vg * 0.3575761 + vb * 0.1804375; + double y = vr * 0.2126729 + vg * 0.7151522 + vb * 0.0721750; + double z = vr * 0.0193339 + vg * 0.1191920 + vb * 0.9503041; + return new double[]{x, y, z}; + } + + private static double pivotRgb(double n) { + return n > 0.04045 ? Math.pow((n + 0.055) / 1.055, 2.4) : n / 12.92; + } + + private static double[] xyz2lab(double[] xyz) { + double fx = pivotXyz(xyz[0]); + double fy = pivotXyz(xyz[1]); + double fz = pivotXyz(xyz[2]); + double l = 116.0 * fy - 16.0; + double a = 500.0 * (fx - fy); + double b = 200.0 * (fy - fz); + return new double[]{l, a, b}; + } + + private static double pivotXyz(double n) { + return n > epsilon ? Math.cbrt(n) : (kappa * n + 16) / 116; + } + + private static double sqr(double n) { + return n * n; + } + + @FunctionalInterface + interface Distance { + double compute(int c1, int c2); + } + + private static class NamedDistance implements Distance { + private final String name; + private final Distance delegate; + + public NamedDistance(String name, Distance delegate) { + this.name = name; + this.delegate = delegate; + } + + @Override + public double compute(int c1, int c2) { + return delegate.compute(c1, c2); + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/Curses.java b/net-cli/src/main/java/org/jline/utils/Curses.java new file mode 100644 index 0000000..3ab2290 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/Curses.java @@ -0,0 +1,485 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.Flushable; +import java.io.IOError; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayDeque; + +/** + * Curses helper methods. + * + * @author Guillaume Nodet + */ +public final class Curses { + + private static final Object[] sv = new Object[26]; + private static final Object[] dv = new Object[26]; + + private static final int IFTE_NONE = 0; + private static final int IFTE_IF = 1; + private static final int IFTE_THEN = 2; + private static final int IFTE_ELSE = 3; + + private Curses() { + } + + /** + * Print the given terminal capabilities + * + * @param cap the capability to output + * @param params optional parameters + * @return the result string + */ + public static String tputs(String cap, Object... params) { + if (cap != null) { + StringWriter sw = new StringWriter(); + tputs(sw, cap, params); + return sw.toString(); + } + return null; + } + + /** + * Print the given terminal capabilities + * + * @param out the output stream + * @param str the capability to output + * @param params optional parameters + */ + public static void tputs(Appendable out, String str, Object... params) { + try { + doTputs(out, str, params); + } catch (Exception e) { + throw new IOError(e); + } + } + + private static void doTputs(Appendable out, String str, Object... params) throws IOException { + int index = 0; + int length = str.length(); + int ifte = IFTE_NONE; + boolean exec = true; + ArrayDeque stack = new ArrayDeque<>(); + while (index < length) { + char ch = str.charAt(index++); + switch (ch) { + case '\\': + ch = str.charAt(index++); + if (ch >= '0' && ch <= '7') { + int val = ch - '0'; + for (int i = 0; i < 2; i++) { + ch = str.charAt(index++); + if (ch < '0' || ch > '7') { + throw new IllegalStateException(); + } + val = val * 8 + (ch - '0'); + } + out.append((char) val); + } else { + switch (ch) { + case 'e': + case 'E': + if (exec) { + out.append((char) 27); // escape + } + break; + case 'n': + out.append('\n'); + break; + // case 'l': + // rawPrint('\l'); + // break; + case 'r': + if (exec) { + out.append('\r'); + } + break; + case 't': + if (exec) { + out.append('\t'); + } + break; + case 'b': + if (exec) { + out.append('\b'); + } + break; + case 'f': + if (exec) { + out.append('\f'); + } + break; + case 's': + if (exec) { + out.append(' '); + } + break; + case ':': + case '^': + case '\\': + if (exec) { + out.append(ch); + } + break; + default: + throw new IllegalArgumentException(); + } + } + break; + case '^': + ch = str.charAt(index++); + if (exec) { + out.append((char) (ch - '@')); + } + break; + case '%': + ch = str.charAt(index++); + switch (ch) { + case '%': + if (exec) { + out.append('%'); + } + break; + case 'p': + ch = str.charAt(index++); + if (exec) { + stack.push(params[ch - '1']); + } + break; + case 'P': + ch = str.charAt(index++); + if (ch >= 'a' && ch <= 'z') { + if (exec) { + dv[ch - 'a'] = stack.pop(); + } + } else if (ch >= 'A' && ch <= 'Z') { + if (exec) { + sv[ch - 'A'] = stack.pop(); + } + } else { + throw new IllegalArgumentException(); + } + break; + case 'g': + ch = str.charAt(index++); + if (ch >= 'a' && ch <= 'z') { + if (exec) { + stack.push(dv[ch - 'a']); + } + } else if (ch >= 'A' && ch <= 'Z') { + if (exec) { + stack.push(sv[ch - 'A']); + } + } else { + throw new IllegalArgumentException(); + } + break; + case '\'': + ch = str.charAt(index++); + if (exec) { + stack.push((int) ch); + } + ch = str.charAt(index++); + if (ch != '\'') { + throw new IllegalArgumentException(); + } + break; + case '{': + int start = index; + while (str.charAt(index++) != '}') + ; + if (exec) { + int v = Integer.parseInt(str.substring(start, index - 1)); + stack.push(v); + } + break; + case 'l': + if (exec) { + stack.push(stack.pop().toString().length()); + } + break; + case '+': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 + v2); + } + break; + case '-': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 - v2); + } + break; + case '*': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 * v2); + } + break; + case '/': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 / v2); + } + break; + case 'm': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 % v2); + } + break; + case '&': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 & v2); + } + break; + case '|': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 | v2); + } + break; + case '^': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 ^ v2); + } + break; + case '=': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 == v2); + } + break; + case '>': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 > v2); + } + break; + case '<': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 < v2); + } + break; + case 'A': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 != 0 && v2 != 0); + } + break; + case '!': + if (exec) { + int v1 = toInteger(stack.pop()); + stack.push(v1 == 0); + } + break; + case '~': + if (exec) { + int v1 = toInteger(stack.pop()); + stack.push(~v1); + } + break; + case 'O': + if (exec) { + int v2 = toInteger(stack.pop()); + int v1 = toInteger(stack.pop()); + stack.push(v1 != 0 || v2 != 0); + } + break; + case '?': + if (ifte != IFTE_NONE) { + throw new IllegalArgumentException(); + } else { + ifte = IFTE_IF; + } + break; + case 't': + if (ifte != IFTE_IF && ifte != IFTE_ELSE) { + throw new IllegalArgumentException(); + } else { + ifte = IFTE_THEN; + } + exec = toInteger(stack.pop()) != 0; + break; + case 'e': + if (ifte != IFTE_THEN) { + throw new IllegalArgumentException(); + } else { + ifte = IFTE_ELSE; + } + exec = !exec; + break; + case ';': + if (ifte == IFTE_NONE || ifte == IFTE_IF) { + throw new IllegalArgumentException(); + } else { + ifte = IFTE_NONE; + } + exec = true; + break; + case 'i': + if (params.length >= 1) { + params[0] = toInteger(params[0]) + 1; + } + if (params.length >= 2) { + params[1] = toInteger(params[1]) + 1; + } + break; + case 'd': + out.append(Integer.toString(toInteger(stack.pop()))); + break; + default: + if (ch == ':') { + ch = str.charAt(index++); + } + boolean alternate = false; + boolean left = false; + boolean space = false; + boolean plus = false; + int width = 0; + int prec = -1; + int cnv; + while ("-+# ".indexOf(ch) >= 0) { + switch (ch) { + case '-': + left = true; + break; + case '+': + plus = true; + break; + case '#': + alternate = true; + break; + case ' ': + space = true; + break; + } + ch = str.charAt(index++); + } + if ("123456789".indexOf(ch) >= 0) { + do { + width = width * 10 + (ch - '0'); + ch = str.charAt(index++); + } while ("0123456789".indexOf(ch) >= 0); + } + if (ch == '.') { + prec = 0; + ch = str.charAt(index++); + } + if ("0123456789".indexOf(ch) >= 0) { + do { + prec = prec * 10 + (ch - '0'); + ch = str.charAt(index++); + } while ("0123456789".indexOf(ch) >= 0); + } + if ("cdoxXs".indexOf(ch) < 0) { + throw new IllegalArgumentException(); + } + cnv = ch; + if (exec) { + String res; + if (cnv == 's') { + res = (String) stack.pop(); + if (prec >= 0) { + res = res.substring(0, prec); + } + } else { + int p = toInteger(stack.pop()); + StringBuilder fmt = new StringBuilder(16); + fmt.append('%'); + if (alternate) { + fmt.append('#'); + } + if (plus) { + fmt.append('+'); + } + if (space) { + fmt.append(' '); + } + if (prec >= 0) { + fmt.append('0'); + fmt.append(prec); + } + fmt.append((char) cnv); + res = String.format(fmt.toString(), p); + } + if (width > res.length()) { + res = String.format("%" + (left ? "-" : "") + width + "s", res); + } + out.append(res); + } + break; + } + break; + case '$': + if (index < length && str.charAt(index) == '<') { + // We don't honour delays, just skip + int nb = 0; + while ((ch = str.charAt(++index)) != '>') { + if (ch >= '0' && ch <= '9') { + nb = nb * 10 + (ch - '0'); + } else if (ch == '*') { + // ignore + } else if (ch == '/') { + // ignore + } else { + // illegal, but ... + } + } + index++; + try { + if (out instanceof Flushable) { + ((Flushable) out).flush(); + } + Thread.sleep(nb); + } catch (InterruptedException e) { + } + } else { + if (exec) { + out.append(ch); + } + } + break; + default: + if (exec) { + out.append(ch); + } + break; + } + } + } + + private static int toInteger(Object pop) { + if (pop instanceof Number) { + return ((Number) pop).intValue(); + } else if (pop instanceof Boolean) { + return (Boolean) pop ? 1 : 0; + } else { + return Integer.parseInt(pop.toString()); + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/DiffHelper.java b/net-cli/src/main/java/org/jline/utils/DiffHelper.java new file mode 100644 index 0000000..a510d55 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/DiffHelper.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.util.LinkedList; +import java.util.List; + +/** + * Class containing the diff method. + * This diff is ANSI aware and will correctly handle text attributes + * so that any text in a Diff object is a valid ansi string. + */ +public class DiffHelper { + + /** + * Compute a list of difference between two lines. + * The result will contain at most 4 Diff objects, as the method + * aims to return the common prefix, inserted text, deleted text and + * common suffix. + * The computation is done on characters and their attributes expressed + * as ansi sequences. + * + * @param text1 the old line + * @param text2 the new line + * @return a list of Diff + */ + public static List diff(AttributedString text1, AttributedString text2) { + int l1 = text1.length(); + int l2 = text2.length(); + int n = Math.min(l1, l2); + int commonStart = 0; + // Given a run of contiguous "hidden" characters (which are + // sequences of uninterrupted escape sequences) we always want to + // print either the entire run or none of it - never a part of it. + int startHiddenRange = -1; + while (commonStart < n + && text1.charAt(commonStart) == text2.charAt(commonStart) + && text1.styleAt(commonStart).equals(text2.styleAt(commonStart))) { + if (text1.isHidden(commonStart)) { + if (startHiddenRange < 0) startHiddenRange = commonStart; + } else startHiddenRange = -1; + commonStart++; + } + if (startHiddenRange >= 0 + && ((l1 > commonStart && text1.isHidden(commonStart)) + || (l2 > commonStart && text2.isHidden(commonStart)))) commonStart = startHiddenRange; + + startHiddenRange = -1; + int commonEnd = 0; + while (commonEnd < n - commonStart + && text1.charAt(l1 - commonEnd - 1) == text2.charAt(l2 - commonEnd - 1) + && text1.styleAt(l1 - commonEnd - 1).equals(text2.styleAt(l2 - commonEnd - 1))) { + if (text1.isHidden(l1 - commonEnd - 1)) { + if (startHiddenRange < 0) startHiddenRange = commonEnd; + } else startHiddenRange = -1; + commonEnd++; + } + if (startHiddenRange >= 0) commonEnd = startHiddenRange; + LinkedList diffs = new LinkedList<>(); + if (commonStart > 0) { + diffs.add(new Diff(Operation.EQUAL, text1.subSequence(0, commonStart))); + } + if (l2 > commonStart + commonEnd) { + diffs.add(new Diff(Operation.INSERT, text2.subSequence(commonStart, l2 - commonEnd))); + } + if (l1 > commonStart + commonEnd) { + diffs.add(new Diff(Operation.DELETE, text1.subSequence(commonStart, l1 - commonEnd))); + } + if (commonEnd > 0) { + diffs.add(new Diff(Operation.EQUAL, text1.subSequence(l1 - commonEnd, l1))); + } + return diffs; + } + + /** + * The data structure representing a diff is a Linked list of Diff objects: + * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), + * Diff(Operation.EQUAL, " world.")} + * which means: delete "Hello", add "Goodbye" and keep " world." + */ + public enum Operation { + DELETE, + INSERT, + EQUAL + } + + /** + * Class representing one diff operation. + */ + public static class Diff { + /** + * One of: INSERT, DELETE or EQUAL. + */ + public final Operation operation; + /** + * The text associated with this diff operation. + */ + public final AttributedString text; + + /** + * Constructor. Initializes the diff with the provided values. + * + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + public Diff(Operation operation, AttributedString text) { + // Construct a diff with the specified operation and text. + this.operation = operation; + this.text = text; + } + + /** + * Display a human-readable version of this Diff. + * + * @return text version. + */ + public String toString() { + return "Diff(" + this.operation + ",\"" + this.text + "\")"; + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/Display.java b/net-cli/src/main/java/org/jline/utils/Display.java new file mode 100644 index 0000000..70d7543 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/Display.java @@ -0,0 +1,490 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import org.jline.terminal.Terminal; +import org.jline.utils.InfoCmp.Capability; + +/** + * Handle display and visual cursor. + * + * @author Guillaume Nodet + */ +public class Display { + + protected final Terminal terminal; + protected final boolean fullScreen; + protected final Map cost = new HashMap<>(); + protected final boolean canScroll; + protected final boolean wrapAtEol; + protected final boolean delayedWrapAtEol; + protected final boolean cursorDownIsNewLine; + protected List oldLines = Collections.emptyList(); + protected int cursorPos; + protected int columns; + protected int columns1; // columns+1 + protected int rows; + protected boolean reset; + protected boolean delayLineWrap; + + @SuppressWarnings("this-escape") + public Display(Terminal terminal, boolean fullscreen) { + this.terminal = terminal; + this.fullScreen = fullscreen; + + this.canScroll = can(Capability.insert_line, Capability.parm_insert_line) + && can(Capability.delete_line, Capability.parm_delete_line); + this.wrapAtEol = terminal.getBooleanCapability(Capability.auto_right_margin); + this.delayedWrapAtEol = this.wrapAtEol && terminal.getBooleanCapability(Capability.eat_newline_glitch); + this.cursorDownIsNewLine = "\n".equals(Curses.tputs(terminal.getStringCapability(Capability.cursor_down))); + } + + private static int[] longestCommon(List l1, List l2) { + int start1 = 0; + int start2 = 0; + int max = 0; + for (int i = 0; i < l1.size(); i++) { + for (int j = 0; j < l2.size(); j++) { + int x = 0; + while (Objects.equals(l1.get(i + x), l2.get(j + x))) { + x++; + if (((i + x) >= l1.size()) || ((j + x) >= l2.size())) break; + } + if (x > max) { + max = x; + start1 = i; + start2 = j; + } + } + } + return max != 0 ? new int[]{start1, start2, max} : null; + } + + /** + * If cursor is at right margin, don't wrap immediately. + * See org.jline.reader.LineReader.Option#DELAY_LINE_WRAP. + * + * @return true if line wrap is delayed, false otherwise + */ + public boolean delayLineWrap() { + return delayLineWrap; + } + + public void setDelayLineWrap(boolean v) { + delayLineWrap = v; + } + + public void resize(int rows, int columns) { + if (rows == 0 || columns == 0) { + columns = Integer.MAX_VALUE - 1; + rows = 1; + } + if (this.rows != rows || this.columns != columns) { + this.rows = rows; + this.columns = columns; + this.columns1 = columns + 1; + oldLines = AttributedString.join(AttributedString.EMPTY, oldLines) + .columnSplitLength(columns, true, delayLineWrap()); + } + } + + public void reset() { + oldLines = Collections.emptyList(); + } + + /** + * Clears the whole screen. + * Use this method only when using full-screen / application mode. + */ + public void clear() { + if (fullScreen) { + reset = true; + } + } + + public void updateAnsi(List newLines, int targetCursorPos) { + update(newLines.stream().map(AttributedString::fromAnsi).collect(Collectors.toList()), targetCursorPos); + } + + /** + * Update the display according to the new lines and flushes the output. + * + * @param newLines the lines to display + * @param targetCursorPos desired cursor position - see Size.cursorPos. + */ + public void update(List newLines, int targetCursorPos) { + update(newLines, targetCursorPos, true); + } + + /** + * Update the display according to the new lines. + * + * @param newLines the lines to display + * @param targetCursorPos desired cursor position - see Size.cursorPos. + * @param flush whether the output should be flushed or not + */ + public void update(List newLines, int targetCursorPos, boolean flush) { + if (reset) { + terminal.puts(Capability.clear_screen); + oldLines.clear(); + cursorPos = 0; + reset = false; + } + + // If dumb display, get rid of ansi sequences now + Integer cols = terminal.getNumericCapability(Capability.max_colors); + if (cols == null || cols < 8) { + newLines = newLines.stream() + .map(s -> new AttributedString(s.toString())) + .collect(Collectors.toList()); + } + + // Detect scrolling + if ((fullScreen || newLines.size() >= rows) && newLines.size() == oldLines.size() && canScroll) { + int nbHeaders = 0; + int nbFooters = 0; + // Find common headers and footers + int l = newLines.size(); + while (nbHeaders < l && Objects.equals(newLines.get(nbHeaders), oldLines.get(nbHeaders))) { + nbHeaders++; + } + while (nbFooters < l - nbHeaders - 1 + && Objects.equals( + newLines.get(newLines.size() - nbFooters - 1), + oldLines.get(oldLines.size() - nbFooters - 1))) { + nbFooters++; + } + List o1 = newLines.subList(nbHeaders, newLines.size() - nbFooters); + List o2 = oldLines.subList(nbHeaders, oldLines.size() - nbFooters); + int[] common = longestCommon(o1, o2); + if (common != null) { + int s1 = common[0]; + int s2 = common[1]; + int sl = common[2]; + if (sl > 1 && s1 < s2) { + moveVisualCursorTo((nbHeaders + s1) * columns1); + int nb = s2 - s1; + deleteLines(nb); + for (int i = 0; i < nb; i++) { + oldLines.remove(nbHeaders + s1); + } + if (nbFooters > 0) { + moveVisualCursorTo((nbHeaders + s1 + sl) * columns1); + insertLines(nb); + for (int i = 0; i < nb; i++) { + oldLines.add(nbHeaders + s1 + sl, new AttributedString("")); + } + } + } else if (sl > 1 && s1 > s2) { + int nb = s1 - s2; + if (nbFooters > 0) { + moveVisualCursorTo((nbHeaders + s2 + sl) * columns1); + deleteLines(nb); + for (int i = 0; i < nb; i++) { + oldLines.remove(nbHeaders + s2 + sl); + } + } + moveVisualCursorTo((nbHeaders + s2) * columns1); + insertLines(nb); + for (int i = 0; i < nb; i++) { + oldLines.add(nbHeaders + s2, new AttributedString("")); + } + } + } + } + + int lineIndex = 0; + int currentPos = 0; + int numLines = Math.min(rows, Math.max(oldLines.size(), newLines.size())); + boolean wrapNeeded = false; + while (lineIndex < numLines) { + AttributedString oldLine = lineIndex < oldLines.size() ? oldLines.get(lineIndex) : AttributedString.NEWLINE; + AttributedString newLine = lineIndex < newLines.size() ? newLines.get(lineIndex) : AttributedString.NEWLINE; + currentPos = lineIndex * columns1; + int curCol = currentPos; + int oldLength = oldLine.length(); + int newLength = newLine.length(); + boolean oldNL = oldLength > 0 && oldLine.charAt(oldLength - 1) == '\n'; + boolean newNL = newLength > 0 && newLine.charAt(newLength - 1) == '\n'; + if (oldNL) { + oldLength--; + oldLine = oldLine.substring(0, oldLength); + } + if (newNL) { + newLength--; + newLine = newLine.substring(0, newLength); + } + if (wrapNeeded && lineIndex == (cursorPos + 1) / columns1 && lineIndex < newLines.size()) { + // move from right margin to next line's left margin + cursorPos++; + if (newLength == 0 || newLine.isHidden(0)) { + // go to next line column zero + rawPrint(' '); + terminal.puts(Capability.key_backspace); + } else { + AttributedString firstChar = newLine.substring(0, 1); + // go to next line column one + rawPrint(firstChar); + cursorPos += firstChar.columnLength(); // normally 1 + newLine = newLine.substring(1, newLength); + newLength--; + if (oldLength > 0) { + oldLine = oldLine.substring(1, oldLength); + oldLength--; + } + currentPos = cursorPos; + } + } + List diffs = DiffHelper.diff(oldLine, newLine); + boolean ident = true; + boolean cleared = false; + for (int i = 0; i < diffs.size(); i++) { + DiffHelper.Diff diff = diffs.get(i); + int width = diff.text.columnLength(); + switch (diff.operation) { + case EQUAL: + if (!ident) { + cursorPos = moveVisualCursorTo(currentPos); + rawPrint(diff.text); + cursorPos += width; + currentPos = cursorPos; + } else { + currentPos += width; + } + break; + case INSERT: + if (i <= diffs.size() - 2 && diffs.get(i + 1).operation == DiffHelper.Operation.EQUAL) { + cursorPos = moveVisualCursorTo(currentPos); + if (insertChars(width)) { + rawPrint(diff.text); + cursorPos += width; + currentPos = cursorPos; + break; + } + } else if (i <= diffs.size() - 2 + && diffs.get(i + 1).operation == DiffHelper.Operation.DELETE + && width == diffs.get(i + 1).text.columnLength()) { + moveVisualCursorTo(currentPos); + rawPrint(diff.text); + cursorPos += width; + currentPos = cursorPos; + i++; // skip delete + break; + } + moveVisualCursorTo(currentPos); + rawPrint(diff.text); + cursorPos += width; + currentPos = cursorPos; + ident = false; + break; + case DELETE: + if (cleared) { + continue; + } + if (currentPos - curCol >= columns) { + continue; + } + if (i <= diffs.size() - 2 && diffs.get(i + 1).operation == DiffHelper.Operation.EQUAL) { + if (currentPos + diffs.get(i + 1).text.columnLength() < columns) { + moveVisualCursorTo(currentPos); + if (deleteChars(width)) { + break; + } + } + } + int oldLen = oldLine.columnLength(); + int newLen = newLine.columnLength(); + int nb = Math.max(oldLen, newLen) - (currentPos - curCol); + moveVisualCursorTo(currentPos); + if (!terminal.puts(Capability.clr_eol)) { + rawPrint(' ', nb); + cursorPos += nb; + } + cleared = true; + ident = false; + break; + } + } + lineIndex++; + boolean newWrap = !newNL && lineIndex < newLines.size(); + if (targetCursorPos + 1 == lineIndex * columns1 && (newWrap || !delayLineWrap)) targetCursorPos++; + boolean atRight = (cursorPos - curCol) % columns1 == columns; + wrapNeeded = false; + if (this.delayedWrapAtEol) { + boolean oldWrap = !oldNL && lineIndex < oldLines.size(); + if (newWrap != oldWrap && !(oldWrap && cleared)) { + moveVisualCursorTo(lineIndex * columns1 - 1, newLines); + if (newWrap) wrapNeeded = true; + else terminal.puts(Capability.clr_eol); + } + } else if (atRight) { + if (this.wrapAtEol) { + if (!fullScreen || (fullScreen && lineIndex < numLines)) { + rawPrint(' '); + terminal.puts(Capability.key_backspace); + cursorPos++; + } + } else { + terminal.puts(Capability.carriage_return); // CR / not newline. + cursorPos = curCol; + } + currentPos = cursorPos; + } + } + if (cursorPos != targetCursorPos) { + moveVisualCursorTo(targetCursorPos < 0 ? currentPos : targetCursorPos, newLines); + } + oldLines = newLines; + + if (flush) { + terminal.flush(); + } + } + + protected boolean deleteLines(int nb) { + return perform(Capability.delete_line, Capability.parm_delete_line, nb); + } + + protected boolean insertLines(int nb) { + return perform(Capability.insert_line, Capability.parm_insert_line, nb); + } + + protected boolean insertChars(int nb) { + return perform(Capability.insert_character, Capability.parm_ich, nb); + } + + protected boolean deleteChars(int nb) { + return perform(Capability.delete_character, Capability.parm_dch, nb); + } + + protected boolean can(Capability single, Capability multi) { + return terminal.getStringCapability(single) != null || terminal.getStringCapability(multi) != null; + } + + protected boolean perform(Capability single, Capability multi, int nb) { + boolean hasMulti = terminal.getStringCapability(multi) != null; + boolean hasSingle = terminal.getStringCapability(single) != null; + if (hasMulti && (!hasSingle || cost(single) * nb > cost(multi))) { + terminal.puts(multi, nb); + return true; + } else if (hasSingle) { + for (int i = 0; i < nb; i++) { + terminal.puts(single); + } + return true; + } else { + return false; + } + } + + private int cost(Capability cap) { + return cost.computeIfAbsent(cap, this::computeCost); + } + + private int computeCost(Capability cap) { + String s = Curses.tputs(terminal.getStringCapability(cap), 0); + return s != null ? s.length() : Integer.MAX_VALUE; + } + + /* + * Move cursor from cursorPos to argument, updating cursorPos + * We're at the right margin if {@code (cursorPos % columns1) == columns}. + * This method knows how to move both *from* and *to* the right margin. + */ + protected void moveVisualCursorTo(int targetPos, List newLines) { + if (cursorPos != targetPos) { + boolean atRight = (targetPos % columns1) == columns; + moveVisualCursorTo(targetPos - (atRight ? 1 : 0)); + if (atRight) { + // There is no portable way to move to the right margin + // except by writing a character in the right-most column. + int row = targetPos / columns1; + AttributedString lastChar = row >= newLines.size() + ? AttributedString.EMPTY + : newLines.get(row).columnSubSequence(columns - 1, columns); + if (lastChar.length() == 0) rawPrint(' '); + else rawPrint(lastChar); + cursorPos++; + } + } + } + + /* + * Move cursor from cursorPos to argument, updating cursorPos + * We're at the right margin if {@code (cursorPos % columns1) == columns}. + * This method knows how to move *from* the right margin, + * but does not know how to move *to* the right margin. + * I.e. {@code (i1 % columns1) == column} is not allowed. + */ + protected int moveVisualCursorTo(int i1) { + int i0 = cursorPos; + if (i0 == i1) return i1; + int width = columns1; + int l0 = i0 / width; + int c0 = i0 % width; + int l1 = i1 / width; + int c1 = i1 % width; + if (c0 == columns) { // at right margin + terminal.puts(Capability.carriage_return); + c0 = 0; + } + if (l0 > l1) { + perform(Capability.cursor_up, Capability.parm_up_cursor, l0 - l1); + } else if (l0 < l1) { + // TODO: clean the following + if (fullScreen) { + if (!terminal.puts(Capability.parm_down_cursor, l1 - l0)) { + for (int i = l0; i < l1; i++) { + terminal.puts(Capability.cursor_down); + } + if (cursorDownIsNewLine) { + c0 = 0; + } + } + } else { + terminal.puts(Capability.carriage_return); + rawPrint('\n', l1 - l0); + c0 = 0; + } + } + if (c0 != 0 && c1 == 0) { + terminal.puts(Capability.carriage_return); + } else if (c0 < c1) { + perform(Capability.cursor_right, Capability.parm_right_cursor, c1 - c0); + } else if (c0 > c1) { + perform(Capability.cursor_left, Capability.parm_left_cursor, c0 - c1); + } + cursorPos = i1; + return i1; + } + + void rawPrint(char c, int num) { + for (int i = 0; i < num; i++) { + rawPrint(c); + } + } + + void rawPrint(int c) { + terminal.writer().write(c); + } + + void rawPrint(AttributedString str) { + str.print(terminal); + } + + public int wcwidth(String str) { + return str != null ? AttributedString.fromAnsi(str).columnLength() : 0; + } +} diff --git a/net-cli/src/main/java/org/jline/utils/ExecHelper.java b/net-cli/src/main/java/org/jline/utils/ExecHelper.java new file mode 100644 index 0000000..391ee02 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/ExecHelper.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.util.Map; +import java.util.Objects; + +/** + * Helper methods for running unix commands. + */ +public final class ExecHelper { + + private ExecHelper() { + } + + public static String exec(boolean redirectInput, final String... cmd) throws IOException { + Objects.requireNonNull(cmd); + try { + Log.trace("Running: ", cmd); + ProcessBuilder pb = new ProcessBuilder(cmd); + if (OSUtils.IS_AIX) { + Map env = pb.environment(); + env.put("PATH", "/opt/freeware/bin:" + env.get("PATH")); + env.put("LANG", "C"); + env.put("LC_ALL", "C"); + } + if (redirectInput) { + pb.redirectInput(ProcessBuilder.Redirect.INHERIT); + } + Process p = pb.start(); + String result = waitAndCapture(p); + Log.trace("Result: ", result); + if (p.exitValue() != 0) { + if (result.endsWith("\n")) { + result = result.substring(0, result.length() - 1); + } + throw new IOException("Error executing '" + String.join(" ", cmd) + "': " + result); + } + return result; + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException("Command interrupted").initCause(e); + } + } + + public static String waitAndCapture(Process p) throws IOException, InterruptedException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + InputStream in = null; + InputStream err = null; + OutputStream out = null; + try { + int c; + in = p.getInputStream(); + while ((c = in.read()) != -1) { + bout.write(c); + } + err = p.getErrorStream(); + while ((c = err.read()) != -1) { + bout.write(c); + } + out = p.getOutputStream(); + p.waitFor(); + } finally { + close(in, out, err); + } + + return bout.toString(); + } + + private static void close(final Closeable... closeables) { + for (Closeable c : closeables) { + if (c != null) { + try { + c.close(); + } catch (Exception e) { + // Ignore + } + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/FastBufferedOutputStream.java b/net-cli/src/main/java/org/jline/utils/FastBufferedOutputStream.java new file mode 100644 index 0000000..ca9ea26 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/FastBufferedOutputStream.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2009-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A simple buffering output stream with no synchronization. + */ +public class FastBufferedOutputStream extends FilterOutputStream { + + protected final byte[] buf = new byte[8192]; + protected int count; + + public FastBufferedOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + if (count >= buf.length) { + flushBuffer(); + } + buf[count++] = (byte) b; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len >= buf.length) { + flushBuffer(); + out.write(b, off, len); + return; + } + if (len > buf.length - count) { + flushBuffer(); + } + System.arraycopy(b, off, buf, count, len); + count += len; + } + + private void flushBuffer() throws IOException { + if (count > 0) { + out.write(buf, 0, count); + count = 0; + } + } + + @Override + public void flush() throws IOException { + flushBuffer(); + out.flush(); + } +} diff --git a/net-cli/src/main/java/org/jline/utils/InfoCmp.java b/net-cli/src/main/java/org/jline/utils/InfoCmp.java new file mode 100644 index 0000000..fa93fc4 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/InfoCmp.java @@ -0,0 +1,640 @@ +/* + * Copyright (c) 2002-2019, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.BufferedReader; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Infocmp helper methods. + * + * @author Guillaume Nodet + */ +public final class InfoCmp { + + private static final Map CAPS = new HashMap<>(); + + static { + for (String s : Arrays.asList( + "dumb", + "dumb-color", + "ansi", + "xterm", + "xterm-256color", + "windows", + "windows-256color", + "windows-conemu", + "windows-vtp", + "screen", + "screen-256color", + "rxvt-unicode", + "rxvt-unicode-256color", + "rxvt-basic", + "rxvt")) { + setDefaultInfoCmp(s, () -> loadDefaultInfoCmp(s)); + } + } + + private InfoCmp() { + } + + public static Map getCapabilitiesByName() { + Map capabilities = new LinkedHashMap<>(); + try (InputStream is = InfoCmp.class.getResourceAsStream("capabilities.txt"); + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + br.lines() + .map(String::trim) + .filter(s -> !s.startsWith("#")) + .filter(s -> !s.isEmpty()) + .forEach(s -> { + String[] names = s.split(", "); + Capability cap = Enum.valueOf(Capability.class, names[0]); + capabilities.put(names[0], cap); + capabilities.put(names[1], cap); + }); + return capabilities; + } catch (IOException e) { + throw new IOError(e); + } + } + + public static void setDefaultInfoCmp(String terminal, String caps) { + CAPS.putIfAbsent(terminal, caps); + } + + public static void setDefaultInfoCmp(String terminal, Supplier caps) { + CAPS.putIfAbsent(terminal, caps); + } + + public static String getInfoCmp(String terminal) throws IOException, InterruptedException { + String caps = getLoadedInfoCmp(terminal); + if (caps == null) { + Process p = new ProcessBuilder(OSUtils.INFOCMP_COMMAND, terminal).start(); + caps = ExecHelper.waitAndCapture(p); + CAPS.put(terminal, caps); + } + return caps; + } + + public static String getLoadedInfoCmp(String terminal) { + Object caps = CAPS.get(terminal); + if (caps instanceof Supplier) { + caps = ((Supplier) caps).get(); + } + return (String) caps; + } + + public static void parseInfoCmp( + String capabilities, + Set bools, + Map ints, + Map strings) { + Map capsByName = getCapabilitiesByName(); + String[] lines = capabilities.split("\n"); + for (int i = 1; i < lines.length; i++) { + Matcher m = Pattern.compile("\\s*(([^,]|\\\\,)+)\\s*[,$]").matcher(lines[i]); + while (m.find()) { + String cap = m.group(1); + if (cap.contains("#")) { + int index = cap.indexOf('#'); + String key = cap.substring(0, index); + String val = cap.substring(index + 1); + int iVal; + if ("0".equals(val)) { + iVal = 0; + } else if (val.startsWith("0x")) { + iVal = Integer.parseInt(val.substring(2), 16); + } else if (val.startsWith("0")) { + iVal = Integer.parseInt(val.substring(1), 8); + } else { + iVal = Integer.parseInt(val); + } + Capability c = capsByName.get(key); + if (c != null) { + ints.put(c, iVal); + } + } else if (cap.contains("=")) { + int index = cap.indexOf('='); + String key = cap.substring(0, index); + String val = cap.substring(index + 1); + Capability c = capsByName.get(key); + if (c != null) { + strings.put(c, val); + } + } else { + Capability c = capsByName.get(cap); + if (c != null) { + bools.add(c); + } + } + } + } + } + + static String loadDefaultInfoCmp(String name) { + try (InputStream is = InfoCmp.class.getResourceAsStream(name + ".caps"); + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + return br.lines().collect(Collectors.joining("\n", "", "\n")); + } catch (IOException e) { + throw new IOError(e); + } + } + + @SuppressWarnings("unused") + public enum Capability { + auto_left_margin, // auto_left_margin, bw, bw + auto_right_margin, // auto_right_margin, am, am + back_color_erase, // back_color_erase, bce, ut + can_change, // can_change, ccc, cc + ceol_standout_glitch, // ceol_standout_glitch, xhp, xs + col_addr_glitch, // col_addr_glitch, xhpa, YA + cpi_changes_res, // cpi_changes_res, cpix, YF + cr_cancels_micro_mode, // cr_cancels_micro_mode, crxm, YB + dest_tabs_magic_smso, // dest_tabs_magic_smso, xt, xt + eat_newline_glitch, // eat_newline_glitch, xenl, xn + erase_overstrike, // erase_overstrike, eo, eo + generic_type, // generic_type, gn, gn + hard_copy, // hard_copy, hc, hc + hard_cursor, // hard_cursor, chts, HC + has_meta_key, // has_meta_key, km, km + has_print_wheel, // has_print_wheel, daisy, YC + has_status_line, // has_status_line, hs, hs + hue_lightness_saturation, // hue_lightness_saturation, hls, hl + insert_null_glitch, // insert_null_glitch, in, in + lpi_changes_res, // lpi_changes_res, lpix, YG + memory_above, // memory_above, da, da + memory_below, // memory_below, db, db + move_insert_mode, // move_insert_mode, mir, mi + move_standout_mode, // move_standout_mode, msgr, ms + needs_xon_xoff, // needs_xon_xoff, nxon, nx + no_esc_ctlc, // no_esc_ctlc, xsb, xb + no_pad_char, // no_pad_char, npc, NP + non_dest_scroll_region, // non_dest_scroll_region, ndscr, ND + non_rev_rmcup, // non_rev_rmcup, nrrmc, NR + over_strike, // over_strike, os, os + prtr_silent, // prtr_silent, mc5i, 5i + row_addr_glitch, // row_addr_glitch, xvpa, YD + semi_auto_right_margin, // semi_auto_right_margin, sam, YE + status_line_esc_ok, // status_line_esc_ok, eslok, es + tilde_glitch, // tilde_glitch, hz, hz + transparent_underline, // transparent_underline, ul, ul + xon_xoff, // xon_xoff, xon, xo + columns, // columns, cols, co + init_tabs, // init_tabs, it, it + label_height, // label_height, lh, lh + label_width, // label_width, lw, lw + lines, // lines, lines, li + lines_of_memory, // lines_of_memory, lm, lm + magic_cookie_glitch, // magic_cookie_glitch, xmc, sg + max_attributes, // max_attributes, ma, ma + max_colors, // max_colors, colors, Co + max_pairs, // max_pairs, pairs, pa + maximum_windows, // maximum_windows, wnum, MW + no_color_video, // no_color_video, ncv, NC + num_labels, // num_labels, nlab, Nl + padding_baud_rate, // padding_baud_rate, pb, pb + virtual_terminal, // virtual_terminal, vt, vt + width_status_line, // width_status_line, wsl, ws + bit_image_entwining, // bit_image_entwining, bitwin, Yo + bit_image_type, // bit_image_type, bitype, Yp + buffer_capacity, // buffer_capacity, bufsz, Ya + buttons, // buttons, btns, BT + dot_horz_spacing, // dot_horz_spacing, spinh, Yc + dot_vert_spacing, // dot_vert_spacing, spinv, Yb + max_micro_address, // max_micro_address, maddr, Yd + max_micro_jump, // max_micro_jump, mjump, Ye + micro_col_size, // micro_col_size, mcs, Yf + micro_line_size, // micro_line_size, mls, Yg + number_of_pins, // number_of_pins, npins, Yh + output_res_char, // output_res_char, orc, Yi + output_res_horz_inch, // output_res_horz_inch, orhi, Yk + output_res_line, // output_res_line, orl, Yj + output_res_vert_inch, // output_res_vert_inch, orvi, Yl + print_rate, // print_rate, cps, Ym + wide_char_size, // wide_char_size, widcs, Yn + acs_chars, // acs_chars, acsc, ac + back_tab, // back_tab, cbt, bt + bell, // bell, bel, bl + carriage_return, // carriage_return, cr, cr + change_char_pitch, // change_char_pitch, cpi, ZA + change_line_pitch, // change_line_pitch, lpi, ZB + change_res_horz, // change_res_horz, chr, ZC + change_res_vert, // change_res_vert, cvr, ZD + change_scroll_region, // change_scroll_region, csr, cs + char_padding, // char_padding, rmp, rP + clear_all_tabs, // clear_all_tabs, tbc, ct + clear_margins, // clear_margins, mgc, MC + clear_screen, // clear_screen, clear, cl + clr_bol, // clr_bol, el1, cb + clr_eol, // clr_eol, el, ce + clr_eos, // clr_eos, ed, cd + column_address, // column_address, hpa, ch + command_character, // command_character, cmdch, CC + create_window, // create_window, cwin, CW + cursor_address, // cursor_address, cup, cm + cursor_down, // cursor_down, cud1, do + cursor_home, // cursor_home, home, ho + cursor_invisible, // cursor_invisible, civis, vi + cursor_left, // cursor_left, cub1, le + cursor_mem_address, // cursor_mem_address, mrcup, CM + cursor_normal, // cursor_normal, cnorm, ve + cursor_right, // cursor_right, cuf1, nd + cursor_to_ll, // cursor_to_ll, ll, ll + cursor_up, // cursor_up, cuu1, up + cursor_visible, // cursor_visible, cvvis, vs + define_char, // define_char, defc, ZE + delete_character, // delete_character, dch1, dc + delete_line, // delete_line, dl1, dl + dial_phone, // dial_phone, dial, DI + dis_status_line, // dis_status_line, dsl, ds + display_clock, // display_clock, dclk, DK + down_half_line, // down_half_line, hd, hd + ena_acs, // ena_acs, enacs, eA + enter_alt_charset_mode, // enter_alt_charset_mode, smacs, as + enter_am_mode, // enter_am_mode, smam, SA + enter_blink_mode, // enter_blink_mode, blink, mb + enter_bold_mode, // enter_bold_mode, bold, md + enter_ca_mode, // enter_ca_mode, smcup, ti + enter_delete_mode, // enter_delete_mode, smdc, dm + enter_dim_mode, // enter_dim_mode, dim, mh + enter_doublewide_mode, // enter_doublewide_mode, swidm, ZF + enter_draft_quality, // enter_draft_quality, sdrfq, ZG + enter_insert_mode, // enter_insert_mode, smir, im + enter_italics_mode, // enter_italics_mode, sitm, ZH + enter_leftward_mode, // enter_leftward_mode, slm, ZI + enter_micro_mode, // enter_micro_mode, smicm, ZJ + enter_near_letter_quality, // enter_near_letter_quality, snlq, ZK + enter_normal_quality, // enter_normal_quality, snrmq, ZL + enter_protected_mode, // enter_protected_mode, prot, mp + enter_reverse_mode, // enter_reverse_mode, rev, mr + enter_secure_mode, // enter_secure_mode, invis, mk + enter_shadow_mode, // enter_shadow_mode, sshm, ZM + enter_standout_mode, // enter_standout_mode, smso, so + enter_subscript_mode, // enter_subscript_mode, ssubm, ZN + enter_superscript_mode, // enter_superscript_mode, ssupm, ZO + enter_underline_mode, // enter_underline_mode, smul, us + enter_upward_mode, // enter_upward_mode, sum, ZP + enter_xon_mode, // enter_xon_mode, smxon, SX + erase_chars, // erase_chars, ech, ec + exit_alt_charset_mode, // exit_alt_charset_mode, rmacs, ae + exit_am_mode, // exit_am_mode, rmam, RA + exit_attribute_mode, // exit_attribute_mode, sgr0, me + exit_ca_mode, // exit_ca_mode, rmcup, te + exit_delete_mode, // exit_delete_mode, rmdc, ed + exit_doublewide_mode, // exit_doublewide_mode, rwidm, ZQ + exit_insert_mode, // exit_insert_mode, rmir, ei + exit_italics_mode, // exit_italics_mode, ritm, ZR + exit_leftward_mode, // exit_leftward_mode, rlm, ZS + exit_micro_mode, // exit_micro_mode, rmicm, ZT + exit_shadow_mode, // exit_shadow_mode, rshm, ZU + exit_standout_mode, // exit_standout_mode, rmso, se + exit_subscript_mode, // exit_subscript_mode, rsubm, ZV + exit_superscript_mode, // exit_superscript_mode, rsupm, ZW + exit_underline_mode, // exit_underline_mode, rmul, ue + exit_upward_mode, // exit_upward_mode, rum, ZX + exit_xon_mode, // exit_xon_mode, rmxon, RX + fixed_pause, // fixed_pause, pause, PA + flash_hook, // flash_hook, hook, fh + flash_screen, // flash_screen, flash, vb + form_feed, // form_feed, ff, ff + from_status_line, // from_status_line, fsl, fs + goto_window, // goto_window, wingo, WG + hangup, // hangup, hup, HU + init_1string, // init_1string, is1, i1 + init_2string, // init_2string, is2, is + init_3string, // init_3string, is3, i3 + init_file, // init_file, if, if + init_prog, // init_prog, iprog, iP + initialize_color, // initialize_color, initc, Ic + initialize_pair, // initialize_pair, initp, Ip + insert_character, // insert_character, ich1, ic + insert_line, // insert_line, il1, al + insert_padding, // insert_padding, ip, ip + key_a1, // key_a1, ka1, K1 + key_a3, // key_a3, ka3, K3 + key_b2, // key_b2, kb2, K2 + key_backspace, // key_backspace, kbs, kb + key_beg, // key_beg, kbeg, @1 + key_btab, // key_btab, kcbt, kB + key_c1, // key_c1, kc1, K4 + key_c3, // key_c3, kc3, K5 + key_cancel, // key_cancel, kcan, @2 + key_catab, // key_catab, ktbc, ka + key_clear, // key_clear, kclr, kC + key_close, // key_close, kclo, @3 + key_command, // key_command, kcmd, @4 + key_copy, // key_copy, kcpy, @5 + key_create, // key_create, kcrt, @6 + key_ctab, // key_ctab, kctab, kt + key_dc, // key_dc, kdch1, kD + key_dl, // key_dl, kdl1, kL + key_down, // key_down, kcud1, kd + key_eic, // key_eic, krmir, kM + key_end, // key_end, kend, @7 + key_enter, // key_enter, kent, @8 + key_eol, // key_eol, kel, kE + key_eos, // key_eos, ked, kS + key_exit, // key_exit, kext, @9 + key_f0, // key_f0, kf0, k0 + key_f1, // key_f1, kf1, k1 + key_f10, // key_f10, kf10, k; + key_f11, // key_f11, kf11, F1 + key_f12, // key_f12, kf12, F2 + key_f13, // key_f13, kf13, F3 + key_f14, // key_f14, kf14, F4 + key_f15, // key_f15, kf15, F5 + key_f16, // key_f16, kf16, F6 + key_f17, // key_f17, kf17, F7 + key_f18, // key_f18, kf18, F8 + key_f19, // key_f19, kf19, F9 + key_f2, // key_f2, kf2, k2 + key_f20, // key_f20, kf20, FA + key_f21, // key_f21, kf21, FB + key_f22, // key_f22, kf22, FC + key_f23, // key_f23, kf23, FD + key_f24, // key_f24, kf24, FE + key_f25, // key_f25, kf25, FF + key_f26, // key_f26, kf26, FG + key_f27, // key_f27, kf27, FH + key_f28, // key_f28, kf28, FI + key_f29, // key_f29, kf29, FJ + key_f3, // key_f3, kf3, k3 + key_f30, // key_f30, kf30, FK + key_f31, // key_f31, kf31, FL + key_f32, // key_f32, kf32, FM + key_f33, // key_f33, kf33, FN + key_f34, // key_f34, kf34, FO + key_f35, // key_f35, kf35, FP + key_f36, // key_f36, kf36, FQ + key_f37, // key_f37, kf37, FR + key_f38, // key_f38, kf38, FS + key_f39, // key_f39, kf39, FT + key_f4, // key_f4, kf4, k4 + key_f40, // key_f40, kf40, FU + key_f41, // key_f41, kf41, FV + key_f42, // key_f42, kf42, FW + key_f43, // key_f43, kf43, FX + key_f44, // key_f44, kf44, FY + key_f45, // key_f45, kf45, FZ + key_f46, // key_f46, kf46, Fa + key_f47, // key_f47, kf47, Fb + key_f48, // key_f48, kf48, Fc + key_f49, // key_f49, kf49, Fd + key_f5, // key_f5, kf5, k5 + key_f50, // key_f50, kf50, Fe + key_f51, // key_f51, kf51, Ff + key_f52, // key_f52, kf52, Fg + key_f53, // key_f53, kf53, Fh + key_f54, // key_f54, kf54, Fi + key_f55, // key_f55, kf55, Fj + key_f56, // key_f56, kf56, Fk + key_f57, // key_f57, kf57, Fl + key_f58, // key_f58, kf58, Fm + key_f59, // key_f59, kf59, Fn + key_f6, // key_f6, kf6, k6 + key_f60, // key_f60, kf60, Fo + key_f61, // key_f61, kf61, Fp + key_f62, // key_f62, kf62, Fq + key_f63, // key_f63, kf63, Fr + key_f7, // key_f7, kf7, k7 + key_f8, // key_f8, kf8, k8 + key_f9, // key_f9, kf9, k9 + key_find, // key_find, kfnd, @0 + key_help, // key_help, khlp, %1 + key_home, // key_home, khome, kh + key_ic, // key_ic, kich1, kI + key_il, // key_il, kil1, kA + key_left, // key_left, kcub1, kl + key_ll, // key_ll, kll, kH + key_mark, // key_mark, kmrk, %2 + key_message, // key_message, kmsg, %3 + key_move, // key_move, kmov, %4 + key_next, // key_next, knxt, %5 + key_npage, // key_npage, knp, kN + key_open, // key_open, kopn, %6 + key_options, // key_options, kopt, %7 + key_ppage, // key_ppage, kpp, kP + key_previous, // key_previous, kprv, %8 + key_print, // key_print, kprt, %9 + key_redo, // key_redo, krdo, %0 + key_reference, // key_reference, kref, &1 + key_refresh, // key_refresh, krfr, &2 + key_replace, // key_replace, krpl, &3 + key_restart, // key_restart, krst, &4 + key_resume, // key_resume, kres, &5 + key_right, // key_right, kcuf1, kr + key_save, // key_save, ksav, &6 + key_sbeg, // key_sbeg, kBEG, &9 + key_scancel, // key_scancel, kCAN, &0 + key_scommand, // key_scommand, kCMD, *1 + key_scopy, // key_scopy, kCPY, *2 + key_screate, // key_screate, kCRT, *3 + key_sdc, // key_sdc, kDC, *4 + key_sdl, // key_sdl, kDL, *5 + key_select, // key_select, kslt, *6 + key_send, // key_send, kEND, *7 + key_seol, // key_seol, kEOL, *8 + key_sexit, // key_sexit, kEXT, *9 + key_sf, // key_sf, kind, kF + key_sfind, // key_sfind, kFND, *0 + key_shelp, // key_shelp, kHLP, #1 + key_shome, // key_shome, kHOM, #2 + key_sic, // key_sic, kIC, #3 + key_sleft, // key_sleft, kLFT, #4 + key_smessage, // key_smessage, kMSG, %a + key_smove, // key_smove, kMOV, %b + key_snext, // key_snext, kNXT, %c + key_soptions, // key_soptions, kOPT, %d + key_sprevious, // key_sprevious, kPRV, %e + key_sprint, // key_sprint, kPRT, %f + key_sr, // key_sr, kri, kR + key_sredo, // key_sredo, kRDO, %g + key_sreplace, // key_sreplace, kRPL, %h + key_sright, // key_sright, kRIT, %i + key_srsume, // key_srsume, kRES, %j + key_ssave, // key_ssave, kSAV, !1 + key_ssuspend, // key_ssuspend, kSPD, !2 + key_stab, // key_stab, khts, kT + key_sundo, // key_sundo, kUND, !3 + key_suspend, // key_suspend, kspd, &7 + key_undo, // key_undo, kund, &8 + key_up, // key_up, kcuu1, ku + keypad_local, // keypad_local, rmkx, ke + keypad_xmit, // keypad_xmit, smkx, ks + lab_f0, // lab_f0, lf0, l0 + lab_f1, // lab_f1, lf1, l1 + lab_f10, // lab_f10, lf10, la + lab_f2, // lab_f2, lf2, l2 + lab_f3, // lab_f3, lf3, l3 + lab_f4, // lab_f4, lf4, l4 + lab_f5, // lab_f5, lf5, l5 + lab_f6, // lab_f6, lf6, l6 + lab_f7, // lab_f7, lf7, l7 + lab_f8, // lab_f8, lf8, l8 + lab_f9, // lab_f9, lf9, l9 + label_format, // label_format, fln, Lf + label_off, // label_off, rmln, LF + label_on, // label_on, smln, LO + meta_off, // meta_off, rmm, mo + meta_on, // meta_on, smm, mm + micro_column_address, // micro_column_address, mhpa, ZY + micro_down, // micro_down, mcud1, ZZ + micro_left, // micro_left, mcub1, Za + micro_right, // micro_right, mcuf1, Zb + micro_row_address, // micro_row_address, mvpa, Zc + micro_up, // micro_up, mcuu1, Zd + newline, // newline, nel, nw + order_of_pins, // order_of_pins, porder, Ze + orig_colors, // orig_colors, oc, oc + orig_pair, // orig_pair, op, op + pad_char, // pad_char, pad, pc + parm_dch, // parm_dch, dch, DC + parm_delete_line, // parm_delete_line, dl, DL + parm_down_cursor, // parm_down_cursor, cud, DO + parm_down_micro, // parm_down_micro, mcud, Zf + parm_ich, // parm_ich, ich, IC + parm_index, // parm_index, indn, SF + parm_insert_line, // parm_insert_line, il, AL + parm_left_cursor, // parm_left_cursor, cub, LE + parm_left_micro, // parm_left_micro, mcub, Zg + parm_right_cursor, // parm_right_cursor, cuf, RI + parm_right_micro, // parm_right_micro, mcuf, Zh + parm_rindex, // parm_rindex, rin, SR + parm_up_cursor, // parm_up_cursor, cuu, UP + parm_up_micro, // parm_up_micro, mcuu, Zi + pkey_key, // pkey_key, pfkey, pk + pkey_local, // pkey_local, pfloc, pl + pkey_xmit, // pkey_xmit, pfx, px + plab_norm, // plab_norm, pln, pn + print_screen, // print_screen, mc0, ps + prtr_non, // prtr_non, mc5p, pO + prtr_off, // prtr_off, mc4, pf + prtr_on, // prtr_on, mc5, po + pulse, // pulse, pulse, PU + quick_dial, // quick_dial, qdial, QD + remove_clock, // remove_clock, rmclk, RC + repeat_char, // repeat_char, rep, rp + req_for_input, // req_for_input, rfi, RF + reset_1string, // reset_1string, rs1, r1 + reset_2string, // reset_2string, rs2, r2 + reset_3string, // reset_3string, rs3, r3 + reset_file, // reset_file, rf, rf + restore_cursor, // restore_cursor, rc, rc + row_address, // row_address, vpa, cv + save_cursor, // save_cursor, sc, sc + scroll_forward, // scroll_forward, ind, sf + scroll_reverse, // scroll_reverse, ri, sr + select_char_set, // select_char_set, scs, Zj + set_attributes, // set_attributes, sgr, sa + set_background, // set_background, setb, Sb + set_bottom_margin, // set_bottom_margin, smgb, Zk + set_bottom_margin_parm, // set_bottom_margin_parm, smgbp, Zl + set_clock, // set_clock, sclk, SC + set_color_pair, // set_color_pair, scp, sp + set_foreground, // set_foreground, setf, Sf + set_left_margin, // set_left_margin, smgl, ML + set_left_margin_parm, // set_left_margin_parm, smglp, Zm + set_right_margin, // set_right_margin, smgr, MR + set_right_margin_parm, // set_right_margin_parm, smgrp, Zn + set_tab, // set_tab, hts, st + set_top_margin, // set_top_margin, smgt, Zo + set_top_margin_parm, // set_top_margin_parm, smgtp, Zp + set_window, // set_window, wind, wi + start_bit_image, // start_bit_image, sbim, Zq + start_char_set_def, // start_char_set_def, scsd, Zr + stop_bit_image, // stop_bit_image, rbim, Zs + stop_char_set_def, // stop_char_set_def, rcsd, Zt + subscript_characters, // subscript_characters, subcs, Zu + superscript_characters, // superscript_characters, supcs, Zv + tab, // tab, ht, ta + these_cause_cr, // these_cause_cr, docr, Zw + to_status_line, // to_status_line, tsl, ts + tone, // tone, tone, TO + underline_char, // underline_char, uc, uc + up_half_line, // up_half_line, hu, hu + user0, // user0, u0, u0 + user1, // user1, u1, u1 + user2, // user2, u2, u2 + user3, // user3, u3, u3 + user4, // user4, u4, u4 + user5, // user5, u5, u5 + user6, // user6, u6, u6 + user7, // user7, u7, u7 + user8, // user8, u8, u8 + user9, // user9, u9, u9 + wait_tone, // wait_tone, wait, WA + xoff_character, // xoff_character, xoffc, XF + xon_character, // xon_character, xonc, XN + zero_motion, // zero_motion, zerom, Zx + alt_scancode_esc, // alt_scancode_esc, scesa, S8 + bit_image_carriage_return, // bit_image_carriage_return, bicr, Yv + bit_image_newline, // bit_image_newline, binel, Zz + bit_image_repeat, // bit_image_repeat, birep, Xy + char_set_names, // char_set_names, csnm, Zy + code_set_init, // code_set_init, csin, ci + color_names, // color_names, colornm, Yw + define_bit_image_region, // define_bit_image_region, defbi, Yx + device_type, // device_type, devt, dv + display_pc_char, // display_pc_char, dispc, S1 + end_bit_image_region, // end_bit_image_region, endbi, Yy + enter_pc_charset_mode, // enter_pc_charset_mode, smpch, S2 + enter_scancode_mode, // enter_scancode_mode, smsc, S4 + exit_pc_charset_mode, // exit_pc_charset_mode, rmpch, S3 + exit_scancode_mode, // exit_scancode_mode, rmsc, S5 + get_mouse, // get_mouse, getm, Gm + key_mouse, // key_mouse, kmous, Km + mouse_info, // mouse_info, minfo, Mi + pc_term_options, // pc_term_options, pctrm, S6 + pkey_plab, // pkey_plab, pfxl, xl + req_mouse_pos, // req_mouse_pos, reqmp, RQ + scancode_escape, // scancode_escape, scesc, S7 + set0_des_seq, // set0_des_seq, s0ds, s0 + set1_des_seq, // set1_des_seq, s1ds, s1 + set2_des_seq, // set2_des_seq, s2ds, s2 + set3_des_seq, // set3_des_seq, s3ds, s3 + set_a_background, // set_a_background, setab, AB + set_a_foreground, // set_a_foreground, setaf, AF + set_color_band, // set_color_band, setcolor, Yz + set_lr_margin, // set_lr_margin, smglr, ML + set_page_length, // set_page_length, slines, YZ + set_tb_margin, // set_tb_margin, smgtb, MT + enter_horizontal_hl_mode, // enter_horizontal_hl_mode, ehhlm, Xh + enter_left_hl_mode, // enter_left_hl_mode, elhlm, Xl + enter_low_hl_mode, // enter_low_hl_mode, elohlm, Xo + enter_right_hl_mode, // enter_right_hl_mode, erhlm, Xr + enter_top_hl_mode, // enter_top_hl_mode, ethlm, Xt + enter_vertical_hl_mode, // enter_vertical_hl_mode, evhlm, Xv + set_a_attributes, // set_a_attributes, sgr1, sA + set_pglen_inch, // set_pglen_inch, slength, sL) + ; + + public static Capability byName(String name) { + return getCapabilitiesByName().get(name); + } + + public String[] getNames() { + return getCapabilitiesByName().entrySet().stream() + .filter(e -> e.getValue() == this) + .map(Map.Entry::getKey) + .toArray(String[]::new); + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/InputStreamReader.java b/net-cli/src/main/java/org/jline/utils/InputStreamReader.java new file mode 100644 index 0000000..2d4ff34 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/InputStreamReader.java @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; + +/** + * NOTE for JLine: the default InputStreamReader that comes from the JRE + * usually read more bytes than needed from the input stream, which + * is not usable in a character per character model used in the terminal. + * We thus use the harmony code which only reads the minimal number of bytes. + */ + +/** + * A class for turning a byte stream into a character stream. Data read from the + * source input stream is converted into characters by either a default or a + * provided character converter. The default encoding is taken from the + * "file.encoding" system property. {@code InputStreamReader} contains a buffer + * of bytes read from the source stream and converts these into characters as + * needed. The buffer size is 8K. + * + * @see OutputStreamWriter + */ +public class InputStreamReader extends Reader { + private static final int BUFFER_SIZE = 4; + CharsetDecoder decoder; + ByteBuffer bytes = ByteBuffer.allocate(BUFFER_SIZE); + char pending = (char) -1; + private InputStream in; + private boolean endOfInput = false; + + /** + * Constructs a new {@code InputStreamReader} on the {@link InputStream} + * {@code in}. This constructor sets the character converter to the encoding + * specified in the "file.encoding" property and falls back to ISO 8859_1 + * (ISO-Latin-1) if the property doesn't exist. + * + * @param in + * the input stream from which to read characters. + */ + public InputStreamReader(InputStream in) { + super(in); + this.in = in; + decoder = Charset.defaultCharset() + .newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + bytes.limit(0); + } + + /** + * Constructs a new InputStreamReader on the InputStream {@code in}. The + * character converter that is used to decode bytes into characters is + * identified by name by {@code enc}. If the encoding cannot be found, an + * UnsupportedEncodingException error is thrown. + * + * @param in + * the InputStream from which to read characters. + * @param enc + * identifies the character converter to use. + * @throws NullPointerException + * if {@code enc} is {@code null}. + * @throws UnsupportedEncodingException + * if the encoding specified by {@code enc} cannot be found. + */ + public InputStreamReader(InputStream in, final String enc) throws UnsupportedEncodingException { + super(in); + if (enc == null) { + throw new NullPointerException(); + } + this.in = in; + try { + decoder = Charset.forName(enc) + .newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + } catch (IllegalArgumentException e) { + throw (UnsupportedEncodingException) new UnsupportedEncodingException(enc).initCause(e); + } + bytes.limit(0); + } + + /** + * Constructs a new InputStreamReader on the InputStream {@code in} and + * CharsetDecoder {@code dec}. + * + * @param in + * the source InputStream from which to read characters. + * @param dec + * the CharsetDecoder used by the character conversion. + */ + public InputStreamReader(InputStream in, CharsetDecoder dec) { + super(in); + dec.averageCharsPerByte(); + this.in = in; + decoder = dec; + bytes.limit(0); + } + + /** + * Constructs a new InputStreamReader on the InputStream {@code in} and + * Charset {@code charset}. + * + * @param in + * the source InputStream from which to read characters. + * @param charset + * the Charset that defines the character converter + */ + public InputStreamReader(InputStream in, Charset charset) { + super(in); + this.in = in; + decoder = charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + bytes.limit(0); + } + + /** + * Closes this reader. This implementation closes the source InputStream and + * releases all local storage. + * + * @throws IOException + * if an error occurs attempting to close this reader. + */ + @Override + public void close() throws IOException { + synchronized (lock) { + decoder = null; + if (in != null) { + in.close(); + in = null; + } + } + } + + /** + * Returns the name of the encoding used to convert bytes into characters. + * The value {@code null} is returned if this reader has been closed. + * + * @return the name of the character converter or {@code null} if this + * reader is closed. + */ + public String getEncoding() { + if (!isOpen()) { + return null; + } + return decoder.charset().name(); + } + + /** + * Reads a single character from this reader and returns it as an integer + * with the two higher-order bytes set to 0. Returns -1 if the end of the + * reader has been reached. The byte value is either obtained from + * converting bytes in this reader's buffer or by first filling the buffer + * from the source InputStream and then reading from the buffer. + * + * @return the character read or -1 if the end of the reader has been + * reached. + * @throws IOException + * if this reader is closed or some other I/O error occurs. + */ + @Override + public int read() throws IOException { + synchronized (lock) { + if (!isOpen()) { + throw new ClosedException("InputStreamReader is closed."); + } + + if (pending != (char) -1) { + char c = pending; + pending = (char) -1; + return c; + } + char[] buf = new char[2]; + int nb = read(buf, 0, 2); + if (nb == 2) { + pending = buf[1]; + } + if (nb > 0) { + return buf[0]; + } else { + return -1; + } + } + } + + /** + * Reads at most {@code length} characters from this reader and stores them + * at position {@code offset} in the character array {@code buf}. Returns + * the number of characters actually read or -1 if the end of the reader has + * been reached. The bytes are either obtained from converting bytes in this + * reader's buffer or by first filling the buffer from the source + * InputStream and then reading from the buffer. + * + * @param buf + * the array to store the characters read. + * @param offset + * the initial position in {@code buf} to store the characters + * read from this reader. + * @param length + * the maximum number of characters to read. + * @return the number of characters read or -1 if the end of the reader has + * been reached. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code length < 0}, or if + * {@code offset + length} is greater than the length of + * {@code buf}. + * @throws IOException + * if this reader is closed or some other I/O error occurs. + */ + @Override + public int read(char[] buf, int offset, int length) throws IOException { + synchronized (lock) { + if (!isOpen()) { + throw new IOException("InputStreamReader is closed."); + } + if (offset < 0 || offset > buf.length - length || length < 0) { + throw new IndexOutOfBoundsException(); + } + if (length == 0) { + return 0; + } + + CharBuffer out = CharBuffer.wrap(buf, offset, length); + CoderResult result = CoderResult.UNDERFLOW; + + // bytes.remaining() indicates number of bytes in buffer + // when 1-st time entered, it'll be equal to zero + boolean needInput = !bytes.hasRemaining(); + + while (out.position() == offset) { + // fill the buffer if needed + if (needInput) { + try { + if ((in.available() == 0) && (out.position() > offset)) { + // we could return the result without blocking read + break; + } + } catch (IOException e) { + // available didn't work so just try the read + } + + int off = bytes.arrayOffset() + bytes.limit(); + int was_red = in.read(bytes.array(), off, 1); + + if (was_red == -1) { + endOfInput = true; + break; + } else if (was_red == 0) { + break; + } + bytes.limit(bytes.limit() + was_red); + } + + // decode bytes + result = decoder.decode(bytes, out, false); + + if (result.isUnderflow()) { + // compact the buffer if no space left + if (bytes.limit() == bytes.capacity()) { + bytes.compact(); + bytes.limit(bytes.position()); + bytes.position(0); + } + needInput = true; + } else { + break; + } + } + + if (result == CoderResult.UNDERFLOW && endOfInput) { + result = decoder.decode(bytes, out, true); + decoder.flush(out); + decoder.reset(); + } + if (result.isMalformed()) { + throw new MalformedInputException(result.length()); + } else if (result.isUnmappable()) { + throw new UnmappableCharacterException(result.length()); + } + + return out.position() - offset == 0 ? -1 : out.position() - offset; + } + } + + /* + * Answer a boolean indicating whether or not this InputStreamReader is + * open. + */ + private boolean isOpen() { + return in != null; + } + + /** + * Indicates whether this reader is ready to be read without blocking. If + * the result is {@code true}, the next {@code read()} will not block. If + * the result is {@code false} then this reader may or may not block when + * {@code read()} is called. This implementation returns {@code true} if + * there are bytes available in the buffer or the source stream has bytes + * available. + * + * @return {@code true} if the receiver will not block when {@code read()} + * is called, {@code false} if unknown or blocking will occur. + * @throws IOException + * if this reader is closed or some other I/O error occurs. + */ + @Override + public boolean ready() throws IOException { + synchronized (lock) { + if (in == null) { + throw new IOException("InputStreamReader is closed."); + } + try { + return bytes.hasRemaining() || in.available() > 0; + } catch (IOException e) { + return false; + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/Levenshtein.java b/net-cli/src/main/java/org/jline/utils/Levenshtein.java new file mode 100644 index 0000000..c5f9c39 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/Levenshtein.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.util.HashMap; +import java.util.Map; + +/** + * The Damerau-Levenshtein Algorithm is an extension to the Levenshtein + * Algorithm which solves the edit distance problem between a source string and + * a target string with the following operations: + * + *
    + *
  • Character Insertion
  • + *
  • Character Deletion
  • + *
  • Character Replacement
  • + *
  • Adjacent Character Swap
  • + *
+ *

+ * Note that the adjacent character swap operation is an edit that may be + * applied when two adjacent characters in the source string match two adjacent + * characters in the target string, but in reverse order, rather than a general + * allowance for adjacent character swaps. + *

+ *

+ * This implementation allows the client to specify the costs of the various + * edit operations with the restriction that the cost of two swap operations + * must not be less than the cost of a delete operation followed by an insert + * operation. This restriction is required to preclude two swaps involving the + * same character being required for optimality which, in turn, enables a fast + * dynamic programming solution. + *

+ *

+ * The running time of the Damerau-Levenshtein algorithm is O(n*m) where n is + * the length of the source string and m is the length of the target string. + * This implementation consumes O(n*m) space. + * + * @author Kevin L. Stern + */ +public class Levenshtein { + + public static int distance(CharSequence lhs, CharSequence rhs) { + return distance(lhs, rhs, 1, 1, 1, 1); + } + + public static int distance( + CharSequence source, CharSequence target, int deleteCost, int insertCost, int replaceCost, int swapCost) { + /* + * Required to facilitate the premise to the algorithm that two swaps of the + * same character are never required for optimality. + */ + if (2 * swapCost < insertCost + deleteCost) { + throw new IllegalArgumentException("Unsupported cost assignment"); + } + if (source.length() == 0) { + return target.length() * insertCost; + } + if (target.length() == 0) { + return source.length() * deleteCost; + } + int[][] table = new int[source.length()][target.length()]; + Map sourceIndexByCharacter = new HashMap<>(); + if (source.charAt(0) != target.charAt(0)) { + table[0][0] = Math.min(replaceCost, deleteCost + insertCost); + } + sourceIndexByCharacter.put(source.charAt(0), 0); + for (int i = 1; i < source.length(); i++) { + int deleteDistance = table[i - 1][0] + deleteCost; + int insertDistance = (i + 1) * deleteCost + insertCost; + int matchDistance = i * deleteCost + (source.charAt(i) == target.charAt(0) ? 0 : replaceCost); + table[i][0] = Math.min(Math.min(deleteDistance, insertDistance), matchDistance); + } + for (int j = 1; j < target.length(); j++) { + int deleteDistance = (j + 1) * insertCost + deleteCost; + int insertDistance = table[0][j - 1] + insertCost; + int matchDistance = j * insertCost + (source.charAt(0) == target.charAt(j) ? 0 : replaceCost); + table[0][j] = Math.min(Math.min(deleteDistance, insertDistance), matchDistance); + } + for (int i = 1; i < source.length(); i++) { + int maxSourceLetterMatchIndex = source.charAt(i) == target.charAt(0) ? 0 : -1; + for (int j = 1; j < target.length(); j++) { + Integer candidateSwapIndex = sourceIndexByCharacter.get(target.charAt(j)); + int jSwap = maxSourceLetterMatchIndex; + int deleteDistance = table[i - 1][j] + deleteCost; + int insertDistance = table[i][j - 1] + insertCost; + int matchDistance = table[i - 1][j - 1]; + if (source.charAt(i) != target.charAt(j)) { + matchDistance += replaceCost; + } else { + maxSourceLetterMatchIndex = j; + } + int swapDistance; + if (candidateSwapIndex != null && jSwap != -1) { + int iSwap = candidateSwapIndex; + int preSwapCost; + if (iSwap == 0 && jSwap == 0) { + preSwapCost = 0; + } else { + preSwapCost = table[Math.max(0, iSwap - 1)][Math.max(0, jSwap - 1)]; + } + swapDistance = preSwapCost + (i - iSwap - 1) * deleteCost + (j - jSwap - 1) * insertCost + swapCost; + } else { + swapDistance = Integer.MAX_VALUE; + } + table[i][j] = Math.min(Math.min(Math.min(deleteDistance, insertDistance), matchDistance), swapDistance); + } + sourceIndexByCharacter.put(source.charAt(i), i); + } + return table[source.length() - 1][target.length() - 1]; + } +} diff --git a/net-cli/src/main/java/org/jline/utils/Log.java b/net-cli/src/main/java/org/jline/utils/Log.java new file mode 100644 index 0000000..3c4c783 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/Log.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * Internal logger. + * + * @author Jason Dillon + * @author Guillaume Nodet + * @since 2.0 + */ +public final class Log { + private static final Logger logger = Logger.getLogger("org.jline"); + + public static void trace(final Object... messages) { + log(Level.FINEST, messages); + } + + public static void trace(Supplier supplier) { + log(Level.FINEST, supplier); + } + + public static void debug(Supplier supplier) { + log(Level.FINE, supplier); + } + + public static void debug(final Object... messages) { + log(Level.FINE, messages); + } + + public static void info(final Object... messages) { + log(Level.INFO, messages); + } + + public static void warn(final Object... messages) { + log(Level.WARNING, messages); + } + + public static void error(final Object... messages) { + log(Level.SEVERE, messages); + } + + public static boolean isDebugEnabled() { + return isEnabled(Level.FINE); + } + + /** + * Helper to support rendering messages. + */ + static void render(final PrintStream out, final Object message) { + if (message != null && message.getClass().isArray()) { + Object[] array = (Object[]) message; + + out.print("["); + for (int i = 0; i < array.length; i++) { + out.print(array[i]); + if (i + 1 < array.length) { + out.print(","); + } + } + out.print("]"); + } else { + out.print(message); + } + } + + static LogRecord createRecord(final Level level, final Object... messages) { + Throwable cause = null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + for (int i = 0; i < messages.length; i++) { + // Special handling for the last message if it's a throwable, render its stack on the next line + if (i + 1 == messages.length && messages[i] instanceof Throwable) { + cause = (Throwable) messages[i]; + } else { + render(ps, messages[i]); + } + } + ps.close(); + LogRecord r = new LogRecord(level, baos.toString()); + r.setThrown(cause); + return r; + } + + static LogRecord createRecord(final Level level, final Supplier message) { + return new LogRecord(level, message.get()); + } + + static void log(final Level level, final Supplier message) { + logr(level, () -> createRecord(level, message)); + } + + static void log(final Level level, final Object... messages) { + logr(level, () -> createRecord(level, messages)); + } + + static void logr(final Level level, final Supplier record) { + if (logger.isLoggable(level)) { + // inform record of the logger-name + LogRecord tmp = record.get(); + tmp.setLoggerName(logger.getName()); + logger.log(tmp); + } + } + + static boolean isEnabled(Level level) { + return logger.isLoggable(level); + } +} diff --git a/net-cli/src/main/java/org/jline/utils/NonBlocking.java b/net-cli/src/main/java/org/jline/utils/NonBlocking.java new file mode 100644 index 0000000..4d2721b --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/NonBlocking.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; + +public class NonBlocking { + + public static NonBlockingPumpReader nonBlockingPumpReader() { + return new NonBlockingPumpReader(); + } + + public static NonBlockingPumpReader nonBlockingPumpReader(int size) { + return new NonBlockingPumpReader(size); + } + + public static NonBlockingPumpInputStream nonBlockingPumpInputStream() { + return new NonBlockingPumpInputStream(); + } + + public static NonBlockingPumpInputStream nonBlockingPumpInputStream(int size) { + return new NonBlockingPumpInputStream(size); + } + + public static NonBlockingInputStream nonBlockingStream(NonBlockingReader reader, Charset encoding) { + return new NonBlockingReaderInputStream(reader, encoding); + } + + public static NonBlockingInputStream nonBlocking(String name, InputStream inputStream) { + if (inputStream instanceof NonBlockingInputStream) { + return (NonBlockingInputStream) inputStream; + } + return new NonBlockingInputStreamImpl(name, inputStream); + } + + public static NonBlockingReader nonBlocking(String name, Reader reader) { + if (reader instanceof NonBlockingReader) { + return (NonBlockingReader) reader; + } + return new NonBlockingReaderImpl(name, reader); + } + + public static NonBlockingReader nonBlocking(String name, InputStream inputStream, Charset encoding) { + return new NonBlockingInputStreamReader(nonBlocking(name, inputStream), encoding); + } + + private static class NonBlockingReaderInputStream extends NonBlockingInputStream { + + private final NonBlockingReader reader; + private final CharsetEncoder encoder; + + // To encode a character with multiple bytes (e.g. certain Unicode characters) + // we need enough space to encode them. Reading would fail if the read() method + // is used to read a single byte in these cases. + // Use this buffer to ensure we always have enough space to encode a character. + private final ByteBuffer bytes; + private final CharBuffer chars; + + private NonBlockingReaderInputStream(NonBlockingReader reader, Charset charset) { + this.reader = reader; + this.encoder = charset.newEncoder() + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .onMalformedInput(CodingErrorAction.REPLACE); + this.bytes = ByteBuffer.allocate(4); + this.chars = CharBuffer.allocate(2); + // No input available after initialization + this.bytes.limit(0); + this.chars.limit(0); + } + + @Override + public int available() { + return (int) (reader.available() * this.encoder.averageBytesPerChar()) + bytes.remaining(); + } + + @Override + public void close() throws IOException { + reader.close(); + } + + @Override + public int read(long timeout, boolean isPeek) throws IOException { + Timeout t = new Timeout(timeout); + while (!bytes.hasRemaining() && !t.elapsed()) { + int c = reader.read(t.timeout()); + if (c == EOF) { + return EOF; + } + if (c >= 0) { + if (!chars.hasRemaining()) { + chars.position(0); + chars.limit(0); + } + int l = chars.limit(); + chars.array()[chars.arrayOffset() + l] = (char) c; + chars.limit(l + 1); + bytes.clear(); + encoder.encode(chars, bytes, false); + bytes.flip(); + } + } + if (bytes.hasRemaining()) { + if (isPeek) { + return bytes.get(bytes.position()); + } else { + return bytes.get(); + } + } else { + return READ_EXPIRED; + } + } + } + + private static class NonBlockingInputStreamReader extends NonBlockingReader { + + private final NonBlockingInputStream input; + private final CharsetDecoder decoder; + private final ByteBuffer bytes; + private final CharBuffer chars; + + public NonBlockingInputStreamReader(NonBlockingInputStream inputStream, Charset encoding) { + this( + inputStream, + (encoding != null ? encoding : Charset.defaultCharset()) + .newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE)); + } + + public NonBlockingInputStreamReader(NonBlockingInputStream input, CharsetDecoder decoder) { + this.input = input; + this.decoder = decoder; + this.bytes = ByteBuffer.allocate(2048); + this.chars = CharBuffer.allocate(1024); + this.bytes.limit(0); + this.chars.limit(0); + } + + @Override + protected int read(long timeout, boolean isPeek) throws IOException { + Timeout t = new Timeout(timeout); + while (!chars.hasRemaining() && !t.elapsed()) { + int b = input.read(t.timeout()); + if (b == EOF) { + return EOF; + } + if (b >= 0) { + if (!bytes.hasRemaining()) { + bytes.position(0); + bytes.limit(0); + } + int l = bytes.limit(); + bytes.array()[bytes.arrayOffset() + l] = (byte) b; + bytes.limit(l + 1); + chars.clear(); + decoder.decode(bytes, chars, false); + chars.flip(); + } + } + if (chars.hasRemaining()) { + if (isPeek) { + return chars.get(chars.position()); + } else { + return chars.get(); + } + } else { + return READ_EXPIRED; + } + } + + @Override + public int readBuffered(char[] b, int off, int len, long timeout) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || off + len < b.length) { + throw new IllegalArgumentException(); + } else if (len == 0) { + return 0; + } else if (chars.hasRemaining()) { + int r = Math.min(len, chars.remaining()); + chars.get(b, off, r); + return r; + } else { + Timeout t = new Timeout(timeout); + while (!chars.hasRemaining() && !t.elapsed()) { + if (!bytes.hasRemaining()) { + bytes.position(0); + bytes.limit(0); + } + int nb = input.readBuffered( + bytes.array(), bytes.limit(), bytes.capacity() - bytes.limit(), t.timeout()); + if (nb < 0) { + return nb; + } + bytes.limit(bytes.limit() + nb); + chars.clear(); + decoder.decode(bytes, chars, false); + chars.flip(); + } + int nb = Math.min(len, chars.remaining()); + chars.get(b, off, nb); + return nb; + } + } + + @Override + public void shutdown() { + input.shutdown(); + } + + @Override + public void close() throws IOException { + input.close(); + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/NonBlockingInputStream.java b/net-cli/src/main/java/org/jline/utils/NonBlockingInputStream.java new file mode 100644 index 0000000..7c40ef1 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/NonBlockingInputStream.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Non blocking input stream + */ +public abstract class NonBlockingInputStream extends InputStream { + + public static final int EOF = -1; + public static final int READ_EXPIRED = -2; + + /** + * Reads the next byte of data from the input stream. The value byte is + * returned as an int in the range 0 to + * 255. If no byte is available because the end of the stream + * has been reached, the value -1 is returned. This method + * blocks until input data is available, the end of the stream is detected, + * or an exception is thrown. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @throws IOException if an I/O error occurs. + */ + @Override + public int read() throws IOException { + return read(0L, false); + } + + /** + * Peeks to see if there is a byte waiting in the input stream without + * actually consuming the byte. + * + * @param timeout The amount of time to wait, 0 == forever + * @return -1 on eof, -2 if the timeout expired with no available input + * or the character that was read (without consuming it). + * @throws IOException if an I/O error occurs. + */ + public int peek(long timeout) throws IOException { + return read(timeout, true); + } + + /** + * Attempts to read a character from the input stream for a specific + * period of time. + * + * @param timeout The amount of time to wait for the character + * @return The character read, -1 if EOF is reached, + * or -2 if the read timed out. + * @throws IOException if an I/O error occurs. + */ + public int read(long timeout) throws IOException { + return read(timeout, false); + } + + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + int c = read(); + if (c == EOF) { + return EOF; + } + b[off] = (byte) c; + return 1; + } + + public int readBuffered(byte[] b) throws IOException { + return readBuffered(b, 0L); + } + + public int readBuffered(byte[] b, long timeout) throws IOException { + return readBuffered(b, 0, b.length, timeout); + } + + public int readBuffered(byte[] b, int off, int len, long timeout) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || off + len < b.length) { + throw new IllegalArgumentException(); + } else if (len == 0) { + return 0; + } else { + Timeout t = new Timeout(timeout); + int nb = 0; + while (!t.elapsed()) { + int r = read(nb > 0 ? 1 : t.timeout()); + if (r < 0) { + return nb > 0 ? nb : r; + } + b[off + nb++] = (byte) r; + if (nb >= len || t.isInfinite()) { + break; + } + } + return nb; + } + } + + /** + * Shuts down the thread that is handling blocking I/O if any. Note that if the + * thread is currently blocked waiting for I/O it may not actually + * shut down until the I/O is received. + */ + public void shutdown() { + } + + public abstract int read(long timeout, boolean isPeek) throws IOException; +} diff --git a/net-cli/src/main/java/org/jline/utils/NonBlockingInputStreamImpl.java b/net-cli/src/main/java/org/jline/utils/NonBlockingInputStreamImpl.java new file mode 100644 index 0000000..e9748c9 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/NonBlockingInputStreamImpl.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; + +/** + * This class wraps a regular input stream and allows it to appear as if it + * is non-blocking; that is, reads can be performed against it that timeout + * if no data is seen for a period of time. This effect is achieved by having + * a separate thread perform all non-blocking read requests and then + * waiting on the thread to complete. + * + *

VERY IMPORTANT NOTES + *

    + *
  • This class is not thread safe. It expects at most one reader. + *
  • The {@link #shutdown()} method must be called in order to shut down + * the thread that handles blocking I/O. + *
+ */ +public class NonBlockingInputStreamImpl extends NonBlockingInputStream { + private final InputStream in; // The actual input stream + private int b = READ_EXPIRED; // Recently read byte + + private final String name; + private boolean threadIsReading = false; + private IOException exception = null; + private final long threadDelay = 60 * 1000; + private Thread thread; + + /** + * Creates a NonBlockingReader out of a normal blocking + * reader. Note that this call also spawn a separate thread to perform the + * blocking I/O on behalf of the thread that is using this class. The + * {@link #shutdown()} method must be called in order to shut this thread down. + * + * @param name The stream name + * @param in The reader to wrap + */ + public NonBlockingInputStreamImpl(String name, InputStream in) { + this.in = in; + this.name = name; + } + + private synchronized void startReadingThreadIfNeeded() { + if (thread == null) { + thread = new Thread(this::run); + thread.setName(name + " non blocking reader thread"); + thread.setDaemon(true); + thread.start(); + } + } + + /** + * Shuts down the thread that is handling blocking I/O. Note that if the + * thread is currently blocked waiting for I/O it will not actually + * shut down until the I/O is received. + */ + public synchronized void shutdown() { + if (thread != null) { + notify(); + } + } + + @Override + public void close() throws IOException { + /* + * The underlying input stream is closed first. This means that if the + * I/O thread was blocked waiting on input, it will be woken for us. + */ + in.close(); + shutdown(); + } + + /** + * Attempts to read a byte from the input stream for a specific + * period of time. + * + * @param timeout The amount of time to wait for the character + * @param isPeek trueif the byte read must not be consumed + * @return The byte read, -1 if EOF is reached, or -2 if the + * read timed out. + * @throws IOException if anything wrong happens + */ + public synchronized int read(long timeout, boolean isPeek) throws IOException { + /* + * If the thread hit an IOException, we report it. + */ + if (exception != null) { + assert b == READ_EXPIRED; + IOException toBeThrown = exception; + if (!isPeek) exception = null; + throw toBeThrown; + } + + /* + * If there was a pending character from the thread, then + * we send it. If the timeout is 0L or the thread was shut down + * then do a local read. + */ + if (b >= -1) { + assert exception == null; + } else if (!isPeek && timeout <= 0L && !threadIsReading) { + b = in.read(); + } else { + /* + * If the thread isn't reading already, then ask it to do so. + */ + if (!threadIsReading) { + threadIsReading = true; + startReadingThreadIfNeeded(); + notifyAll(); + } + + /* + * So the thread is currently doing the reading for us. So + * now we play the waiting game. + */ + Timeout t = new Timeout(timeout); + while (!t.elapsed()) { + try { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + wait(t.timeout()); + } catch (InterruptedException e) { + exception = (IOException) new InterruptedIOException().initCause(e); + } + + if (exception != null) { + assert b == READ_EXPIRED; + + IOException toBeThrown = exception; + if (!isPeek) exception = null; + throw toBeThrown; + } + + if (b >= -1) { + assert exception == null; + break; + } + } + } + + /* + * b is the character that was just read. Either we set it because + * a local read was performed or the read thread set it (or failed to + * change it). We will return it's value, but if this was a peek + * operation, then we leave it in place. + */ + int ret = b; + if (!isPeek) { + b = READ_EXPIRED; + } + return ret; + } + + private void run() { + Log.debug("NonBlockingInputStream start"); + boolean needToRead; + + try { + while (true) { + + /* + * Synchronize to grab variables accessed by both this thread + * and the accessing thread. + */ + synchronized (this) { + needToRead = this.threadIsReading; + + try { + /* + * Nothing to do? Then wait. + */ + if (!needToRead) { + wait(threadDelay); + } + } catch (InterruptedException e) { + /* IGNORED */ + } + + needToRead = this.threadIsReading; + if (!needToRead) { + return; + } + } + + /* + * We're not shutting down, but we need to read. This cannot + * happen while we are holding the lock (which we aren't now). + */ + int byteRead = READ_EXPIRED; + IOException failure = null; + try { + byteRead = in.read(); + } catch (IOException e) { + failure = e; + } + + /* + * Re-grab the lock to update the state. + */ + synchronized (this) { + exception = failure; + b = byteRead; + threadIsReading = false; + notify(); + } + + // If end of stream, exit the loop thread + if (byteRead < 0) { + return; + } + } + } catch (Throwable t) { + Log.warn("Error in NonBlockingInputStream thread", t); + } finally { + Log.debug("NonBlockingInputStream shutdown"); + synchronized (this) { + thread = null; + threadIsReading = false; + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/NonBlockingPumpInputStream.java b/net-cli/src/main/java/org/jline/utils/NonBlockingPumpInputStream.java new file mode 100644 index 0000000..a8a6962 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/NonBlockingPumpInputStream.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2002-2017, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +public class NonBlockingPumpInputStream extends NonBlockingInputStream { + + private static final int DEFAULT_BUFFER_SIZE = 4096; + + // Read and write buffer are backed by the same array + private final ByteBuffer readBuffer; + private final ByteBuffer writeBuffer; + + private final OutputStream output; + + private boolean closed; + + private IOException ioException; + + public NonBlockingPumpInputStream() { + this(DEFAULT_BUFFER_SIZE); + } + + public NonBlockingPumpInputStream(int bufferSize) { + byte[] buf = new byte[bufferSize]; + this.readBuffer = ByteBuffer.wrap(buf); + this.writeBuffer = ByteBuffer.wrap(buf); + this.output = new NbpOutputStream(); + // There are no bytes available to read after initialization + readBuffer.limit(0); + } + + private static boolean rewind(ByteBuffer buffer, ByteBuffer other) { + // Extend limit of other buffer if there is additional input/output available + if (buffer.position() > other.position()) { + other.limit(buffer.position()); + } + // If we have reached the end of the buffer, rewind and set the new limit + if (buffer.position() == buffer.capacity()) { + buffer.rewind(); + buffer.limit(other.position()); + return true; + } else { + return false; + } + } + + public OutputStream getOutputStream() { + return this.output; + } + + private int wait(ByteBuffer buffer, long timeout) throws IOException { + Timeout t = new Timeout(timeout); + while (!closed && !buffer.hasRemaining() && !t.elapsed()) { + // Wake up waiting readers/writers + notifyAll(); + try { + wait(t.timeout()); + checkIoException(); + } catch (InterruptedException e) { + checkIoException(); + throw new InterruptedIOException(); + } + } + return buffer.hasRemaining() ? 0 : closed ? EOF : READ_EXPIRED; + } + + public synchronized int available() { + int count = readBuffer.remaining(); + if (writeBuffer.position() < readBuffer.position()) { + count += writeBuffer.position(); + } + return count; + } + + @Override + public synchronized int read(long timeout, boolean isPeek) throws IOException { + checkIoException(); + // Blocks until more input is available or the reader is closed. + int res = wait(readBuffer, timeout); + if (res >= 0) { + res = readBuffer.get() & 0x00FF; + } + rewind(readBuffer, writeBuffer); + return res; + } + + @Override + public synchronized int readBuffered(byte[] b, int off, int len, long timeout) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || off + len < b.length) { + throw new IllegalArgumentException(); + } else if (len == 0) { + return 0; + } else { + checkIoException(); + int res = wait(readBuffer, timeout); + if (res >= 0) { + res = 0; + while (res < len && readBuffer.hasRemaining()) { + b[off + res++] = (byte) (readBuffer.get() & 0x00FF); + } + } + rewind(readBuffer, writeBuffer); + return res; + } + } + + public synchronized void setIoException(IOException exception) { + this.ioException = exception; + notifyAll(); + } + + protected synchronized void checkIoException() throws IOException { + if (ioException != null) { + throw ioException; + } + } + + synchronized void write(byte[] cbuf, int off, int len) throws IOException { + while (len > 0) { + // Blocks until there is new space available for buffering or the + // reader is closed. + if (wait(writeBuffer, 0L) == EOF) { + throw new ClosedException(); + } + // Copy as much characters as we can + int count = Math.min(len, writeBuffer.remaining()); + writeBuffer.put(cbuf, off, count); + off += count; + len -= count; + // Update buffer states and rewind if necessary + rewind(writeBuffer, readBuffer); + } + } + + synchronized void flush() { + // Avoid waking up readers when there is nothing to read + if (readBuffer.hasRemaining()) { + // Notify readers + notifyAll(); + } + } + + @Override + public synchronized void close() throws IOException { + this.closed = true; + notifyAll(); + } + + private class NbpOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + NonBlockingPumpInputStream.this.write(new byte[]{(byte) b}, 0, 1); + } + + @Override + public void write(byte[] cbuf, int off, int len) throws IOException { + NonBlockingPumpInputStream.this.write(cbuf, off, len); + } + + @Override + public void flush() throws IOException { + NonBlockingPumpInputStream.this.flush(); + } + + @Override + public void close() throws IOException { + NonBlockingPumpInputStream.this.close(); + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/NonBlockingPumpReader.java b/net-cli/src/main/java/org/jline/utils/NonBlockingPumpReader.java new file mode 100644 index 0000000..bd922f7 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/NonBlockingPumpReader.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2002-2017, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.Writer; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +public class NonBlockingPumpReader extends NonBlockingReader { + + private static final int DEFAULT_BUFFER_SIZE = 4096; + /** + * Main lock guarding all access + */ + final ReentrantLock lock; + private final char[] buffer; + /** + * Condition for waiting takes + */ + private final Condition notEmpty; + /** + * Condition for waiting puts + */ + private final Condition notFull; + private final Writer writer; + private int read; + private int write; + private int count; + private boolean closed; + + public NonBlockingPumpReader() { + this(DEFAULT_BUFFER_SIZE); + } + + public NonBlockingPumpReader(int bufferSize) { + this.buffer = new char[bufferSize]; + this.writer = new NbpWriter(); + this.lock = new ReentrantLock(); + this.notEmpty = lock.newCondition(); + this.notFull = lock.newCondition(); + } + + public Writer getWriter() { + return this.writer; + } + + @Override + public boolean ready() { + return available() > 0; + } + + public int available() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + return count; + } finally { + lock.unlock(); + } + } + + @Override + protected int read(long timeout, boolean isPeek) throws IOException { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + // Blocks until more input is available or the reader is closed. + if (!closed && count == 0) { + try { + if (timeout > 0L) { + notEmpty.await(timeout, TimeUnit.MILLISECONDS); + } else { + notEmpty.await(); + } + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException().initCause(e); + } + } + if (closed) { + return EOF; + } else if (count == 0) { + return READ_EXPIRED; + } else { + if (isPeek) { + return buffer[read]; + } else { + int res = buffer[read]; + if (++read == buffer.length) { + read = 0; + } + --count; + notFull.signal(); + return res; + } + } + } finally { + lock.unlock(); + } + } + + @Override + public int readBuffered(char[] b, int off, int len, long timeout) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || off + len < b.length) { + throw new IllegalArgumentException(); + } else if (len == 0) { + return 0; + } else { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + if (!closed && count == 0) { + try { + if (timeout > 0) { + if (!notEmpty.await(timeout, TimeUnit.MILLISECONDS)) { + throw new IOException("Timeout reading"); + } + } else { + notEmpty.await(); + } + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException().initCause(e); + } + } + if (closed) { + return EOF; + } else if (count == 0) { + return READ_EXPIRED; + } else { + int r = Math.min(len, count); + for (int i = 0; i < r; i++) { + b[off + i] = buffer[read++]; + if (read == buffer.length) { + read = 0; + } + } + count -= r; + notFull.signal(); + return r; + } + } finally { + lock.unlock(); + } + } + } + + void write(char[] cbuf, int off, int len) throws IOException { + if (len > 0) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + while (len > 0) { + // Blocks until there is new space available for buffering or the + // reader is closed. + if (!closed && count == buffer.length) { + try { + notFull.await(); + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException().initCause(e); + } + } + if (closed) { + throw new IOException("Closed"); + } + while (len > 0 && count < buffer.length) { + buffer[write++] = cbuf[off++]; + count++; + len--; + if (write == buffer.length) { + write = 0; + } + } + notEmpty.signal(); + } + } finally { + lock.unlock(); + } + } + } + + @Override + public void close() throws IOException { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + this.closed = true; + this.notEmpty.signalAll(); + this.notFull.signalAll(); + } finally { + lock.unlock(); + } + } + + private class NbpWriter extends Writer { + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + NonBlockingPumpReader.this.write(cbuf, off, len); + } + + @Override + public void flush() throws IOException { + } + + @Override + public void close() throws IOException { + NonBlockingPumpReader.this.close(); + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/NonBlockingReader.java b/net-cli/src/main/java/org/jline/utils/NonBlockingReader.java new file mode 100644 index 0000000..8b25ff5 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/NonBlockingReader.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.Reader; + +/** + * Non blocking reader + */ +public abstract class NonBlockingReader extends Reader { + public static final int EOF = -1; + public static final int READ_EXPIRED = -2; + + /** + * Shuts down the thread that is handling blocking I/O. Note that if the + * thread is currently blocked waiting for I/O it will not actually + * shut down until the I/O is received. + */ + public void shutdown() { + } + + @Override + public int read() throws IOException { + return read(0L, false); + } + + /** + * Peeks to see if there is a byte waiting in the input stream without + * actually consuming the byte. + * + * @param timeout The amount of time to wait, 0 == forever + * @return -1 on eof, -2 if the timeout expired with no available input + * or the character that was read (without consuming it). + * @throws IOException if anything wrong happens + */ + public int peek(long timeout) throws IOException { + return read(timeout, true); + } + + /** + * Attempts to read a character from the input stream for a specific + * period of time. + * + * @param timeout The amount of time to wait for the character + * @return The character read, -1 if EOF is reached, or -2 if the + * read timed out. + * @throws IOException if anything wrong happens + */ + public int read(long timeout) throws IOException { + return read(timeout, false); + } + + /** + * This version of read() is very specific to jline's purposes, it + * will always always return a single byte at a time, rather than filling + * the entire buffer. + * + * @param b the buffer + * @param off the offset in the buffer + * @param len the maximum number of chars to read + * @throws IOException if anything wrong happens + */ + @Override + public int read(char[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + int c = this.read(0L); + + if (c == EOF) { + return EOF; + } + b[off] = (char) c; + return 1; + } + + public int readBuffered(char[] b) throws IOException { + return readBuffered(b, 0L); + } + + public int readBuffered(char[] b, long timeout) throws IOException { + return readBuffered(b, 0, b.length, timeout); + } + + public abstract int readBuffered(char[] b, int off, int len, long timeout) throws IOException; + + public int available() { + return 0; + } + + /** + * Attempts to read a character from the input stream for a specific + * period of time. + * + * @param timeout The amount of time to wait for the character + * @param isPeek trueif the character read must not be consumed + * @return The character read, -1 if EOF is reached, or -2 if the + * read timed out. + * @throws IOException if anything wrong happens + */ + protected abstract int read(long timeout, boolean isPeek) throws IOException; +} diff --git a/net-cli/src/main/java/org/jline/utils/NonBlockingReaderImpl.java b/net-cli/src/main/java/org/jline/utils/NonBlockingReaderImpl.java new file mode 100644 index 0000000..d483bb6 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/NonBlockingReaderImpl.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.Reader; + +/** + * This class wraps a regular reader and allows it to appear as if it + * is non-blocking; that is, reads can be performed against it that timeout + * if no data is seen for a period of time. This effect is achieved by having + * a separate thread perform all non-blocking read requests and then + * waiting on the thread to complete. + * + *

VERY IMPORTANT NOTES + *

    + *
  • This class is not thread safe. It expects at most one reader. + *
  • The {@link #shutdown()} method must be called in order to shut down + * the thread that handles blocking I/O. + *
+ * + * @author Scott C. Gray <scottgray1@gmail.com> + * @since 2.7 + */ +public class NonBlockingReaderImpl extends NonBlockingReader { + public static final int READ_EXPIRED = -2; + + private final Reader in; // The actual input stream + private int ch = READ_EXPIRED; // Recently read character + + private final String name; + private boolean threadIsReading = false; + private IOException exception = null; + private final long threadDelay = 60 * 1000; + private Thread thread; + + /** + * Creates a NonBlockingReader out of a normal blocking + * reader. Note that this call also spawn a separate thread to perform the + * blocking I/O on behalf of the thread that is using this class. The + * {@link #shutdown()} method must be called in order to shut this thread down. + * + * @param name The reader name + * @param in The reader to wrap + */ + public NonBlockingReaderImpl(String name, Reader in) { + this.in = in; + this.name = name; + } + + private synchronized void startReadingThreadIfNeeded() { + if (thread == null) { + thread = new Thread(this::run); + thread.setName(name + " non blocking reader thread"); + thread.setDaemon(true); + thread.start(); + } + } + + /** + * Shuts down the thread that is handling blocking I/O. Note that if the + * thread is currently blocked waiting for I/O it will not actually + * shut down until the I/O is received. + */ + public synchronized void shutdown() { + if (thread != null) { + notify(); + } + } + + @Override + public void close() throws IOException { + /* + * The underlying input stream is closed first. This means that if the + * I/O thread was blocked waiting on input, it will be woken for us. + */ + in.close(); + shutdown(); + } + + @Override + public synchronized boolean ready() throws IOException { + return ch >= 0 || in.ready(); + } + + @Override + public int readBuffered(char[] b, int off, int len, long timeout) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || off + len < b.length) { + throw new IllegalArgumentException(); + } else if (len == 0) { + return 0; + } else if (exception != null) { + assert ch == READ_EXPIRED; + IOException toBeThrown = exception; + exception = null; + throw toBeThrown; + } else if (ch >= -1) { + b[0] = (char) ch; + ch = READ_EXPIRED; + return 1; + } else if (!threadIsReading && timeout <= 0) { + return in.read(b, off, len); + } else { + // TODO: rework implementation to read as much as possible + int c = read(timeout, false); + if (c >= 0) { + b[off] = (char) c; + return 1; + } else { + return c; + } + } + } + + /** + * Attempts to read a character from the input stream for a specific + * period of time. + * + * @param timeout The amount of time to wait for the character + * @return The character read, -1 if EOF is reached, or -2 if the + * read timed out. + */ + protected synchronized int read(long timeout, boolean isPeek) throws IOException { + /* + * If the thread hit an IOException, we report it. + */ + if (exception != null) { + assert ch == READ_EXPIRED; + IOException toBeThrown = exception; + if (!isPeek) exception = null; + throw toBeThrown; + } + + /* + * If there was a pending character from the thread, then + * we send it. If the timeout is 0L or the thread was shut down + * then do a local read. + */ + if (ch >= -1) { + assert exception == null; + } else if (!isPeek && timeout <= 0L && !threadIsReading) { + ch = in.read(); + } else { + /* + * If the thread isn't reading already, then ask it to do so. + */ + if (!threadIsReading) { + threadIsReading = true; + startReadingThreadIfNeeded(); + notifyAll(); + } + + /* + * So the thread is currently doing the reading for us. So + * now we play the waiting game. + */ + Timeout t = new Timeout(timeout); + while (!t.elapsed()) { + try { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + wait(t.timeout()); + } catch (InterruptedException e) { + exception = (IOException) new InterruptedIOException().initCause(e); + } + + if (exception != null) { + assert ch == READ_EXPIRED; + + IOException toBeThrown = exception; + if (!isPeek) exception = null; + throw toBeThrown; + } + + if (ch >= -1) { + assert exception == null; + break; + } + } + } + + /* + * ch is the character that was just read. Either we set it because + * a local read was performed or the read thread set it (or failed to + * change it). We will return it's value, but if this was a peek + * operation, then we leave it in place. + */ + int ret = ch; + if (!isPeek) { + ch = READ_EXPIRED; + } + return ret; + } + + private void run() { + Log.debug("NonBlockingReader start"); + boolean needToRead; + + try { + while (true) { + + /* + * Synchronize to grab variables accessed by both this thread + * and the accessing thread. + */ + synchronized (this) { + needToRead = this.threadIsReading; + + try { + /* + * Nothing to do? Then wait. + */ + if (!needToRead) { + wait(threadDelay); + } + } catch (InterruptedException e) { + /* IGNORED */ + } + + needToRead = this.threadIsReading; + if (!needToRead) { + return; + } + } + + /* + * We're not shutting down, but we need to read. This cannot + * happen while we are holding the lock (which we aren't now). + */ + int charRead = READ_EXPIRED; + IOException failure = null; + try { + charRead = in.read(); + // if (charRead < 0) { + // continue; + // } + } catch (IOException e) { + failure = e; + // charRead = -1; + } + + /* + * Re-grab the lock to update the state. + */ + synchronized (this) { + exception = failure; + ch = charRead; + threadIsReading = false; + notify(); + } + } + } catch (Throwable t) { + Log.warn("Error in NonBlockingReader thread", t); + } finally { + Log.debug("NonBlockingReader shutdown"); + synchronized (this) { + thread = null; + threadIsReading = false; + } + } + } + + public synchronized void clear() throws IOException { + while (ready()) { + read(); + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/OSUtils.java b/net-cli/src/main/java/org/jline/utils/OSUtils.java new file mode 100644 index 0000000..5da9fb2 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/OSUtils.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.File; + +public class OSUtils { + + public static final boolean IS_LINUX = + System.getProperty("os.name").toLowerCase().contains("linux"); + + public static final boolean IS_WINDOWS = + System.getProperty("os.name").toLowerCase().contains("win"); + + public static final boolean IS_OSX = + System.getProperty("os.name").toLowerCase().contains("mac"); + + public static final boolean IS_AIX = + System.getProperty("os.name").toLowerCase().contains("aix"); + + public static final boolean IS_CYGWIN = + IS_WINDOWS && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/"); + + @Deprecated + public static final boolean IS_MINGW = IS_WINDOWS + && System.getenv("MSYSTEM") != null + && System.getenv("MSYSTEM").startsWith("MINGW"); + + public static final boolean IS_MSYSTEM = IS_WINDOWS + && System.getenv("MSYSTEM") != null + && (System.getenv("MSYSTEM").startsWith("MINGW") + || System.getenv("MSYSTEM").equals("MSYS")); + + public static final boolean IS_WSL = System.getenv("WSL_DISTRO_NAME") != null; + + public static final boolean IS_WSL1 = IS_WSL && System.getenv("WSL_INTEROP") == null; + + public static final boolean IS_WSL2 = IS_WSL && !IS_WSL1; + + public static final boolean IS_CONEMU = IS_WINDOWS && System.getenv("ConEmuPID") != null; + + public static String TTY_COMMAND; + public static String STTY_COMMAND; + public static String STTY_F_OPTION; + public static String INFOCMP_COMMAND; + public static String TEST_COMMAND; + + static { + boolean cygwinOrMsys = OSUtils.IS_CYGWIN || OSUtils.IS_MSYSTEM; + String suffix = cygwinOrMsys ? ".exe" : ""; + String tty = null; + String stty = null; + String sttyfopt = null; + String infocmp = null; + String test = null; + String path = System.getenv("PATH"); + if (path != null) { + String[] paths = path.split(File.pathSeparator); + for (String p : paths) { + File ttyFile = new File(p, "tty" + suffix); + if (tty == null && isExecutable(ttyFile)) { + tty = ttyFile.getAbsolutePath(); + } + File sttyFile = new File(p, "stty" + suffix); + if (stty == null && isExecutable(sttyFile)) { + stty = sttyFile.getAbsolutePath(); + } + File infocmpFile = new File(p, "infocmp" + suffix); + if (infocmp == null && isExecutable(infocmpFile)) { + infocmp = infocmpFile.getAbsolutePath(); + } + File testFile = new File(p, "test" + suffix); + if (test == null && isExecutable(testFile)) { + test = testFile.getAbsolutePath(); + } + } + } + if (tty == null) { + tty = "tty" + suffix; + } + if (stty == null) { + stty = "stty" + suffix; + } + if (infocmp == null) { + infocmp = "infocmp" + suffix; + } + if (test == null) { + test = "test" + suffix; + } + sttyfopt = IS_OSX ? "-f" : "-F"; + TTY_COMMAND = tty; + STTY_COMMAND = stty; + STTY_F_OPTION = sttyfopt; + INFOCMP_COMMAND = infocmp; + TEST_COMMAND = test; + } + + private static boolean isExecutable(File f) { + return f.canExecute() && !f.isDirectory(); + } +} diff --git a/net-cli/src/main/java/org/jline/utils/PumpReader.java b/net-cli/src/main/java/org/jline/utils/PumpReader.java new file mode 100644 index 0000000..e89b5d0 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/PumpReader.java @@ -0,0 +1,467 @@ +/* + * Copyright (c) 2002-2017, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +public class PumpReader extends Reader { + + private static final int EOF = -1; + private static final int DEFAULT_BUFFER_SIZE = 4096; + + // Read and write buffer are backed by the same array + private final CharBuffer readBuffer; + private final CharBuffer writeBuffer; + + private final Writer writer; + + private boolean closed; + + public PumpReader() { + this(DEFAULT_BUFFER_SIZE); + } + + public PumpReader(int bufferSize) { + char[] buf = new char[Math.max(bufferSize, 2)]; + this.readBuffer = CharBuffer.wrap(buf); + this.writeBuffer = CharBuffer.wrap(buf); + this.writer = new Writer(this); + + // There are no bytes available to read after initialization + readBuffer.limit(0); + } + + private static boolean rewind(CharBuffer buffer, CharBuffer other) { + // Extend limit of other buffer if there is additional input/output available + if (buffer.position() > other.position()) { + other.limit(buffer.position()); + } + + // If we have reached the end of the buffer, rewind and set the new limit + if (buffer.position() == buffer.capacity()) { + buffer.rewind(); + buffer.limit(other.position()); + return true; + } else { + return false; + } + } + + public java.io.Writer getWriter() { + return this.writer; + } + + public java.io.InputStream createInputStream(Charset charset) { + return new InputStream(this, charset); + } + + /** + * Blocks until more input is available, even if {@link #readBuffer} already + * contains some chars; or until the reader is closed. + * + * @return true if more input is available, false if no additional input is + * available and the reader is closed + * @throws InterruptedIOException If {@link #wait()} is interrupted + */ + private boolean waitForMoreInput() throws InterruptedIOException { + if (!writeBuffer.hasRemaining()) { + throw new AssertionError("No space in write buffer"); + } + + int oldRemaining = readBuffer.remaining(); + + do { + if (closed) { + return false; + } + + // Wake up waiting writers + notifyAll(); + + try { + wait(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + } while (readBuffer.remaining() <= oldRemaining); + + return true; + } + + /** + * Waits until {@code buffer.hasRemaining() == true}, or it is false and + * the reader is {@link #closed}. + * + * @return true if {@code buffer.hasRemaining() == true}; false otherwise + * when reader is closed + */ + private boolean wait(CharBuffer buffer) throws InterruptedIOException { + while (!buffer.hasRemaining()) { + if (closed) { + return false; + } + + // Wake up waiting readers/writers + notifyAll(); + + try { + wait(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + } + + return true; + } + + /** + * Blocks until input is available or the reader is closed. + * + * @return true if input is available, false if no input is available and the reader is closed + * @throws InterruptedIOException If {@link #wait()} is interrupted + */ + private boolean waitForInput() throws InterruptedIOException { + return wait(readBuffer); + } + + /** + * Blocks until there is new space available for buffering or the + * reader is closed. + * + * @throws InterruptedIOException If {@link #wait()} is interrupted + * @throws ClosedException If the reader was closed + */ + private void waitForBufferSpace() throws InterruptedIOException, ClosedException { + // Check `closed` to throw even if writer buffer has space available + if (!wait(writeBuffer) || closed) { + throw new ClosedException(); + } + } + + /** + * Attempts to find additional input by rewinding the {@link #readBuffer}. + * Updates the {@link #writeBuffer} to make read bytes available for buffering. + * + * @return If more input is available + */ + private boolean rewindReadBuffer() { + boolean rw = rewind(readBuffer, writeBuffer) && readBuffer.hasRemaining(); + notifyAll(); + return rw; + } + + /** + * Attempts to find additional buffer space by rewinding the {@link #writeBuffer}. + * Updates the {@link #readBuffer} to make written bytes available to the reader. + */ + private void rewindWriteBuffer() { + rewind(writeBuffer, readBuffer); + notifyAll(); + } + + @Override + public synchronized boolean ready() { + return readBuffer.hasRemaining(); + } + + public synchronized int available() { + int count = readBuffer.remaining(); + if (writeBuffer.position() < readBuffer.position()) { + count += writeBuffer.position(); + } + return count; + } + + @Override + public synchronized int read() throws IOException { + if (!waitForInput()) { + return EOF; + } + + int b = readBuffer.get(); + rewindReadBuffer(); + return b; + } + + private int copyFromBuffer(char[] cbuf, int off, int len) { + len = Math.min(len, readBuffer.remaining()); + readBuffer.get(cbuf, off, len); + return len; + } + + @Override + public synchronized int read(char[] cbuf, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + + if (!waitForInput()) { + return EOF; + } + + int count = copyFromBuffer(cbuf, off, len); + if (rewindReadBuffer() && count < len) { + count += copyFromBuffer(cbuf, off + count, len - count); + rewindReadBuffer(); + } + + return count; + } + + @Override + public synchronized int read(CharBuffer target) throws IOException { + if (!target.hasRemaining()) { + return 0; + } + + if (!waitForInput()) { + return EOF; + } + + int count = readBuffer.read(target); + if (rewindReadBuffer() && target.hasRemaining()) { + count += readBuffer.read(target); + rewindReadBuffer(); + } + + return count; + } + + private void encodeBytes(CharsetEncoder encoder, ByteBuffer output) throws IOException { + int oldPos = output.position(); + CoderResult result = encoder.encode(readBuffer, output, false); + int encodedCount = output.position() - oldPos; + + if (result.isUnderflow()) { + boolean hasMoreInput = rewindReadBuffer(); + boolean reachedEndOfInput = false; + + // If encoding did not make any progress must block for more input + if (encodedCount == 0 && !hasMoreInput) { + reachedEndOfInput = !waitForMoreInput(); + } + + result = encoder.encode(readBuffer, output, reachedEndOfInput); + if (result.isError()) { + result.throwException(); + } + if (!reachedEndOfInput && output.position() - oldPos == 0) { + throw new AssertionError("Failed to encode any chars"); + } + rewindReadBuffer(); + } else if (result.isOverflow()) { + if (encodedCount == 0) { + throw new AssertionError("Output buffer has not enough space"); + } + } else { + result.throwException(); + } + } + + synchronized int readBytes(CharsetEncoder encoder, byte[] b, int off, int len) throws IOException { + if (!waitForInput()) { + return 0; + } + + ByteBuffer output = ByteBuffer.wrap(b, off, len); + encodeBytes(encoder, output); + return output.position() - off; + } + + synchronized void readBytes(CharsetEncoder encoder, ByteBuffer output) throws IOException { + if (!waitForInput()) { + return; + } + + encodeBytes(encoder, output); + } + + synchronized void write(char c) throws IOException { + waitForBufferSpace(); + writeBuffer.put(c); + rewindWriteBuffer(); + } + + synchronized void write(char[] cbuf, int off, int len) throws IOException { + while (len > 0) { + waitForBufferSpace(); + + // Copy as much characters as we can + int count = Math.min(len, writeBuffer.remaining()); + writeBuffer.put(cbuf, off, count); + + off += count; + len -= count; + + // Update buffer states and rewind if necessary + rewindWriteBuffer(); + } + } + + synchronized void write(String str, int off, int len) throws IOException { + char[] buf = writeBuffer.array(); + + while (len > 0) { + waitForBufferSpace(); + + // Copy as much characters as we can + int count = Math.min(len, writeBuffer.remaining()); + // CharBuffer.put(String) doesn't use getChars so do it manually + str.getChars(off, off + count, buf, writeBuffer.position()); + writeBuffer.position(writeBuffer.position() + count); + + off += count; + len -= count; + + // Update buffer states and rewind if necessary + rewindWriteBuffer(); + } + } + + synchronized void flush() { + // Avoid waking up readers when there is nothing to read + if (readBuffer.hasRemaining()) { + // Notify readers + notifyAll(); + } + } + + @Override + public synchronized void close() throws IOException { + this.closed = true; + notifyAll(); + } + + private static class Writer extends java.io.Writer { + + private final PumpReader reader; + + private Writer(PumpReader reader) { + this.reader = reader; + } + + @Override + public void write(int c) throws IOException { + reader.write((char) c); + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + reader.write(cbuf, off, len); + } + + @Override + public void write(String str, int off, int len) throws IOException { + reader.write(str, off, len); + } + + @Override + public void flush() throws IOException { + reader.flush(); + } + + @Override + public void close() throws IOException { + reader.close(); + } + } + + private static class InputStream extends java.io.InputStream { + + private final PumpReader reader; + private final CharsetEncoder encoder; + + // To encode a character with multiple bytes (e.g. certain Unicode characters) + // we need enough space to encode them. Reading would fail if the read() method + // is used to read a single byte in these cases. + // Use this buffer to ensure we always have enough space to encode a character. + private final ByteBuffer buffer; + + private InputStream(PumpReader reader, Charset charset) { + this.reader = reader; + this.encoder = charset.newEncoder() + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .onMalformedInput(CodingErrorAction.REPLACE); + this.buffer = ByteBuffer.allocate((int) Math.ceil(encoder.maxBytesPerChar() * 2)); + + // No input available after initialization + buffer.limit(0); + } + + @Override + public int available() throws IOException { + return (int) (reader.available() * (double) this.encoder.averageBytesPerChar()) + buffer.remaining(); + } + + @Override + public int read() throws IOException { + if (!buffer.hasRemaining() && !readUsingBuffer()) { + return EOF; + } + + return buffer.get() & 0xFF; + } + + private boolean readUsingBuffer() throws IOException { + buffer.clear(); // Reset buffer + reader.readBytes(encoder, buffer); + buffer.flip(); + return buffer.hasRemaining(); + } + + private int copyFromBuffer(byte[] b, int off, int len) { + len = Math.min(len, buffer.remaining()); + buffer.get(b, off, len); + return len; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + + int read; + if (buffer.hasRemaining()) { + read = copyFromBuffer(b, off, len); + if (read == len) { + return len; + } + + off += read; + len -= read; + } else { + read = 0; + } + + // Do we have enough space to avoid buffering? + if (len >= buffer.capacity()) { + read += reader.readBytes(this.encoder, b, off, len); + } else if (readUsingBuffer()) { + read += copyFromBuffer(b, off, len); + } + + // Return EOF if we didn't read any bytes + return read == 0 ? EOF : read; + } + + @Override + public void close() throws IOException { + reader.close(); + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/ShutdownHooks.java b/net-cli/src/main/java/org/jline/utils/ShutdownHooks.java new file mode 100644 index 0000000..cbd71ee --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/ShutdownHooks.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Manages the JLine shutdown-hook thread and tasks to execute on shutdown. + * + * @author Jason Dillon + * @since 2.7 + */ +public final class ShutdownHooks { + private static final List tasks = new ArrayList<>(); + + private static Thread hook; + + public static synchronized T add(final T task) { + Objects.requireNonNull(task); + + // Install the hook thread if needed + if (hook == null) { + hook = addHook(new Thread("JLine Shutdown Hook") { + @Override + public void run() { + runTasks(); + } + }); + } + + // Track the task + Log.debug("Adding shutdown-hook task: ", task); + tasks.add(task); + + return task; + } + + private static synchronized void runTasks() { + Log.debug("Running all shutdown-hook tasks"); + + // Iterate through copy of tasks list + for (Task task : tasks.toArray(new Task[tasks.size()])) { + Log.debug("Running task: ", task); + try { + task.run(); + } catch (Throwable e) { + Log.warn("Task failed", e); + } + } + + tasks.clear(); + } + + private static Thread addHook(final Thread thread) { + Log.debug("Registering shutdown-hook: ", thread); + Runtime.getRuntime().addShutdownHook(thread); + return thread; + } + + public static synchronized void remove(final Task task) { + Objects.requireNonNull(task); + + // ignore if hook never installed + if (hook == null) { + return; + } + + // Drop the task + tasks.remove(task); + + // If there are no more tasks, then remove the hook thread + if (tasks.isEmpty()) { + removeHook(hook); + hook = null; + } + } + + private static void removeHook(final Thread thread) { + Log.debug("Removing shutdown-hook: ", thread); + + try { + Runtime.getRuntime().removeShutdownHook(thread); + } catch (IllegalStateException e) { + // The VM is shutting down, not a big deal; ignore + } + } + + /** + * Essentially a {@link Runnable} which allows running to throw an exception. + */ + public interface Task { + void run() throws Exception; + } +} diff --git a/net-cli/src/main/java/org/jline/utils/Signals.java b/net-cli/src/main/java/org/jline/utils/Signals.java new file mode 100644 index 0000000..ae5db98 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/Signals.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2002-2020, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.util.Objects; + +/** + * Signals helpers. + * + * @author Guillaume Nodet + * @since 3.0 + */ +public final class Signals { + + private Signals() { + } + + /** + * @param name the signal, CONT, STOP, etc... + * @param handler the callback to run + * @return an object that needs to be passed to the {@link #unregister(String, Object)} + * method to unregister the handler + */ + public static Object register(String name, Runnable handler) { + Objects.requireNonNull(handler); + return register(name, handler, handler.getClass().getClassLoader()); + } + + public static Object register(String name, final Runnable handler, ClassLoader loader) { + try { + Class signalHandlerClass = Class.forName("sun.misc.SignalHandler"); + // Implement signal handler + Object signalHandler = + Proxy.newProxyInstance(loader, new Class[]{signalHandlerClass}, (proxy, method, args) -> { + // only method we are proxying is handle() + if (method.getDeclaringClass() == Object.class) { + if ("toString".equals(method.getName())) { + return handler.toString(); + } + } else if (method.getDeclaringClass() == signalHandlerClass) { + Log.trace(() -> "Calling handler " + toString(handler) + " for signal " + name); + handler.run(); + } + return null; + }); + return doRegister(name, signalHandler); + } catch (Exception e) { + // Ignore this one too, if the above failed, the signal API is incompatible with what we're expecting + Log.debug("Error registering handler for signal ", name, e); + return null; + } + } + + public static Object registerDefault(String name) { + try { + Class signalHandlerClass = Class.forName("sun.misc.SignalHandler"); + return doRegister(name, signalHandlerClass.getField("SIG_DFL").get(null)); + } catch (Exception e) { + // Ignore this one too, if the above failed, the signal API is incompatible with what we're expecting + Log.debug("Error registering default handler for signal ", name, e); + return null; + } + } + + public static void unregister(String name, Object previous) { + try { + // We should make sure the current signal is the one we registered + if (previous != null) { + doRegister(name, previous); + } + } catch (Exception e) { + // Ignore + Log.debug("Error unregistering handler for signal ", name, e); + } + } + + private static Object doRegister(String name, Object handler) throws Exception { + Log.trace(() -> "Registering signal " + name + " with handler " + toString(handler)); + Class signalClass = Class.forName("sun.misc.Signal"); + Constructor constructor = signalClass.getConstructor(String.class); + Object signal; + try { + signal = constructor.newInstance(name); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof IllegalArgumentException) { + Log.trace(() -> "Ignoring unsupported signal " + name); + } else { + Log.debug("Error registering handler for signal ", name, e); + } + return null; + } + Class signalHandlerClass = Class.forName("sun.misc.SignalHandler"); + return signalClass.getMethod("handle", signalClass, signalHandlerClass).invoke(null, signal, handler); + } + + @SuppressWarnings("") + private static String toString(Object handler) { + try { + Class signalHandlerClass = Class.forName("sun.misc.SignalHandler"); + if (handler == signalHandlerClass.getField("SIG_DFL").get(null)) { + return "SIG_DFL"; + } + if (handler == signalHandlerClass.getField("SIG_IGN").get(null)) { + return "SIG_IGN"; + } + } catch (Throwable t) { + // ignore + } + return handler != null ? handler.toString() : "null"; + } +} diff --git a/net-cli/src/main/java/org/jline/utils/Status.java b/net-cli/src/main/java/org/jline/utils/Status.java new file mode 100644 index 0000000..bff9968 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/Status.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2002-2019, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.impl.AbstractTerminal; +import org.jline.utils.InfoCmp.Capability; + +public class Status { + + protected final Terminal terminal; + protected final boolean supported; + private final AttributedString ellipsis = + new AttributedStringBuilder().append("…", AttributedStyle.INVERSE).toAttributedString(); + protected boolean suspended = false; + protected AttributedString borderString; + protected int border = 0; + protected Display display; + protected List lines = Collections.emptyList(); + protected int scrollRegion; + + @SuppressWarnings("this-escape") + public Status(Terminal terminal) { + this.terminal = Objects.requireNonNull(terminal, "terminal can not be null"); + this.supported = terminal.getStringCapability(Capability.change_scroll_region) != null + && terminal.getStringCapability(Capability.save_cursor) != null + && terminal.getStringCapability(Capability.restore_cursor) != null + && terminal.getStringCapability(Capability.cursor_address) != null + && isValid(terminal.getSize()); + if (supported) { + display = new MovingCursorDisplay(terminal); + resize(); + display.reset(); + scrollRegion = display.rows - 1; + } + } + + public static Status getStatus(Terminal terminal) { + return getStatus(terminal, true); + } + + public static Optional getExistingStatus(Terminal terminal) { + return Optional.ofNullable(getStatus(terminal, false)); + } + + public static Status getStatus(Terminal terminal, boolean create) { + return terminal instanceof AbstractTerminal ? ((AbstractTerminal) terminal).getStatus(create) : null; + } + + private boolean isValid(Size size) { + return size.getRows() > 0 && size.getRows() < 1000 && size.getColumns() > 0 && size.getColumns() < 1000; + } + + public void close() { + terminal.puts(Capability.save_cursor); + terminal.puts(Capability.change_scroll_region, 0, display.rows - 1); + terminal.puts(Capability.restore_cursor); + terminal.flush(); + } + + public void setBorder(boolean border) { + this.border = border ? 1 : 0; + } + + public void resize() { + resize(terminal.getSize()); + } + + public void resize(Size size) { + display.resize(size.getRows(), size.getColumns()); + } + + public void reset() { + if (supported) { + display.reset(); + scrollRegion = display.rows; + terminal.puts(Capability.change_scroll_region, 0, scrollRegion); + } + } + + public void redraw() { + if (suspended) { + return; + } + update(lines); + } + + public void hide() { + update(Collections.emptyList()); + } + + public void update(List lines) { + update(lines, true); + } + + /** + * Returns true if the cursor may be misplaced and should + * be updated. + */ + public void update(List lines, boolean flush) { + if (!supported) { + return; + } + this.lines = new ArrayList<>(lines); + if (suspended) { + return; + } + + lines = new ArrayList<>(lines); + // add border + int rows = display.rows; + int columns = display.columns; + if (border == 1 && !lines.isEmpty() && rows > 1) { + lines.add(0, getBorderString(columns)); + } + // trim or complete lines to the full width + for (int i = 0; i < lines.size(); i++) { + AttributedString str = lines.get(i); + if (str.columnLength() > columns) { + str = new AttributedStringBuilder(columns) + .append(lines.get(i).columnSubSequence(0, columns - ellipsis.columnLength())) + .append(ellipsis) + .toAttributedString(); + } else if (str.columnLength() < columns) { + str = new AttributedStringBuilder(columns) + .append(str) + .append(' ', columns - str.columnLength()) + .toAttributedString(); + } + lines.set(i, str); + } + + List oldLines = this.display.oldLines; + + int newScrollRegion = display.rows - 1 - lines.size(); + // Update the scroll region if needed. + // Note that settings the scroll region usually moves the cursor, so we need to get ready for that. + if (newScrollRegion < scrollRegion) { + // We need to scroll up to grow the status bar + terminal.puts(Capability.save_cursor); + terminal.puts(Capability.cursor_address, scrollRegion, 0); + for (int i = newScrollRegion; i < scrollRegion; i++) { + terminal.puts(Capability.cursor_down); + } + terminal.puts(Capability.change_scroll_region, 0, newScrollRegion); + terminal.puts(Capability.restore_cursor); + for (int i = newScrollRegion; i < scrollRegion; i++) { + terminal.puts(Capability.cursor_up); + } + scrollRegion = newScrollRegion; + } else if (newScrollRegion > scrollRegion) { + terminal.puts(Capability.save_cursor); + terminal.puts(Capability.change_scroll_region, 0, newScrollRegion); + terminal.puts(Capability.restore_cursor); + scrollRegion = newScrollRegion; + } + + // if the display has more lines, we need to add empty ones to make sure they will be erased + List toDraw = new ArrayList<>(lines); + int nbToDraw = toDraw.size(); + int nbOldLines = oldLines.size(); + if (nbOldLines > nbToDraw) { + terminal.puts(Capability.save_cursor); + terminal.puts(Capability.cursor_address, display.rows - nbOldLines, 0); + for (int i = 0; i < nbOldLines - nbToDraw; i++) { + terminal.puts(Capability.clr_eol); + if (i < nbOldLines - nbToDraw - 1) { + terminal.puts(Capability.cursor_down); + } + oldLines.remove(0); + } + terminal.puts(Capability.restore_cursor); + } + // update display + display.update(lines, -1, flush); + } + + private AttributedString getBorderString(int columns) { + if (borderString == null || borderString.length() != columns) { + char borderChar = '─'; + AttributedStringBuilder bb = new AttributedStringBuilder(); + for (int i = 0; i < columns; i++) { + bb.append(borderChar); + } + borderString = bb.toAttributedString(); + } + return borderString; + } + + /** + * The {@code suspend} method is used when a full-screen. + * If the status was not already suspended, the lines + * used by the status are cleared during this call. + */ + public void suspend() { + if (!suspended) { + suspended = true; + } + } + + /** + * The {@code restore()} call is the opposite of {@code suspend()} and + * will make the status bar be updated again. + * If the status was suspended, the lines + * used by the status will be drawn during this call. + */ + public void restore() { + if (suspended) { + suspended = false; + update(this.lines); + } + } + + public int size() { + return size(this.lines); + } + + private int size(List lines) { + int l = lines.size(); + return l > 0 ? l + border : 0; + } + + @Override + public String toString() { + return "Status[" + "supported=" + supported + ']'; + } + + static class MovingCursorDisplay extends Display { + protected int firstLine; + + public MovingCursorDisplay(Terminal terminal) { + super(terminal, false); + } + + @Override + public void update(List newLines, int targetCursorPos, boolean flush) { + cursorPos = -1; + firstLine = rows - newLines.size(); + super.update(newLines, targetCursorPos, flush); + if (cursorPos != -1) { + terminal.puts(Capability.restore_cursor); + } + } + + @Override + protected void moveVisualCursorTo(int targetPos, List newLines) { + initCursor(); + super.moveVisualCursorTo(targetPos, newLines); + } + + @Override + protected int moveVisualCursorTo(int i1) { + initCursor(); + return super.moveVisualCursorTo(i1); + } + + void initCursor() { + if (cursorPos == -1) { + terminal.puts(Capability.save_cursor); + terminal.puts(Capability.cursor_address, firstLine, 0); + cursorPos = 0; + } + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/StyleResolver.java b/net-cli/src/main/java/org/jline/utils/StyleResolver.java new file mode 100644 index 0000000..c493b83 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/StyleResolver.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.util.Locale; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import static java.util.Objects.requireNonNull; +import static org.jline.utils.AttributedStyle.BLACK; +import static org.jline.utils.AttributedStyle.BLUE; +import static org.jline.utils.AttributedStyle.BRIGHT; +import static org.jline.utils.AttributedStyle.CYAN; +import static org.jline.utils.AttributedStyle.DEFAULT; +import static org.jline.utils.AttributedStyle.GREEN; +import static org.jline.utils.AttributedStyle.MAGENTA; +import static org.jline.utils.AttributedStyle.RED; +import static org.jline.utils.AttributedStyle.WHITE; +import static org.jline.utils.AttributedStyle.YELLOW; + +// TODO: document style specification + +/** + * Resolves named (or source-referenced) {@link AttributedStyle}. + * + * @since 3.6 + */ +public class StyleResolver { + private static final Logger log = Logger.getLogger(StyleResolver.class.getName()); + + private final Function source; + + public StyleResolver(final Function source) { + this.source = requireNonNull(source); + } + + /** + * Returns the RGB color for the given name. + *

+ * Bright color can be specified with: {@code !} or {@code bright-}. + *

+ * Full xterm256 color can be specified with: {@code ~}. + * RGB colors can be specified with: {@code x} or {@code #} where {@code rgb} is + * a 24 bits hexadecimal color. + * + * @param name the name of the color + * @return color code, or {@code null} if unable to determine. + */ + private static Integer colorRgb(String name) { + name = name.toLowerCase(Locale.US); + // check hexadecimal color + if (name.charAt(0) == 'x' || name.charAt(0) == '#') { + try { + return Integer.parseInt(name.substring(1), 16); + } catch (NumberFormatException e) { + log.warning("Invalid hexadecimal color: " + name); + return null; + } + } else { + // load indexed color + Integer color = color(name); + if (color != null && color != -1) { + color = Colors.DEFAULT_COLORS_256[color]; + } + return color; + } + } + + /** + * Returns the color identifier for the given name. + *

+ * Bright color can be specified with: {@code !} or {@code bright-}. + *

+ * Full xterm256 color can be specified with: {@code ~}. + * + * @param name the name of the color + * @return color code, or {@code null} if unable to determine. + */ + private static Integer color(String name) { + int flags = 0; + + if (name.equals("default")) { + return -1; + } + // extract bright flag from color name + else if (name.charAt(0) == '!') { + name = name.substring(1); + flags = BRIGHT; + } else if (name.startsWith("bright-")) { + name = name.substring(7); + flags = BRIGHT; + } else if (name.charAt(0) == '~') { + name = name.substring(1); + try { + return Colors.rgbColor(name); + } catch (IllegalArgumentException e) { + log.warning("Invalid style-color name: " + name); + return null; + } + } + + switch (name) { + case "black": + case "k": + return flags + BLACK; + + case "red": + case "r": + return flags + RED; + + case "green": + case "g": + return flags + GREEN; + + case "yellow": + case "y": + return flags + YELLOW; + + case "blue": + case "b": + return flags + BLUE; + + case "magenta": + case "m": + return flags + MAGENTA; + + case "cyan": + case "c": + return flags + CYAN; + + case "white": + case "w": + return flags + WHITE; + } + + return null; + } + + // TODO: could consider a small cache to reduce style calculations? + + /** + * Resolve the given style specification. + *

+ * If for some reason the specification is invalid, then {@link AttributedStyle#DEFAULT} will be used. + * + * @param spec the specification + * @return the style + */ + public AttributedStyle resolve(final String spec) { + requireNonNull(spec); + + if (log.isLoggable(Level.FINEST)) { + log.finest("Resolve: " + spec); + } + + int i = spec.indexOf(":-"); + if (i != -1) { + String[] parts = spec.split(":-"); + return resolve(parts[0].trim(), parts[1].trim()); + } + + return apply(DEFAULT, spec); + } + + /** + * Resolve the given style specification. + *

+ * If this resolves to {@link AttributedStyle#DEFAULT} then given default specification is used if non-null. + * + * @param spec the specification + * @param defaultSpec the default specifiaction + * @return the style + */ + public AttributedStyle resolve(final String spec, final String defaultSpec) { + requireNonNull(spec); + + if (log.isLoggable(Level.FINEST)) { + log.finest(String.format("Resolve: %s; default: %s", spec, defaultSpec)); + } + + AttributedStyle style = apply(DEFAULT, spec); + if (style == DEFAULT && defaultSpec != null) { + style = apply(style, defaultSpec); + } + return style; + } + + /** + * Apply style specification. + * + * @param style the style to apply to + * @param spec the specification + * @return the new style + */ + private AttributedStyle apply(AttributedStyle style, final String spec) { + if (log.isLoggable(Level.FINEST)) { + log.finest("Apply: " + spec); + } + + for (String item : spec.split(",")) { + item = item.trim(); + if (item.isEmpty()) { + continue; + } + + if (item.startsWith(".")) { + style = applyReference(style, item); + } else if (item.contains(":")) { + style = applyColor(style, item); + } else if (item.matches("[0-9]+(;[0-9]+)*")) { + style = applyAnsi(style, item); + } else { + style = applyNamed(style, item); + } + } + + return style; + } + + private AttributedStyle applyAnsi(final AttributedStyle style, final String spec) { + if (log.isLoggable(Level.FINEST)) { + log.finest("Apply-ansi: " + spec); + } + + return new AttributedStringBuilder() + .style(style) + .ansiAppend("\033[" + spec + "m") + .style(); + } + + /** + * Apply source-referenced named style. + * + * @param style the style to apply to + * @param spec the specification + * @return the new style + */ + private AttributedStyle applyReference(final AttributedStyle style, final String spec) { + if (log.isLoggable(Level.FINEST)) { + log.finest("Apply-reference: " + spec); + } + + if (spec.length() == 1) { + log.warning("Invalid style-reference; missing discriminator: " + spec); + } else { + String name = spec.substring(1); + String resolvedSpec = source.apply(name); + if (resolvedSpec != null) { + return apply(style, resolvedSpec); + } + // null is normal if source has not be configured with named style + } + + return style; + } + + /** + * Apply default named styles. + * + * @param style the style to apply to + * @param name the named style + * @return the new style + */ + private AttributedStyle applyNamed(final AttributedStyle style, final String name) { + if (log.isLoggable(Level.FINEST)) { + log.finest("Apply-named: " + name); + } + + // TODO: consider short aliases for named styles + + switch (name.toLowerCase(Locale.US)) { + case "default": + return DEFAULT; + + case "bold": + return style.bold(); + + case "faint": + return style.faint(); + + case "italic": + return style.italic(); + + case "underline": + return style.underline(); + + case "blink": + return style.blink(); + + case "inverse": + return style.inverse(); + + case "inverse-neg": + case "inverseneg": + return style.inverseNeg(); + + case "conceal": + return style.conceal(); + + case "crossed-out": + case "crossedout": + return style.crossedOut(); + + case "hidden": + return style.hidden(); + + default: + log.warning("Unknown style: " + name); + return style; + } + } + + // TODO: consider simplify and always using StyleColor, for now for compat with other bits leaving syntax complexity + + /** + * Apply color styles specification. + * + * @param style The style to apply to + * @param spec Color specification: {@code :} + * @return The new style + */ + private AttributedStyle applyColor(final AttributedStyle style, final String spec) { + if (log.isLoggable(Level.FINEST)) { + log.finest("Apply-color: " + spec); + } + + // extract color-mode:color-name + String[] parts = spec.split(":", 2); + String colorMode = parts[0].trim(); + String colorName = parts[1].trim(); + + // resolve the color-name + Integer color; + // resolve and apply color-mode + switch (colorMode.toLowerCase(Locale.US)) { + case "foreground": + case "fg": + case "f": + color = color(colorName); + if (color == null) { + log.warning("Invalid color-name: " + colorName); + break; + } + return color >= 0 ? style.foreground(color) : style.foregroundDefault(); + + case "background": + case "bg": + case "b": + color = color(colorName); + if (color == null) { + log.warning("Invalid color-name: " + colorName); + break; + } + return color >= 0 ? style.background(color) : style.backgroundDefault(); + + case "foreground-rgb": + case "fg-rgb": + case "f-rgb": + color = colorRgb(colorName); + if (color == null) { + log.warning("Invalid color-name: " + colorName); + break; + } + return color >= 0 ? style.foregroundRgb(color) : style.foregroundDefault(); + + case "background-rgb": + case "bg-rgb": + case "b-rgb": + color = colorRgb(colorName); + if (color == null) { + log.warning("Invalid color-name: " + colorName); + break; + } + return color >= 0 ? style.backgroundRgb(color) : style.backgroundDefault(); + + default: + log.warning("Invalid color-mode: " + colorMode); + } + return style; + } +} diff --git a/net-cli/src/main/java/org/jline/utils/Timeout.java b/net-cli/src/main/java/org/jline/utils/Timeout.java new file mode 100644 index 0000000..ed42fa1 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/Timeout.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2002-2018, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +/** + * Helper class ti use during I/O operations with an eventual timeout. + */ +public class Timeout { + + private final long timeout; + private long cur = 0; + private long end = Long.MAX_VALUE; + + public Timeout(long timeout) { + this.timeout = timeout; + } + + public boolean isInfinite() { + return timeout <= 0; + } + + public boolean isFinite() { + return timeout > 0; + } + + public boolean elapsed() { + if (timeout > 0) { + cur = System.currentTimeMillis(); + if (end == Long.MAX_VALUE) { + end = cur + timeout; + } + return cur >= end; + } else { + return false; + } + } + + public long timeout() { + return timeout > 0 ? Math.max(1, end - cur) : timeout; + } +} diff --git a/net-cli/src/main/java/org/jline/utils/WCWidth.java b/net-cli/src/main/java/org/jline/utils/WCWidth.java new file mode 100644 index 0000000..22baaf5 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/WCWidth.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2002-2016, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +public final class WCWidth { + + /* sorted list of non-overlapping intervals of non-spacing characters */ + /* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */ + static Interval[] combining = { + new Interval(0x0300, 0x036F), new Interval(0x0483, 0x0486), new Interval(0x0488, 0x0489), + new Interval(0x0591, 0x05BD), new Interval(0x05BF, 0x05BF), new Interval(0x05C1, 0x05C2), + new Interval(0x05C4, 0x05C5), new Interval(0x05C7, 0x05C7), new Interval(0x0600, 0x0603), + new Interval(0x0610, 0x0615), new Interval(0x064B, 0x065E), new Interval(0x0670, 0x0670), + new Interval(0x06D6, 0x06E4), new Interval(0x06E7, 0x06E8), new Interval(0x06EA, 0x06ED), + new Interval(0x070F, 0x070F), new Interval(0x0711, 0x0711), new Interval(0x0730, 0x074A), + new Interval(0x07A6, 0x07B0), new Interval(0x07EB, 0x07F3), new Interval(0x0901, 0x0902), + new Interval(0x093C, 0x093C), new Interval(0x0941, 0x0948), new Interval(0x094D, 0x094D), + new Interval(0x0951, 0x0954), new Interval(0x0962, 0x0963), new Interval(0x0981, 0x0981), + new Interval(0x09BC, 0x09BC), new Interval(0x09C1, 0x09C4), new Interval(0x09CD, 0x09CD), + new Interval(0x09E2, 0x09E3), new Interval(0x0A01, 0x0A02), new Interval(0x0A3C, 0x0A3C), + new Interval(0x0A41, 0x0A42), new Interval(0x0A47, 0x0A48), new Interval(0x0A4B, 0x0A4D), + new Interval(0x0A70, 0x0A71), new Interval(0x0A81, 0x0A82), new Interval(0x0ABC, 0x0ABC), + new Interval(0x0AC1, 0x0AC5), new Interval(0x0AC7, 0x0AC8), new Interval(0x0ACD, 0x0ACD), + new Interval(0x0AE2, 0x0AE3), new Interval(0x0B01, 0x0B01), new Interval(0x0B3C, 0x0B3C), + new Interval(0x0B3F, 0x0B3F), new Interval(0x0B41, 0x0B43), new Interval(0x0B4D, 0x0B4D), + new Interval(0x0B56, 0x0B56), new Interval(0x0B82, 0x0B82), new Interval(0x0BC0, 0x0BC0), + new Interval(0x0BCD, 0x0BCD), new Interval(0x0C3E, 0x0C40), new Interval(0x0C46, 0x0C48), + new Interval(0x0C4A, 0x0C4D), new Interval(0x0C55, 0x0C56), new Interval(0x0CBC, 0x0CBC), + new Interval(0x0CBF, 0x0CBF), new Interval(0x0CC6, 0x0CC6), new Interval(0x0CCC, 0x0CCD), + new Interval(0x0CE2, 0x0CE3), new Interval(0x0D41, 0x0D43), new Interval(0x0D4D, 0x0D4D), + new Interval(0x0DCA, 0x0DCA), new Interval(0x0DD2, 0x0DD4), new Interval(0x0DD6, 0x0DD6), + new Interval(0x0E31, 0x0E31), new Interval(0x0E34, 0x0E3A), new Interval(0x0E47, 0x0E4E), + new Interval(0x0EB1, 0x0EB1), new Interval(0x0EB4, 0x0EB9), new Interval(0x0EBB, 0x0EBC), + new Interval(0x0EC8, 0x0ECD), new Interval(0x0F18, 0x0F19), new Interval(0x0F35, 0x0F35), + new Interval(0x0F37, 0x0F37), new Interval(0x0F39, 0x0F39), new Interval(0x0F71, 0x0F7E), + new Interval(0x0F80, 0x0F84), new Interval(0x0F86, 0x0F87), new Interval(0x0F90, 0x0F97), + new Interval(0x0F99, 0x0FBC), new Interval(0x0FC6, 0x0FC6), new Interval(0x102D, 0x1030), + new Interval(0x1032, 0x1032), new Interval(0x1036, 0x1037), new Interval(0x1039, 0x1039), + new Interval(0x1058, 0x1059), new Interval(0x1160, 0x11FF), new Interval(0x135F, 0x135F), + new Interval(0x1712, 0x1714), new Interval(0x1732, 0x1734), new Interval(0x1752, 0x1753), + new Interval(0x1772, 0x1773), new Interval(0x17B4, 0x17B5), new Interval(0x17B7, 0x17BD), + new Interval(0x17C6, 0x17C6), new Interval(0x17C9, 0x17D3), new Interval(0x17DD, 0x17DD), + new Interval(0x180B, 0x180D), new Interval(0x18A9, 0x18A9), new Interval(0x1920, 0x1922), + new Interval(0x1927, 0x1928), new Interval(0x1932, 0x1932), new Interval(0x1939, 0x193B), + new Interval(0x1A17, 0x1A18), new Interval(0x1B00, 0x1B03), new Interval(0x1B34, 0x1B34), + new Interval(0x1B36, 0x1B3A), new Interval(0x1B3C, 0x1B3C), new Interval(0x1B42, 0x1B42), + new Interval(0x1B6B, 0x1B73), new Interval(0x1DC0, 0x1DCA), new Interval(0x1DFE, 0x1DFF), + new Interval(0x200B, 0x200F), new Interval(0x202A, 0x202E), new Interval(0x2060, 0x2063), + new Interval(0x206A, 0x206F), new Interval(0x20D0, 0x20EF), new Interval(0x302A, 0x302F), + new Interval(0x3099, 0x309A), new Interval(0xA806, 0xA806), new Interval(0xA80B, 0xA80B), + new Interval(0xA825, 0xA826), new Interval(0xFB1E, 0xFB1E), new Interval(0xFE00, 0xFE0F), + new Interval(0xFE20, 0xFE23), new Interval(0xFEFF, 0xFEFF), new Interval(0xFFF9, 0xFFFB), + new Interval(0x10A01, 0x10A03), new Interval(0x10A05, 0x10A06), new Interval(0x10A0C, 0x10A0F), + new Interval(0x10A38, 0x10A3A), new Interval(0x10A3F, 0x10A3F), new Interval(0x1D167, 0x1D169), + new Interval(0x1D173, 0x1D182), new Interval(0x1D185, 0x1D18B), new Interval(0x1D1AA, 0x1D1AD), + new Interval(0x1D242, 0x1D244), new Interval(0x1F3FB, 0x1F3FF), new Interval(0xE0001, 0xE0001), + new Interval(0xE0020, 0xE007F), new Interval(0xE0100, 0xE01EF) + }; + + private WCWidth() { + } + + /* The following two functions define the column width of an ISO 10646 + * character as follows: + * + * - The null character (U+0000) has a column width of 0. + * + * - Other C0/C1 control characters and DEL will lead to a return + * value of -1. + * + * - Non-spacing and enclosing combining characters (general + * category code Mn or Me in the Unicode database) have a + * column width of 0. + * + * - SOFT HYPHEN (U+00AD) has a column width of 1. + * + * - Other format characters (general category code Cf in the Unicode + * database) and ZERO WIDTH SPACE (U+200B) have a column width of 0. + * + * - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF) + * have a column width of 0. + * + * - Spacing characters in the East Asian Wide (W) or East Asian + * Full-width (F) category as defined in Unicode Technical + * Report #11 have a column width of 2. + * + * - All remaining characters (including all printable + * ISO 8859-1 and WGL4 characters, Unicode control characters, + * etc.) have a column width of 1. + * + * This implementation assumes that wchar_t characters are encoded + * in ISO 10646. + */ + public static int wcwidth(int ucs) { + + /* test for 8-bit control characters */ + if (ucs == 0) return 0; + if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) return -1; + + /* binary search in table of non-spacing characters */ + if (bisearch(ucs, combining, combining.length - 1)) return 0; + + /* if we arrive here, ucs is not a combining or C0/C1 control character */ + return 1 + + ((ucs >= 0x1100 + && (ucs <= 0x115f + || /* Hangul Jamo init. consonants */ ucs == 0x2329 + || ucs == 0x232a + || (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs != 0x303f) + || /* CJK ... Yi */ (ucs >= 0xac00 && ucs <= 0xd7a3) + || /* Hangul Syllables */ (ucs >= 0xf900 && ucs <= 0xfaff) + || /* CJK Compatibility Ideographs */ (ucs >= 0xfe10 && ucs <= 0xfe19) + || /* Vertical forms */ (ucs >= 0xfe30 && ucs <= 0xfe6f) + || /* CJK Compatibility Forms */ (ucs >= 0xff00 && ucs <= 0xff60) + || /* Fullwidth Forms */ (ucs >= 0xffe0 && ucs <= 0xffe6) + || (ucs >= 0x1f000 && ucs <= 0x1feee) + || (ucs >= 0x20000 && ucs <= 0x2fffd) + || (ucs >= 0x30000 && ucs <= 0x3fffd))) + ? 1 + : 0); + } + + /* auxiliary function for binary search in interval table */ + private static boolean bisearch(int ucs, Interval[] table, int max) { + int min = 0; + int mid; + + if (ucs < table[0].first || ucs > table[max].last) return false; + while (max >= min) { + mid = (min + max) / 2; + if (ucs > table[mid].last) min = mid + 1; + else if (ucs < table[mid].first) max = mid - 1; + else return true; + } + + return false; + } + + private static class Interval { + public final int first; + public final int last; + + public Interval(int first, int last) { + this.first = first; + this.last = last; + } + } +} diff --git a/net-cli/src/main/java/org/jline/utils/WriterOutputStream.java b/net-cli/src/main/java/org/jline/utils/WriterOutputStream.java new file mode 100644 index 0000000..26afeb4 --- /dev/null +++ b/net-cli/src/main/java/org/jline/utils/WriterOutputStream.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2002-2017, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +/** + * Redirects an {@link OutputStream} to a {@link Writer} by decoding the data + * using the specified {@link Charset}. + * + *

Note: This class should only be used if it is necessary to + * redirect an {@link OutputStream} to a {@link Writer} for compatibility + * purposes. It is much more efficient to write to the {@link Writer} + * directly.

+ */ +public class WriterOutputStream extends OutputStream { + + private final Writer out; + private final CharsetDecoder decoder; + private final ByteBuffer decoderIn = ByteBuffer.allocate(256); + private final CharBuffer decoderOut = CharBuffer.allocate(128); + + public WriterOutputStream(Writer out, Charset charset) { + this( + out, + charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE)); + } + + public WriterOutputStream(Writer out, CharsetDecoder decoder) { + this.out = out; + this.decoder = decoder; + } + + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) b}, 0, 1); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + while (len > 0) { + final int c = Math.min(len, decoderIn.remaining()); + decoderIn.put(b, off, c); + processInput(false); + len -= c; + off += c; + } + flush(); + } + + @Override + public void flush() throws IOException { + flushOutput(); + out.flush(); + } + + @Override + public void close() throws IOException { + processInput(true); + flush(); + out.close(); + } + + /** + * Decode the contents of the input ByteBuffer into a CharBuffer. + * + * @param endOfInput indicates end of input + * @throws IOException if an I/O error occurs + */ + private void processInput(final boolean endOfInput) throws IOException { + // Prepare decoderIn for reading + decoderIn.flip(); + CoderResult coderResult; + while (true) { + coderResult = decoder.decode(decoderIn, decoderOut, endOfInput); + if (coderResult.isOverflow()) { + flushOutput(); + } else if (coderResult.isUnderflow()) { + break; + } else { + // The decoder is configured to replace malformed input and unmappable characters, + // so we should not get here. + throw new IOException("Unexpected coder result"); + } + } + // Discard the bytes that have been read + decoderIn.compact(); + } + + /** + * Flush the output. + * + * @throws IOException if an I/O error occurs + */ + private void flushOutput() throws IOException { + if (decoderOut.position() > 0) { + out.write(decoderOut.array(), 0, decoderOut.position()); + decoderOut.rewind(); + } + } +} diff --git a/net/src/test/resources/org/xbib/net/test/urltestdata.json b/net/src/test/resources/org/xbib/net/test/urltestdata.json index 56bb67f..e8df158 100644 --- a/net/src/test/resources/org/xbib/net/test/urltestdata.json +++ b/net/src/test/resources/org/xbib/net/test/urltestdata.json @@ -6192,7 +6192,7 @@ "username": "", "password": "", "host": "0177.0.0.0189", - "hostname": "177-0-0-189.cbace701.dsl.brasiltelecom.net.br", + "hostname": "177-0-0-189.dsl.brasiltelecom.net.br", "port": "", "pathname": "/", "search": "", diff --git a/settings.gradle b/settings.gradle index 29a377f..06cbd3f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,6 +34,7 @@ dependencyResolutionManagement { include 'net' include 'net-bouncycastle' +include 'net-cli' include 'net-mail' include 'net-mime' include 'net-path'