add net-cli
Some checks are pending
CodeQL / Analyze (push) Waiting to run

This commit is contained in:
Jörg Prante 2024-12-25 12:55:13 +01:00
parent e9fde15d5e
commit 52525afe72
140 changed files with 46348 additions and 1 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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;
}
}

View file

@ -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 <T> option type
* @param option option name
* @param defval default value
* @return option value
*/
<T> T consoleOption(String option, T defval);
}

View file

@ -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);
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<T> {
private final String regexp;
private final BiFunction<T, String, Boolean> matcher;
private volatile State start;
public NfaMatcher(String regexp, BiFunction<T, String, Boolean> matcher) {
this.regexp = regexp;
this.matcher = matcher;
}
static State toNfa(List<String> postfix) {
Deque<Frag> 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<String> toPostFix(String regexp) {
List<String> postfix = new ArrayList<>();
int s = -1;
int natom = 0;
int nalt = 0;
Deque<Integer> natoms = new ArrayDeque<>();
Deque<Integer> 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<T> args) {
Set<State> clist = new HashSet<>();
compile();
addState(clist, start);
for (T arg : args) {
Set<State> 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<String> matchPartial(List<T> args) {
Set<State> clist = new HashSet<>();
compile();
addState(clist, start);
for (T arg : args) {
Set<State> 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<State> 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<Consumer<State>> out = new ArrayList<>();
public Frag(State start, Collection<Consumer<State>> l) {
this.start = start;
this.out.addAll(l);
}
public Frag(State start, Collection<Consumer<State>> l1, Collection<Consumer<State>> l2) {
this.start = start;
this.out.addAll(l1);
this.out.addAll(l2);
}
public Frag(State start, Consumer<State> c) {
this.start = start;
this.out.add(c);
}
public Frag(State start, Collection<Consumer<State>> l, Consumer<State> c) {
this.start = start;
this.out.addAll(l);
this.out.add(c);
}
public void patch(State s) {
out.forEach(c -> c.accept(s));
}
}
}

View file

@ -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.
* <p>
* 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<String, Boolean> unmodifiableOptSet;
private final Map<String, Object> unmodifiableOptArg;
private final Map<String, Boolean> optSet = new HashMap<>();
private final Map<String, Object> optArg = new HashMap<>();
private final Map<String, String> optName = new HashMap<>();
private final Map<String, String> optAlias = new HashMap<>();
private final List<Object> xargs = new ArrayList<>();
private final String[] spec;
private final String[] gspec;
private final String defOpts;
private final String[] defArgs;
private List<String> 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<String, String> env) {
this.gspec = gspec;
if (gspec == null && opt == null) {
this.spec = spec;
} else {
ArrayList<String> 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<String, Boolean> myOptSet = new HashMap<>();
Map<String, Object> myOptArg = new HashMap<>();
parseSpec(myOptSet, myOptArg);
if (opt != null) {
for (Entry<String, Boolean> e : opt.optSet.entrySet()) {
if (e.getValue()) myOptSet.put(e.getKey(), true);
}
for (Entry<String, Object> 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<String, String> 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<String, String> 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<Object> list = getObjectList(name);
return list.isEmpty() ? "" : list.get(list.size() - 1);
}
@SuppressWarnings("unchecked")
public List<Object> getObjectList(String name) {
List<Object> 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<Object>) arg;
}
return list;
}
public List<String> getList(String name) {
ArrayList<String> 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<Object> list;
Object arg = optArg.get(name);
if (arg instanceof String) { // default value
list = new ArrayList<>();
optArg.put(name, list);
} else {
list = (List<Object>) 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<Object> argObjects() {
return xargs;
}
public List<String> 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<String, Boolean> myOptSet, Map<String, Object> 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<Object> 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<String> 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<String, String> 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 <code>--help</code> 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;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<String> 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<String> 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;
}
}
}

View file

@ -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<String> 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> 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<String, String> 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<String> COLORS_8 =
Arrays.asList("white", "black", "red", "blue", "green", "yellow", "magenta", "cyan");
// https://github.com/lhmouse/nano-win/commit/a7aab18dfeef8a0e8073d5fa420677dc8fe548da
private static final Map<String, Integer> 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<String, String> colors;
private final Map<String, String> tokenColors;
private final boolean nanoStyle;
public StyleCompiler(Map<String, String> colors) {
this(colors, false);
}
public StyleCompiler(Map<String, String> 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();
}
}
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
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<String, List<HighlightRule>> 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<String, List<HighlightRule>> defaultRules = new HashMap<>();
defaultRules.put(TOKEN_NANORC, new ArrayList<>());
rules.putAll(defaultRules);
}
protected static SyntaxHighlighter build(List<Path> syntaxFiles, String file, String syntaxName) {
return build(syntaxFiles, file, syntaxName, false);
}
protected static SyntaxHighlighter build(
List<Path> syntaxFiles, String file, String syntaxName, boolean ignoreErrors) {
SyntaxHighlighter out = new SyntaxHighlighter();
Map<String, String> 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<String> 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<Path> 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<String> 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<Path> syntaxFiles) throws IOException {
addFiles(parameter, s -> s.forEach(syntaxFiles::add));
}
protected static void nanorcTheme(String parameter, List<Path> syntaxFiles) throws IOException {
addFiles(parameter, s -> s.findFirst().ifPresent(p -> syntaxFiles.add(0, p)));
}
protected static void addFiles(String parameter, Consumer<Stream<Path>> consumer) throws IOException {
if (parameter.contains("*") || parameter.contains("?")) {
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + parameter);
try (Stream<Path> 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<String, List<HighlightRule>> 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<ParsedToken> 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<HighlightRule> rules) {
return _highlight(line, rules, null, null);
}
private AttributedStringBuilder _highlight(
AttributedString line, List<HighlightRule> 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<String> 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<String, List<HighlightRule>> highlightRules = new HashMap<>();
private final BufferedReader reader;
private Map<String, String> colorTheme = new HashMap<>();
private boolean matches = false;
private String syntaxName = "unknown";
private Parser parser;
public NanorcParser(Path file, String name, String target, Map<String, String> 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<String> parts = RuleSplitter.split(fixRegexes(line));
if (parts.get(0).equals("syntax")) {
syntaxName = parts.get(1);
List<Pattern> 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<String> 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<String, List<HighlightRule>> getHighlightRules() {
return highlightRules;
}
public boolean isDefault() {
return syntaxName.equals(DEFAULT_SYNTAX);
}
private void addHighlightRule(String reference, List<String> parts, boolean caseInsensitive, String tokenName) {
Map<String, String> 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<String> split(String s) {
List<String> 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<ParsedToken> 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<ParsedToken> 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);
}
}
}

View file

@ -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.
* <p>
* 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<String, Column> columns = new LinkedHashMap<>();
private final Terminal terminal;
private final Display display;
private final BindingReader bindingReader;
private final KeyMap<Operation> keys;
private final Size size = new Size();
public List<String> sort;
public long delay;
public List<String> stats;
public int nthreads;
private Comparator<Map<String, Comparable<?>>> comparator;
// Internal cache data
private final Map<Long, Map<String, Object>> previous = new HashMap<>();
private final Map<Long, Map<String, Long>> changes = new HashMap<>();
private final Map<String, Integer> 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<Object, String> 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<Operation> 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<Map<String, Comparable<?>>> 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<Map<String, Comparable<?>>> threads = new ArrayList<>();
for (ThreadInfo ti : infos) {
Map<String, Comparable<?>> 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<AttributedString> 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<Map<String, Comparable<?>>> threads = infos();
Collections.sort(threads, comparator);
int nb = Math.min(size.getRows() - lines.size() - 2, nthreads > 0 ? nthreads : threads.size());
// Compute values
List<Map<String, String>> 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<String> 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<String, Comparable<?>> 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<Map<String, Comparable<?>>> buildComparator(List<String> sort) {
if (sort == null || sort.isEmpty()) {
sort = Collections.singletonList(STAT_TID);
}
Comparator<Map<String, Comparable<?>>> 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<Map<String, Comparable<?>>> 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<Object, String> 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<Operation> 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<Object, String> format;
Column(String name, Align align, String header, Function<Object, String> format) {
this.name = name;
this.align = align;
this.header = header;
this.format = format;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<AttributedString> description;
public ArgDesc(String name) {
this(name, new ArrayList<>());
}
public ArgDesc(String name, List<AttributedString> 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<ArgDesc> doArgNames(List<String> names) {
List<ArgDesc> out = new ArrayList<>();
for (String n : names) {
out.add(new ArgDesc(n));
}
return out;
}
public String getName() {
return name;
}
public List<AttributedString> getDescription() {
return description;
}
}

View file

@ -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<AttributedString> mainDesc;
private List<ArgDesc> argsDesc;
private TreeMap<String, List<AttributedString>> 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<ArgDesc> argsDesc) {
this(new ArrayList<>(), argsDesc, new HashMap<>());
}
public CmdDesc(List<ArgDesc> argsDesc, Map<String, List<AttributedString>> optsDesc) {
this(new ArrayList<>(), argsDesc, optsDesc);
}
public CmdDesc(
List<AttributedString> mainDesc, List<ArgDesc> argsDesc, Map<String, List<AttributedString>> 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<AttributedString> mainDesc) {
this.mainDesc = new ArrayList<>(mainDesc);
return this;
}
public List<AttributedString> getMainDesc() {
return mainDesc;
}
public void setMainDesc(List<AttributedString> mainDesc) {
this.mainDesc = new ArrayList<>(mainDesc);
}
public TreeMap<String, List<AttributedString>> 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<ArgDesc> 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("");
}
}

View file

@ -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<String> 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<String> 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<String> 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
}
}

View file

@ -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);
}
}

View file

@ -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<CommandInput, ?> execute;
Function<String, List<Completer>> compileCompleter;
public CommandMethods(Function<CommandInput, ?> execute, Function<String, List<Completer>> compileCompleter) {
this.execute = execute;
this.compileCompleter = compileCompleter;
}
public CommandMethods(Consumer<CommandInput> execute, Function<String, List<Completer>> compileCompleter) {
this.execute = (CommandInput i) -> {
execute.accept(i);
return null;
};
this.compileCompleter = compileCompleter;
}
public Function<CommandInput, ?> execute() {
return execute;
}
public Function<String, List<Completer>> compileCompleter() {
return compileCompleter;
}
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
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<String> commandNames();
/**
* Returns a map of alias-to-command names known by this registry.
*
* @return a map with alias keys and command name values
*/
Map<String, String> commandAliases();
/**
* Returns a short info about command known by this registry.
*
* @param command the command name
* @return a short info about command
*/
List<String> 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<String> 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;
}
}
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
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<String> params);
/**
* Returns all scripts found from PATH
*
* @return map keys have script file names and value is true if it is console script
*/
Map<String, Boolean> 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<String, List<String>> getPipes();
/**
* Returns named pipe names
*
* @return list of named pipe names
*/
List<String> getNamedPipes();
/**
* Returns script and variable completers
*
* @return script and variable completers
*/
List<Completer> 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 <T> option type
* @param option option name
* @param defval default value
* @return option value
*/
<T> 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;
}
}
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
public interface Printer {
/**
* Value: Boolean<br>
* Applies: TABLE<br>
* Ignore columnsOut configuration.
*/
String ALL = "all";
//
// option names
//
// 1) command options
//
/**
* Value: {@code List<String>}<br>
* Applies: MAP and TABLE<br>
* Display given keys/columns on map/table.
*/
String COLUMNS = "columns";
/**
* Value: {@code List<String>}<br>
* Applies: TABLE<br>
* Exclude given columns on table.
*/
String EXCLUDE = "exclude";
/**
* Value: {@code List<String>}<br>
* Applies: TABLE<br>
* Include given columns on table.
*/
String INCLUDE = "include";
/**
* Value: Integer<br>
* Applies: MAP<br>
* Indention size.
*/
String INDENTION = "indention";
/**
* Value: Integer<br>
* Applies: MAP and TABLE<br>
* Maximum column width.
*/
String MAX_COLUMN_WIDTH = "maxColumnWidth";
/**
* Value: Integer<br>
* Applies: MAP<br>
* Maximum depth objects are resolved.
*/
String MAX_DEPTH = "maxDepth";
/**
* Value: Integer<br>
* Applies: MAP and TABLE<br>
* Maximum number of lines to display.
*/
String MAXROWS = "maxrows";
/**
* Value: Boolean<br>
* Applies: TABLE<br>
* Display one row data on table.
*/
String ONE_ROW_TABLE = "oneRowTable";
/**
* Value: Boolean<br>
* Applies: TABLE<br>
* Display table row numbers.
*/
String ROWNUM = "rownum";
/**
* Value: Boolean<br>
* Applies: TABLE<br>
* Truncate table column names: property.field to field.
*/
String SHORT_NAMES = "shortNames";
/**
* Value: Boolean<br>
* Applies: MAP and TABLE<br>
* Ignore all options defined in PRNT_OPTIONS.
*/
String SKIP_DEFAULT_OPTIONS = "skipDefaultOptions";
/**
* Value: Boolean<br>
* Applies: TABLE<br>
* Display object structures and lists on table.
*/
String STRUCT_ON_TABLE = "structsOnTable";
/**
* Value: String<br>
* Use nanorc STYLE<br>
*/
String STYLE = "style";
/**
* Value: Boolean<br>
* Applies: MAP and TABLE<br>
* 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<br>
* Applies: MAP and TABLE<br>
* Nanorc syntax style used to highlight values.
*/
String VALUE_STYLE = "valueStyle";
/**
* Value: Integer<br>
* Applies: MAP and TABLE<br>
* Display width (default terminal width).
*/
String WIDTH = "width";
/**
* Value: String<br>
* Applies: TABLE<br>
* Table cell vertical border character.
*/
String BORDER = "border";
/**
* Value: TableRows<br>
* Applies: TABLE<br>
* Highlight table rows.
*/
String ROW_HIGHLIGHT = "rowHighlight";
/**
* Value: {@code List<String>}<br>
* Applies: TABLE<br>
* 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<String>}<br>
* Applies: TABLE<br>
* These map values will not be inserted to the table.
*/
String COLUMNS_OUT = "columnsOut";
/**
* Value: {@code Map<regex, function>}.<br>
* Applies: TABLE<br>
* 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<br>
* Applies: MAP and TABLE<br>
* 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<class, function>}<br>
* Applies: MAP and TABLE<br>
* Overrides the ScriptEngine toMap() method.
*/
String OBJECT_TO_MAP = "objectToMap";
/**
* Value: {@code Map<class, function>}<br>
* Applies: MAP and TABLE<br>
* Overrides the ScriptEngine toString() method.
*/
String OBJECT_TO_STRING = "objectToString";
/**
* Value: Boolean<br>
* Applies: MAP and TABLE<br>
* 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<br>
* Applies: TABLE<br>
* List the collection of simple values in multiple columns
* DEFAULT: list values in one column
*/
String MULTI_COLUMNS = "multiColumns";
List<String> 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<String, Object> options, Object object);
default Exception prntCommand(CommandInput input) {
return null;
}
/**
* Clear printer syntax highlighter cache
*/
boolean refresh();
enum TableRows {
EVEN,
ODD,
ALL
}
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
public interface ScriptEngine {
/**
* @return scriptEngine name
*/
String getEngineName();
/**
* @return script file name extensions
*/
Collection<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String> getSerializationFormats();
/**
* @return Supported deserialization formats
*/
List<String> 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);
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
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<String> 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> 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<Long, SystemRegistry> 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());
}
}
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
public abstract class AbstractCommandRegistry implements CommandRegistry {
private CmdRegistry cmdRegistry;
private Exception exception;
public AbstractCommandRegistry() {
}
public CmdDesc doHelpDesc(String command, List<String> info, CmdDesc cmdDesc) {
List<AttributedString> 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 <T extends Enum<T>> void registerCommands(
Map<T, String> commandName, Map<T, CommandMethods> commandExecute) {
cmdRegistry = new EnumCmdRegistry<>(commandName, commandExecute);
}
public void registerCommands(Map<String, CommandMethods> 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<String> commandNames() {
return cmdRegistry.commandNames();
}
@Override
public Map<String, String> commandAliases() {
return cmdRegistry.commandAliases();
}
public <V extends Enum<V>> 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<String> commandNames();
Map<String, String> commandAliases();
Object command(String command);
<V extends Enum<V>> void rename(V command, String newName);
void alias(String alias, String command);
SystemCompleter compileCompleters();
CommandMethods getCommandMethods(String command);
}
private static class EnumCmdRegistry<T extends Enum<T>> implements CmdRegistry {
private final Map<T, String> commandName;
private final Map<T, CommandMethods> commandExecute;
private final Map<String, String> aliasCommand = new HashMap<>();
private Map<String, T> nameCommand = new HashMap<>();
public EnumCmdRegistry(Map<T, String> commandName, Map<T, CommandMethods> 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<String> commandNames() {
return nameCommand.keySet();
}
public Map<String, String> commandAliases() {
return aliasCommand;
}
@SuppressWarnings("unchecked")
public <V extends Enum<V>> 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<T, String> 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<String, CommandMethods> commandExecute;
private final Map<String, String> aliasCommand = new HashMap<>();
public NameCmdRegistry(Map<String, CommandMethods> commandExecute) {
this.commandExecute = commandExecute;
}
public Set<String> commandNames() {
return commandExecute.keySet();
}
public Map<String, String> commandAliases() {
return aliasCommand;
}
public <V extends Enum<V>> 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));
}
}
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
public class Builtins extends JlineCommandRegistry implements CommandRegistry {
private final ConfigurationPath configPath;
private final Function<String, Widget> widgetCreator;
private final Supplier<Path> workDir;
private LineReader reader;
public Builtins(Path workDir, ConfigurationPath configPath, Function<String, Widget> widgetCreator) {
this(null, () -> workDir, configPath, widgetCreator);
}
public Builtins(
Set<Command> commands, Path workDir, ConfigurationPath configpath, Function<String, Widget> widgetCreator) {
this(commands, () -> workDir, configpath, widgetCreator);
}
public Builtins(Supplier<Path> workDir, ConfigurationPath configPath, Function<String, Widget> widgetCreator) {
this(null, workDir, configPath, widgetCreator);
}
@SuppressWarnings("this-escape")
public Builtins(
Set<Command> commands,
Supplier<Path> workDir,
ConfigurationPath configpath,
Function<String, Widget> widgetCreator) {
super();
Objects.requireNonNull(configpath);
this.configPath = configpath;
this.widgetCreator = widgetCreator;
this.workDir = workDir;
Set<Command> cmds;
Map<Command, String> commandName = new HashMap<>();
Map<Command, CommandMethods> 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<String> unsetOptions(boolean set) {
List<String> 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<Completer> highlighterCompleter(String name) {
List<Completer> completers = new ArrayList<>();
List<OptDesc> optDescs = commandOptions(name);
completers.add(new ArgumentCompleter(
NullCompleter.INSTANCE, new OptionCompleter(NullCompleter.INSTANCE, optDescs, 1)));
return completers;
}
private Set<String> allWidgets() {
Set<String> out = new HashSet<>();
for (String s : reader.getWidgets().keySet()) {
out.add(s);
out.add(reader.getWidgets().get(s).toString());
}
return out;
}
private List<Completer> lessCompleter(String name) {
List<Completer> completers = new ArrayList<>();
completers.add(new ArgumentCompleter(
NullCompleter.INSTANCE, new OptionCompleter(new FilesCompleter(workDir), this::commandOptions, 1)));
return completers;
}
private List<Completer> historyCompleter(String name) {
List<Completer> completers = new ArrayList<>();
List<OptDesc> 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<Completer> widgetCompleter(String name) {
List<Completer> completers = new ArrayList<>();
List<OptDesc> optDescs = commandOptions(name);
Candidate aliasOption = new Candidate("-A", "-A", null, null, null, null, true);
Iterator<OptDesc> 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<Completer> setvarCompleter(String name) {
List<Completer> completers = new ArrayList<>();
completers.add(new ArgumentCompleter(
NullCompleter.INSTANCE,
new StringsCompleter(() -> reader.getVariables().keySet()),
NullCompleter.INSTANCE));
return completers;
}
private List<Completer> setoptCompleter(String name) {
List<Completer> completers = new ArrayList<>();
completers.add(new ArgumentCompleter(NullCompleter.INSTANCE, new StringsCompleter(() -> unsetOptions(true))));
return completers;
}
private List<Completer> unsetoptCompleter(String name) {
List<Completer> 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
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
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<AttributedString> main = new ArrayList<>();
Map<String, List<AttributedString>> 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<OptDesc> compileCommandOptions(String helpMessage) {
List<OptDesc> 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<String> compileCommandInfo(String helpMessage) {
List<String> 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<String> 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<String> 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<OptDesc> commandOptions(String command) {
try {
invoke(new CommandSession(), command, "--help");
} catch (HelpException e) {
return compileCommandOptions(e.getMessage());
} catch (Exception e) {
// ignore
}
return null;
}
public List<Completer> defaultCompleter(String command) {
List<Completer> 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;
}
}
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
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<String, FileHighlightCommand> fileHighlight = new HashMap<>();
protected final Map<String, SyntaxHighlighter> specificHighlighter = new HashMap<>();
private final List<Supplier<Boolean>> 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<String, String> 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<String, String> 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<Boolean> refresh : externalHighlightersRefresh) {
refresh.get();
}
resolver = Styles.lsStyle();
} catch (IOException e) {
Log.warn(e.getMessage());
}
}
}
public void addExternalHighlighterRefresh(Supplier<Boolean> 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<String> 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<String> 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<String> 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<Path> 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<String> fileOptions = new ArrayList<>();
public FileHighlightCommand() {
this(null, new ArrayList<>());
}
public FileHighlightCommand(String subcommand, Collection<String> 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<String> getFileOptions() {
return fileOptions;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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 <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
*/
public class BindingReader {
protected final NonBlockingReader reader;
protected final StringBuilder opBuffer = new StringBuilder();
protected final Deque<Integer> 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.
* <p>
* 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 (&gt;= 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 &lt;= 0, the current bound operation
* will be returned.
*
* @param keys the KeyMap to use for decoding the input stream
* @param <T> the type of bindings to be read
* @return the decoded binding or <code>null</code> if the end of
* stream has been reached
*/
public <T> T readBinding(KeyMap<T> keys) {
return readBinding(keys, null, true);
}
public <T> T readBinding(KeyMap<T> keys, KeyMap<T> local) {
return readBinding(keys, local, true);
}
public <T> T readBinding(KeyMap<T> keys, KeyMap<T> 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;
}
}

View file

@ -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 <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
* @since 2.6
*/
public class KeyMap<T> {
public static final int KEYMAP_LENGTH = 128;
public static final long DEFAULT_AMBIGUOUS_TIMEOUT = 1000L;
public static final Comparator<String> 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<String> 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<String> 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 <T> void doGetBoundKeys(KeyMap<T> keyMap, String prefix, Map<String, T> 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<T>) 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> T unbind(KeyMap<T> map, CharSequence keySeq) {
KeyMap<T> 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<T>) 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 <T> void bind(KeyMap<T> 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<T> 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<String, T> getBoundKeys() {
Map<String, T> 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<T>) 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<? extends CharSequence> 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);
}
}
}

View file

@ -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 <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
* @see Macro
* @see Reference
* @see Widget
* @see org.jline.keymap.KeyMap
*/
public interface Binding {
}

View file

@ -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();
}

View file

@ -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 <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
*/
public class Candidate implements Comparable<Candidate> {
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 + "}";
}
}

View file

@ -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 <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
* @since 2.3
*/
public interface Completer {
/**
* Populates <i>candidates</i> with a list of possible completions for the <i>command line</i>.
* <p>
* 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<Candidate> candidates);
}

View file

@ -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();
}

View file

@ -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<LineReader.Option, Boolean> 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<Candidate> matches(List<Candidate> 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();
}

View file

@ -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;
}
}

View file

@ -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<String> files) throws IOException;
void run() throws IOException;
void setRestricted(boolean restricted);
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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 <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @since 2.3
*/
public interface History extends Iterable<History.Entry> {
/**
* 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 <code>true</code> only the events that
* are not contained within the internal list are added.
*
* @param file History file
* @param checkDuplicates If <code>true</code>, 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 <code>true</code> if the given entry should be persisted, <code>false</code> otherwise
*/
default boolean isPersistable(Entry entry) {
return true;
}
//
// Entries
//
ListIterator<Entry> iterator(int index);
default ListIterator<Entry> iterator() {
return iterator(first());
}
default Iterator<Entry> reverseIterator() {
return reverseIterator(last());
}
default Iterator<Entry> reverseIterator(int index) {
return new Iterator<Entry>() {
private final ListIterator<Entry> 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();
}
}

View file

@ -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.
*
* <h2>Thread safety</h2>
* The <code>LineReader</code> 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 <code>readLine</code> call while one is
* already executing in a different thread will immediately result in an
* <code>IllegalStateException</code> 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.
*
* <h2>Prompt strings</h2>
* 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:
* <ul>
* <li> The normal prompt at the start (left) of the initial line of a command.
* <li> An optional right prompt at the right border of the initial line.
* <li> A start (left) prompt for continuation lines. I.e. the lines
* after the first line of a multi-line command.
* </ul>
* <p>
* All of these are specified with prompt templates,
* which are similar to {@code printf} format strings,
* using the character {@code '%'} to indicate special functionality.
* </p>
* The pattern may include ANSI escapes.
* It may include these template markers:
* <dl>
* <dt>{@code %N}</dt>
* <dd>A line number. This is the sum of {@code getLineNumber()}
* and a counter starting with 1 for the first continuation line.
* </dd>
* <dt>{@code %M}</dt>
* <dd>A short word explaining what is "missing". This is supplied from
* the {@link EOFError#getMissing()} method, if provided.
* Defaults to an empty string.
* </dd>
* <dt>{@code %}<var>n</var>{@code P}<var>c</var></dt>
* <dd>Insert padding at this position, repeating the following
* character <var>c</var> as needed to bring the total prompt
* column width as specified by the digits <var>n</var>.
* </dd>
* <dt>{@code %P}<var>c</var></dt>
* <dd>As before, but use width from the initial prompt.
* </dd>
* <dt>{@code %%}</dt>
* <dd>A literal {@code '%'}.
* </dd>
* <dt><code>%{</code></dt><dt><code>%}</code></dt>
* <dd>Text between a <code>%{</code>...<code>%}</code> 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 <code>%{</code>...<code>%}</code> pair
* (though can be) since JLine knows how to deal with them. However,
* these delimiters are needed for unusual non-standard escape sequences.
* </dd>
* </dl>
*/
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<String, KeyMap<Binding>> defaultKeyMaps();
/**
* Read the next line and return the contents of the buffer.
* <p>
* Equivalent to <code>readLine(null, null, null)</code>.
*
* @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.
* <p>
* Equivalent to <code>readLine(null, mask, null)</code>
*
* @param mask The mask character, <code>null</code> or <code>0</code>.
* @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.
* <p>
* Equivalent to <code>readLine(prompt, null, null)</code>
*
* @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 <i>in</i> {@link InputStream}, and return the line
* (without any trailing newlines).
* <p>
* Equivalent to <code>readLine(prompt, mask, null)</code>
*
* @param prompt The prompt to issue to the terminal, may be null.
* @param mask The mask character, <code>null</code> or <code>0</code>.
* @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 <i>in</i> {@link InputStream}, and return the line
* (without any trailing newlines).
* <p>
* Equivalent to <code>readLine(prompt, null, mask, buffer)</code>
*
* @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 <i>in</i> {@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 <i>in</i> {@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 <code>readLine()</code> call.
*
* @return <code>true</code> if there is an ongoing <code>readLine()</code> call.
*/
boolean isReading();
LineReader variable(String name, Object value);
LineReader option(Option option, boolean value);
void callWidget(String name);
//
// Chainable setters
//
Map<String, Object> 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<String, Widget> getWidgets();
Map<String, Widget> 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<String, KeyMap<Binding>> getKeyMaps();
String getKeyMap();
boolean setKeyMap(String name);
KeyMap<Binding> getKeys();
ParsedLine getParsedLine();
String getSearchTerm();
RegionType getRegionActive();
int getRegionMark();
void addCommandsInBuffer(Collection<String> 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 <code>&lt;tab&gt;</code> 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<Option, Boolean> 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
}
}

View file

@ -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<String, Object> variables = new HashMap<>();
Map<LineReader.Option, Boolean> 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<String, Object> variables) {
Map<String, Object> 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<LineReader.Option, Boolean> e : options.entrySet()) {
reader.option(e.getKey(), e.getValue());
}
return reader;
}
}

View file

@ -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 + ']';
}
}

View file

@ -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);
}

View file

@ -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;
/**
* <code>ParsedLine</code> objects are returned by the {@link Parser}
* during completion or when accepting the line.
* <p>
* 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<String> 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();
}

View file

@ -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
}
}

View file

@ -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.
*
* <p>Example:</p>
* <pre>
* LineReader reader = LineReaderBuilder.builder().terminal(terminal).parser(parser).build();
* PrintAboveWriter printAbove = new PrintAboveWriter(reader);
* printAbove.write(new char[] { 'h', 'i', '!', '\n'});
* </pre>
*/
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);
}
}
}

View file

@ -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 + ']';
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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 <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @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 <i>where</i> characters.
*
* @param num If less than 0, move abs(<i>where</i>) to the left, otherwise move <i>where</i> 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 <em>num</em> 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);
}
}

View file

@ -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<String> exact;
protected List<Function<Map<String, List<Candidate>>, Map<String, List<Candidate>>>> matchers;
private Map<String, List<Candidate>> 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<LineReader.Option, Boolean> options,
boolean prefix,
CompletingParsedLine line,
boolean caseInsensitive,
int errors,
String originalGroupName) {
reset(caseInsensitive);
defaultMatchers(options, prefix, line, caseInsensitive, errors, originalGroupName);
}
@Override
public List<Candidate> matches(List<Candidate> candidates) {
matching = Collections.emptyMap();
Map<String, List<Candidate>> sortedCandidates = sort(candidates);
for (Function<Map<String, List<Candidate>>, Map<String, List<Candidate>>> 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<LineReader.Option, Boolean> 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<String, List<Candidate>>, Map<String, List<Candidate>>> simpleMatcher(
Predicate<String> predicate) {
return m -> m.entrySet().stream()
.filter(e -> predicate.test(e.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
protected Function<Map<String, List<Candidate>>, Map<String, List<Candidate>>> typoMatcher(
String word, int errors, boolean caseInsensitive, String originalGroupName) {
return m -> {
Map<String, List<Candidate>> 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<String, List<Candidate>> sort(List<Candidate> candidates) {
// Build a list of sorted candidates
Map<String, List<Candidate>> 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);
}
}

View file

@ -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<Entry> 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;
}
}

View file

@ -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();
}
}

View file

@ -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<Bracket> 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<String> 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<String> 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<Integer> 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 <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
*/
public class ArgumentList implements ParsedLine, CompletingParsedLine {
private final String line;
private final List<String> 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<String> 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<String> 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<String> 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<Integer> 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;
}
}
}

View file

@ -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<Boolean> 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);
}
}
}
}

View file

@ -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;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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<T> {
private final Consumer<T> state;
private final Node parent;
private Node current;
@SuppressWarnings("this-escape")
public UndoTree(Consumer<T> 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;
}
}
}

View file

@ -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 <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @since 2.3
*/
public class AggregateCompleter implements Completer {
private final Collection<Completer> 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<Completer> completers) {
assert completers != null;
this.completers = completers;
}
/**
* Retrieve the collection of completers currently being aggregated.
*
* @return the aggregated completers
*/
public Collection<Completer> getCompleters() {
return completers;
}
/**
* Perform a completion operation across all aggregated completers.
* <p>
* The effect is similar to the following code:
* <blockquote><pre>{@code completers.forEach(c -> c.complete(reader, line, candidates));}</pre></blockquote>
*
* @see Completer#complete(LineReader, ParsedLine, List)
*/
public void complete(LineReader reader, final ParsedLine line, final List<Candidate> 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 + '}';
}
}

View file

@ -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 <i>separator</i> argument.
* This can be used instead of the individual completers having to know about argument parsing semantics.
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @since 2.3
*/
public class ArgumentCompleter implements Completer {
private final List<Completer> completers = new ArrayList<>();
private boolean strict = true;
private boolean strictCommand = true;
/**
* Create a new completer.
*
* @param completers The embedded completers
*/
public ArgumentCompleter(final Collection<Completer> 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 <code>ArgumentCompleter</code>.
*
* @return The list of completers.
* @since 2.3
*/
public List<Completer> getCompleters() {
return completers;
}
public void complete(LineReader reader, ParsedLine line, final List<Candidate> candidates) {
Objects.requireNonNull(line);
Objects.requireNonNull(candidates);
if (line.wordIndex() < 0) {
return;
}
List<Completer> 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<? extends CharSequence> args = line.words();
String arg = (args == null || i >= args.size()) ? "" : args.get(i).toString();
List<Candidate> 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<String> words() {
return Collections.singletonList(word);
}
@Override
public String line() {
return word;
}
@Override
public int cursor() {
return cursor;
}
}
}

View file

@ -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 <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @since 2.3
*/
public class EnumCompleter extends StringsCompleter {
public EnumCompleter(Class<? extends Enum<?>> source) {
Objects.requireNonNull(source);
for (Enum<?> n : source.getEnumConstants()) {
candidates.add(new Candidate(n.name().toLowerCase()));
}
}
}

View file

@ -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.
* <p>
* This completer tries to behave as similar as possible to
* <i>bash</i>'s file name completion (using GNU readline)
* with the following exceptions:
* <ul>
* <li>Candidates that are directories will end with "/"</li>
* <li>Wildcard regular expressions are not evaluated or replaced</li>
* <li>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</li>
* </ul>
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @since 2.3
* @deprecated use <code>org.jline.builtins.Completers$FileNameCompleter</code> instead
*/
@Deprecated
public class FileNameCompleter implements Completer {
public void complete(LineReader reader, ParsedLine commandLine, final List<Candidate> 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<Path> 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;
}
}

View file

@ -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 <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @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<Candidate> candidates) {
}
}

View file

@ -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 <a href="mailto:jason@planet57.com">Jason Dillon</a>
* @since 2.3
*/
public class StringsCompleter implements Completer {
protected Collection<Candidate> candidates;
protected Supplier<Collection<String>> stringsSupplier;
public StringsCompleter() {
this(Collections.<Candidate>emptyList());
}
public StringsCompleter(Supplier<Collection<String>> stringsSupplier) {
assert stringsSupplier != null;
candidates = null;
this.stringsSupplier = stringsSupplier;
}
public StringsCompleter(String... strings) {
this(Arrays.asList(strings));
}
public StringsCompleter(Iterable<String> 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<Candidate> candidates) {
assert candidates != null;
this.candidates = new ArrayList<>(candidates);
}
@Override
public void complete(LineReader reader, final ParsedLine commandLine, final List<Candidate> 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;
}
}

View file

@ -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 <a href="mailto:matti.rintanikkola@gmail.com">Matti Rinta-Nikkola</a>
*/
public class SystemCompleter implements Completer {
private Map<String, List<Completer>> completers = new HashMap<>();
private final Map<String, String> aliasCommand = new HashMap<>();
private StringsCompleter commands;
private boolean compiled = false;
public SystemCompleter() {
}
@Override
public void complete(LineReader reader, ParsedLine commandLine, List<Candidate> 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<Completer> completers) {
for (Completer c : completers) {
add(command, c);
}
}
public void add(List<String> 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<Completer>());
}
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<String, List<Completer>> entry : other.getCompleters().entrySet()) {
for (Completer c : entry.getValue()) {
add(entry.getKey(), c);
}
}
addAliases(other.getAliases());
}
public void addAliases(Map<String, String> aliasCommand) {
if (compiled) {
throw new IllegalStateException();
}
this.aliasCommand.putAll(aliasCommand);
}
private Map<String, String> getAliases() {
return aliasCommand;
}
public void compile() {
if (compiled) {
return;
}
Map<String, List<Completer>> compiledCompleters = new HashMap<>();
for (Map.Entry<String, List<Completer>> entry : completers.entrySet()) {
if (entry.getValue().size() == 1) {
compiledCompleters.put(entry.getKey(), entry.getValue());
} else {
compiledCompleters.put(entry.getKey(), new ArrayList<Completer>());
compiledCompleters.get(entry.getKey()).add(new AggregateCompleter(entry.getValue()));
}
}
completers = compiledCompleters;
Set<String> cmds = new HashSet<>(completers.keySet());
cmds.addAll(aliasCommand.keySet());
commands = new StringsCompleter(cmds);
compiled = true;
}
public Map<String, List<Completer>> getCompleters() {
return completers;
}
}

View file

@ -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;

View file

@ -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.
* <p>
* Implementers should install shutdown hook to call {@link DefaultHistory#save}
* to save history to disk.
* </p>
*/
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<Entry> items = new LinkedList<>();
private LineReader reader;
private Map<String, HistoryFileData> historyFiles = new HashMap<>();
private int offset = 0;
private int index = 0;
public DefaultHistory() {
}
@SuppressWarnings("this-escape")
public DefaultHistory(LineReader reader) {
attach(reader);
}
static List<Entry> doTrimHistory(List<Entry> allItems, int max) {
int idx = 0;
while (idx < allItems.size()) {
int ridx = allItems.size() - idx - 1;
String line = allItems.get(ridx).line().trim();
ListIterator<Entry> 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<Entry> 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<Entry> 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<Entry> 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<Entry> iterator(int index) {
return items.listIterator(index - offset);
}
@Override
public Spliterator<Entry> 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;
}
}
}

View file

@ -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;

View file

@ -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<InputFlag> iflag = EnumSet.noneOf(InputFlag.class);
final EnumSet<OutputFlag> oflag = EnumSet.noneOf(OutputFlag.class);
final EnumSet<ControlFlag> cflag = EnumSet.noneOf(ControlFlag.class);
final EnumSet<LocalFlag> lflag = EnumSet.noneOf(LocalFlag.class);
final EnumMap<ControlChar, Integer> cchars = new EnumMap<>(ControlChar.class);
public Attributes() {
}
@SuppressWarnings("this-escape")
public Attributes(Attributes attr) {
copy(attr);
}
public EnumSet<InputFlag> getInputFlags() {
return iflag;
}
public void setInputFlags(EnumSet<InputFlag> flags) {
iflag.clear();
iflag.addAll(flags);
}
public boolean getInputFlag(InputFlag flag) {
return iflag.contains(flag);
}
public void setInputFlags(EnumSet<InputFlag> 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<OutputFlag> getOutputFlags() {
return oflag;
}
public void setOutputFlags(EnumSet<OutputFlag> flags) {
oflag.clear();
oflag.addAll(flags);
}
public boolean getOutputFlag(OutputFlag flag) {
return oflag.contains(flag);
}
public void setOutputFlags(EnumSet<OutputFlag> 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<ControlFlag> getControlFlags() {
return cflag;
}
public void setControlFlags(EnumSet<ControlFlag> flags) {
cflag.clear();
cflag.addAll(flags);
}
public boolean getControlFlag(ControlFlag flag) {
return cflag.contains(flag);
}
public void setControlFlags(EnumSet<ControlFlag> 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<LocalFlag> getLocalFlags() {
return lflag;
}
public void setLocalFlags(EnumSet<LocalFlag> flags) {
lflag.clear();
lflag.addAll(flags);
}
public boolean getLocalFlag(LocalFlag flag) {
return lflag.contains(flag);
}
public void setLocalFlags(EnumSet<LocalFlag> 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<ControlChar, Integer> getControlChars() {
return cchars;
}
public void setControlChars(EnumMap<ControlChar, Integer> 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 = "<undef>";
} 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 <T extends Enum<T>> String append(EnumSet<T> set) {
return append(set, e -> e.name().toLowerCase());
}
private <T extends Enum<T>> String append(EnumSet<T> set, Function<T, String> 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 */
}
}

View file

@ -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 + ']';
}
}

View file

@ -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<Modifier> modifiers;
private final int x;
private final int y;
public MouseEvent(Type type, Button button, EnumSet<Modifier> 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<Modifier> 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
}
}

View file

@ -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.
* <p>
* Note each row has {@code col+1} different column positions,
* including the right margin.
* </p>
*
* @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 + ']';
}
}

View file

@ -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.
* <p>
* 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}.
* <p>
* 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 <code>Reader</code> 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 <code>Writer</code> 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 <code>true</code> 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 <code>getBufferSize()</code> 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.
* <p>
* 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
* <code>org.jline.keymap.BindingReader#runMacro(String)</code>
*
* @param discarded a consumer receiving discarded characters
* @return <code>null</code> if cursor position reporting
* is not supported or a valid cursor position
*/
Cursor getCursorPosition(IntConsumer discarded);
/**
* Returns <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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);
}
}

View file

@ -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<String> 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<Terminal> SYSTEM_TERMINAL = new AtomicReference<>();
private static final AtomicReference<Terminal> 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.
*
* <p>
* This call is equivalent to:
* <code>builder().build()</code>
* </p>
*
* @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<SystemStream, Boolean> 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> S load(Class<S> 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 <code>LineReader</code> of the {@link Terminal}. For example, the sbt
* build tool uses a <code>LineReader</code> to implement an interactive shell.
* One of its supported commands is <code>console</code> which invokes
* the scala REPL. The scala REPL also uses a <code>LineReader</code> and it
* is necessary to override the {@link Terminal} used by the the REPL to
* share the same {@link Terminal} instance used by sbt.
*
* <p>
* 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}
* </p>
*
* <p>
* 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}.
* </p>
*
* @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.
*
* <p>Use {@link Terminal#encoding()} to get the {@link Charset} that
* should be used for a {@link Terminal}.</p>
*
* @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.
*
* <p>Use {@link Terminal#encoding()} to get the {@link Charset} that
* should be used to read/write from a {@link Terminal}.</p>
*
* @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
* <code>false</code>.
*
* @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
* <code>false</code>.
*
* @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<TerminalProvider> 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<SystemStream, Boolean> 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<TerminalProvider> getProviders(String provider, IllegalStateException exception) {
List<TerminalProvider> 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<String> 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<TerminalProvider> 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<SystemStream, Boolean> 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
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}
}
}

View file

@ -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<Signal, SignalHandler> handlers = new ConcurrentHashMap<>();
protected final Set<Capability> bools = new HashSet<>();
protected final Map<Capability, Integer> ints = new HashMap<>();
protected final Map<Capability, String> 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;
}
}

View file

@ -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() {
}
}

View file

@ -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...
* <p>
* 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<Console> 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<Signal, Object> 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<Signal, Object> 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=<keyCode> - 'A' + 1
* 2). Ctrl + Backspace(0x08) : uchar=0x7f
* 3). Ctrl + Enter(0x0d) : uchar=0x0a
* 4). Ctrl + Space(0x20) : uchar=0x20
* 5). Ctrl + <Other key> : uchar=0
* 6). Ctrl + Alt + <Any key> : 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;
}
}

View file

@ -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);
}
}
}

View file

@ -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<Integer> 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);
}
}
}

View file

@ -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;
}
}

View file

@ -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() + "]";
}
}

View file

@ -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.
* <p>
* 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;
}
}

View file

@ -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.
* <p>
* 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 = <undef>;
// eol2 = <undef>; 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();
}
}
}

View file

@ -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<MouseEvent.Modifier> 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);
}
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}
}

View file

@ -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<Signal, Object> 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<Signal, Object> entry : nativeHandlers.entrySet()) {
Signals.unregister(entry.getKey().name(), entry.getValue());
}
super.doClose();
// Do not call reader.close()
reader.shutdown();
}
}

View file

@ -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 ("<UNDEF>".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<String> 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<String> getFlagsToSet(Attributes attr, Attributes current) {
List<String> 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]" : "]");
}
}

View file

@ -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<ProcessBuilder.Redirect> constructor;
private final Field fdField;
@SuppressWarnings("unchecked")
ReflectionRedirectPipeCreator() throws Exception {
Class<?> rpi = Class.forName("java.lang.ProcessBuilder$RedirectPipeImpl");
constructor = (Constructor<ProcessBuilder.Redirect>) 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);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more