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:
036: public static final short DEL_THIRD = 51;
037: public static final short DEL_SECOND = 126;
038:
039: private Map terminfo;
040: private boolean echoEnabled;
041: private String ttyConfig;
042: private boolean backspaceDeleteSwitched = false;
043: private static String sttyCommand = System.getProperty(
044: "jline.sttyCommand", "stty");
045:
046: String encoding = System.getProperty("input.encoding", "UTF-8");
047: ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(
048: encoding);
049: InputStreamReader replayReader;
050:
051: public UnixTerminal() {
052: try {
053: replayReader = new InputStreamReader(replayStream, encoding);
054: } catch (Exception e) {
055: throw new RuntimeException(e);
056: }
057: }
058:
059: protected void checkBackspace() {
060: String[] ttyConfigSplit = ttyConfig.split(":|=");
061:
062: if (ttyConfigSplit.length < 7)
063: return;
064:
065: if (ttyConfigSplit[6] == null)
066: return;
067:
068: backspaceDeleteSwitched = ttyConfigSplit[6].equals("7f");
069: }
070:
071: /**
072: * Remove line-buffered input by invoking "stty -icanon min 1"
073: * against the current terminal.
074: */
075: public void initializeTerminal() throws IOException,
076: InterruptedException {
077: // save the initial tty configuration
078: ttyConfig = stty("-g");
079:
080: // sanity check
081: if ((ttyConfig.length() == 0)
082: || ((ttyConfig.indexOf("=") == -1) && (ttyConfig
083: .indexOf(":") == -1))) {
084: throw new IOException("Unrecognized stty code: "
085: + ttyConfig);
086: }
087:
088: checkBackspace();
089:
090: // set the console to be character-buffered instead of line-buffered
091: stty("-icanon min 1");
092:
093: // disable character echoing
094: stty("-echo");
095: echoEnabled = false;
096:
097: // at exit, restore the original tty configuration (for JDK 1.3+)
098: try {
099: Runtime.getRuntime().addShutdownHook(new Thread() {
100: public void start() {
101: try {
102: restoreTerminal();
103: } catch (Exception e) {
104: consumeException(e);
105: }
106: }
107: });
108: } catch (AbstractMethodError ame) {
109: // JDK 1.3+ only method. Bummer.
110: consumeException(ame);
111: }
112: }
113:
114: /**
115: * Restore the original terminal configuration, which can be used when
116: * shutting down the console reader. The ConsoleReader cannot be
117: * used after calling this method.
118: */
119: public void restoreTerminal() throws Exception {
120: if (ttyConfig != null) {
121: stty(ttyConfig);
122: ttyConfig = null;
123: }
124: resetTerminal();
125: }
126:
127: public int readVirtualKey(InputStream in) throws IOException {
128: int c = readCharacter(in);
129:
130: if (backspaceDeleteSwitched)
131: if (c == DELETE)
132: c = '\b';
133: else if (c == '\b')
134: c = DELETE;
135:
136: // in Unix terminals, arrow keys are represented by
137: // a sequence of 3 characters. E.g., the up arrow
138: // key yields 27, 91, 68
139: if (c == ARROW_START) {
140: //also the escape key is 27
141: //thats why we read until we
142: //have something different than 27
143: //this is a bugfix, because otherwise
144: //pressing escape and than an arrow key
145: //was an undefined state
146: while (c == ARROW_START)
147: c = readCharacter(in);
148: if (c == ARROW_PREFIX || c == O_PREFIX) {
149: c = readCharacter(in);
150: if (c == ARROW_UP) {
151: return CTRL_P;
152: } else if (c == ARROW_DOWN) {
153: return CTRL_N;
154: } else if (c == ARROW_LEFT) {
155: return CTRL_B;
156: } else if (c == ARROW_RIGHT) {
157: return CTRL_F;
158: } else if (c == HOME_CODE) {
159: return CTRL_A;
160: } else if (c == END_CODE) {
161: return CTRL_E;
162: } else if (c == DEL_THIRD) {
163: c = readCharacter(in); // read 4th
164: return DELETE;
165: }
166: }
167: }
168: // handle unicode characters, thanks for a patch from amyi@inf.ed.ac.uk
169: if (c > 128) {
170: // handle unicode characters longer than 2 bytes,
171: // thanks to Marc.Herbert@continuent.com
172: replayStream.setInput(c, in);
173: // replayReader = new InputStreamReader(replayStream, encoding);
174: c = replayReader.read();
175:
176: }
177:
178: return c;
179: }
180:
181: /**
182: * No-op for exceptions we want to silently consume.
183: */
184: private void consumeException(Throwable e) {
185: }
186:
187: public boolean isSupported() {
188: return true;
189: }
190:
191: public boolean getEcho() {
192: return false;
193: }
194:
195: /**
196: * Returns the value of "stty size" width param.
197: *
198: * <strong>Note</strong>: this method caches the value from the
199: * first time it is called in order to increase speed, which means
200: * that changing to size of the terminal will not be reflected
201: * in the console.
202: */
203: public int getTerminalWidth() {
204: int val = -1;
205:
206: try {
207: val = getTerminalProperty("columns");
208: } catch (Exception e) {
209: }
210:
211: if (val == -1) {
212: val = 80;
213: }
214:
215: return val;
216: }
217:
218: /**
219: * Returns the value of "stty size" height param.
220: *
221: * <strong>Note</strong>: this method caches the value from the
222: * first time it is called in order to increase speed, which means
223: * that changing to size of the terminal will not be reflected
224: * in the console.
225: */
226: public int getTerminalHeight() {
227: int val = -1;
228:
229: try {
230: val = getTerminalProperty("rows");
231: } catch (Exception e) {
232: }
233:
234: if (val == -1) {
235: val = 24;
236: }
237:
238: return val;
239: }
240:
241: private static int getTerminalProperty(String prop)
242: throws IOException, InterruptedException {
243: // need to be able handle both output formats:
244: // speed 9600 baud; 24 rows; 140 columns;
245: // and:
246: // speed 38400 baud; rows = 49; columns = 111; ypixels = 0; xpixels = 0;
247: String props = stty("-a");
248:
249: for (StringTokenizer tok = new StringTokenizer(props, ";\n"); tok
250: .hasMoreTokens();) {
251: String str = tok.nextToken().trim();
252:
253: if (str.startsWith(prop)) {
254: int index = str.lastIndexOf(" ");
255:
256: return Integer.parseInt(str.substring(index).trim());
257: } else if (str.endsWith(prop)) {
258: int index = str.indexOf(" ");
259:
260: return Integer.parseInt(str.substring(0, index).trim());
261: }
262: }
263:
264: return -1;
265: }
266:
267: /**
268: * Execute the stty command with the specified arguments
269: * against the current active terminal.
270: */
271: private static String stty(final String args) throws IOException,
272: InterruptedException {
273: return exec("stty " + args + " < /dev/tty").trim();
274: }
275:
276: /**
277: * Execute the specified command and return the output
278: * (both stdout and stderr).
279: */
280: private static String exec(final String cmd) throws IOException,
281: InterruptedException {
282: return exec(new String[] { "sh", "-c", cmd });
283: }
284:
285: /**
286: * Execute the specified command and return the output
287: * (both stdout and stderr).
288: */
289: private static String exec(final String[] cmd) throws IOException,
290: InterruptedException {
291: ByteArrayOutputStream bout = new ByteArrayOutputStream();
292:
293: Process p = Runtime.getRuntime().exec(cmd);
294: int c;
295: InputStream in;
296:
297: in = p.getInputStream();
298:
299: while ((c = in.read()) != -1) {
300: bout.write(c);
301: }
302:
303: in = p.getErrorStream();
304:
305: while ((c = in.read()) != -1) {
306: bout.write(c);
307: }
308:
309: p.waitFor();
310:
311: String result = new String(bout.toByteArray());
312:
313: return result;
314: }
315:
316: /**
317: * The command to use to set the terminal options. Defaults
318: * to "stty", or the value of the system property "jline.sttyCommand".
319: */
320: public static void setSttyCommand(String cmd) {
321: sttyCommand = cmd;
322: }
323:
324: /**
325: * The command to use to set the terminal options. Defaults
326: * to "stty", or the value of the system property "jline.sttyCommand".
327: */
328: public static String getSttyCommand() {
329: return sttyCommand;
330: }
331:
332: public synchronized boolean isEchoEnabled() {
333: return echoEnabled;
334: }
335:
336: public synchronized void enableEcho() {
337: try {
338: stty("echo");
339: echoEnabled = true;
340: } catch (Exception e) {
341: consumeException(e);
342: }
343: }
344:
345: public synchronized void disableEcho() {
346: try {
347: stty("-echo");
348: echoEnabled = false;
349: } catch (Exception e) {
350: consumeException(e);
351: }
352: }
353:
354: /**
355: * This is awkward and inefficient, but probably the minimal way to add
356: * UTF-8 support to JLine
357: *
358: * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a>
359: */
360: static class ReplayPrefixOneCharInputStream extends InputStream {
361: byte firstByte;
362: int byteLength;
363: InputStream wrappedStream;
364: int byteRead;
365:
366: final String encoding;
367:
368: public ReplayPrefixOneCharInputStream(String encoding) {
369: this .encoding = encoding;
370: }
371:
372: public void setInput(int recorded, InputStream wrapped)
373: throws IOException {
374: this .byteRead = 0;
375: this .firstByte = (byte) recorded;
376: this .wrappedStream = wrapped;
377:
378: byteLength = 1;
379: if (encoding.equalsIgnoreCase("UTF-8"))
380: setInputUTF8(recorded, wrapped);
381: else if (encoding.equalsIgnoreCase("UTF-16"))
382: byteLength = 2;
383: else if (encoding.equalsIgnoreCase("UTF-32"))
384: byteLength = 4;
385: }
386:
387: public void setInputUTF8(int recorded, InputStream wrapped)
388: throws IOException {
389: // 110yyyyy 10zzzzzz
390: if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
391: this .byteLength = 2;
392: // 1110xxxx 10yyyyyy 10zzzzzz
393: else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
394: this .byteLength = 3;
395: // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
396: else if ((firstByte & (byte) 0xF8) == (byte) 0xF0)
397: this .byteLength = 4;
398: else
399: throw new IOException("invalid UTF-8 first byte: "
400: + firstByte);
401: }
402:
403: public int read() throws IOException {
404: if (available() == 0)
405: return -1;
406:
407: byteRead++;
408:
409: if (byteRead == 1)
410: return firstByte;
411:
412: return wrappedStream.read();
413: }
414:
415: /**
416: * InputStreamReader is greedy and will try to read bytes in advance. We
417: * do NOT want this to happen since we use a temporary/"losing bytes"
418: * InputStreamReader above, that's why we hide the real
419: * wrappedStream.available() here.
420: */
421: public int available() {
422: return byteLength - byteRead;
423: }
424: }
425: }
|