/*
 * This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
 *
 * 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.KeyDecodingProfile;
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.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
/** * 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 = Charset.forName("UTF-8"); private final InputStream terminalInput; private final OutputStream terminalOutput; private final Charset terminalCharset; private final InputDecoder inputDecoder; private final Queue<KeyStroke> 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<KeyStroke>(); this.readLock = new ReentrantLock(); this.lastReportedCursorPosition = null; //noinspection ConstantConditions }
{@inheritDoc} The StreamBasedTerminal class will attempt to translate some unicode characters to VT100 if the encoding attached to this Terminal isn't UTF-8.
/** * {@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.
Params:
  • bytes – Bytes to write to the terminal (synchronized)
Throws:
/** * 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(); }
Adds a KeyDecodingProfile to be used when converting raw user input characters to Key objects.
Params:
  • profile – Decoding profile to add
See Also:
Deprecated:Use getInputDecoder().addProfile(profile) instead
/** * Adds a KeyDecodingProfile to be used when converting raw user input characters to {@code Key} objects. * * @see KeyDecodingProfile * @param profile Decoding profile to add * @deprecated Use {@code getInputDecoder().addProfile(profile)} instead */
@Deprecated @SuppressWarnings("WeakerAccess") public void addKeyDecodingProfile(KeyDecodingProfile profile) { inputDecoder.addProfile(profile); }
Returns the InputDecoder attached to this StreamBasedTerminal. Can be used to add additional character patterns to recognize and tune the way input is turned in KeyStroke:s.
Returns:InputDecoder attached to this StreamBasedTerminal
/** * 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
/** * 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.
Throws:
Returns:Current position of the cursor, or null if the terminal didn't report it in time.
/** * 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(); } }