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