/*
* Copyright 2012 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Eclipse Public License version 1.0, available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.jboss.aesh.console;
import org.jboss.aesh.complete.CompleteOperation;
import org.jboss.aesh.complete.Completion;
import org.jboss.aesh.console.alias.Alias;
import org.jboss.aesh.console.alias.AliasCompletion;
import org.jboss.aesh.console.alias.AliasManager;
import org.jboss.aesh.console.helper.Search;
import org.jboss.aesh.console.operator.ControlOperator;
import org.jboss.aesh.console.operator.ControlOperatorParser;
import org.jboss.aesh.console.operator.RedirectionCompletion;
import org.jboss.aesh.console.settings.Settings;
import org.jboss.aesh.edit.EditMode;
import org.jboss.aesh.edit.Mode;
import org.jboss.aesh.edit.PasteManager;
import org.jboss.aesh.edit.ViEditMode;
import org.jboss.aesh.edit.actions.Action;
import org.jboss.aesh.edit.actions.EditAction;
import org.jboss.aesh.edit.actions.EditActionManager;
import org.jboss.aesh.edit.actions.Operation;
import org.jboss.aesh.edit.actions.PrevWordAction;
import org.jboss.aesh.edit.actions.Movement;
import org.jboss.aesh.history.FileHistory;
import org.jboss.aesh.history.History;
import org.jboss.aesh.history.InMemoryHistory;
import org.jboss.aesh.history.SearchDirection;
import org.jboss.aesh.terminal.Terminal;
import org.jboss.aesh.undo.UndoAction;
import org.jboss.aesh.undo.UndoManager;
import org.jboss.aesh.util.ANSI;
import org.jboss.aesh.util.FileUtils;
import org.jboss.aesh.util.LoggerUtil;
import org.jboss.aesh.util.Parser;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A console reader.
* Supports ansi terminals
*
* @author Ståle W. Pedersen <stale.pedersen@jboss.org>
*/
public class Console {
private Buffer buffer;
private Terminal terminal;
private UndoManager undoManager;
private PasteManager pasteManager;
private EditMode editMode;
private History history;
private List<Completion> completionList;
private Settings settings;
private Search search;
private Action prevAction = Action.EDIT;
private ConsoleCommand command;
private boolean displayCompletion = false;
private boolean askDisplayCompletion = false;
private boolean running = false;
private StringBuilder redirectPipeOutBuffer;
private StringBuilder redirectPipeErrBuffer;
private List<ConsoleOperation> operations;
private ConsoleOperation currentOperation;
private AliasManager aliasManager;
private Logger logger = LoggerUtil.getLogger(getClass().getName());
public Console() throws IOException {
this(Settings.getInstance());
}
public Console(Settings settings) throws IOException {
reset(settings);
Runtime.getRuntime().addShutdownHook(new Thread() {
public void start() {
try {
if(Settings.getInstance().isAliasEnabled())
aliasManager.persist();
Settings.getInstance().getTerminal().reset();
Settings.getInstance().quit();
}
catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Reset the Console with Settings
* Can only be called after stop()
*
* @param settings with given settings
* @throws IOException stream
*/
public void reset(Settings settings) throws IOException {
if(running)
throw new RuntimeException("Cant reset an already running Console, must stop if first!");
if(Settings.getInstance().doReadInputrc())
Config.parseInputrc(Settings.getInstance());
Config.readRuntimeProperties(Settings.getInstance());
setTerminal(settings.getTerminal(),
settings.getInputStream(), settings.getStdOut(), settings.getStdErr());
editMode = settings.getFullEditMode();
undoManager = new UndoManager();
pasteManager = new PasteManager();
buffer = new Buffer(null);
if(settings.isHistoryPersistent())
history = new FileHistory(settings.getHistoryFile().getAbsolutePath(),
settings.getHistorySize());
else
history = new InMemoryHistory(settings.getHistorySize());
completionList = new ArrayList<Completion>();
//enable completion for redirection
completionList.add(new RedirectionCompletion());
//enable aliasing
if(Settings.getInstance().isAliasEnabled()) {
aliasManager = new AliasManager(Settings.getInstance().getAliasFile());
completionList.add(new AliasCompletion(aliasManager));
}
operations = new ArrayList<ConsoleOperation>();
currentOperation = null;
redirectPipeOutBuffer = new StringBuilder();
redirectPipeErrBuffer = new StringBuilder();
this.settings = settings;
running = true;
}
private void setTerminal(Terminal term, InputStream in, OutputStream stdOut, OutputStream stdErr) {
terminal = term;
terminal.init(in, stdOut, stdErr);
}
/**
* Get the terminal height
*
* @return height
*/
public int getTerminalHeight() {
return terminal.getHeight();
}
/**
* Get the terminal width
*
* @return width
*/
public int getTerminalWidth() {
return terminal.getWidth();
}
/**
* Get the History object
*
* @return history
*/
public History getHistory() {
return history;
}
/**
* Push text to the console, note that this will not update the internal
* cursor position.
*
* @param input text
* @throws IOException stream
*/
public void pushToStdOut(String input) throws IOException {
if(input != null && input.length() > 0) {
//if redirection enabled, put it into a buffer
if(currentOperation != null &&
ControlOperator.isRedirectionOut(currentOperation.getControlOperator()))
redirectPipeOutBuffer.append(input);
else
terminal.writeToStdOut(input);
}
}
/**
* @see #pushToStdOut
*
* @param input chars
* @throws IOException stream
*/
public void pushToStdOut(char[] input) throws IOException {
if(input != null && input.length > 0) {
//if redirection enabled, put it into a buffer
if(currentOperation != null &&
ControlOperator.isRedirectionOut(currentOperation.getControlOperator()))
redirectPipeOutBuffer.append(input);
else
terminal.writeToStdOut(input);
}
}
public void pushToStdErr(String input) throws IOException {
if(input != null && input.length() > 0) {
if(currentOperation != null &&
ControlOperator.isRedirectionErr(currentOperation.getControlOperator()))
redirectPipeErrBuffer.append(input);
else
terminal.writeToStdErr(input);
}
}
public void pushToStdErr(char[] input) throws IOException {
if(input != null && input.length > 0) {
if(currentOperation != null &&
ControlOperator.isRedirectionErr(currentOperation.getControlOperator()))
redirectPipeErrBuffer.append(input);
else
terminal.writeToStdErr(input);
}
}
/**
* Add a Completion to the completion list
*
* @param completion comp
*/
public void addCompletion(Completion completion) {
completionList.add(completion);
}
/**
* Add a list of completions to the completion list
*
* @param completionList comps
*/
public void addCompletions(List<Completion> completionList) {
this.completionList.addAll(completionList);
}
/**
* Stop the Console, close streams, and reset terminals.
* WARNING: After this is called the Console object must be reset
* before its used.
* @throws IOException stream
*/
public void stop() throws IOException {
settings.getInputStream().close();
//setting it to null to prevent uncertain state
settings.setInputStream(null);
terminal.reset();
terminal = null;
running = false;
}
/**
* Used by ConsoleCommand to attach itself to the Console
*
* @param cc command
* @throws IOException stream
*/
protected void attachProcess(ConsoleCommand cc) throws IOException {
command = cc;
}
/**
* Remove the current running command from Console
*
* @throws IOException stream
*/
private void detachProcess() throws IOException {
command = null;
terminal.writeToStdOut(buffer.getPrompt());
}
/**
* Read from the input stream, perform action according to mapped
* operations/completions/etc
* Return the stream when a new line is found.
*
* @param prompt starting prompt
* @return input stream
* @throws IOException stream
*/
public ConsoleOutput read(String prompt) throws IOException {
return read(prompt, null);
}
/**
* Read from the input stream, perform action according to mapped
* operations/completions/etc
* Return the stream when a new line is found.
*
* @param prompt starting prompt
* @param mask if set typed chars will be masked with this specified char
* @return input stream
* @throws IOException stream
*/
public ConsoleOutput read(String prompt, Character mask) throws IOException {
if(!running)
throw new RuntimeException("Cant reuse a stopped Console before its reset again!");
if(currentOperation != null) {
ConsoleOutput output = parseCurrentOperation();
if(output != null)
return output;
}
buffer.reset(prompt, mask);
if(command == null)
terminal.writeToStdOut(buffer.getPrompt());
search = null;
while(true) {
if(command != null && !command.isAttached()) {
detachProcess();
}
int[] in = terminal.read(settings.isReadAhead());
//for(int i : in)
// System.out.println("got int:"+i);
if (in[0] == -1) {
return null;
}
Operation operation = editMode.parseInput(in);
operation.setInput(in);
String result = null;
if(command != null)
command.processOperation(operation);
else
result = parseOperation(operation, mask);
if(result != null) {
operations = ControlOperatorParser.findAllControlOperators(result);
ConsoleOutput output = parseOperations();
output = processInternalCommands(output);
if(output.getBuffer() != null) {
return output;
}
else {
buffer.reset(prompt, mask);
terminal.writeToStdOut(buffer.getPrompt());
search = null;
}
}
}
}
/**
* Parse the current operation
*
* @param operation operation
* @param mask if set typed chars will be masked with this specified char
* @return out
* @throws IOException stream
*/
private String parseOperation(Operation operation, Character mask) throws IOException {
Action action = operation.getAction();
if(askDisplayCompletion) {
askDisplayCompletion = false;
if('y' == (char) operation.getInput()[0]) {
displayCompletion = true;
complete();
}
//do not display complete, but make sure that the previous line
// is restored correctly
else {
terminal.writeToStdOut(Config.getLineSeparator());
terminal.writeToStdOut(buffer.getLineWithPrompt());
syncCursor();
}
}
else if (action == Action.EDIT) {
writeChars(operation.getInput(), mask);
}
//make sure that every action except delete is ignored when masking is enabled
else if(mask != null) {
if(action == Action.DELETE) {
if(mask == 0)
deleteWithMaskEnabled();
else
performAction(EditActionManager.parseAction(operation, buffer.getCursor(), buffer.length()));
}
}
// For search movement is used a bit differently.
// It only triggers what kind of search action thats performed
else if(action == Action.SEARCH && !settings.isHistoryDisabled()) {
if(search == null)
search = new Search(operation, operation.getInput()[0]);
else {
search.setOperation(operation);
search.setInput(operation.getInput()[0]);
}
doSearch(search);
if(search.isFinished())
return search.getResult();
}
else if(action == Action.MOVE || action == Action.DELETE ||
action == Action.CHANGE || action == Action.YANK) {
performAction(EditActionManager.parseAction(operation, buffer.getCursor(), buffer.length()));
}
else if(action == Action.ABORT) {
}
else if(action == Action.CASE) {
addActionToUndoStack();
changeCase();
}
else if(action == Action.COMPLETE) {
complete();
}
else if(action == Action.EXIT) {
//deleteCurrentCharacter();
}
else if(action == Action.HISTORY) {
if(operation.getMovement() == Movement.NEXT)
getHistoryElement(true);
else if(operation.getMovement() == Movement.PREV)
getHistoryElement(false);
}
else if(action == Action.UNDO) {
undo();
}
else if(action == Action.PASTE_FROM_CLIPBOARD) {
addActionToUndoStack();
//paste();
}
else if(action == Action.PASTE) {
if(operation.getMovement() == Movement.NEXT)
doPaste(0, true);
else
doPaste(0, false);
}
else if(action == Action.CHANGE_EDITMODE) {
changeEditMode(operation.getMovement());
}
else if(action == Action.CLEAR) {
clear(true);
}
else if(action == Action.REPLACE) {
replace(operation.getInput()[0]);
}
else if(action == Action.NO_ACTION) {
//atm do nothing
}
//a hack to get history working
if(action == Action.HISTORY && !settings.isHistoryDisabled())
prevAction = action;
//in the end we check for a newline
if(action == Action.NEWLINE) {
// clear the undo stack for each new line
clearUndoStack();
if(mask == null) // dont push to history if masking
addToHistory(buffer.getLine());
prevAction = Action.NEWLINE;
//moveToEnd();
printNewline(); // output newline
return buffer.getLineNoMask();
}
return null;
}
/**
* Parse the Search object
*
* @param search search
* @throws IOException stream
*/
private void doSearch(Search search) throws IOException {
switch (search.getOperation().getMovement()) {
//init a previous doSearch
case PREV:
history.setSearchDirection(SearchDirection.REVERSE);
search.setSearchTerm( new StringBuilder(buffer.getLine()));
if (search.getSearchTerm().length() > 0) {
search.setResult( history.search(search.getSearchTerm().toString()));
}
break;
case NEXT:
history.setSearchDirection(SearchDirection.FORWARD);
search.setSearchTerm(new StringBuilder(buffer.getLine()));
if (search.getSearchTerm().length() > 0) {
search.setResult( history.search(search.getSearchTerm().toString()));
}
break;
case PREV_WORD:
history.setSearchDirection(SearchDirection.REVERSE);
if (search.getSearchTerm().length() > 0)
search.setResult( history.search(search.getSearchTerm().toString()));
break;
case NEXT_WORD:
history.setSearchDirection(SearchDirection.FORWARD);
if(search.getSearchTerm().length() > 0)
search.setResult(history.search(search.getSearchTerm().toString()));
break;
case PREV_BIG_WORD:
if (search.getSearchTerm().length() > 0)
search.getSearchTerm().deleteCharAt(search.getSearchTerm().length() - 1);
break;
// new doSearch input, append to doSearch
case ALL:
search.getSearchTerm().appendCodePoint(search.getInput());
//check if the new searchTerm will find anything
String tmpResult = history.search(search.getSearchTerm().toString());
if(tmpResult == null) {
search.getSearchTerm().deleteCharAt(search.getSearchTerm().length()-1);
}
else {
search.setResult(tmpResult);
}
break;
// pressed enter, ending the doSearch
case END:
// Set buffer to the found string.
if (search.getResult() != null) {
moveCursor(-buffer.getCursor());
setBufferLine(search.getResult());
redrawLine();
printNewline();
search.setResult( buffer.getLineNoMask());
search.setFinished(true);
return;
}
else {
moveCursor(-buffer.getCursor());
setBufferLine("");
redrawLine();
}
break;
//exiting doSearch (with esc)
case NEXT_BIG_WORD:
if(search.getResult() != null) {
moveCursor(-buffer.getCursor());
setBufferLine(search.getResult());
search.setResult(null);
}
else {
moveCursor(-buffer.getCursor());
setBufferLine("");
}
//redrawLine();
break;
}
// if we're still in doSearch mode, print the doSearch status
if (editMode.getCurrentAction() == Action.SEARCH) {
if (search.getSearchTerm().length() == 0) {
if(search.getResult() != null)
printSearch("", search.getResult());
else
printSearch("", "");
}
else {
if (search.getResult() == null) {
//beep();
}
else {
printSearch(search.getSearchTerm().toString(),
search.getResult());
}
}
}
// otherwise, restore the line
else {
redrawLine();
terminal.writeToStdOut(Buffer.printAnsi((buffer.getPrompt().length() + 1) + "G"));
}
}
/**
* If movement == PREV setting VI mode
* if movement == NEXT setting EMACS mode
*
* @param movement specifying vi/emacs mode
*/
private void changeEditMode(Movement movement) {
if(editMode.getMode() == Mode.EMACS && movement == Movement.PREV) {
Settings.getInstance().setEditMode(Mode.VI);
Settings.getInstance().resetEditMode();
}
else if(editMode.getMode() == Mode.VI && movement == Movement.NEXT) {
Settings.getInstance().setEditMode(Mode.EMACS);
Settings.getInstance().resetEditMode();
}
editMode = Settings.getInstance().getFullEditMode();
}
private void getHistoryElement(boolean first) throws IOException {
if(settings.isHistoryDisabled())
return;
// first add current line to history
if(prevAction == Action.NEWLINE) {
history.setCurrent(buffer.getLine());
}
//get next
String fromHistory;
if(first)
fromHistory = history.getNextFetch();
// get previous
else
fromHistory = history.getPreviousFetch();
if(fromHistory != null) {
setBufferLine(fromHistory);
moveCursor(-buffer.getCursor()+buffer.length());
redrawLine();
}
prevAction = Action.HISTORY;
}
private void setBufferLine(String newLine) throws IOException {
//must make sure that there are enough space for the
// line thats about to be injected
if((newLine.length()+buffer.getPrompt().length()) >= getTerminalWidth() &&
newLine.length() >= buffer.getLine().length()) {
int currentRow = getCurrentRow();
if(currentRow > -1) {
int cursorRow = buffer.getCursorWithPrompt() / getTerminalWidth();
if(currentRow + (newLine.length() / getTerminalWidth()) - cursorRow >= getTerminalHeight()) {
int numNewRows = currentRow + ((newLine.length()+buffer.getPrompt().length()) / getTerminalWidth()) - cursorRow - getTerminalHeight();
//if the line is exactly equal to termWidth we need to add another row
if((newLine.length()+buffer.getPrompt().length()) % getTerminalWidth() == 0)
numNewRows++;
if(numNewRows > 0) {
if(Settings.getInstance().isLogging()) {
int totalRows = (newLine.length()+buffer.getPrompt().length()) / getTerminalWidth() +1;
logger.info("ADDING "+numNewRows+", totalRows:"+totalRows+
", currentRow:"+currentRow+", cursorRow:"+cursorRow);
}
terminal.writeToStdOut(Buffer.printAnsi(numNewRows + "S"));
terminal.writeToStdOut(Buffer.printAnsi(numNewRows + "A"));
}
}
}
}
buffer.setLine(newLine);
}
private void insertBufferLine(String insert, int position) throws IOException {
if((insert.length()+buffer.totalLength()) >= getTerminalWidth()) { //&&
//(insert.length()+buffer.totalLength()) > buffer.getLine().length()) {
int currentRow = getCurrentRow();
if(currentRow > -1) {
int newLine = insert.length()+buffer.totalLength();
int cursorRow = buffer.getCursorWithPrompt() / getTerminalWidth();
if(currentRow + (newLine / getTerminalWidth()) - cursorRow >= getTerminalHeight()) {
int numNewRows = currentRow + (newLine / getTerminalWidth()) - cursorRow - getTerminalHeight();
//if the line is exactly equal to termWidth we need to add another row
if((insert.length()+buffer.totalLength()) % getTerminalWidth() == 0)
numNewRows++;
if(numNewRows > 0) {
terminal.writeToStdOut(Buffer.printAnsi(numNewRows + "S"));
terminal.writeToStdOut(Buffer.printAnsi(numNewRows + "A"));
}
}
}
}
buffer.insert(position, insert);
}
private void addToHistory(String line) {
if(!settings.isHistoryDisabled())
history.push(line);
}
private void writeChars(int[] chars, Character mask) throws IOException {
for(int c : chars)
writeChar(c,mask);
}
private void writeChar(int c, Character mask) throws IOException {
buffer.write((char) c);
//if mask is set and not set to 0 (nullvalue) we write out
//the masked char. if masked is set to 0 we write nothing
if(mask != null) {
if(mask != 0)
terminal.writeToStdOut(mask);
}
else {
terminal.writeToStdOut((char) c);
}
// add a 'fake' new line when inserting at the edge of terminal
if(buffer.getCursorWithPrompt() > getTerminalWidth() &&
buffer.getCursorWithPrompt() % getTerminalWidth() == 1) {
terminal.writeToStdOut((char) 32);
terminal.writeToStdOut((char) 13);
}
// if we insert somewhere other than the end of the line we need to redraw from cursor
if(buffer.getCursor() < buffer.length()) {
//check if we just started a new line, if we did we need to make sure that we add one
if(buffer.totalLength() > getTerminalWidth() &&
(buffer.totalLength()-1) % getTerminalWidth() == 1) {
int ansiCurrentRow = getCurrentRow();
int currentRow = (buffer.getCursorWithPrompt() / getTerminalWidth());
if(currentRow > 0 && buffer.getCursorWithPrompt() % getTerminalWidth() == 0)
currentRow--;
int totalRows = buffer.totalLength() / getTerminalWidth();
if(totalRows > 0 && buffer.totalLength() % getTerminalWidth() == 0)
totalRows--;
if(ansiCurrentRow+(totalRows-currentRow) > getTerminalHeight()) {
terminal.writeToStdOut(Buffer.printAnsi("1S")); //adding a line
terminal.writeToStdOut(Buffer.printAnsi("1A")); // moving up a line
}
}
redrawLine();
}
}
/**
* A simple hack to ensure that delete works when masking is enabled and
* the mask character is set to null (empty).
* The only operation that will work when the mask character is set to 0 is
* delete.
*
* @throws IOException
*/
private void deleteWithMaskEnabled() throws IOException {
if(buffer.getLineNoMask().length() > 0)
buffer.delete(buffer.getLineNoMask().length()-1, buffer.getLineNoMask().length());
}
/**
* Perform the designated action created by an event
*
* @param action console action
* @return true if nothing goes wrong
* @throws IOException stream
*/
private boolean performAction(EditAction action) throws IOException {
action.doAction(buffer.getLine());
if(action.getAction() == Action.MOVE) {
moveCursor((action.getEnd() - action.getStart()));
return true;
}
else if(action.getAction() == Action.DELETE || action.getAction() == Action.CHANGE) {
//first trigger undo action
addActionToUndoStack();
if(action.getEnd() > action.getStart()) {
// only if start != cursor we need to move it
if(action.getStart() != buffer.getCursor()) {
moveCursor(action.getStart() - buffer.getCursor());
}
addToPaste(buffer.getLine().substring(action.getStart(), action.getEnd()));
buffer.delete(action.getStart(), action.getEnd());
}
else {
addToPaste(buffer.getLine().substring(action.getEnd(), action.getStart()));
buffer.delete(action.getEnd(), action.getStart());
moveCursor((action.getEnd() - action.getStart()));
}
if(editMode.getMode() == Mode.VI && buffer.getCursor() == buffer.length()) {
if(!((ViEditMode) editMode).isInEditMode())
moveCursor(-1);
}
redrawLine();
}
else if(action.getAction() == Action.YANK) {
if(action.getEnd() > action.getStart()) {
addToPaste(buffer.getLine().substring(action.getStart(), action.getEnd()));
}
else {
addToPaste(buffer.getLine().substring(action.getEnd(), action.getStart()));
}
}
return true;
}
/**
* Add current text and cursor position to the undo stack
*
* @throws IOException if getCursorPosition() fails
*/
private void addActionToUndoStack() throws IOException {
UndoAction ua = new UndoAction(buffer.getCursor(), buffer.getLine());
undoManager.addUndo(ua);
}
private void clearUndoStack() {
undoManager.clear();
}
private void addToPaste(String buffer) {
pasteManager.addText(new StringBuilder(buffer));
}
/**
* Paste previous yanked word/char either before or on the cursor position
*
* @param index which yank index
* @param before cursor
* @return true if everything went as expected
* @throws IOException if redraw failed
*/
private boolean doPaste(int index, boolean before) throws IOException {
StringBuilder pasteBuffer = pasteManager.get(index);
if(pasteBuffer == null)
return false;
addActionToUndoStack();
if(before || buffer.getCursor() >= buffer.getLine().length()) {
insertBufferLine(pasteBuffer.toString(), buffer.getCursor());
redrawLine();
}
else {
insertBufferLine(pasteBuffer.toString(), buffer.getCursor()+1);
redrawLine();
//move cursor one char
moveCursor(1);
}
return true;
}
public final void moveCursor(final int where) throws IOException {
if(editMode.getMode() == Mode.VI &&
(editMode.getCurrentAction() == Action.MOVE ||
editMode.getCurrentAction() == Action.DELETE)) {
terminal.writeToStdOut(buffer.move(where, getTerminalWidth(), true));
}
else {
terminal.writeToStdOut(buffer.move(where, getTerminalWidth()));
}
}
private void redrawLine() throws IOException {
drawLine(buffer.getPrompt()+ buffer.getLine());
}
private void drawLine(String line) throws IOException {
//need to clear more than one line
if(line.length() > getTerminalWidth() ||
(line.length()+ Math.abs(buffer.getDelta()) > getTerminalWidth())) {
int currentRow = 0;
if(buffer.getCursorWithPrompt() > 0)
currentRow = buffer.getCursorWithPrompt() / getTerminalWidth();
if(currentRow > 0 && buffer.getCursorWithPrompt() % getTerminalWidth() == 0)
currentRow--;
if(Settings.getInstance().isLogging()) {
logger.info("actualRow:"+getCurrentRow()+", actualColumn:"+getCurrentColumn());
logger.info("currentRow:"+currentRow+", cursorWithPrompt:"+buffer.getCursorWithPrompt()
+", width:"+getTerminalWidth()+", height:"+getTerminalHeight()
+", delta:"+buffer.getDelta() +", buffer:"+buffer.getLine());
}
terminal.writeToStdOut(Buffer.printAnsi("s")); //save cursor
if(currentRow > 0)
for(int i=0; i<currentRow; i++)
terminal.writeToStdOut(Buffer.printAnsi("A")); //move to top
terminal.writeToStdOut(Buffer.printAnsi("0G")); //clear
terminal.writeToStdOut(line);
//if the current line.length < compared to previous we add spaces to the end
// to overwrite the old chars (wtb a better way of doing this)
if(buffer.getDelta() < 0) {
StringBuilder sb = new StringBuilder();
for(int i=0; i > buffer.getDelta(); i--)
sb.append(' ');
terminal.writeToStdOut(sb.toString());
}
// move cursor to saved pos
terminal.writeToStdOut(Buffer.printAnsi("u"));
}
// only clear the current line
else {
terminal.writeToStdOut(Buffer.printAnsi("s")); //save cursor
//move cursor to 0. - need to do this to clear the entire line
terminal.writeToStdOut(Buffer.printAnsi("0G"));
terminal.writeToStdOut(Buffer.printAnsi("2K")); // clear line
terminal.writeToStdOut(line);
// move cursor to saved pos
terminal.writeToStdOut(Buffer.printAnsi("u"));
}
}
private void printSearch(String searchTerm, String result) throws IOException {
//cursor should be placed at the index of searchTerm
int cursor = result.indexOf(searchTerm);
StringBuilder out;
if(history.getSearchDirection() == SearchDirection.REVERSE)
out = new StringBuilder("(reverse-i-search) `");
else
out = new StringBuilder("(forward-i-search) `");
out.append(searchTerm).append("': ");
cursor += out.length();
out.append(result);
buffer.disablePrompt(true);
moveCursor(-buffer.getCursor());
terminal.writeToStdOut(ANSI.moveCursorToBeginningOfLine());
setBufferLine(out.toString());
moveCursor(cursor);
drawLine(buffer.getLine());
buffer.disablePrompt(false);
}
/**
* Insert a newline
*
* @throws java.io.IOException stream
*/
private void printNewline() throws IOException {
moveCursor(buffer.totalLength());
terminal.writeToStdOut(Config.getLineSeparator());
}
/**
* Switch case if the character is a letter
*
* @throws java.io.IOException stream
*/
private void changeCase() throws IOException {
if(buffer.changeCase()) {
moveCursor(1);
redrawLine();
}
}
/**
* Perform an undo
*
* @throws IOException if redraw fails
*/
private void undo() throws IOException {
UndoAction ua = undoManager.getNext();
if(ua != null) {
setBufferLine(ua.getBuffer());
redrawLine();
moveCursor(ua.getCursorPosition() - buffer.getCursor());
}
}
/**
* Display possible completions.
* 1. Find all possible completions
* 2. If we find only one, display it.
* 3. If we find more than one, display them,
* but not more than 100 at once
*
* @throws IOException stream
*/
private void complete() throws IOException {
if(completionList.size() < 1)
return;
List<CompleteOperation> possibleCompletions = new ArrayList<CompleteOperation>();
int pipeLinePos = 0;
if(ControlOperatorParser.doStringContainPipeline(buffer.getLine())) {
pipeLinePos = ControlOperatorParser.findLastPipelinePositionBeforeCursor(buffer.getLine(), buffer.getCursor());
if(ControlOperatorParser.findLastRedirectionPositionBeforeCursor(buffer.getLine(), buffer.getCursor()) > pipeLinePos)
pipeLinePos = 0;
}
for(Completion completion : completionList) {
CompleteOperation co;
if(pipeLinePos > 0) {
co = findAliases(buffer.getLine().substring(pipeLinePos, buffer.getCursor()), buffer.getCursor() - pipeLinePos);
}
else {
co = findAliases(buffer.getLine(), buffer.getCursor());
}
completion.complete(co);
if(co.getCompletionCandidates() != null && co.getCompletionCandidates().size() > 0)
possibleCompletions.add(co);
}
if(Settings.getInstance().isLogging())
logger.info("Found completions: "+possibleCompletions);
// not hits, just return (perhaps we should beep?)
if(possibleCompletions.size() < 1) {
//do nothing atm
}
// only one hit, do a completion
else if(possibleCompletions.size() == 1 &&
possibleCompletions.get(0).getCompletionCandidates().size() == 1) {
//some formatted completions might not be valid and shouldnt be displayed
displayCompletion(possibleCompletions.get(0).getCompletionCandidates().get(0),
possibleCompletions.get(0).getFormattedCompletionCandidates().get(0), true);
}
// more than one hit...
else {
String startsWith = Parser.findStartsWithOperation(possibleCompletions);
if(startsWith.length() > 0)
displayCompletion("", startsWith, false);
// display all
// check size
else {
List<String> completions = new ArrayList<String>();
for(CompleteOperation co : possibleCompletions)
completions.addAll(co.getCompletionCandidates());
if(completions.size() > 100) {
if(displayCompletion) {
displayCompletions(completions);
displayCompletion = false;
}
else {
askDisplayCompletion = true;
terminal.writeToStdOut(Config.getLineSeparator() + "Display all " + completions.size() + " possibilities? (y or n)");
}
}
// display all
else {
displayCompletions(completions);
}
}
}
}
/**
* Display the completion string in the terminal.
* If !completion.startsWith(buffer.getLine()) the completion will be added to the line,
* else it will replace whats at the buffer line.
*
* @param fullCompletion the while completion
* @param completion partial completion
* @param appendSpace if its an actual complete
* @throws java.io.IOException stream
*/
private void displayCompletion(String fullCompletion, String completion, boolean appendSpace) throws IOException {
if(completion.startsWith(buffer.getLine())) {
performAction(new PrevWordAction(buffer.getCursor(), Action.DELETE));
buffer.write(completion);
terminal.writeToStdOut(completion);
//only append space if its an actual complete, not a partial
}
else {
buffer.write(completion);
terminal.writeToStdOut(completion);
}
if(appendSpace && fullCompletion.startsWith(buffer.getLine())) {
buffer.write(' ');
terminal.writeToStdOut(' ');
}
redrawLine();
}
/**
* Display all possible completions
*
* @param completions all completion items
* @throws IOException stream
*/
private void displayCompletions(List<String> completions) throws IOException {
//printNewline reset cursor pos, so we need to store it
int oldCursorPos = buffer.getCursor();
printNewline();
buffer.setCursor(oldCursorPos);
terminal.writeToStdOut(Parser.formatDisplayList(completions, terminal.getHeight(), terminal.getWidth()));
terminal.writeToStdOut(buffer.getLineWithPrompt());
//if we do a complete and the cursor is not at the end of the
//buffer we need to move it to the correct place
syncCursor();
}
private void syncCursor() throws IOException {
if(buffer.getCursor() != buffer.getLine().length())
terminal.writeToStdOut(Buffer.printAnsi((
Math.abs(buffer.getCursor() -
buffer.getLine().length()) + "D")));
}
private void replace(int rChar) throws IOException {
addActionToUndoStack();
buffer.replaceChar((char) rChar);
redrawLine();
}
/**
* Return the row position if we use a ansi terminal
* Send a terminal: '<ESC>[6n'
* and we receive the position as: '<ESC>[n;mR'
* where n = current row and m = current column
*
* @return current row
*/
private int getCurrentRow() {
if(settings.isAnsiConsole() && Config.isOSPOSIXCompatible()) {
try {
terminal.writeToStdOut(ANSI.getCurrentCursorPos());
StringBuilder builder = new StringBuilder(8);
int row;
while((row = terminal.read(false)[0]) > -1 && row != 'R') {
if (row != 27 && row != '[') {
builder.append((char) row);
}
}
return Integer.parseInt(builder.substring(0, builder.indexOf(";")));
}
catch (Exception e) {
if(settings.isLogging())
logger.log(Level.SEVERE, "Failed to find current row with ansi code: ",e);
return -1;
}
}
return -1;
}
private int getCurrentColumn() {
if(settings.isAnsiConsole() && Config.isOSPOSIXCompatible()) {
try {
terminal.writeToStdOut(ANSI.getCurrentCursorPos());
StringBuilder builder = new StringBuilder(8);
int row;
while((row = settings.getInputStream().read()) > -1 && row != 'R' ) {
if (row != 27 && row != '[') {
builder.append((char) row);
}
}
return Integer.parseInt(builder.substring(builder.lastIndexOf(";") + 1, builder.length()));
}
catch (Exception e) {
if(settings.isLogging())
logger.log(Level.SEVERE, "Failed to find current column with ansi code: ",e);
return -1;
}
}
return -1;
}
/**
* Clear a ansi terminal
*
* @throws IOException stream
*/
public void clear() throws IOException {
clear(false);
}
/**
* Clear an ansi terminal.
* Set includeBuffer to true if the current buffer should be
* printed again after clear.
*
* @param includeBuffer if true include the current buffer line
* @throws IOException stream
*/
public void clear(boolean includeBuffer) throws IOException {
//(windows fix)
if(!Config.isOSPOSIXCompatible())
printNewline();
//first clear console
terminal.writeToStdOut(ANSI.clearScreen());
//move cursor to correct position
terminal.writeToStdOut(Buffer.printAnsi("1;1H"));
//then writeToStdOut prompt
if(includeBuffer)
terminal.writeToStdOut(buffer.getLineWithPrompt());
}
private ConsoleOutput parseCurrentOperation() throws IOException {
if(currentOperation.getControlOperator() == ControlOperator.OVERWRITE_OUT
|| currentOperation.getControlOperator() == ControlOperator.OVERWRITE_ERR
|| currentOperation.getControlOperator() == ControlOperator.APPEND_OUT
|| currentOperation.getControlOperator() == ControlOperator.APPEND_ERR
|| currentOperation.getControlOperator() == ControlOperator.OVERWRITE_OUT_AND_ERR) {
ConsoleOperation nextOperation = operations.remove(0);
persistRedirection(nextOperation.getBuffer(), currentOperation.getControlOperator());
if(nextOperation.getControlOperator() == ControlOperator.NONE) {
redirectPipeErrBuffer = new StringBuilder();
redirectPipeOutBuffer = new StringBuilder();
currentOperation = null;
return null;
}
else {
redirectPipeErrBuffer = new StringBuilder();
redirectPipeOutBuffer = new StringBuilder();
currentOperation = nextOperation;
return parseCurrentOperation();
}
}
else if(currentOperation.getControlOperator() == ControlOperator.PIPE
|| currentOperation.getControlOperator() == ControlOperator.PIPE_OUT_AND_ERR) {
return parseOperations();
}
//this should never happen (all overwrite_in should be parsed in parseOperations())
else if(currentOperation.getControlOperator() == ControlOperator.OVERWRITE_IN) {
if(Settings.getInstance().isLogging())
logger.info(settings.getName()+": syntax error while reading token: \'<\'");
pushToStdErr(settings.getName()+": syntax error while reading token: \'<\'");
return null;
}
//ControlOperator.NONE
else {
//do nothing
return null;
}
}
/**
* Find the next ConsoleOutput based on operations
*
* @return next ConsoleOutput
*/
private ConsoleOutput parseOperations() throws IOException {
ConsoleOutput output = null;
ConsoleOperation op = operations.remove(0);
if(op.getControlOperator() == ControlOperator.OVERWRITE_OUT
|| op.getControlOperator() == ControlOperator.OVERWRITE_ERR
|| op.getControlOperator() == ControlOperator.APPEND_OUT
|| op.getControlOperator() == ControlOperator.APPEND_ERR
|| op.getControlOperator() == ControlOperator.OVERWRITE_OUT_AND_ERR
|| op.getControlOperator() == ControlOperator.PIPE_OUT_AND_ERR
|| op.getControlOperator() == ControlOperator.PIPE ) {
if(operations.size() == 0) {
//throw some sort of exception
}
else {
currentOperation = op;
output = new ConsoleOutput(op,
redirectPipeOutBuffer.toString(), redirectPipeErrBuffer.toString());
}
}
else if(op.getControlOperator() == ControlOperator.OVERWRITE_IN) {
//1. we need to find next operation
//2. use the buffer from the next operation to read file to buffer
//3. switch redirection operation with next one
if(operations.size() > 0) {
ConsoleOperation nextOperation = operations.remove(0);
if( nextOperation.getBuffer().length() > 0) {
List<String> files = Parser.findAllWords(nextOperation.getBuffer());
currentOperation = new ConsoleOperation(nextOperation.getControlOperator(), op.getBuffer());
try {
output = new ConsoleOutput(new ConsoleOperation(nextOperation.getControlOperator(),op.getBuffer()),
FileUtils.readFile(new File(Parser.switchEscapedSpacesToSpacesInWord(files.get(0)))),
redirectPipeErrBuffer.toString());
}
//if we get any io error reading the file:
catch (IOException ioe) {
pushToStdErr(settings.getName()+": "+ioe.getMessage()+Config.getLineSeparator());
currentOperation = null;
output = new ConsoleOutput(new ConsoleOperation(ControlOperator.NONE, ""));
}
}
else {
if(Settings.getInstance().isLogging())
logger.info(settings.getName()+": syntax error near unexpected token '<'"+Config.getLineSeparator());
pushToStdErr(settings.getName()+": syntax error near unexpected token '<'"+Config.getLineSeparator());
currentOperation = null;
output = new ConsoleOutput(new ConsoleOperation(ControlOperator.NONE, ""));
}
}
else {
if(Settings.getInstance().isLogging())
logger.info(settings.getName()+": syntax error near unexpected token 'newline'"+Config.getLineSeparator());
pushToStdErr(settings.getName()+": syntax error near unexpected token 'newline'"+Config.getLineSeparator());
currentOperation = null;
output = new ConsoleOutput(new ConsoleOperation(ControlOperator.NONE, ""));
}
}
else {
currentOperation = null;
output = new ConsoleOutput(op,
redirectPipeOutBuffer.toString(), redirectPipeErrBuffer.toString());
}
if(redirectPipeOutBuffer.length() > 0)
redirectPipeOutBuffer = new StringBuilder();
if(redirectPipeErrBuffer.length() > 0)
redirectPipeErrBuffer = new StringBuilder();
return findAliases(output);
}
private ConsoleOutput processInternalCommands(ConsoleOutput output) throws IOException {
if(output.getBuffer() != null) {
if(settings.isAliasEnabled() &&
output.getBuffer().startsWith(InternalCommands.ALIAS.getCommand())) {
String out = aliasManager.parseAlias(output.getBuffer());
if(out != null) {
pushToStdOut(out);
}
//empty output, will result
return new ConsoleOutput(new ConsoleOperation(ControlOperator.NONE, null));
}
else if(settings.isAliasEnabled() &&
output.getBuffer().startsWith(InternalCommands.UNALIAS.getCommand())) {
String out = aliasManager.removeAlias(output.getBuffer());
if(out != null)
pushToStdOut(out);
return new ConsoleOutput(new ConsoleOperation(ControlOperator.NONE, null));
}
}
return output;
}
private ConsoleOutput findAliases(ConsoleOutput operation) {
if(settings.isAliasEnabled()) {
String command = Parser.findFirstWord(operation.getBuffer());
Alias alias = aliasManager.getAlias(command);
if(alias != null) {
operation.setConsoleOperation( new ConsoleOperation(operation.getControlOperator(),
alias.getValue() + operation.getBuffer().substring(command.length())));
}
}
return operation;
}
private CompleteOperation findAliases(String buffer, int cursor) {
if(settings.isAliasEnabled()) {
String command = Parser.findFirstWord(buffer);
Alias alias = aliasManager.getAlias(command);
if(alias != null) {
return new CompleteOperation( alias.getValue()+buffer.substring(command.length()),
cursor+(alias.getValue().length()-command.length()));
}
}
return new CompleteOperation(buffer, cursor);
}
private void persistRedirection(String fileName, ControlOperator redirection) throws IOException {
List<String> fileNames = Parser.findAllWords(fileName);
if(fileNames.size() > 1) {
if(Settings.getInstance().isLogging())
logger.info(settings.getName()+": can't redirect to more than one file."+Config.getLineSeparator());
pushToStdErr(settings.getName()+": can't redirect to more than one file."+Config.getLineSeparator());
return;
}
//this is safe since we check that buffer do contain text earlier
else
fileName = fileNames.get(0);
try {
if(redirection == ControlOperator.OVERWRITE_OUT)
FileUtils.saveFile(new File(Parser.switchEscapedSpacesToSpacesInWord( fileName)), redirectPipeOutBuffer.toString(), false);
else if(redirection == ControlOperator.OVERWRITE_ERR)
FileUtils.saveFile(new File(Parser.switchEscapedSpacesToSpacesInWord( fileName)), redirectPipeErrBuffer.toString(), false);
else if(redirection == ControlOperator.APPEND_OUT)
FileUtils.saveFile(new File(Parser.switchEscapedSpacesToSpacesInWord( fileName)), redirectPipeOutBuffer.toString(), true);
else if(redirection == ControlOperator.APPEND_ERR)
FileUtils.saveFile(new File(Parser.switchEscapedSpacesToSpacesInWord( fileName)), redirectPipeErrBuffer.toString(), true);
}
catch (IOException e) {
if(Settings.getInstance().isLogging())
logger.log(Level.SEVERE, "Saving file "+fileName+" to disk failed: ", e);
pushToStdErr(e.getMessage());
}
redirectPipeOutBuffer = new StringBuilder();
redirectPipeErrBuffer = new StringBuilder();
}
}