All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jline.builtins.Commands Maven / Gradle / Ivy

/*
 * Copyright (c) 2002-2021, 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
 */
package org.jline.builtins;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.FileSystems;
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.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

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;

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 nano(Terminal terminal, PrintStream out, PrintStream err,
            Path currentDir,
            String[] argv) throws Exception {
        nano(terminal, out, err, currentDir, argv, null);
    }

    public static void nano(Terminal terminal, PrintStream out, PrintStream err,
            Path currentDir,
            String[] argv,
            ConfigurationPath configPath) throws Exception {
        Options opt = Options.compile(Nano.usage()).parse(argv);
        if (opt.isSet("help")) {
            throw new HelpException(opt.usage());
        }
        Nano edit = new Nano(terminal, currentDir, opt, configPath);
        edit.open(opt.args());
        edit.run();
    }

    public static void less(Terminal terminal, InputStream in, PrintStream out, PrintStream err,
            Path currentDir,
            String[] 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,
                            String[] 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.args().isEmpty()) {
            opt.args().add("-");
        }
        for (String arg : opt.args()) {
            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));
            }
        }
        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.toString() + "/") + files;
            regex = regex.replaceAll("\\\\", "/").replaceAll("//", "/");
            searchRoot = root;
        }
        PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:"+regex);
        return Files.find(searchRoot, Integer.MAX_VALUE, (path, f)->pathMatcher.matches(path)).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.",
                "  -e                              Uses the nano editor to edit the commands before executing",
                "  -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 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(Arrays.asList(replaceParam(command)));
        }

        private String replaceParam(String command) {
            String out = command;
            if (oldParam != null && newParam != null) {
                out = command.replaceAll(oldParam, newParam);
            }
            return out;
        }

        public void editCommandsAndClose(LineReader reader) throws Exception {
            if (edit) {
                cmdWriter.close();
                try {
                    reader.editAndAddInBuffer(cmdFile);
                } finally {
                    cmdFile.delete();
                }
            }
        }
    }

    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 (Map.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;
                Map.Entry begin = null;
                String last = null;
                Iterator> iterator = bound.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.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.toString());
                                } 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.toString());
                                }
                                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.toString());
        }
    }

    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 (Map.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 Options.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 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");
        boolean name;
        boolean rgb;
        private final Terminal terminal;
        private final PrintStream out;
        private boolean fixedBg;
        private String fixedStyle;
        int r, g, b;

        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.toString() + field + rp.toString();
        }

        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.toString() + 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 java.io.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);
            }
        }
    }

}