001 /*
002 * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
003 *
004 * This software is distributable under the BSD license. See the terms of the
005 * BSD license in the documentation provided with this software.
006 */
007 package jline;
008
009 import java.io.*;
010 import java.util.*;
011
012 /**
013 * <p>
014 * Terminal that is used for unix platforms. Terminal initialization
015 * is handled by issuing the <em>stty</em> command against the
016 * <em>/dev/tty</em> file to disable character echoing and enable
017 * character input. All known unix systems (including
018 * Linux and Macintosh OS X) support the <em>stty</em>), so this
019 * implementation should work for an reasonable POSIX system.
020 * </p>
021 *
022 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
023 * @author Updates <a href="mailto:dwkemp@gmail.com">Dale Kemp</a> 2005-12-03
024 */
025 public class UnixTerminal extends Terminal {
026 public static final short ARROW_START = 27;
027 public static final short ARROW_PREFIX = 91;
028 public static final short ARROW_LEFT = 68;
029 public static final short ARROW_RIGHT = 67;
030 public static final short ARROW_UP = 65;
031 public static final short ARROW_DOWN = 66;
032 public static final short O_PREFIX = 79;
033 public static final short HOME_CODE = 72;
034 public static final short END_CODE = 70;
035 private Map terminfo;
036 private boolean echoEnabled;
037 private String ttyConfig;
038 private static String sttyCommand =
039 System.getProperty("jline.sttyCommand", "stty");
040
041 /**
042 * Remove line-buffered input by invoking "stty -icanon min 1"
043 * against the current terminal.
044 */
045 public void initializeTerminal() throws IOException, InterruptedException {
046 // save the initial tty configuration
047 ttyConfig = stty("-g");
048
049 // sanity check
050 if ((ttyConfig.length() == 0)
051 || ((ttyConfig.indexOf("=") == -1)
052 && (ttyConfig.indexOf(":") == -1))) {
053 throw new IOException("Unrecognized stty code: " + ttyConfig);
054 }
055
056 // set the console to be character-buffered instead of line-buffered
057 stty("-icanon min 1");
058
059 // disable character echoing
060 stty("-echo");
061 echoEnabled = false;
062
063 // at exit, restore the original tty configuration (for JDK 1.3+)
064 try {
065 Runtime.getRuntime().addShutdownHook(new Thread() {
066 public void start() {
067 try {
068 restoreTerminal();
069 } catch (Exception e) {
070 consumeException(e);
071 }
072 }
073 });
074 } catch (AbstractMethodError ame) {
075 // JDK 1.3+ only method. Bummer.
076 consumeException(ame);
077 }
078 }
079
080 /**
081 * Restore the original terminal configuration, which can be used when
082 * shutting down the console reader. The ConsoleReader cannot be
083 * used after calling this method.
084 */
085 public void restoreTerminal() throws Exception {
086 if (ttyConfig != null) {
087 stty(ttyConfig);
088 ttyConfig = null;
089 }
090 resetTerminal();
091 }
092
093 public int readVirtualKey(InputStream in) throws IOException {
094 int c = readCharacter(in);
095
096 // in Unix terminals, arrow keys are represented by
097 // a sequence of 3 characters. E.g., the up arrow
098 // key yields 27, 91, 68
099 if (c == ARROW_START) {
100 c = readCharacter(in);
101 if (c == ARROW_PREFIX || c == O_PREFIX) {
102 c = readCharacter(in);
103 if (c == ARROW_UP) {
104 return CTRL_P;
105 } else if (c == ARROW_DOWN) {
106 return CTRL_N;
107 } else if (c == ARROW_LEFT) {
108 return CTRL_B;
109 } else if (c == ARROW_RIGHT) {
110 return CTRL_F;
111 } else if (c == HOME_CODE) {
112 return CTRL_A;
113 } else if (c == END_CODE) {
114 return CTRL_E;
115 }
116 }
117 }
118 // handle unicode characters, thanks for a patch from amyi@inf.ed.ac.uk
119 if (c > 128)
120 // handle unicode characters longer than 2 bytes,
121 // thanks to Marc.Herbert@continuent.com
122 c = new InputStreamReader(new ReplayPrefixOneCharInputStream(c, in),
123 "UTF-8").read();
124
125 return c;
126 }
127
128 /**
129 * No-op for exceptions we want to silently consume.
130 */
131 private void consumeException(Throwable e) {
132 }
133
134 public boolean isSupported() {
135 return true;
136 }
137
138 public boolean getEcho() {
139 return false;
140 }
141
142 /**
143 * Returns the value of "stty size" width param.
144 *
145 * <strong>Note</strong>: this method caches the value from the
146 * first time it is called in order to increase speed, which means
147 * that changing to size of the terminal will not be reflected
148 * in the console.
149 */
150 public int getTerminalWidth() {
151 int val = -1;
152
153 try {
154 val = getTerminalProperty("columns");
155 } catch (Exception e) {
156 }
157
158 if (val == -1) {
159 val = 80;
160 }
161
162 return val;
163 }
164
165 /**
166 * Returns the value of "stty size" height param.
167 *
168 * <strong>Note</strong>: this method caches the value from the
169 * first time it is called in order to increase speed, which means
170 * that changing to size of the terminal will not be reflected
171 * in the console.
172 */
173 public int getTerminalHeight() {
174 int val = -1;
175
176 try {
177 val = getTerminalProperty("rows");
178 } catch (Exception e) {
179 }
180
181 if (val == -1) {
182 val = 24;
183 }
184
185 return val;
186 }
187
188 private static int getTerminalProperty(String prop)
189 throws IOException, InterruptedException {
190 // need to be able handle both output formats:
191 // speed 9600 baud; 24 rows; 140 columns;
192 // and:
193 // speed 38400 baud; rows = 49; columns = 111; ypixels = 0; xpixels = 0;
194 String props = stty("-a");
195
196 for (StringTokenizer tok = new StringTokenizer(props, ";\n");
197 tok.hasMoreTokens();) {
198 String str = tok.nextToken().trim();
199
200 if (str.startsWith(prop)) {
201 int index = str.lastIndexOf(" ");
202
203 return Integer.parseInt(str.substring(index).trim());
204 } else if (str.endsWith(prop)) {
205 int index = str.indexOf(" ");
206
207 return Integer.parseInt(str.substring(0, index).trim());
208 }
209 }
210
211 return -1;
212 }
213
214 /**
215 * Execute the stty command with the specified arguments
216 * against the current active terminal.
217 */
218 private static String stty(final String args)
219 throws IOException, InterruptedException {
220 return exec("stty " + args + " < /dev/tty").trim();
221 }
222
223 /**
224 * Execute the specified command and return the output
225 * (both stdout and stderr).
226 */
227 private static String exec(final String cmd)
228 throws IOException, InterruptedException {
229 return exec(new String[] {
230 "sh",
231 "-c",
232 cmd
233 });
234 }
235
236 /**
237 * Execute the specified command and return the output
238 * (both stdout and stderr).
239 */
240 private static String exec(final String[] cmd)
241 throws IOException, InterruptedException {
242 ByteArrayOutputStream bout = new ByteArrayOutputStream();
243
244 Process p = Runtime.getRuntime().exec(cmd);
245 int c;
246 InputStream in;
247
248 in = p.getInputStream();
249
250 while ((c = in.read()) != -1) {
251 bout.write(c);
252 }
253
254 in = p.getErrorStream();
255
256 while ((c = in.read()) != -1) {
257 bout.write(c);
258 }
259
260 p.waitFor();
261
262 String result = new String(bout.toByteArray());
263
264 return result;
265 }
266
267 /**
268 * The command to use to set the terminal options. Defaults
269 * to "stty", or the value of the system property "jline.sttyCommand".
270 */
271 public static void setSttyCommand(String cmd) {
272 sttyCommand = cmd;
273 }
274
275 /**
276 * The command to use to set the terminal options. Defaults
277 * to "stty", or the value of the system property "jline.sttyCommand".
278 */
279 public static String getSttyCommand() {
280 return sttyCommand;
281 }
282
283 public synchronized boolean isEchoEnabled() {
284 return echoEnabled;
285 }
286
287
288 public synchronized void enableEcho() {
289 try {
290 stty("echo");
291 echoEnabled = true;
292 } catch (Exception e) {
293 consumeException(e);
294 }
295 }
296
297 public synchronized void disableEcho() {
298 try {
299 stty("-echo");
300 echoEnabled = false;
301 } catch (Exception e) {
302 consumeException(e);
303 }
304 }
305
306 /**
307 * This is awkward and inefficient, but probably the minimal way to add
308 * UTF-8 support to JLine
309 *
310 * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a>
311 */
312 static class ReplayPrefixOneCharInputStream extends InputStream {
313 final byte firstByte;
314 final int byteLength;
315 final InputStream wrappedStream;
316 int byteRead;
317
318 public ReplayPrefixOneCharInputStream(int recorded, InputStream wrapped)
319 throws IOException {
320 this.wrappedStream = wrapped;
321 this.byteRead = 0;
322
323 this.firstByte = (byte) recorded;
324
325 // 110yyyyy 10zzzzzz
326 if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
327 this.byteLength = 2;
328 // 1110xxxx 10yyyyyy 10zzzzzz
329 else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
330 this.byteLength = 3;
331 // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
332 else if ((firstByte & (byte) 0xF8) == (byte) 0xF0)
333 this.byteLength = 4;
334 else
335 throw new IOException("invalid UTF-8 first byte: " + firstByte);
336 }
337
338 public int read() throws IOException {
339 if (available() == 0)
340 return -1;
341
342 byteRead++;
343
344 if (byteRead == 1)
345 return firstByte;
346
347 return wrappedStream.read();
348 }
349
350 /**
351 * InputStreamReader is greedy and will try to read bytes in advance. We
352 * do NOT want this to happen since we use a temporary/"losing bytes"
353 * InputStreamReader above, that's why we hide the real
354 * wrappedStream.available() here.
355 */
356 public int available() {
357 return byteLength - byteRead;
358 }
359 }
360 }