/*
* $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.awt.event.KeyEvent;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import org.jnode.driver.console.InputCompleter;
import org.jnode.driver.console.TextConsole;
import org.jnode.driver.input.KeyboardEvent;
import org.jnode.system.event.FocusEvent;
import org.jnode.system.event.FocusListener;
import org.jnode.util.ConsoleStream;
/**
* KeyboardReader maps keyboard events into a stream of characters. Current functionality includes:
* <ul>
* <li>line buffering and line editing, using a text console,
* <li>integrated input history and completion,
* <li>CTRL-D is interpreted as a 'soft' EOF mark,
* <li>binding of keyboard events to various actions is soft,
* <li>listens to keyboard focus events.
* </ul>
* <p/>
* Possible future enhancements include:
* <ul>
* <li>a "raw" mode in which characters and other keyboard events are delivered without line editing,
* <li>a "no echo" mode in which line editing occurs without echoing of input characters,
* <li>making completion and history context sensitive; e.g. when switching between a shell and
* an application, and
* <li>code refactoring to support classical terminal devices and remote consoles.
* </ul>
* <p/>
* Bugs:
* <ul>
* <li>The current method of echoing the input is suboptimal, and is broken in the case where an
* application outputs a prompt string to stdout or stderr.
* </ul>
*
* @author crawley@jnode.org
*/
public class KeyboardReader extends Reader
implements FocusListener, ConsoleStream {
private boolean eof;
private char[] buffer;
private int pos;
private int lim;
private static final char NO_CHAR = KeyEvent.CHAR_UNDEFINED;
private final Line currentLine;
private final TextConsole console;
private InputCompleter completer;
private final Writer out;
private ConsoleKeyEventBindings bindings = ConsoleKeyEventBindings.createDefault();
private String currentPrompt;
/**
* Contains an index to the current history line, counting from zero. The
* value -1 denotes the current line.
*/
private int historyIndex = -1;
/**
* Contains the current line; i.e. the text being entered by the user.
*/
private String savedCurrentLine;
private final KeyboardHandler keyboardHandler;
private final FocusListener focusListener;
/**
* This constructor is used by wrappers.
*/
protected KeyboardReader() {
this.keyboardHandler = null;
this.focusListener = null;
this.currentLine = null;
this.out = null;
this.console = null;
}
/**
* Create KeyboardReader using the supplied handler as the source of
* keyboard events and the supplied TextConsole to echo typed characters.
*
* @param kbHandler
* @param console
*/
public KeyboardReader(KeyboardHandler kbHandler, TextConsole console) {
this.keyboardHandler = kbHandler;
this.console = console;
this.out = console.getOut();
this.currentLine = new Line(console);
this.pos = this.lim = 0;
if (keyboardHandler instanceof FocusListener) {
this.focusListener = (FocusListener) keyboardHandler;
} else {
this.focusListener = null;
}
}
/**
* When we process a 'soft EOF' character; e.g. CTRL-D we go into a state where
* all subsequent 'read' calls will return <code>-1</code> until the {@link #clearSoftEOF()}
* method is called. This method tests if we are in that state.
*
* @return Return <code>true</code> if we are in 'soft EOF' state; otherwise <code>false</code>.
*/
public boolean isSoftEOF() {
return eof;
}
/**
* Clear the 'soft EOF' state; see {@link #isSoftEOF()}.
*/
public void clearSoftEOF() {
eof = false;
}
/**
* Get a snapshot of the reader's key event bindings.
*
* @return a copy of the current bindings.
*/
public ConsoleKeyEventBindings getKeyEventBindings() {
return new ConsoleKeyEventBindings(bindings);
}
/**
* Set the reader's key event bindings.
*
* @param bindings the new bindings.
*/
public void setKeyEventBindings(ConsoleKeyEventBindings bindings) {
this.bindings = new ConsoleKeyEventBindings(bindings);
}
@Override
public boolean ready() throws IOException {
return eof || currentLine.getLineLength() > 0;
}
/**
* @see java.io.Reader#close()
*/
public void close() throws IOException {
keyboardHandler.close();
}
/**
* Pull a keyboard event from the queue and process it.
*
* @return true if the event should commit the characters in the
* input (line editing) buffer to the Reader's character stream.
*/
private boolean processEvent() throws IOException {
KeyboardEvent event = keyboardHandler.getEvent();
if (!event.isConsumed()) {
KeyboardReaderAction action = bindings.getKeyboardEventAction(event);
boolean breakChar = false;
boolean consume = true;
switch (action) {
case KR_DELETE_BEFORE:
// Delete character before cursor
if (currentLine.backspace()) {
refreshCurrentLine();
}
break;
case KR_ENTER:
// Append event character to the line and commit.
currentLine.moveEnd();
refreshCurrentLine();
out.write('\n');
currentLine.appendChar(event.getKeyChar());
breakChar = true;
historyIndex = -1;
break;
case KR_COMPLETE:
// Perform completion
if (completer != null) {
if (currentLine.complete(completer)) {
currentLine.start(true);
}
out.write(currentPrompt);
refreshCurrentLine();
}
break;
case KR_HELP:
// Request incremental help
if (completer != null) {
if (currentLine.help(completer)) {
currentLine.start(true);
}
out.write(currentPrompt);
refreshCurrentLine();
}
break;
case KR_SOFT_EOF:
// Set soft EOF status and commit
currentLine.moveEnd();
refreshCurrentLine();
out.write('\n');
eof = true;
breakChar = true;
break;
case KR_KILL_LINE:
// Kill the current input line (and clear the screen)
this.console.clear();
this.console.setCursor(0, 0);
out.write(currentPrompt);
currentLine.start();
refreshCurrentLine();
break;
case KR_INSERT:
// Insert event's character
currentLine.appendChar(event.getKeyChar());
refreshCurrentLine();
historyIndex = -1;
break;
case KR_HISTORY_UP:
// Previous history item
if (completer != null) {
if (historyIndex == -1) {
historyIndex = completer.getInputHistory().size();
savedCurrentLine = currentLine.getContent();
}
historyIndex--;
updateCurrentLine();
}
break;
case KR_HISTORY_DOWN:
// Next history item
if (completer != null) {
if (historyIndex == -1) {
savedCurrentLine = currentLine.getContent();
}
if (historyIndex == completer.getInputHistory().size() - 1) {
historyIndex = -2;
}
historyIndex++;
updateCurrentLine();
}
break;
case KR_CURSOR_LEFT:
// Move the cursor left
if (currentLine.moveLeft()) {
refreshCurrentLine();
}
break;
case KR_CURSOR_RIGHT:
// Move the cursor right
if (currentLine.moveRight()) {
refreshCurrentLine();
}
break;
case KR_CURSOR_TO_START:
// Move the cursor to the start of the line
currentLine.moveBegin();
refreshCurrentLine();
break;
case KR_CURSOR_TO_END:
// Move the cursor to the end of the line
currentLine.moveEnd();
refreshCurrentLine();
break;
case KR_DELETE_AFTER:
// Delete the character after the cursor
currentLine.delete();
refreshCurrentLine();
break;
case KR_CONSUME:
// Comsume (and ignore) the event
break;
case KR_IGNORE:
// Leave the event unconsumed.
consume = false;
break;
}
if (consume) {
event.consume();
}
return breakChar;
} else {
return false;
}
}
private void updateCurrentLine() throws IOException {
if (historyIndex > -1) {
currentLine.setContent(completer.getInputHistory().getLineAt(historyIndex));
} else {
currentLine.setContent(savedCurrentLine);
}
refreshCurrentLine();
currentLine.moveEnd();
}
private void refreshCurrentLine() throws IOException {
currentLine.refreshCurrentLine();
}
private boolean fillBuffer() throws IOException {
int x = console.getCursorX();
int y = console.getCursorY();
StringBuffer sb = new StringBuffer(x);
for (int i = 0; i < x; i++) {
sb.append(console.getChar(i, y));
}
currentPrompt = sb.toString();
currentLine.start();
while (!processEvent()) { /* */
}
buffer = currentLine.consumeChars();
lim = buffer.length;
pos = 0;
return pos < lim;
}
@Override
public int read() throws IOException {
if (pos >= lim) {
if (eof || !fillBuffer()) {
return -1;
}
}
return buffer[pos++];
}
@Override
public int read(char[] buff, int off, int len) throws IOException {
int nosRead = 0;
if (pos >= lim) {
if (eof || !fillBuffer()) {
return -1;
}
}
while (nosRead < len && pos < lim) {
buff[nosRead + off] = buffer[pos];
nosRead++;
pos++;
}
return nosRead;
}
@Override
public int read(char[] buff) throws IOException {
return read(buff, 0, buff.length);
}
@Override
public void mark(int readLimit) {
}
@Override
public boolean markSupported() {
return false;
}
@Override
public void reset() throws IOException {
throw new UnsupportedOperationException("Mark/reset not supported");
}
@Override
public long skip(long n) throws IOException {
// I don't expect this method will be used much.
for (long i = 0; i < n; i++) {
if (read() == -1) {
return i;
}
}
return n;
}
/**
* Get the TextConsole associated with this KeyboardReader.
*
* @return the associated TextConsole object.
*/
public TextConsole getTextConsole() {
return console;
}
/**
* Get the InputCompleter associated with this KeyboardReader. This
* is typically the CommandShell.
*
* @return the associated InputCompleter object.
*/
public InputCompleter getCompleter() {
return completer;
}
/**
* Set this KeyboardReader's InputCompleter.
*
* @param completer the new completer value
*/
public void setCompleter(InputCompleter completer) {
this.completer = completer;
}
public void focusGained(FocusEvent event) {
if (focusListener != null) {
focusListener.focusGained(event);
}
}
public void focusLost(FocusEvent event) {
if (focusListener != null) {
focusListener.focusLost(event);
}
}
}