Change build scripts
[jvcard.git] / src / com / googlecode / lanterna / terminal / ansi / StreamBasedTerminal.java
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 }