| /******************************************************************************* |
| * Copyright (c) 2010, 2017 SAP AG and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Lazar Kirchev, SAP AG - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.equinox.console.common; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.felix.service.command.CommandSession; |
| import org.eclipse.equinox.console.completion.CompletionHandler; |
| import org.osgi.framework.BundleContext; |
| |
| /** |
| * This class performs the processing of the input special characters, |
| * and updates respectively what is displayed in the output. It handles |
| * escape sequences, delete, backspace, arrows, insert, home, end, pageup, pagedown, tab completion. |
| */ |
| public class ConsoleInputScanner extends Scanner { |
| |
| private static final byte TAB = 9; |
| private boolean isCR = false; |
| private boolean replace = false; |
| private boolean isCompletionMode = false; |
| // shows if command history should be saved - it is turned off in cases when passwords are to be entered |
| private boolean isHistoryEnabled = true; |
| |
| private final HistoryHolder history; |
| private final SimpleByteBuffer buffer; |
| private CommandSession session; |
| private BundleContext context; |
| private Candidates candidates; |
| private int originalCursorPos; |
| |
| public ConsoleInputScanner(ConsoleInputStream toShell, OutputStream toTelnet) { |
| super(toShell, toTelnet); |
| history = new HistoryHolder(); |
| buffer = new SimpleByteBuffer(); |
| } |
| |
| public void toggleHistoryEnabled(boolean isEnabled) { |
| isHistoryEnabled = isEnabled; |
| } |
| |
| public void setSession(CommandSession session) { |
| this.session = session; |
| } |
| |
| public void setContext(BundleContext context) { |
| this.context = context; |
| } |
| |
| @Override |
| public void scan(int b) throws IOException { |
| b &= 0xFF; |
| if (isCR) { |
| isCR = false; |
| if (b == LF) { |
| return; |
| } |
| } |
| |
| if (b != TAB) { |
| if (isCompletionMode == true) { |
| isCompletionMode = false; |
| candidates = null; |
| originalCursorPos = 0; |
| } |
| } |
| |
| if (isEsc) { |
| scanEsc(b); |
| } else { |
| if (b == getBackspace()) { |
| backSpace(); |
| } else if(b == TAB) { |
| if (isCompletionMode == false) { |
| isCompletionMode = true; |
| processTab(); |
| } else { |
| processNextTab(); |
| } |
| } else if (b == CR) { |
| isCR = true; |
| processData(); |
| } else if (b == LF) { |
| processData(); |
| } else if (b == ESC) { |
| startEsc(); |
| } else if (b == getDel()) { |
| delete(); |
| } else { |
| if (b >= SPACE && b < MAX_CHAR) { |
| newChar(b); |
| } |
| } |
| } |
| } |
| |
| private void delete() throws IOException { |
| clearLine(); |
| buffer.delete(); |
| echoBuff(); |
| flush(); |
| } |
| |
| private void backSpace() throws IOException { |
| clearLine(); |
| buffer.backSpace(); |
| echoBuff(); |
| flush(); |
| } |
| |
| protected void clearLine() throws IOException { |
| int size = buffer.getSize(); |
| int pos = buffer.getPos(); |
| for (int i = size - pos; i < size; i++) { |
| echo(BS); |
| } |
| for (int i = 0; i < size; i++) { |
| echo(SPACE); |
| } |
| for (int i = 0; i < size; i++) { |
| echo(BS); |
| } |
| } |
| |
| protected void echoBuff() throws IOException { |
| byte[] data = buffer.copyCurrentData(); |
| for (byte b : data) { |
| echo(b); |
| } |
| int pos = buffer.getPos(); |
| for (int i = data.length; i > pos; i--) { |
| echo(BS); |
| } |
| } |
| |
| protected void newChar(int b) throws IOException { |
| if (buffer.getPos() < buffer.getSize()) { |
| if (replace) { |
| buffer.replace(b); |
| } else { |
| buffer.insert(b); |
| } |
| clearLine(); |
| echoBuff(); |
| flush(); |
| } else { |
| if (replace) { |
| buffer.replace(b); |
| } else { |
| buffer.insert(b); |
| } |
| } |
| } |
| |
| protected void processTab() throws IOException { |
| CompletionHandler completionHandler = new CompletionHandler(context, session); |
| Map<String, Integer> completionCandidates = completionHandler.getCandidates(buffer.copyCurrentData(), buffer.getPos()); |
| |
| if (completionCandidates.size() == 1) { |
| completeSingleCandidate(completionCandidates); |
| isCompletionMode = false; |
| return; |
| } |
| printNewLine(); |
| if (completionCandidates.size() == 0) { |
| printCompletionError(); |
| isCompletionMode = false; |
| } else { |
| processCandidates(completionCandidates); |
| } |
| printNewLine(); |
| printPrompt(); |
| } |
| |
| protected void processCandidates(Map<String, Integer> completionCandidates) throws IOException{ |
| Set<String> candidatesNamesSet = completionCandidates.keySet(); |
| String[] candidatesNames = (candidatesNamesSet.toArray(new String[0])); |
| originalCursorPos = buffer.getPos(); |
| String[] candidateSuffixes = new String[candidatesNames.length]; |
| for (int i = 0; i < candidatesNames.length; i++) { |
| String candidateName = candidatesNames[i]; |
| candidateSuffixes[i] = getCandidateSuffix(candidateName, completionCandidates.get(candidateName), originalCursorPos); |
| for (byte symbol : candidateName.getBytes()) { |
| echo(symbol); |
| } |
| printNewLine(); |
| } |
| |
| String commonPrefix = getCommonPrefix(candidateSuffixes); |
| candidates = new Candidates(removeCommonPrefix(candidateSuffixes, commonPrefix)); |
| printString(commonPrefix, false); |
| originalCursorPos = buffer.getPos(); |
| } |
| |
| protected void processNextTab() throws IOException { |
| if (candidates == null) { |
| return; |
| } |
| |
| while (originalCursorPos < buffer.getPos()) { |
| backSpace(); |
| } |
| |
| String candidate = candidates.getCurrent(); |
| if(!candidate.equals("")) { |
| printString(candidate, true); |
| } |
| } |
| |
| protected void printCandidate(String candidate, int startIndex, int completionIndex) throws IOException { |
| String suffix = getCandidateSuffix(candidate, startIndex, completionIndex); |
| if(suffix.equals("")) { |
| return; |
| } |
| printString(suffix, true); |
| } |
| |
| protected void printString(String st, boolean isEcho) throws IOException { |
| for (byte symbol : st.getBytes()) { |
| buffer.insert(symbol); |
| if (isEcho){ |
| echo(symbol); |
| } |
| } |
| flush(); |
| } |
| |
| protected String getCommonPrefix(String[] names) { |
| if (names.length == 0) { |
| return ""; |
| } |
| |
| if (names.length == 1) { |
| return names[0]; |
| } |
| |
| StringBuilder builder = new StringBuilder(); |
| char[] name = names[0].toCharArray(); |
| for(char c : name) { |
| String prefix = builder.append(c).toString(); |
| for (int i = 1; i < names.length; i ++) { |
| if (!names[i].startsWith(prefix)) { |
| return prefix.substring(0, prefix.length() - 1); |
| } |
| } |
| } |
| |
| return builder.toString(); |
| } |
| |
| protected String[] removeCommonPrefix(String [] names, String commonPrefix){ |
| ArrayList<String> result = new ArrayList<>(); |
| for (String name : names) { |
| String nameWithoutPrefix = name.substring(commonPrefix.length()); |
| if (nameWithoutPrefix.length() > 0) { |
| result.add(nameWithoutPrefix); |
| } |
| } |
| result.add(""); |
| return result.toArray(new String[0]); |
| } |
| |
| protected String getCandidateSuffix(String candidate, int startIndex, int completionIndex) { |
| int partialLength = completionIndex - startIndex; |
| if (partialLength >= candidate.length()) { |
| return ""; |
| } |
| return candidate.substring(partialLength); |
| } |
| |
| protected void completeSingleCandidate(Map<String, Integer> completionCandidates) throws IOException { |
| Set<String> keys = completionCandidates.keySet(); |
| String key = (keys.toArray(new String[0]))[0]; |
| int startIndex = completionCandidates.get(key); |
| printCandidate(key, startIndex, buffer.getPos()); |
| } |
| |
| protected void printCompletionError() throws IOException { |
| byte[] curr = buffer.getCurrentData(); |
| if (isHistoryEnabled == true) { |
| history.add(curr); |
| } |
| |
| String errorMessage = "No completion available"; |
| for (byte symbol : errorMessage.getBytes()) { |
| echo(symbol); |
| } |
| } |
| |
| protected void printNewLine() throws IOException{ |
| echo(CR); |
| echo(LF); |
| flush(); |
| } |
| |
| protected void printPrompt() throws IOException{ |
| echo('o'); |
| echo('s'); |
| echo('g'); |
| echo('i'); |
| echo('>'); |
| echo(SPACE); |
| echoBuff(); |
| flush(); |
| } |
| |
| private void processData() throws IOException { |
| // buffer.add(CR); |
| buffer.add(LF); |
| echo(CR); |
| echo(LF); |
| flush(); |
| byte[] curr = buffer.getCurrentData(); |
| if (isHistoryEnabled == true) { |
| history.add(curr); |
| } |
| toShell.add(curr); |
| } |
| |
| public void resetHistory() { |
| history.reset(); |
| } |
| |
| @Override |
| protected void scanEsc(final int b) throws IOException { |
| esc += (char) b; |
| KEYS key = checkEscape(esc); |
| if (key == KEYS.UNFINISHED) { |
| return; |
| } |
| if (key == KEYS.UNKNOWN) { |
| isEsc = false; |
| scan(b); |
| return; |
| } |
| isEsc = false; |
| switch (key) { |
| case UP: |
| processUpArrow(); |
| break; |
| case DOWN: |
| processDownArrow(); |
| break; |
| case RIGHT: |
| processRightArrow(); |
| break; |
| case LEFT: |
| processLeftArrow(); |
| break; |
| case HOME: |
| processHome(); |
| break; |
| case END: |
| processEnd(); |
| break; |
| case PGUP: |
| processPgUp(); |
| break; |
| case PGDN: |
| processPgDn(); |
| break; |
| case INS: |
| processIns(); |
| break; |
| case DEL: |
| delete(); |
| break; |
| default: //CENTER |
| break; |
| } |
| } |
| |
| private static final byte[] INVERSE_ON = {ESC, '[', '7', 'm'}; |
| private static final byte[] INVERSE_OFF = {ESC, '[', '2', '7', 'm'}; |
| |
| private void echo(byte[] data) throws IOException { |
| for (byte b : data) { |
| echo(b); |
| } |
| } |
| |
| private void processIns() throws IOException { |
| replace = !replace; |
| int b = buffer.getCurrentChar(); |
| echo(INVERSE_ON); |
| echo(replace ? 'R' : 'I'); |
| flush(); |
| try { |
| Thread.sleep(300); |
| } catch (InterruptedException e) { |
| //do not care $JL-EXC$ |
| } |
| echo(INVERSE_OFF); |
| echo(BS); |
| echo(b == -1 ? SPACE : b); |
| echo(BS); |
| flush(); |
| } |
| |
| private void processPgDn() throws IOException { |
| byte[] last = history.last(); |
| if (last != null) { |
| clearLine(); |
| buffer.set(last); |
| echoBuff(); |
| flush(); |
| } |
| } |
| |
| private void processPgUp() throws IOException { |
| byte[] first = history.first(); |
| if (first != null) { |
| clearLine(); |
| buffer.set(first); |
| echoBuff(); |
| flush(); |
| } |
| } |
| |
| private void processHome() throws IOException { |
| int pos = buffer.resetPos(); |
| if (pos > 0) { |
| for (int i = 0; i < pos; i++) { |
| echo(BS); |
| } |
| flush(); |
| } |
| } |
| |
| private void processEnd() throws IOException { |
| int b; |
| while ((b = buffer.goRight()) != -1) { |
| echo(b); |
| } |
| flush(); |
| } |
| |
| private void processLeftArrow() throws IOException { |
| if (buffer.goLeft()) { |
| echo(BS); |
| flush(); |
| } |
| } |
| |
| private void processRightArrow() throws IOException { |
| int b = buffer.goRight(); |
| if (b != -1) { |
| echo(b); |
| flush(); |
| } |
| } |
| |
| private void processDownArrow() throws IOException { |
| byte[] next = history.next(); |
| if (next != null) { |
| clearLine(); |
| buffer.set(next); |
| echoBuff(); |
| flush(); |
| } |
| } |
| |
| private void processUpArrow() throws IOException { |
| clearLine(); |
| byte[] prev = history.prev(); |
| buffer.set(prev); |
| echoBuff(); |
| flush(); |
| } |
| |
| private static class Candidates { |
| private String[] candidates; |
| private int currentCandidateIndex = 0; |
| |
| public Candidates(String[] candidates) { |
| this.candidates = candidates.clone(); |
| } |
| |
| public String getCurrent() { |
| if (currentCandidateIndex >= candidates.length) { |
| currentCandidateIndex = 0; |
| } |
| |
| return candidates[currentCandidateIndex++]; |
| } |
| } |
| } |