/* * 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 . * * Copyright (C) 2010-2015 Martin */ 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.CharBuffer; import java.nio.charset.Charset; import com.googlecode.lanterna.Symbols; 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 com.googlecode.lanterna.TerminalSize; 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 keyQueue; private final Lock readLock; @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(); //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 { 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(); } /** * 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; } @SuppressWarnings("ConstantConditions") TerminalSize waitForTerminalSizeReport() throws IOException { long startTime = System.currentTimeMillis(); readLock.lock(); try { while(true) { KeyStroke key = inputDecoder.getNextCharacter(false); if(key == null) { if(System.currentTimeMillis() - startTime > 1000) { //Wait 1 second for the terminal size report to come, is this reasonable? throw new IOException( "Timeout while waiting for terminal size report! Your terminal may have refused to go into cbreak mode."); } try { Thread.sleep(1); } catch(InterruptedException ignored) {} continue; } // check both: real ScreenInfoActions and F3 keystrokes with modifiers: ScreenInfoAction report = ScreenInfoCharacterPattern.tryToAdopt(key); if (report == null) { keyQueue.add(key); } else { TerminalPosition reportedTerminalPosition = report.getPosition(); onResized(reportedTerminalPosition.getColumn(), reportedTerminalPosition.getRow()); return new TerminalSize(reportedTerminalPosition.getColumn(), reportedTerminalPosition.getRow()); } } } finally { readLock.unlock(); } } @Override public KeyStroke pollInput() throws IOException { return readInput(false); } @Override public KeyStroke readInput() throws IOException { return readInput(true); } private KeyStroke readInput(boolean blocking) throws IOException { readLock.lock(); try { if(!keyQueue.isEmpty()) { return keyQueue.poll(); } KeyStroke key = inputDecoder.getNextCharacter(blocking); if (key instanceof ScreenInfoAction) { TerminalPosition reportedTerminalPosition = ((ScreenInfoAction)key).getPosition(); onResized(reportedTerminalPosition.getColumn(), reportedTerminalPosition.getRow()); return pollInput(); } else { return key; } } finally { readLock.unlock(); } } @Override public void flush() throws IOException { synchronized(terminalOutput) { terminalOutput.flush(); } } 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(); } }