/* * 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-2017 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 */ 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 {@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(); } /** * 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 {@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(); } }