Commit | Line | Data |
---|---|---|
a3b510ab NR |
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 | } |