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 }