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 c = readCharacter(in); 141 if (c == ARROW_PREFIX || c == O_PREFIX) { 142 c = readCharacter(in); 143 if (c == ARROW_UP) { 144 return CTRL_P; 145 } else if (c == ARROW_DOWN) { 146 return CTRL_N; 147 } else if (c == ARROW_LEFT) { 148 return CTRL_B; 149 } else if (c == ARROW_RIGHT) { 150 return CTRL_F; 151 } else if (c == HOME_CODE) { 152 return CTRL_A; 153 } else if (c == END_CODE) { 154 return CTRL_E; 155 } else if (c == DEL_THIRD) { 156 c = readCharacter(in); // read 4th 157 return DELETE; 158 } 159 } 160 } 161 // handle unicode characters, thanks for a patch from amyi@inf.ed.ac.uk 162 if (c > 128) { 163 // handle unicode characters longer than 2 bytes, 164 // thanks to Marc.Herbert@continuent.com 165 replayStream.setInput(c, in); 166 // replayReader = new InputStreamReader(replayStream, encoding); 167 c = replayReader.read(); 168 169 } 170 171 return c; 172 } 173 174 /** 175 * No-op for exceptions we want to silently consume. 176 */ 177 private void consumeException(Throwable e) { 178 } 179 180 public boolean isSupported() { 181 return true; 182 } 183 184 public boolean getEcho() { 185 return false; 186 } 187 188 /** 189 * Returns the value of "stty size" width param. 190 * 191 * <strong>Note</strong>: this method caches the value from the 192 * first time it is called in order to increase speed, which means 193 * that changing to size of the terminal will not be reflected 194 * in the console. 195 */ 196 public int getTerminalWidth() { 197 int val = -1; 198 199 try { 200 val = getTerminalProperty("columns"); 201 } catch (Exception e) { 202 } 203 204 if (val == -1) { 205 val = 80; 206 } 207 208 return val; 209 } 210 211 /** 212 * Returns the value of "stty size" height param. 213 * 214 * <strong>Note</strong>: this method caches the value from the 215 * first time it is called in order to increase speed, which means 216 * that changing to size of the terminal will not be reflected 217 * in the console. 218 */ 219 public int getTerminalHeight() { 220 int val = -1; 221 222 try { 223 val = getTerminalProperty("rows"); 224 } catch (Exception e) { 225 } 226 227 if (val == -1) { 228 val = 24; 229 } 230 231 return val; 232 } 233 234 private static int getTerminalProperty(String prop) 235 throws IOException, InterruptedException { 236 // need to be able handle both output formats: 237 // speed 9600 baud; 24 rows; 140 columns; 238 // and: 239 // speed 38400 baud; rows = 49; columns = 111; ypixels = 0; xpixels = 0; 240 String props = stty("-a"); 241 242 for (StringTokenizer tok = new StringTokenizer(props, ";\n"); 243 tok.hasMoreTokens();) { 244 String str = tok.nextToken().trim(); 245 246 if (str.startsWith(prop)) { 247 int index = str.lastIndexOf(" "); 248 249 return Integer.parseInt(str.substring(index).trim()); 250 } else if (str.endsWith(prop)) { 251 int index = str.indexOf(" "); 252 253 return Integer.parseInt(str.substring(0, index).trim()); 254 } 255 } 256 257 return -1; 258 } 259 260 /** 261 * Execute the stty command with the specified arguments 262 * against the current active terminal. 263 */ 264 private static String stty(final String args) 265 throws IOException, InterruptedException { 266 return exec("stty " + args + " < /dev/tty").trim(); 267 } 268 269 /** 270 * Execute the specified command and return the output 271 * (both stdout and stderr). 272 */ 273 private static String exec(final String cmd) 274 throws IOException, InterruptedException { 275 return exec(new String[] { 276 "sh", 277 "-c", 278 cmd 279 }); 280 } 281 282 /** 283 * Execute the specified command and return the output 284 * (both stdout and stderr). 285 */ 286 private static String exec(final String[] cmd) 287 throws IOException, InterruptedException { 288 ByteArrayOutputStream bout = new ByteArrayOutputStream(); 289 290 Process p = Runtime.getRuntime().exec(cmd); 291 int c; 292 InputStream in; 293 294 in = p.getInputStream(); 295 296 while ((c = in.read()) != -1) { 297 bout.write(c); 298 } 299 300 in = p.getErrorStream(); 301 302 while ((c = in.read()) != -1) { 303 bout.write(c); 304 } 305 306 p.waitFor(); 307 308 String result = new String(bout.toByteArray()); 309 310 return result; 311 } 312 313 /** 314 * The command to use to set the terminal options. Defaults 315 * to "stty", or the value of the system property "jline.sttyCommand". 316 */ 317 public static void setSttyCommand(String cmd) { 318 sttyCommand = cmd; 319 } 320 321 /** 322 * The command to use to set the terminal options. Defaults 323 * to "stty", or the value of the system property "jline.sttyCommand". 324 */ 325 public static String getSttyCommand() { 326 return sttyCommand; 327 } 328 329 public synchronized boolean isEchoEnabled() { 330 return echoEnabled; 331 } 332 333 334 public synchronized void enableEcho() { 335 try { 336 stty("echo"); 337 echoEnabled = true; 338 } catch (Exception e) { 339 consumeException(e); 340 } 341 } 342 343 public synchronized void disableEcho() { 344 try { 345 stty("-echo"); 346 echoEnabled = false; 347 } catch (Exception e) { 348 consumeException(e); 349 } 350 } 351 352 /** 353 * This is awkward and inefficient, but probably the minimal way to add 354 * UTF-8 support to JLine 355 * 356 * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a> 357 */ 358 static class ReplayPrefixOneCharInputStream extends InputStream { 359 byte firstByte; 360 int byteLength; 361 InputStream wrappedStream; 362 int byteRead; 363 364 final String encoding; 365 366 public ReplayPrefixOneCharInputStream(String encoding) { 367 this.encoding = encoding; 368 } 369 370 public void setInput(int recorded, InputStream wrapped) throws IOException { 371 this.byteRead = 0; 372 this.firstByte = (byte) recorded; 373 this.wrappedStream = wrapped; 374 375 byteLength = 1; 376 if (encoding.equalsIgnoreCase("UTF-8")) 377 setInputUTF8(recorded, wrapped); 378 else if (encoding.equalsIgnoreCase("UTF-16")) 379 byteLength = 2; 380 else if (encoding.equalsIgnoreCase("UTF-32")) 381 byteLength = 4; 382 } 383 384 385 public void setInputUTF8(int recorded, InputStream wrapped) throws IOException { 386 // 110yyyyy 10zzzzzz 387 if ((firstByte & (byte) 0xE0) == (byte) 0xC0) 388 this.byteLength = 2; 389 // 1110xxxx 10yyyyyy 10zzzzzz 390 else if ((firstByte & (byte) 0xF0) == (byte) 0xE0) 391 this.byteLength = 3; 392 // 11110www 10xxxxxx 10yyyyyy 10zzzzzz 393 else if ((firstByte & (byte) 0xF8) == (byte) 0xF0) 394 this.byteLength = 4; 395 else 396 throw new IOException("invalid UTF-8 first byte: " + firstByte); 397 } 398 399 public int read() throws IOException { 400 if (available() == 0) 401 return -1; 402 403 byteRead++; 404 405 if (byteRead == 1) 406 return firstByte; 407 408 return wrappedStream.read(); 409 } 410 411 /** 412 * InputStreamReader is greedy and will try to read bytes in advance. We 413 * do NOT want this to happen since we use a temporary/"losing bytes" 414 * InputStreamReader above, that's why we hide the real 415 * wrappedStream.available() here. 416 */ 417 public int available() { 418 return byteLength - byteRead; 419 } 420 } 421 }