diff --git a/src/main/java/de/codeshelf/consoleui/Basic.java b/src/main/java/de/codeshelf/consoleui/Basic.java index cef614e4..1c540540 100644 --- a/src/main/java/de/codeshelf/consoleui/Basic.java +++ b/src/main/java/de/codeshelf/consoleui/Basic.java @@ -6,6 +6,10 @@ import de.codeshelf.consoleui.prompt.ConsolePrompt.UiConfig; import de.codeshelf.consoleui.prompt.PromptResultItemIF; import de.codeshelf.consoleui.prompt.builder.PromptBuilder; +import org.jline.builtins.Completers; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.impl.completer.StringsCompleter; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; import org.jline.utils.AttributedString; @@ -13,6 +17,7 @@ import org.jline.utils.AttributedStyle; import org.jline.utils.OSUtils; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -51,7 +56,12 @@ public static void main(String[] args) { } else { config = new UiConfig("\u276F", "\u25EF ", "\u25C9 ", "\u25EF "); } - ConsolePrompt prompt = new ConsolePrompt(terminal, config); + // + // LineReader is needed only if you are adding JLine Completers in your prompts. + // If you are not using Completers you do not need to create LineReader. + // + LineReader reader = LineReaderBuilder.builder().terminal(terminal).build(); + ConsolePrompt prompt = new ConsolePrompt(reader, terminal, config); PromptBuilder promptBuilder = prompt.getPromptBuilder(); @@ -59,7 +69,9 @@ public static void main(String[] args) { .name("name") .message("Please enter your name") .defaultValue("John Doe") - // .mask('*') + //.mask('*') + .addCompleter(new Completers.FilesCompleter(() -> Paths.get(System.getProperty("user.dir")))) +// .addCompleter(new StringsCompleter("Jim", "Jack", "John")) .addPrompt(); promptBuilder.createListPrompt() diff --git a/src/main/java/de/codeshelf/consoleui/elements/InputValue.java b/src/main/java/de/codeshelf/consoleui/elements/InputValue.java index 05e3d7dd..91975d05 100644 --- a/src/main/java/de/codeshelf/consoleui/elements/InputValue.java +++ b/src/main/java/de/codeshelf/consoleui/elements/InputValue.java @@ -1,5 +1,7 @@ package de.codeshelf.consoleui.elements; +import org.jline.reader.Completer; + /** * User: Andreas Wegmann * Date: 06.01.16 @@ -8,6 +10,7 @@ public class InputValue extends AbstractPromptableElement { private String value; private String defaultValue; private Character mask; + private Completer completer; public InputValue(String name, String message) { super(message, name); @@ -38,4 +41,13 @@ public void setMask(Character mask) { public Character getMask() { return mask; } + + public void setCompleter(Completer completer) { + this.completer = completer; + } + + public Completer getCompleter() { + return completer; + } + } diff --git a/src/main/java/de/codeshelf/consoleui/prompt/AbstractPrompt.java b/src/main/java/de/codeshelf/consoleui/prompt/AbstractPrompt.java index 6b438140..c8f1a3b2 100644 --- a/src/main/java/de/codeshelf/consoleui/prompt/AbstractPrompt.java +++ b/src/main/java/de/codeshelf/consoleui/prompt/AbstractPrompt.java @@ -14,13 +14,13 @@ import de.codeshelf.consoleui.prompt.ConsolePrompt.UiConfig; import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; +import org.jline.reader.*; +import org.jline.reader.impl.CompletionMatcherImpl; import org.jline.terminal.Size; import org.jline.terminal.Terminal; import org.jline.utils.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -39,7 +39,7 @@ public abstract class AbstractPrompt { protected final List items; protected final int firstItemRow; private final Size size = new Size(); - private final ConsolePrompt.UiConfig config; + protected final ConsolePrompt.UiConfig config; private Display display; private ListRange range = null; @@ -66,7 +66,7 @@ protected void resetDisplay() { } protected void refreshDisplay(int row) { - refreshDisplay(row, 0, null); + refreshDisplay(row, 0, null, false); } protected void refreshDisplay(int row, Set selected) { @@ -89,8 +89,74 @@ protected void refreshDisplay(int row, int column, String buffer, boolean newlin display.update(displayLines(row, asb.toAttributedString(), newline), size.cursorPos(crow, column)); } - protected void refreshDisplay(int row, int column, String buffer) { - refreshDisplay(row, column, buffer, false); + protected void refreshDisplay(int buffRow, int buffCol, String buffer, int candRow, int candCol, List candidates) { + display.resize(size.getRows(), size.getColumns()); + AttributedStringBuilder asb = new AttributedStringBuilder(); + if (buffer != null) { + asb.style(AttributedStyle.DEFAULT).append(buffer); + } + display.update(displayLines(candRow, candCol, asb.toAttributedString(), candidates), size.cursorPos(buffRow, buffCol)); + } + + private int candidateStartPosition(int candidatesColumn, String buffer, List cands) { + List values = cands.stream().map(c -> AttributedString.stripAnsi(c.displ())) + .filter(c -> !c.matches("\\w+") && c.length() > 1).collect(Collectors.toList()); + Set notDelimiters = new HashSet<>(); + values.forEach(v -> v.substring(0, v.length() - 1).chars() + .filter(c -> !Character.isDigit(c) && !Character.isAlphabetic(c)) + .forEach(c -> notDelimiters.add(Character.toString((char)c)))); + int out = candidatesColumn; + for (int i = buffer.length(); i > 0; i--) { + if (buffer.substring(0, i).matches(".*\\W") + && !notDelimiters.contains(buffer.substring(i - 1, i))) { + out += i; + break; + } + } + return out; + } + + private List displayLines(int cursorRow, int candidatesColumn, AttributedString buffer + , List candidates) { + List out = new ArrayList<>(header); + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(message); + asb.append(buffer); + out.add(asb.toAttributedString()); + if (candidates.size() < size.getRows() - header.size()) { + int listStart; + if (cursorRow - firstItemRow >= 0) { + String dc = candidates.get(cursorRow - firstItemRow).displ(); + listStart = candidatesColumn + buffer.columnLength() - display.wcwidth(dc) + + (AttributedString.stripAnsi(dc).endsWith("*") ? 1 : 0); + } else { + listStart = candidateStartPosition(candidatesColumn, buffer.toString(), candidates); + } + int width = Math.max(candidates.stream().map(Candidate::displ).mapToInt(display::wcwidth).max().orElse(20), 20); + int i = firstItemRow; + for (Candidate c : candidates) { + asb = new AttributedStringBuilder(); + AttributedStringBuilder tmp = new AttributedStringBuilder(); + tmp.ansiAppend(c.displ()); + asb.style(tmp.styleAt(0)); + if (i == cursorRow) { + asb.style(new AttributedStyle().inverse()); + } + asb.append(AttributedString.stripAnsi(c.displ())); + int cl = asb.columnLength(); + for (int k = cl; k < width; k++) { + asb.append(" "); + } + AttributedStringBuilder asb2 = new AttributedStringBuilder(); + asb2.tabs(listStart); + asb2.append("\t"); + asb2.style(config.style(".cb")); + asb2.append(asb).append(" "); + out.add(asb2.toAttributedString()); + i++; + } + } + return out; } private List displayLines(int cursorRow, Set selected) { @@ -334,7 +400,7 @@ public ConfirmResult execute() { StringBuilder buffer = new StringBuilder(); ConfirmChoice.ConfirmationValue confirm = defaultValue; while (true) { - refreshDisplay(row, column, buffer.toString()); + refreshDisplay(row, column, buffer.toString(), false); Operation op = bindingReader.readBinding(keyMap); buffer = new StringBuilder(); switch (op) { @@ -360,22 +426,27 @@ public ConfirmResult execute() { } protected static class InputValuePrompt extends AbstractPrompt { - private enum Operation {INSERT, BACKSPACE, DELETE, RIGHT, LEFT, BEGINNING_OF_LINE, END_OF_LINE, EXIT} + private enum Operation {INSERT, BACKSPACE, DELETE, RIGHT, LEFT, BEGINNING_OF_LINE, END_OF_LINE, SELECT_CANDIDATE, EXIT} + private enum SelectOp {FORWARD_ONE_LINE, BACKWARD_ONE_LINE, EXIT} private final int startColumn; private final String defaultValue; private final Character mask; + private final LineReader reader; + private final Completer completer; - private InputValuePrompt(Terminal terminal, List header, AttributedString message + private InputValuePrompt(LineReader reader, Terminal terminal, List header, AttributedString message , InputValue inputValue, UiConfig cfg) { super(terminal, header, message, cfg); + this.reader = reader; defaultValue = inputValue.getDefaultValue(); startColumn = message.columnLength(); mask = inputValue.getMask(); + this.completer = inputValue.getCompleter(); } - public static InputValuePrompt getPrompt(Terminal terminal, List header, AttributedString message + public static InputValuePrompt getPrompt(LineReader reader, Terminal terminal, List header, AttributedString message , InputValue inputValue, UiConfig cfg) { - return new InputValuePrompt(terminal, header, message, inputValue, cfg); + return new InputValuePrompt(reader, terminal, header, message, inputValue, cfg); } private void bindKeys(KeyMap map) { @@ -393,17 +464,35 @@ private void bindKeys(KeyMap map) { map.bind(Operation.END_OF_LINE, ctrl('E'), key(terminal, InfoCmp.Capability.key_end)); map.bind(Operation.RIGHT, ctrl('F')); map.bind(Operation.LEFT, ctrl('B')); + map.bind(Operation.SELECT_CANDIDATE, "\t"); + } + + private void bindSelectKeys(KeyMap map) { + map.bind(SelectOp.FORWARD_ONE_LINE, "\t", "e", ctrl('E'), key(terminal, InfoCmp.Capability.key_down)); + map.bind(SelectOp.BACKWARD_ONE_LINE, "y", ctrl('Y'), key(terminal, InfoCmp.Capability.key_up)); + map.bind(SelectOp.EXIT,"\r"); } public InputResult execute() { resetDisplay(); int row = firstItemRow - 1; int column = startColumn; + List matches = new ArrayList<>(); KeyMap keyMap = new KeyMap<>(); bindKeys(keyMap); StringBuilder buffer = new StringBuilder(); + CompletionMatcher completionMatcher = new CompletionMatcherImpl(); while (true) { - refreshDisplay(row, column, buffer.toString()); + if (completer != null && reader != null) { + List possible = new ArrayList<>(); + CompletingWord completingWord = new CompletingWord(buffer.toString()); + completer.complete(reader, completingWord, possible); + completionMatcher.compile(config.completionOptions(), false, completingWord, false, 0 + , null); + matches = completionMatcher.matches(possible).stream().sorted(Comparator.naturalOrder()) + .collect(Collectors.toList()); + } + refreshDisplay(firstItemRow - 1, column, buffer.toString(), row, startColumn, matches); Operation op = bindingReader.readBinding(keyMap); switch (op) { case LEFT: @@ -423,8 +512,8 @@ public InputResult execute() { case BACKSPACE: if (column > startColumn) { buffer.deleteCharAt(column - startColumn - 1); + column--; } - column--; break; case DELETE: if (column < startColumn + buffer.length() && column >= startColumn) { @@ -437,6 +526,12 @@ public InputResult execute() { case END_OF_LINE: column = startColumn + buffer.length(); break; + case SELECT_CANDIDATE: + String selected = selectCandidate(firstItemRow - 1, buffer.toString(),row + 1, startColumn, matches); + buffer.delete(0, buffer.length()); + buffer.append(selected); + column = startColumn + buffer.length(); + break; case EXIT: if (buffer.toString().isEmpty()) { buffer.append(defaultValue); @@ -446,6 +541,91 @@ public InputResult execute() { } } + String selectCandidate(int buffRow, String buffer, int row, int column, List candidates) { + if (candidates.isEmpty()) { + return buffer; + } else if (candidates.size() == 1) { + return candidates.get(0).value(); + } + KeyMap keyMap = new KeyMap<>(); + bindSelectKeys(keyMap); + while (true) { + String selected = candidates.get(row - buffRow - 1).value(); + refreshDisplay(buffRow, column + selected.length(), selected, row, column, candidates); + SelectOp op = bindingReader.readBinding(keyMap); + switch (op) { + case FORWARD_ONE_LINE: + if (row < buffRow + candidates.size()) { + row++; + } else { + row = buffRow + 1; + } + break; + case BACKWARD_ONE_LINE: + if (row > buffRow + 1) { + row--; + } else { + row = buffRow + candidates.size() - 1; + } + break; + case EXIT: + return selected; + } + } + } + } + + private static class CompletingWord implements CompletingParsedLine { + private final String word; + + public CompletingWord(String word) { + this.word = word; + } + + @Override + public CharSequence escape(CharSequence candidate, boolean complete) { + return null; + } + + @Override + public int rawWordCursor() { + return word.length(); + } + + @Override + public int rawWordLength() { + return word.length(); + } + + @Override + public String word() { + return word; + } + + @Override + public int wordCursor() { + return word.length(); + } + + @Override + public int wordIndex() { + return 0; + } + + @Override + public List words() { + return new ArrayList<>(Collections.singletonList(word)); + } + + @Override + public String line() { + return word; + } + + @Override + public int cursor() { + return word.length(); + } } private static int nextRow(int row, int firstItemRow, List items) { diff --git a/src/main/java/de/codeshelf/consoleui/prompt/ConsolePrompt.java b/src/main/java/de/codeshelf/consoleui/prompt/ConsolePrompt.java index 127e0018..0bebcefe 100644 --- a/src/main/java/de/codeshelf/consoleui/prompt/ConsolePrompt.java +++ b/src/main/java/de/codeshelf/consoleui/prompt/ConsolePrompt.java @@ -6,6 +6,7 @@ import de.codeshelf.consoleui.prompt.builder.PromptBuilder; import org.jline.builtins.Styles; import de.codeshelf.consoleui.elements.items.ConsoleUIItemIF; +import org.jline.reader.LineReader; import org.jline.terminal.Attributes; import org.jline.terminal.Terminal; import org.jline.utils.*; @@ -20,6 +21,7 @@ * @author Matti Rinta-Nikkola */ public class ConsolePrompt { + private final LineReader reader; private final Terminal terminal; private final UiConfig config; @@ -28,16 +30,20 @@ public class ConsolePrompt { * @param terminal the terminal. */ public ConsolePrompt(Terminal terminal) { - this(terminal, new UiConfig()); + this(null, terminal, new UiConfig()); + } + public ConsolePrompt(Terminal terminal, UiConfig config) { + this(null, terminal, config); } /** * * @param terminal the terminal. * @param config ConsolePrompt cursor pointer and checkbox configuration */ - public ConsolePrompt(Terminal terminal, UiConfig config) { + public ConsolePrompt(LineReader reader, Terminal terminal, UiConfig config) { this.terminal = terminal; this.config = config; + this.reader = reader; } /** @@ -91,7 +97,7 @@ public Map prompt(List header if (ip.getDefaultValue() != null) { asb.append("(").append(ip.getDefaultValue()).append(") "); } - result = InputValuePrompt.getPrompt(terminal, header, asb.toAttributedString(), ip, config).execute(); + result = InputValuePrompt.getPrompt(reader, terminal, header, asb.toAttributedString(), ip, config).execute(); } else if (pe instanceof ExpandableChoice) { ExpandableChoice ec = (ExpandableChoice) pe; asb.append("("); @@ -168,7 +174,7 @@ public PromptBuilder getPromptBuilder() { * ConsoleUI colors are configurable using UI_COLORS environment variable */ public static class UiConfig { - static final String DEFAULT_UI_COLORS = "cu=36:be=32:bd=37:pr=32:me=1:an=36:se=36"; + static final String DEFAULT_UI_COLORS = "cu=36:be=32:bd=37:pr=32:me=1:an=36:se=36:cb=100"; static final String UI_COLORS = "UI_COLORS"; private final AttributedString indicator; private final AttributedString uncheckedBox; @@ -176,6 +182,7 @@ public static class UiConfig { private final AttributedString unavailable; private final StyleResolver resolver; private final ResourceBundle resourceBundle; + private Map completionOptions = new HashMap<>(); public UiConfig() { this(null, null, null, null); @@ -223,6 +230,14 @@ public ResourceBundle resourceBundle() { return resourceBundle; } + public void setCompletionOptions(Map completionOptions) { + this.completionOptions = completionOptions; + } + + public Map completionOptions() { + return completionOptions; + } + private static StyleResolver resolver(String style) { Map colors = Arrays.stream(style.split(":")) .collect(Collectors.toMap(s -> s.substring(0, s.indexOf('=')), diff --git a/src/main/java/de/codeshelf/consoleui/prompt/builder/InputValueBuilder.java b/src/main/java/de/codeshelf/consoleui/prompt/builder/InputValueBuilder.java index 4526b24f..01fbb894 100644 --- a/src/main/java/de/codeshelf/consoleui/prompt/builder/InputValueBuilder.java +++ b/src/main/java/de/codeshelf/consoleui/prompt/builder/InputValueBuilder.java @@ -1,6 +1,7 @@ package de.codeshelf.consoleui.prompt.builder; import de.codeshelf.consoleui.elements.InputValue; +import org.jline.reader.Completer; /** * Created by andy on 22.01.16. @@ -11,6 +12,7 @@ public class InputValueBuilder { private String defaultValue; private String message; private Character mask; + private Completer completer; public InputValueBuilder(PromptBuilder promptBuilder) { this.promptBuilder = promptBuilder; @@ -41,8 +43,15 @@ public PromptBuilder addPrompt() { if (mask != null) { inputValue.setMask(mask); } + if (completer != null) { + inputValue.setCompleter(completer); + } promptBuilder.addPrompt(inputValue); return promptBuilder; } + public InputValueBuilder addCompleter(Completer completer) { + this.completer = completer; + return this; + } }