/*
* $Id$
*
* Copyright (C) 2003-2015 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.driver.console.textscreen;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.SortedSet;
import org.apache.log4j.Logger;
import org.jnode.driver.console.CompletionInfo;
import org.jnode.driver.console.InputCompleter;
import org.jnode.driver.console.ScrollableTextConsole;
import org.jnode.driver.console.TextConsole;
import org.jnode.driver.console.spi.ConsoleWriter;
/**
* A class that handles the content of the current command line in the shell.
* That can be : - a new command that the user is being editing - an existing
* command (from the command history)
* <p/>
* This class also handles the current cursor position in the command line and
* keep trace of the position (consoleX, consoleY) of the first character of the
* command line (to handle commands that are multilines).
*
* @author Ewout Prangsma (epr@users.sourceforge.net)
* @author Fabien DUMINY (fduminy@jnode.org)
*/
class Line {
//TODO get the real screen width (in columns)
private static final int SCREEN_WIDTH = 80;
private int consoleX;
private int consoleY;
/**
* Contains the current position of the cursor on the currentLine
*/
private int posOnCurrentLine = 0;
/**
* Contains the current line *
*/
private StringBuffer currentLine = new StringBuffer(80);
private boolean shortened = true;
private int oldLength = 0;
private int maxLength = 0;
private final TextConsole console;
private ConsoleWriter out;
public Line(TextConsole console) {
this.console = console;
this.out = (ConsoleWriter) console.getOut();
}
public void start() {
start(false);
}
public boolean isEmpty() {
return currentLine.toString().trim().length() == 0;
}
public void start(boolean keepContent) {
if (keepContent) {
// we stay at the same position in X coordinate
// only Y may have changed
consoleY = console.getCursorY();
} else {
consoleX = console.getCursorX();
consoleY = console.getCursorY();
setContent("");
console.setCursor(consoleX, consoleY); //move the cursor to the start of the line.
}
}
public String getContent() {
return currentLine.toString();
}
public char[] getChars() {
char[] res = new char[currentLine.length()];
currentLine.getChars(0, currentLine.length(), res, 0);
return res;
}
public char[] consumeChars() {
char[] res = getChars();
currentLine.setLength(0);
return res;
}
public void setContent(String content) {
startModif();
currentLine.setLength(0);
currentLine.append(content);
moveEnd();
endModif();
}
public boolean moveLeft() {
if (posOnCurrentLine > 0) {
posOnCurrentLine--;
return true;
}
return false;
}
public boolean moveRight() {
if (posOnCurrentLine < currentLine.length()) {
posOnCurrentLine++;
return true;
}
return false;
}
public void moveEnd() {
posOnCurrentLine = currentLine.length();
}
public void moveBegin() {
posOnCurrentLine = 0;
}
public boolean backspace() {
if (posOnCurrentLine > 0) {
moveLeft();
delete();
return true;
}
return false;
}
public void delete() {
if ((posOnCurrentLine >= 0)
&& (posOnCurrentLine < currentLine.length())) {
startModif();
currentLine.deleteCharAt(posOnCurrentLine);
endModif();
}
}
/**
* Perform completion on the current input, output any completion alternatives
* then rebuild line the input with the common completion appended.
*
* @param completer the object (e.g. shell) responsible for completion.
* @return <code>true</code> if we output a list of completion alternatives.
* @throws IOException
*/
public boolean complete(InputCompleter completer) throws IOException {
CompletionInfo info = null;
String ending =
posOnCurrentLine != currentLine.length() ? currentLine.substring(posOnCurrentLine) : "";
info = completer.complete(currentLine.substring(0, posOnCurrentLine));
boolean res = printList(info);
String completion = info.getCompletion();
if (completion != null) {
int startPos = info.getCompletionStart();
if (startPos == -1) {
setContent(currentLine.substring(0, posOnCurrentLine) + completion + ending);
} else {
setContent(currentLine.substring(0, startPos) + completion + ending);
}
// (This is the updated line's length ...)
posOnCurrentLine = currentLine.length() - ending.length();
}
return res;
}
/**
* Get and output incremental help for the current input line.
*
* @param completer the object (e.g. shell) responsible for providing help.
* @return <code>true</code> if we output any help.
* @throws IOException
* @throws IOException
*/
public boolean help(InputCompleter completer) throws IOException {
CharArrayWriter caw = new CharArrayWriter();
PrintWriter pw = new PrintWriter(caw);
boolean res = completer.help(currentLine.substring(0, posOnCurrentLine), pw);
if (!res) {
return false;
}
char[] chars = caw.toCharArray();
if (chars.length == 0 || chars.length == 1 && chars[0] == '\n') {
return false;
}
int oldPosOnCurrentLine = posOnCurrentLine;
moveEnd();
refreshCurrentLine();
out.write('\n');
out.write(chars);
if (chars[chars.length - 1] != '\n') {
out.write('\n');
}
posOnCurrentLine = oldPosOnCurrentLine;
return true;
}
protected boolean printList(CompletionInfo info) throws IOException {
SortedSet<String> completions = info.getCompletions();
if (completions == null || completions.size() <= 1) {
return false;
}
int oldPosOnCurrentLine = posOnCurrentLine;
moveEnd();
refreshCurrentLine();
out.write('\n');
String[] list = completions.toArray(new String[completions.size()]);
final int minItemsToSplit = 5;
if (list.length > minItemsToSplit) {
list = splitInColumns(list);
}
// display items column (may be single or multiple columns)
for (String item : list) {
// item may actually be a single item or in fact multiple items
if (item.length() % SCREEN_WIDTH == 0) {
// we are already at the first column of the next line
out.write(item);
} else {
// we aren't at the first column of the next line
out.write(item);
out.write('\n');
}
}
posOnCurrentLine = oldPosOnCurrentLine;
return true;
}
protected String[] splitInColumns(String[] items) {
final int separatorWidth = 3;
// compute the maximum width of items
int maxWidth = 0;
for (String item : items) {
if (item.length() > maxWidth) {
maxWidth = item.length();
}
}
final int columnWidth = Math.min(SCREEN_WIDTH, maxWidth + separatorWidth);
final int nbColumns = SCREEN_WIDTH / columnWidth;
final boolean lastLineIsFull = ((items.length % nbColumns) == 0);
final int nbLines = (items.length / nbColumns) + (lastLineIsFull ? 0 : 1);
String[] lines = new String[nbLines];
int lineNum = 0;
for (int itemNum = 0; itemNum < items.length;) {
StringBuilder line = new StringBuilder(SCREEN_WIDTH);
for (int c = 0; c < nbColumns; c++) {
final String item = items[itemNum++];
line.append(item);
// add some blanks
final int nbBlanks = columnWidth - item.length();
for (int i = 0; i < nbBlanks; i++) {
line.append(' ');
}
if (itemNum >= items.length) break;
}
lines[lineNum++] = line.toString();
}
return lines;
}
public void appendChar(char c) {
startModif();
if (posOnCurrentLine == currentLine.length()) {
currentLine.append(c);
} else {
currentLine.insert(posOnCurrentLine, c);
}
posOnCurrentLine++;
endModif();
}
protected void startModif() {
shortened = false;
oldLength = currentLine.length();
}
protected void endModif() {
maxLength = Math.max(oldLength, currentLine.length());
shortened = oldLength > currentLine.length();
oldLength = 0;
}
private volatile char[] mySpaces;
private char[] getSpaces(int count) {
char[] res = mySpaces;
if (res == null || res.length < count) {
res = new char[count];
Arrays.fill(res, ' ');
mySpaces = res;
}
return res;
}
public void refreshCurrentLine() throws IOException {
try {
int x = consoleX;
int width = console.getWidth();
int nbLines = ((x + maxLength) / width);
if (((x + maxLength) % width) != 0)
nbLines++;
// output the input line buffer contents with the screen cursor hidden
console.setCursorVisible(false);
console.setCursor(consoleX, consoleY);
out.write(currentLine.toString());
// get position of end of line
// FIXME ... there's a problem here if some application simultaneously
// writes to console output.
int newConsoleX = console.getCursorX();
int newConsoleY = console.getCursorY();
// blank to the end of the screen region
if (newConsoleX > 0) {
int len = width - newConsoleX;
console.setChar(newConsoleX, newConsoleY, getSpaces(len),
0, len, out.getFgColor());
newConsoleY++;
}
for (int i = newConsoleY; i < consoleY + nbLines; i++) {
console.clearRow(i);
}
// reset the screen cursor and reveal it
// FIXME ... there's a problem here if the input buffer contains
// characters that do not render as one screen character; e.g. \t or \n.
int inputCursorX = x + posOnCurrentLine;
int inputCursorY = consoleY;
if (inputCursorX >= width) {
inputCursorY += inputCursorX / width;
inputCursorX = (inputCursorX % width);
}
console.setCursor(inputCursorX, inputCursorY);
console.setCursorVisible(true);
// if the line has not been shortened (delete, backspace...)
if (!shortened) {
// ensure that the location of the input cursor is included.
if (console instanceof ScrollableTextConsole) {
((ScrollableTextConsole) console).ensureVisible(inputCursorY);
} else {
// since the console is not scrollable, we can't do anything
// if the row is not visible (the row is completely lost)
}
}
console.setCursorVisible(true);
} catch (Exception e) {
Logger.getLogger(Line.class).debug("Exception in Line.refreshCurrentLine()", e);
}
}
public int getLineLength() {
return currentLine.length();
}
}