| 1 | /* |
| 2 | * This file is part of lanterna (http://code.google.com/p/lanterna/). |
| 3 | * |
| 4 | * lanterna is free software: you can redistribute it and/or modify |
| 5 | * it under the terms of the GNU Lesser General Public License as published by |
| 6 | * the Free Software Foundation, either version 3 of the License, or |
| 7 | * (at your option) any later version. |
| 8 | * |
| 9 | * This program is distributed in the hope that it will be useful, |
| 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | * GNU Lesser General Public License for more details. |
| 13 | * |
| 14 | * You should have received a copy of the GNU Lesser General Public License |
| 15 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | * |
| 17 | * Copyright (C) 2010-2015 Martin |
| 18 | */ |
| 19 | package com.googlecode.lanterna.terminal.ansi; |
| 20 | |
| 21 | import java.io.IOException; |
| 22 | import java.io.InputStream; |
| 23 | import java.io.InputStreamReader; |
| 24 | import java.io.OutputStream; |
| 25 | import java.nio.CharBuffer; |
| 26 | import java.nio.charset.Charset; |
| 27 | |
| 28 | import com.googlecode.lanterna.Symbols; |
| 29 | import com.googlecode.lanterna.input.InputDecoder; |
| 30 | import com.googlecode.lanterna.input.KeyDecodingProfile; |
| 31 | import com.googlecode.lanterna.input.KeyStroke; |
| 32 | import com.googlecode.lanterna.input.ScreenInfoAction; |
| 33 | import com.googlecode.lanterna.input.ScreenInfoCharacterPattern; |
| 34 | import com.googlecode.lanterna.terminal.AbstractTerminal; |
| 35 | import com.googlecode.lanterna.TerminalPosition; |
| 36 | import com.googlecode.lanterna.TerminalSize; |
| 37 | import java.io.ByteArrayOutputStream; |
| 38 | import java.util.LinkedList; |
| 39 | import java.util.Queue; |
| 40 | import java.util.concurrent.TimeUnit; |
| 41 | import java.util.concurrent.locks.Lock; |
| 42 | import java.util.concurrent.locks.ReentrantLock; |
| 43 | |
| 44 | /** |
| 45 | * An abstract terminal implementing functionality for terminals using OutputStream/InputStream. You can extend from |
| 46 | * this class if your terminal implementation is using standard input and standard output but not ANSI escape codes (in |
| 47 | * which case you should extend ANSITerminal). This class also contains some automatic UTF-8 to VT100 character |
| 48 | * conversion when the terminal is not set to read UTF-8. |
| 49 | * |
| 50 | * @author Martin |
| 51 | */ |
| 52 | public abstract class StreamBasedTerminal extends AbstractTerminal { |
| 53 | |
| 54 | private static final Charset UTF8_REFERENCE = Charset.forName("UTF-8"); |
| 55 | |
| 56 | private final InputStream terminalInput; |
| 57 | private final OutputStream terminalOutput; |
| 58 | private final Charset terminalCharset; |
| 59 | |
| 60 | private final InputDecoder inputDecoder; |
| 61 | private final Queue<KeyStroke> keyQueue; |
| 62 | private final Lock readLock; |
| 63 | |
| 64 | @SuppressWarnings("WeakerAccess") |
| 65 | public StreamBasedTerminal(InputStream terminalInput, OutputStream terminalOutput, Charset terminalCharset) { |
| 66 | this.terminalInput = terminalInput; |
| 67 | this.terminalOutput = terminalOutput; |
| 68 | if(terminalCharset == null) { |
| 69 | this.terminalCharset = Charset.defaultCharset(); |
| 70 | } |
| 71 | else { |
| 72 | this.terminalCharset = terminalCharset; |
| 73 | } |
| 74 | this.inputDecoder = new InputDecoder(new InputStreamReader(this.terminalInput, this.terminalCharset)); |
| 75 | this.keyQueue = new LinkedList<KeyStroke>(); |
| 76 | this.readLock = new ReentrantLock(); |
| 77 | //noinspection ConstantConditions |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * {@inheritDoc} |
| 82 | * |
| 83 | * The {@code StreamBasedTerminal} class will attempt to translate some unicode characters to VT100 if the encoding |
| 84 | * attached to this {@code Terminal} isn't UTF-8. |
| 85 | */ |
| 86 | @Override |
| 87 | public void putCharacter(char c) throws IOException { |
| 88 | writeToTerminal(translateCharacter(c)); |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * This method will write a list of bytes directly to the output stream of the terminal. |
| 93 | * @param bytes Bytes to write to the terminal (synchronized) |
| 94 | * @throws java.io.IOException If there was an underlying I/O error |
| 95 | */ |
| 96 | @SuppressWarnings("WeakerAccess") |
| 97 | protected void writeToTerminal(byte... bytes) throws IOException { |
| 98 | synchronized(terminalOutput) { |
| 99 | terminalOutput.write(bytes); |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | @Override |
| 104 | public byte[] enquireTerminal(int timeout, TimeUnit timeoutTimeUnit) throws IOException { |
| 105 | synchronized(terminalOutput) { |
| 106 | terminalOutput.write(5); //ENQ |
| 107 | flush(); |
| 108 | } |
| 109 | |
| 110 | //Wait for input |
| 111 | long startTime = System.currentTimeMillis(); |
| 112 | while(terminalInput.available() == 0) { |
| 113 | if(System.currentTimeMillis() - startTime > timeoutTimeUnit.toMillis(timeout)) { |
| 114 | return new byte[0]; |
| 115 | } |
| 116 | try { |
| 117 | Thread.sleep(1); |
| 118 | } |
| 119 | catch(InterruptedException e) { |
| 120 | return new byte[0]; |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | //We have at least one character, read as far as we can and return |
| 125 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
| 126 | while(terminalInput.available() > 0) { |
| 127 | buffer.write(terminalInput.read()); |
| 128 | } |
| 129 | return buffer.toByteArray(); |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Adds a KeyDecodingProfile to be used when converting raw user input characters to {@code Key} objects. |
| 134 | * |
| 135 | * @see KeyDecodingProfile |
| 136 | * @param profile Decoding profile to add |
| 137 | * @deprecated Use {@code getInputDecoder().addProfile(profile)} instead |
| 138 | */ |
| 139 | @Deprecated |
| 140 | @SuppressWarnings("WeakerAccess") |
| 141 | public void addKeyDecodingProfile(KeyDecodingProfile profile) { |
| 142 | inputDecoder.addProfile(profile); |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * Returns the {@code InputDecoder} attached to this {@code StreamBasedTerminal}. Can be used to add additional |
| 147 | * character patterns to recognize and tune the way input is turned in {@code KeyStroke}:s. |
| 148 | * @return {@code InputDecoder} attached to this {@code StreamBasedTerminal} |
| 149 | */ |
| 150 | public InputDecoder getInputDecoder() { |
| 151 | return inputDecoder; |
| 152 | } |
| 153 | |
| 154 | @SuppressWarnings("ConstantConditions") |
| 155 | TerminalSize waitForTerminalSizeReport() throws IOException { |
| 156 | long startTime = System.currentTimeMillis(); |
| 157 | readLock.lock(); |
| 158 | try { |
| 159 | while(true) { |
| 160 | KeyStroke key = inputDecoder.getNextCharacter(false); |
| 161 | if(key == null) { |
| 162 | if(System.currentTimeMillis() - startTime > 1000) { //Wait 1 second for the terminal size report to come, is this reasonable? |
| 163 | throw new IOException( |
| 164 | "Timeout while waiting for terminal size report! Your terminal may have refused to go into cbreak mode."); |
| 165 | } |
| 166 | try { |
| 167 | Thread.sleep(1); |
| 168 | } |
| 169 | catch(InterruptedException ignored) {} |
| 170 | continue; |
| 171 | } |
| 172 | |
| 173 | // check both: real ScreenInfoActions and F3 keystrokes with modifiers: |
| 174 | ScreenInfoAction report = ScreenInfoCharacterPattern.tryToAdopt(key); |
| 175 | if (report == null) { |
| 176 | keyQueue.add(key); |
| 177 | } |
| 178 | else { |
| 179 | TerminalPosition reportedTerminalPosition = report.getPosition(); |
| 180 | onResized(reportedTerminalPosition.getColumn(), reportedTerminalPosition.getRow()); |
| 181 | return new TerminalSize(reportedTerminalPosition.getColumn(), reportedTerminalPosition.getRow()); |
| 182 | } |
| 183 | } |
| 184 | } |
| 185 | finally { |
| 186 | readLock.unlock(); |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | @Override |
| 191 | public KeyStroke pollInput() throws IOException { |
| 192 | return readInput(false); |
| 193 | } |
| 194 | |
| 195 | @Override |
| 196 | public KeyStroke readInput() throws IOException { |
| 197 | return readInput(true); |
| 198 | } |
| 199 | |
| 200 | private KeyStroke readInput(boolean blocking) throws IOException { |
| 201 | readLock.lock(); |
| 202 | try { |
| 203 | if(!keyQueue.isEmpty()) { |
| 204 | return keyQueue.poll(); |
| 205 | } |
| 206 | KeyStroke key = inputDecoder.getNextCharacter(blocking); |
| 207 | if (key instanceof ScreenInfoAction) { |
| 208 | TerminalPosition reportedTerminalPosition = ((ScreenInfoAction)key).getPosition(); |
| 209 | onResized(reportedTerminalPosition.getColumn(), reportedTerminalPosition.getRow()); |
| 210 | return pollInput(); |
| 211 | } else { |
| 212 | return key; |
| 213 | } |
| 214 | } |
| 215 | finally { |
| 216 | readLock.unlock(); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | @Override |
| 221 | public void flush() throws IOException { |
| 222 | synchronized(terminalOutput) { |
| 223 | terminalOutput.flush(); |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | protected Charset getCharset() { |
| 228 | return terminalCharset; |
| 229 | } |
| 230 | |
| 231 | @SuppressWarnings("WeakerAccess") |
| 232 | protected byte[] translateCharacter(char input) { |
| 233 | if(UTF8_REFERENCE != null && UTF8_REFERENCE == terminalCharset) { |
| 234 | return convertToCharset(input); |
| 235 | } |
| 236 | //Convert ACS to ordinary terminal codes |
| 237 | switch(input) { |
| 238 | case Symbols.ARROW_DOWN: |
| 239 | return convertToVT100('v'); |
| 240 | case Symbols.ARROW_LEFT: |
| 241 | return convertToVT100('<'); |
| 242 | case Symbols.ARROW_RIGHT: |
| 243 | return convertToVT100('>'); |
| 244 | case Symbols.ARROW_UP: |
| 245 | return convertToVT100('^'); |
| 246 | case Symbols.BLOCK_DENSE: |
| 247 | case Symbols.BLOCK_MIDDLE: |
| 248 | case Symbols.BLOCK_SOLID: |
| 249 | case Symbols.BLOCK_SPARSE: |
| 250 | return convertToVT100((char) 97); |
| 251 | case Symbols.HEART: |
| 252 | case Symbols.CLUB: |
| 253 | case Symbols.SPADES: |
| 254 | return convertToVT100('?'); |
| 255 | case Symbols.FACE_BLACK: |
| 256 | case Symbols.FACE_WHITE: |
| 257 | case Symbols.DIAMOND: |
| 258 | return convertToVT100((char) 96); |
| 259 | case Symbols.BULLET: |
| 260 | return convertToVT100((char) 102); |
| 261 | case Symbols.DOUBLE_LINE_CROSS: |
| 262 | case Symbols.SINGLE_LINE_CROSS: |
| 263 | return convertToVT100((char) 110); |
| 264 | case Symbols.DOUBLE_LINE_HORIZONTAL: |
| 265 | case Symbols.SINGLE_LINE_HORIZONTAL: |
| 266 | return convertToVT100((char) 113); |
| 267 | case Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER: |
| 268 | case Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER: |
| 269 | return convertToVT100((char) 109); |
| 270 | case Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER: |
| 271 | case Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER: |
| 272 | return convertToVT100((char) 106); |
| 273 | case Symbols.DOUBLE_LINE_T_DOWN: |
| 274 | case Symbols.SINGLE_LINE_T_DOWN: |
| 275 | case Symbols.DOUBLE_LINE_T_SINGLE_DOWN: |
| 276 | case Symbols.SINGLE_LINE_T_DOUBLE_DOWN: |
| 277 | return convertToVT100((char) 119); |
| 278 | case Symbols.DOUBLE_LINE_T_LEFT: |
| 279 | case Symbols.SINGLE_LINE_T_LEFT: |
| 280 | case Symbols.DOUBLE_LINE_T_SINGLE_LEFT: |
| 281 | case Symbols.SINGLE_LINE_T_DOUBLE_LEFT: |
| 282 | return convertToVT100((char) 117); |
| 283 | case Symbols.DOUBLE_LINE_T_RIGHT: |
| 284 | case Symbols.SINGLE_LINE_T_RIGHT: |
| 285 | case Symbols.DOUBLE_LINE_T_SINGLE_RIGHT: |
| 286 | case Symbols.SINGLE_LINE_T_DOUBLE_RIGHT: |
| 287 | return convertToVT100((char) 116); |
| 288 | case Symbols.DOUBLE_LINE_T_UP: |
| 289 | case Symbols.SINGLE_LINE_T_UP: |
| 290 | case Symbols.DOUBLE_LINE_T_SINGLE_UP: |
| 291 | case Symbols.SINGLE_LINE_T_DOUBLE_UP: |
| 292 | return convertToVT100((char) 118); |
| 293 | case Symbols.DOUBLE_LINE_TOP_LEFT_CORNER: |
| 294 | case Symbols.SINGLE_LINE_TOP_LEFT_CORNER: |
| 295 | return convertToVT100((char) 108); |
| 296 | case Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER: |
| 297 | case Symbols.SINGLE_LINE_TOP_RIGHT_CORNER: |
| 298 | return convertToVT100((char) 107); |
| 299 | case Symbols.DOUBLE_LINE_VERTICAL: |
| 300 | case Symbols.SINGLE_LINE_VERTICAL: |
| 301 | return convertToVT100((char) 120); |
| 302 | default: |
| 303 | return convertToCharset(input); |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | private byte[] convertToVT100(char code) { |
| 308 | //Warning! This might be terminal type specific!!!! |
| 309 | //So far it's worked everywhere I've tried it (xterm, gnome-terminal, putty) |
| 310 | return new byte[]{27, 40, 48, (byte) code, 27, 40, 66}; |
| 311 | } |
| 312 | |
| 313 | private byte[] convertToCharset(char input) { |
| 314 | return terminalCharset.encode(Character.toString(input)).array(); |
| 315 | } |
| 316 | } |