com.googlecode.lanterna.terminal.ansi.StreamBasedTerminal Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lanterna Show documentation
Show all versions of lanterna Show documentation
Java library for creating text-based terminal GUIs
/*
* This file is part of lanterna (https://github.com/mabe02/lanterna).
*
* lanterna 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 program. If not, see .
*
* Copyright (C) 2010-2020 Martin Berglund
*/
package com.googlecode.lanterna.terminal.ansi;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;
import com.googlecode.lanterna.Symbols;
import com.googlecode.lanterna.TerminalTextUtils;
import com.googlecode.lanterna.input.InputDecoder;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.ScreenInfoAction;
import com.googlecode.lanterna.input.ScreenInfoCharacterPattern;
import com.googlecode.lanterna.terminal.AbstractTerminal;
import com.googlecode.lanterna.TerminalPosition;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* An abstract terminal implementing functionality for terminals using OutputStream/InputStream. You can extend from
* this class if your terminal implementation is using standard input and standard output but not ANSI escape codes (in
* which case you should extend ANSITerminal). This class also contains some automatic UTF-8 to VT100 character
* conversion when the terminal is not set to read UTF-8.
*
* @author Martin
*/
public abstract class StreamBasedTerminal extends AbstractTerminal {
private static final Charset UTF8_REFERENCE = StandardCharsets.UTF_8;
private final InputStream terminalInput;
private final OutputStream terminalOutput;
private final Charset terminalCharset;
private final InputDecoder inputDecoder;
private final Queue keyQueue;
private final Lock readLock;
private volatile TerminalPosition lastReportedCursorPosition;
@SuppressWarnings("WeakerAccess")
public StreamBasedTerminal(InputStream terminalInput, OutputStream terminalOutput, Charset terminalCharset) {
this.terminalInput = terminalInput;
this.terminalOutput = terminalOutput;
if(terminalCharset == null) {
this.terminalCharset = Charset.defaultCharset();
}
else {
this.terminalCharset = terminalCharset;
}
this.inputDecoder = new InputDecoder(new InputStreamReader(this.terminalInput, this.terminalCharset));
this.keyQueue = new LinkedList<>();
this.readLock = new ReentrantLock();
this.lastReportedCursorPosition = null;
}
/**
* {@inheritDoc}
*
* The {@code StreamBasedTerminal} class will attempt to translate some unicode characters to VT100 if the encoding
* attached to this {@code Terminal} isn't UTF-8.
*/
@Override
public void putCharacter(char c) throws IOException {
if(TerminalTextUtils.isPrintableCharacter(c)) {
writeToTerminal(translateCharacter(c));
}
}
/**
* This method will write a list of bytes directly to the output stream of the terminal.
* @param bytes Bytes to write to the terminal (synchronized)
* @throws java.io.IOException If there was an underlying I/O error
*/
@SuppressWarnings("WeakerAccess")
protected void writeToTerminal(byte... bytes) throws IOException {
synchronized(terminalOutput) {
terminalOutput.write(bytes);
}
}
@Override
public byte[] enquireTerminal(int timeout, TimeUnit timeoutTimeUnit) throws IOException {
synchronized(terminalOutput) {
terminalOutput.write(5); //ENQ
flush();
}
//Wait for input
long startTime = System.currentTimeMillis();
while(terminalInput.available() == 0) {
if(System.currentTimeMillis() - startTime > timeoutTimeUnit.toMillis(timeout)) {
return new byte[0];
}
try {
Thread.sleep(1);
}
catch(InterruptedException e) {
return new byte[0];
}
}
//We have at least one character, read as far as we can and return
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
while(terminalInput.available() > 0) {
buffer.write(terminalInput.read());
}
return buffer.toByteArray();
}
@Override
public void bell() throws IOException {
terminalOutput.write((byte)7);
terminalOutput.flush();
}
/**
* Returns the {@code InputDecoder} attached to this {@code StreamBasedTerminal}. Can be used to add additional
* character patterns to recognize and tune the way input is turned in {@code KeyStroke}:s.
* @return {@code InputDecoder} attached to this {@code StreamBasedTerminal}
*/
public InputDecoder getInputDecoder() {
return inputDecoder;
}
/**
* Used by the cursor reporting methods to reset any previous position memorized, so we're guaranteed to return the
* next reported position
*/
void resetMemorizedCursorPosition() {
lastReportedCursorPosition = null;
}
/**
* Waits for up to 5 seconds for a terminal cursor position report to appear in the input stream. If the timeout
* expires, it will return null. You should have sent the cursor position query already before
* calling this method.
* @return Current position of the cursor, or null if the terminal didn't report it in time.
* @throws IOException If there was an I/O error
*/
synchronized TerminalPosition waitForCursorPositionReport() throws IOException {
long startTime = System.currentTimeMillis();
TerminalPosition cursorPosition = lastReportedCursorPosition;
while(cursorPosition == null) {
if(System.currentTimeMillis() - startTime > 5000) {
//throw new IllegalStateException("Terminal didn't send any position report for 5 seconds, please file a bug with a reproduce!");
return null;
}
KeyStroke keyStroke = readInput(false, false);
if(keyStroke != null) {
keyQueue.add(keyStroke);
}
else {
try { Thread.sleep(1); } catch(InterruptedException ignored) {}
}
cursorPosition = lastReportedCursorPosition;
}
return cursorPosition;
}
@Override
public KeyStroke pollInput() throws IOException {
return readInput(false, true);
}
@Override
public KeyStroke readInput() throws IOException {
return readInput(true, true);
}
private KeyStroke readInput(boolean blocking, boolean useKeyQueue) throws IOException {
while(true) {
if(useKeyQueue) {
KeyStroke previouslyReadKey = keyQueue.poll();
if(previouslyReadKey != null) {
return previouslyReadKey;
}
}
if(blocking) {
readLock.lock();
}
else {
// If we are in non-blocking readInput(), don't wait for the lock, just return null right away
if(!readLock.tryLock()) {
return null;
}
}
try {
KeyStroke key = inputDecoder.getNextCharacter(blocking);
ScreenInfoAction report = ScreenInfoCharacterPattern.tryToAdopt(key);
if (lastReportedCursorPosition == null && report != null) {
lastReportedCursorPosition = report.getPosition();
}
else {
return key;
}
}
finally {
readLock.unlock();
}
}
}
@Override
public void flush() throws IOException {
synchronized(terminalOutput) {
terminalOutput.flush();
}
}
@Override
public void close() throws IOException {
// Should we close the input/output streams here?
// If someone uses lanterna just temporarily and want to switch back to using System.out/System.in manually,
// they won't be too happy if we closed the streams
}
protected Charset getCharset() {
return terminalCharset;
}
@SuppressWarnings("WeakerAccess")
protected byte[] translateCharacter(char input) {
if(UTF8_REFERENCE != null && UTF8_REFERENCE == terminalCharset) {
return convertToCharset(input);
}
//Convert ACS to ordinary terminal codes
switch(input) {
case Symbols.ARROW_DOWN:
return convertToVT100('v');
case Symbols.ARROW_LEFT:
return convertToVT100('<');
case Symbols.ARROW_RIGHT:
return convertToVT100('>');
case Symbols.ARROW_UP:
return convertToVT100('^');
case Symbols.BLOCK_DENSE:
case Symbols.BLOCK_MIDDLE:
case Symbols.BLOCK_SOLID:
case Symbols.BLOCK_SPARSE:
return convertToVT100((char) 97);
case Symbols.HEART:
case Symbols.CLUB:
case Symbols.SPADES:
return convertToVT100('?');
case Symbols.FACE_BLACK:
case Symbols.FACE_WHITE:
case Symbols.DIAMOND:
return convertToVT100((char) 96);
case Symbols.BULLET:
return convertToVT100((char) 102);
case Symbols.DOUBLE_LINE_CROSS:
case Symbols.SINGLE_LINE_CROSS:
return convertToVT100((char) 110);
case Symbols.DOUBLE_LINE_HORIZONTAL:
case Symbols.SINGLE_LINE_HORIZONTAL:
return convertToVT100((char) 113);
case Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER:
case Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER:
return convertToVT100((char) 109);
case Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER:
case Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER:
return convertToVT100((char) 106);
case Symbols.DOUBLE_LINE_T_DOWN:
case Symbols.SINGLE_LINE_T_DOWN:
case Symbols.DOUBLE_LINE_T_SINGLE_DOWN:
case Symbols.SINGLE_LINE_T_DOUBLE_DOWN:
return convertToVT100((char) 119);
case Symbols.DOUBLE_LINE_T_LEFT:
case Symbols.SINGLE_LINE_T_LEFT:
case Symbols.DOUBLE_LINE_T_SINGLE_LEFT:
case Symbols.SINGLE_LINE_T_DOUBLE_LEFT:
return convertToVT100((char) 117);
case Symbols.DOUBLE_LINE_T_RIGHT:
case Symbols.SINGLE_LINE_T_RIGHT:
case Symbols.DOUBLE_LINE_T_SINGLE_RIGHT:
case Symbols.SINGLE_LINE_T_DOUBLE_RIGHT:
return convertToVT100((char) 116);
case Symbols.DOUBLE_LINE_T_UP:
case Symbols.SINGLE_LINE_T_UP:
case Symbols.DOUBLE_LINE_T_SINGLE_UP:
case Symbols.SINGLE_LINE_T_DOUBLE_UP:
return convertToVT100((char) 118);
case Symbols.DOUBLE_LINE_TOP_LEFT_CORNER:
case Symbols.SINGLE_LINE_TOP_LEFT_CORNER:
return convertToVT100((char) 108);
case Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER:
case Symbols.SINGLE_LINE_TOP_RIGHT_CORNER:
return convertToVT100((char) 107);
case Symbols.DOUBLE_LINE_VERTICAL:
case Symbols.SINGLE_LINE_VERTICAL:
return convertToVT100((char) 120);
default:
return convertToCharset(input);
}
}
private byte[] convertToVT100(char code) {
//Warning! This might be terminal type specific!!!!
//So far it's worked everywhere I've tried it (xterm, gnome-terminal, putty)
return new byte[]{27, 40, 48, (byte) code, 27, 40, 66};
}
private byte[] convertToCharset(char input) {
return terminalCharset.encode(Character.toString(input)).array();
}
}