This commit is contained in:
parent
e9fde15d5e
commit
52525afe72
140 changed files with 46348 additions and 1 deletions
1626
net-cli/src/main/java/org/jline/builtins/Commands.java
Normal file
1626
net-cli/src/main/java/org/jline/builtins/Commands.java
Normal file
File diff suppressed because it is too large
Load diff
1002
net-cli/src/main/java/org/jline/builtins/Completers.java
Normal file
1002
net-cli/src/main/java/org/jline/builtins/Completers.java
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
30
net-cli/src/main/java/org/jline/builtins/InputRC.java
Normal file
30
net-cli/src/main/java/org/jline/builtins/InputRC.java
Normal 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);
|
||||
}
|
||||
}
|
1587
net-cli/src/main/java/org/jline/builtins/Less.java
Normal file
1587
net-cli/src/main/java/org/jline/builtins/Less.java
Normal file
File diff suppressed because it is too large
Load diff
292
net-cli/src/main/java/org/jline/builtins/NfaMatcher.java
Normal file
292
net-cli/src/main/java/org/jline/builtins/NfaMatcher.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
588
net-cli/src/main/java/org/jline/builtins/Options.java
Normal file
588
net-cli/src/main/java/org/jline/builtins/Options.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
1976
net-cli/src/main/java/org/jline/builtins/ScreenTerminal.java
Normal file
1976
net-cli/src/main/java/org/jline/builtins/ScreenTerminal.java
Normal file
File diff suppressed because it is too large
Load diff
172
net-cli/src/main/java/org/jline/builtins/Source.java
Normal file
172
net-cli/src/main/java/org/jline/builtins/Source.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
221
net-cli/src/main/java/org/jline/builtins/Styles.java
Normal file
221
net-cli/src/main/java/org/jline/builtins/Styles.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
995
net-cli/src/main/java/org/jline/builtins/SyntaxHighlighter.java
Normal file
995
net-cli/src/main/java/org/jline/builtins/SyntaxHighlighter.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
644
net-cli/src/main/java/org/jline/builtins/TTop.java
Normal file
644
net-cli/src/main/java/org/jline/builtins/TTop.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
2095
net-cli/src/main/java/org/jline/builtins/Tmux.java
Normal file
2095
net-cli/src/main/java/org/jline/builtins/Tmux.java
Normal file
File diff suppressed because it is too large
Load diff
46
net-cli/src/main/java/org/jline/console/ArgDesc.java
Normal file
46
net-cli/src/main/java/org/jline/console/ArgDesc.java
Normal 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;
|
||||
}
|
||||
}
|
132
net-cli/src/main/java/org/jline/console/CmdDesc.java
Normal file
132
net-cli/src/main/java/org/jline/console/CmdDesc.java
Normal 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("");
|
||||
}
|
||||
}
|
72
net-cli/src/main/java/org/jline/console/CmdLine.java
Normal file
72
net-cli/src/main/java/org/jline/console/CmdLine.java
Normal 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
|
||||
}
|
||||
}
|
75
net-cli/src/main/java/org/jline/console/CommandInput.java
Normal file
75
net-cli/src/main/java/org/jline/console/CommandInput.java
Normal 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);
|
||||
}
|
||||
}
|
40
net-cli/src/main/java/org/jline/console/CommandMethods.java
Normal file
40
net-cli/src/main/java/org/jline/console/CommandMethods.java
Normal 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;
|
||||
}
|
||||
}
|
163
net-cli/src/main/java/org/jline/console/CommandRegistry.java
Normal file
163
net-cli/src/main/java/org/jline/console/CommandRegistry.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
318
net-cli/src/main/java/org/jline/console/ConsoleEngine.java
Normal file
318
net-cli/src/main/java/org/jline/console/ConsoleEngine.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
228
net-cli/src/main/java/org/jline/console/Printer.java
Normal file
228
net-cli/src/main/java/org/jline/console/Printer.java
Normal 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
|
||||
}
|
||||
}
|
196
net-cli/src/main/java/org/jline/console/ScriptEngine.java
Normal file
196
net-cli/src/main/java/org/jline/console/ScriptEngine.java
Normal 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);
|
||||
}
|
223
net-cli/src/main/java/org/jline/console/SystemRegistry.java
Normal file
223
net-cli/src/main/java/org/jline/console/SystemRegistry.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
287
net-cli/src/main/java/org/jline/console/impl/Builtins.java
Normal file
287
net-cli/src/main/java/org/jline/console/impl/Builtins.java
Normal 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
|
||||
}
|
||||
}
|
1389
net-cli/src/main/java/org/jline/console/impl/ConsoleEngineImpl.java
Normal file
1389
net-cli/src/main/java/org/jline/console/impl/ConsoleEngineImpl.java
Normal file
File diff suppressed because it is too large
Load diff
1239
net-cli/src/main/java/org/jline/console/impl/DefaultPrinter.java
Normal file
1239
net-cli/src/main/java/org/jline/console/impl/DefaultPrinter.java
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
2087
net-cli/src/main/java/org/jline/console/impl/SystemRegistryImpl.java
Normal file
2087
net-cli/src/main/java/org/jline/console/impl/SystemRegistryImpl.java
Normal file
File diff suppressed because it is too large
Load diff
235
net-cli/src/main/java/org/jline/keymap/BindingReader.java
Normal file
235
net-cli/src/main/java/org/jline/keymap/BindingReader.java
Normal 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 (>= 128) characters will be matched to {@link KeyMap#getUnicode()}.
|
||||
* If the current key sequence is ambiguous, i.e. the sequence is bound but
|
||||
* it's also a prefix to other sequences, then the {@link KeyMap#getAmbiguousTimeout()}
|
||||
* timeout will be used to wait for another incoming character.
|
||||
* If a character comes, the disambiguation will be done. If the timeout elapses
|
||||
* and no character came in, or if the timeout is <= 0, the current bound operation
|
||||
* will be returned.
|
||||
*
|
||||
* @param keys the KeyMap to use for decoding the input stream
|
||||
* @param <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;
|
||||
}
|
||||
}
|
450
net-cli/src/main/java/org/jline/keymap/KeyMap.java
Normal file
450
net-cli/src/main/java/org/jline/keymap/KeyMap.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
21
net-cli/src/main/java/org/jline/reader/Binding.java
Normal file
21
net-cli/src/main/java/org/jline/reader/Binding.java
Normal 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 {
|
||||
}
|
91
net-cli/src/main/java/org/jline/reader/Buffer.java
Normal file
91
net-cli/src/main/java/org/jline/reader/Buffer.java
Normal 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();
|
||||
}
|
204
net-cli/src/main/java/org/jline/reader/Candidate.java
Normal file
204
net-cli/src/main/java/org/jline/reader/Candidate.java
Normal 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 + "}";
|
||||
}
|
||||
}
|
37
net-cli/src/main/java/org/jline/reader/Completer.java
Normal file
37
net-cli/src/main/java/org/jline/reader/Completer.java
Normal 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);
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
45
net-cli/src/main/java/org/jline/reader/EOFError.java
Normal file
45
net-cli/src/main/java/org/jline/reader/EOFError.java
Normal 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;
|
||||
}
|
||||
}
|
20
net-cli/src/main/java/org/jline/reader/Editor.java
Normal file
20
net-cli/src/main/java/org/jline/reader/Editor.java
Normal 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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
16
net-cli/src/main/java/org/jline/reader/Expander.java
Normal file
16
net-cli/src/main/java/org/jline/reader/Expander.java
Normal 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);
|
||||
}
|
44
net-cli/src/main/java/org/jline/reader/Highlighter.java
Normal file
44
net-cli/src/main/java/org/jline/reader/Highlighter.java
Normal 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);
|
||||
}
|
218
net-cli/src/main/java/org/jline/reader/History.java
Normal file
218
net-cli/src/main/java/org/jline/reader/History.java
Normal 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();
|
||||
}
|
||||
}
|
829
net-cli/src/main/java/org/jline/reader/LineReader.java
Normal file
829
net-cli/src/main/java/org/jline/reader/LineReader.java
Normal 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><tab></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
|
||||
}
|
||||
}
|
155
net-cli/src/main/java/org/jline/reader/LineReaderBuilder.java
Normal file
155
net-cli/src/main/java/org/jline/reader/LineReaderBuilder.java
Normal 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;
|
||||
}
|
||||
}
|
40
net-cli/src/main/java/org/jline/reader/Macro.java
Normal file
40
net-cli/src/main/java/org/jline/reader/Macro.java
Normal 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 + ']';
|
||||
}
|
||||
}
|
34
net-cli/src/main/java/org/jline/reader/MaskingCallback.java
Normal file
34
net-cli/src/main/java/org/jline/reader/MaskingCallback.java
Normal 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);
|
||||
}
|
67
net-cli/src/main/java/org/jline/reader/ParsedLine.java
Normal file
67
net-cli/src/main/java/org/jline/reader/ParsedLine.java
Normal 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();
|
||||
}
|
90
net-cli/src/main/java/org/jline/reader/Parser.java
Normal file
90
net-cli/src/main/java/org/jline/reader/Parser.java
Normal 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
|
||||
}
|
||||
}
|
41
net-cli/src/main/java/org/jline/reader/PrintAboveWriter.java
Normal file
41
net-cli/src/main/java/org/jline/reader/PrintAboveWriter.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
43
net-cli/src/main/java/org/jline/reader/Reference.java
Normal file
43
net-cli/src/main/java/org/jline/reader/Reference.java
Normal 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 + ']';
|
||||
}
|
||||
}
|
31
net-cli/src/main/java/org/jline/reader/SyntaxError.java
Normal file
31
net-cli/src/main/java/org/jline/reader/SyntaxError.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
18
net-cli/src/main/java/org/jline/reader/Widget.java
Normal file
18
net-cli/src/main/java/org/jline/reader/Widget.java
Normal 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();
|
||||
}
|
374
net-cli/src/main/java/org/jline/reader/impl/BufferImpl.java
Normal file
374
net-cli/src/main/java/org/jline/reader/impl/BufferImpl.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
209
net-cli/src/main/java/org/jline/reader/impl/DefaultExpander.java
Normal file
209
net-cli/src/main/java/org/jline/reader/impl/DefaultExpander.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
811
net-cli/src/main/java/org/jline/reader/impl/DefaultParser.java
Normal file
811
net-cli/src/main/java/org/jline/reader/impl/DefaultParser.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
393
net-cli/src/main/java/org/jline/reader/impl/InputRC.java
Normal file
393
net-cli/src/main/java/org/jline/reader/impl/InputRC.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
176
net-cli/src/main/java/org/jline/reader/impl/KillRing.java
Normal file
176
net-cli/src/main/java/org/jline/reader/impl/KillRing.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
6429
net-cli/src/main/java/org/jline/reader/impl/LineReaderImpl.java
Normal file
6429
net-cli/src/main/java/org/jline/reader/impl/LineReaderImpl.java
Normal file
File diff suppressed because it is too large
Load diff
80
net-cli/src/main/java/org/jline/reader/impl/ReaderUtils.java
Normal file
80
net-cli/src/main/java/org/jline/reader/impl/ReaderUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
75
net-cli/src/main/java/org/jline/reader/impl/UndoTree.java
Normal file
75
net-cli/src/main/java/org/jline/reader/impl/UndoTree.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 + '}';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
349
net-cli/src/main/java/org/jline/terminal/Attributes.java
Normal file
349
net-cli/src/main/java/org/jline/terminal/Attributes.java
Normal 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 */
|
||||
}
|
||||
}
|
52
net-cli/src/main/java/org/jline/terminal/Cursor.java
Normal file
52
net-cli/src/main/java/org/jline/terminal/Cursor.java
Normal 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 + ']';
|
||||
}
|
||||
}
|
80
net-cli/src/main/java/org/jline/terminal/MouseEvent.java
Normal file
80
net-cli/src/main/java/org/jline/terminal/MouseEvent.java
Normal 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
|
||||
}
|
||||
}
|
80
net-cli/src/main/java/org/jline/terminal/Size.java
Normal file
80
net-cli/src/main/java/org/jline/terminal/Size.java
Normal 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 + ']';
|
||||
}
|
||||
}
|
396
net-cli/src/main/java/org/jline/terminal/Terminal.java
Normal file
396
net-cli/src/main/java/org/jline/terminal/Terminal.java
Normal 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);
|
||||
}
|
||||
}
|
865
net-cli/src/main/java/org/jline/terminal/TerminalBuilder.java
Normal file
865
net-cli/src/main/java/org/jline/terminal/TerminalBuilder.java
Normal 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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
234
net-cli/src/main/java/org/jline/terminal/impl/AbstractPty.java
Normal file
234
net-cli/src/main/java/org/jline/terminal/impl/AbstractPty.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
108
net-cli/src/main/java/org/jline/terminal/impl/CursorSupport.java
Normal file
108
net-cli/src/main/java/org/jline/terminal/impl/CursorSupport.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
194
net-cli/src/main/java/org/jline/terminal/impl/Diag.java
Normal file
194
net-cli/src/main/java/org/jline/terminal/impl/Diag.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
165
net-cli/src/main/java/org/jline/terminal/impl/DumbTerminal.java
Normal file
165
net-cli/src/main/java/org/jline/terminal/impl/DumbTerminal.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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() + "]";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
140
net-cli/src/main/java/org/jline/terminal/impl/MouseSupport.java
Normal file
140
net-cli/src/main/java/org/jline/terminal/impl/MouseSupport.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
310
net-cli/src/main/java/org/jline/terminal/impl/exec/ExecPty.java
Normal file
310
net-cli/src/main/java/org/jline/terminal/impl/exec/ExecPty.java
Normal 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]" : "]");
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue