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 = 044 System.getProperty("jline.sttyCommand", "stty"); 045 046 047 String encoding = System.getProperty("input.encoding", "UTF-8"); 048 ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(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, InterruptedException { 076 // save the initial tty configuration 077 ttyConfig = stty("-g"); 078 079 // sanity check 080 if ((ttyConfig.length() == 0) 081 || ((ttyConfig.indexOf("=") == -1) 082 && (ttyConfig.indexOf(":") == -1))) { 083 throw new IOException("Unrecognized stty code: " + ttyConfig); 084 } 085 086 checkBackspace(); 087 088 // set the console to be character-buffered instead of line-buffered 089 stty("-icanon min 1"); 090 091 // disable character echoing 092 stty("-echo"); 093 echoEnabled = false; 094 095 // at exit, restore the original tty configuration (for JDK 1.3+) 096 try { 097 Runtime.getRuntime().addShutdownHook(new Thread() { 098 public void start() { 099 try { 100 restoreTerminal(); 101 } catch (Exception e) { 102 consumeException(e); 103 } 104 } 105 }); 106 } catch (AbstractMethodError ame) { 107 // JDK 1.3+ only method. Bummer. 108 consumeException(ame); 109 } 110 } 111 112 /** 113 * Restore the original terminal configuration, which can be used when 114 * shutting down the console reader. The ConsoleReader cannot be 115 * used after calling this method. 116 */ 117 public void restoreTerminal() throws Exception { 118 if (ttyConfig != null) { 119 stty(ttyConfig); 120 ttyConfig = null; 121 } 122 resetTerminal(); 123 } 124 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"); 250 tok.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) 272 throws IOException, 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) 281 throws IOException, InterruptedException { 282 return exec(new String[] { 283 "sh", 284 "-c", 285 cmd 286 }); 287 } 288 289 /** 290 * Execute the specified command and return the output 291 * (both stdout and stderr). 292 */ 293 private static String exec(final String[] cmd) 294 throws IOException, InterruptedException { 295 ByteArrayOutputStream bout = new ByteArrayOutputStream(); 296 297 Process p = Runtime.getRuntime().exec(cmd); 298 int c; 299 InputStream in; 300 301 in = p.getInputStream(); 302 303 while ((c = in.read()) != -1) { 304 bout.write(c); 305 } 306 307 in = p.getErrorStream(); 308 309 while ((c = in.read()) != -1) { 310 bout.write(c); 311 } 312 313 p.waitFor(); 314 315 String result = new String(bout.toByteArray()); 316 317 return result; 318 } 319 320 /** 321 * The command to use to set the terminal options. Defaults 322 * to "stty", or the value of the system property "jline.sttyCommand". 323 */ 324 public static void setSttyCommand(String cmd) { 325 sttyCommand = cmd; 326 } 327 328 /** 329 * The command to use to set the terminal options. Defaults 330 * to "stty", or the value of the system property "jline.sttyCommand". 331 */ 332 public static String getSttyCommand() { 333 return sttyCommand; 334 } 335 336 public synchronized boolean isEchoEnabled() { 337 return echoEnabled; 338 } 339 340 341 public synchronized void enableEcho() { 342 try { 343 stty("echo"); 344 echoEnabled = true; 345 } catch (Exception e) { 346 consumeException(e); 347 } 348 } 349 350 public synchronized void disableEcho() { 351 try { 352 stty("-echo"); 353 echoEnabled = false; 354 } catch (Exception e) { 355 consumeException(e); 356 } 357 } 358 359 /** 360 * This is awkward and inefficient, but probably the minimal way to add 361 * UTF-8 support to JLine 362 * 363 * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a> 364 */ 365 static class ReplayPrefixOneCharInputStream extends InputStream { 366 byte firstByte; 367 int byteLength; 368 InputStream wrappedStream; 369 int byteRead; 370 371 final String encoding; 372 373 public ReplayPrefixOneCharInputStream(String encoding) { 374 this.encoding = encoding; 375 } 376 377 public void setInput(int recorded, InputStream wrapped) throws IOException { 378 this.byteRead = 0; 379 this.firstByte = (byte) recorded; 380 this.wrappedStream = wrapped; 381 382 byteLength = 1; 383 if (encoding.equalsIgnoreCase("UTF-8")) 384 setInputUTF8(recorded, wrapped); 385 else if (encoding.equalsIgnoreCase("UTF-16")) 386 byteLength = 2; 387 else if (encoding.equalsIgnoreCase("UTF-32")) 388 byteLength = 4; 389 } 390 391 392 public void setInputUTF8(int recorded, InputStream wrapped) throws IOException { 393 // 110yyyyy 10zzzzzz 394 if ((firstByte & (byte) 0xE0) == (byte) 0xC0) 395 this.byteLength = 2; 396 // 1110xxxx 10yyyyyy 10zzzzzz 397 else if ((firstByte & (byte) 0xF0) == (byte) 0xE0) 398 this.byteLength = 3; 399 // 11110www 10xxxxxx 10yyyyyy 10zzzzzz 400 else if ((firstByte & (byte) 0xF8) == (byte) 0xF0) 401 this.byteLength = 4; 402 else 403 throw new IOException("invalid UTF-8 first byte: " + firstByte); 404 } 405 406 public int read() throws IOException { 407 if (available() == 0) 408 return -1; 409 410 byteRead++; 411 412 if (byteRead == 1) 413 return firstByte; 414 415 return wrappedStream.read(); 416 } 417 418 /** 419 * InputStreamReader is greedy and will try to read bytes in advance. We 420 * do NOT want this to happen since we use a temporary/"losing bytes" 421 * InputStreamReader above, that's why we hide the real 422 * wrappedStream.available() here. 423 */ 424 public int available() { 425 return byteLength - byteRead; 426 } 427 } 428 }