1 /* 2 * Copyright (c) 2002-2018, the original author or authors. 3 * 4 * This software is distributable under the BSD license. See the terms of the 5 * BSD license in the documentation provided with this software. 6 * 7 * http://www.opensource.org/licenses/bsd-license.php 8 */ 9 package jdk.internal.org.jline.terminal; 10 11 import java.io.FileDescriptor; 12 import java.io.FileInputStream; 13 import java.io.FileOutputStream; 14 import java.io.IOException; 15 import java.io.InputStream; 16 import java.io.OutputStream; 17 import java.lang.reflect.Method; 18 import java.nio.charset.Charset; 19 import java.nio.charset.UnsupportedCharsetException; 20 import java.util.Optional; 21 import java.util.ServiceLoader; 22 import java.util.function.Function; 23 24 import jdk.internal.org.jline.terminal.impl.AbstractPosixTerminal; 25 import jdk.internal.org.jline.terminal.impl.DumbTerminal; 26 import jdk.internal.org.jline.terminal.impl.ExecPty; 27 import jdk.internal.org.jline.terminal.impl.ExternalTerminal; 28 import jdk.internal.org.jline.terminal.impl.PosixPtyTerminal; 29 import jdk.internal.org.jline.terminal.impl.PosixSysTerminal; 30 import jdk.internal.org.jline.terminal.spi.JansiSupport; 31 import jdk.internal.org.jline.terminal.spi.JnaSupport; 32 import jdk.internal.org.jline.terminal.spi.Pty; 33 import jdk.internal.org.jline.utils.Log; 34 import jdk.internal.org.jline.utils.OSUtils; 35 36 import static jdk.internal.org.jline.terminal.impl.AbstractWindowsTerminal.TYPE_WINDOWS; 37 import static jdk.internal.org.jline.terminal.impl.AbstractWindowsTerminal.TYPE_WINDOWS_256_COLOR; 38 39 /** 40 * Builder class to create terminals. 41 */ 42 public final class TerminalBuilder { 43 44 // 45 // System properties 46 // 47 48 public static final String PROP_ENCODING = "org.jline.terminal.encoding"; 49 public static final String PROP_CODEPAGE = "org.jline.terminal.codepage"; 50 public static final String PROP_TYPE = "org.jline.terminal.type"; 51 public static final String PROP_JNA = "org.jline.terminal.jna"; 52 public static final String PROP_JANSI = "org.jline.terminal.jansi"; 53 public static final String PROP_EXEC = "org.jline.terminal.exec"; 54 public static final String PROP_DUMB = "org.jline.terminal.dumb"; 55 public static final String PROP_DUMB_COLOR = "org.jline.terminal.dumb.color"; 56 57 // 58 // Other system properties controlling various jline parts 59 // 60 61 public static final String PROP_NON_BLOCKING_READS = "org.jline.terminal.pty.nonBlockingReads"; 62 public static final String PROP_COLOR_DISTANCE = "org.jline.utils.colorDistance"; 63 public static final String PROP_DISABLE_ALTERNATE_CHARSET = "org.jline.utils.disableAlternateCharset"; 64 65 /** 66 * Returns the default system terminal. 67 * Terminals should be closed properly using the {@link Terminal#close()} 68 * method in order to restore the original terminal state. 69 * 70 * <p> 71 * This call is equivalent to: 72 * <code>builder().build()</code> 73 * </p> 74 * 75 * @return the default system terminal 76 * @throws IOException if an error occurs 77 */ terminal()78 public static Terminal terminal() throws IOException { 79 return builder().build(); 80 } 81 82 /** 83 * Creates a new terminal builder instance. 84 * 85 * @return a builder 86 */ builder()87 public static TerminalBuilder builder() { 88 return new TerminalBuilder(); 89 } 90 91 private String name; 92 private InputStream in; 93 private OutputStream out; 94 private String type; 95 private Charset encoding; 96 private int codepage; 97 private Boolean system; 98 private Boolean jna; 99 private Boolean jansi; 100 private Boolean exec; 101 private Boolean dumb; 102 private Attributes attributes; 103 private Size size; 104 private boolean nativeSignals = false; 105 private Terminal.SignalHandler signalHandler = Terminal.SignalHandler.SIG_DFL; 106 private boolean paused = false; 107 private Function<InputStream, InputStream> inputStreamWrapper = in -> in; 108 TerminalBuilder()109 private TerminalBuilder() { 110 } 111 name(String name)112 public TerminalBuilder name(String name) { 113 this.name = name; 114 return this; 115 } 116 streams(InputStream in, OutputStream out)117 public TerminalBuilder streams(InputStream in, OutputStream out) { 118 this.in = in; 119 this.out = out; 120 return this; 121 } 122 system(boolean system)123 public TerminalBuilder system(boolean system) { 124 this.system = system; 125 return this; 126 } 127 jna(boolean jna)128 public TerminalBuilder jna(boolean jna) { 129 this.jna = jna; 130 return this; 131 } 132 jansi(boolean jansi)133 public TerminalBuilder jansi(boolean jansi) { 134 this.jansi = jansi; 135 return this; 136 } 137 exec(boolean exec)138 public TerminalBuilder exec(boolean exec) { 139 this.exec = exec; 140 return this; 141 } 142 dumb(boolean dumb)143 public TerminalBuilder dumb(boolean dumb) { 144 this.dumb = dumb; 145 return this; 146 } 147 type(String type)148 public TerminalBuilder type(String type) { 149 this.type = type; 150 return this; 151 } 152 153 /** 154 * Set the encoding to use for reading/writing from the console. 155 * If {@code null} (the default value), JLine will automatically select 156 * a {@link Charset}, usually the default system encoding. However, 157 * on some platforms (e.g. Windows) it may use a different one depending 158 * on the {@link Terminal} implementation. 159 * 160 * <p>Use {@link Terminal#encoding()} to get the {@link Charset} that 161 * should be used for a {@link Terminal}.</p> 162 * 163 * @param encoding The encoding to use or null to automatically select one 164 * @return The builder 165 * @throws UnsupportedCharsetException If the given encoding is not supported 166 * @see Terminal#encoding() 167 */ encoding(String encoding)168 public TerminalBuilder encoding(String encoding) throws UnsupportedCharsetException { 169 return encoding(encoding != null ? Charset.forName(encoding) : null); 170 } 171 172 /** 173 * Set the {@link Charset} to use for reading/writing from the console. 174 * If {@code null} (the default value), JLine will automatically select 175 * a {@link Charset}, usually the default system encoding. However, 176 * on some platforms (e.g. Windows) it may use a different one depending 177 * on the {@link Terminal} implementation. 178 * 179 * <p>Use {@link Terminal#encoding()} to get the {@link Charset} that 180 * should be used to read/write from a {@link Terminal}.</p> 181 * 182 * @param encoding The encoding to use or null to automatically select one 183 * @return The builder 184 * @see Terminal#encoding() 185 */ encoding(Charset encoding)186 public TerminalBuilder encoding(Charset encoding) { 187 this.encoding = encoding; 188 return this; 189 } 190 191 /** 192 * @param codepage the codepage 193 * @return The builder 194 * @deprecated JLine now writes Unicode output independently from the selected 195 * code page. Using this option will only make it emulate the selected code 196 * page for {@link Terminal#input()} and {@link Terminal#output()}. 197 */ 198 @Deprecated codepage(int codepage)199 public TerminalBuilder codepage(int codepage) { 200 this.codepage = codepage; 201 return this; 202 } 203 204 /** 205 * Attributes to use when creating a non system terminal, 206 * i.e. when the builder has been given the input and 207 * outut streams using the {@link #streams(InputStream, OutputStream)} method 208 * or when {@link #system(boolean)} has been explicitely called with 209 * <code>false</code>. 210 * 211 * @param attributes the attributes to use 212 * @return The builder 213 * @see #size(Size) 214 * @see #system(boolean) 215 */ attributes(Attributes attributes)216 public TerminalBuilder attributes(Attributes attributes) { 217 this.attributes = attributes; 218 return this; 219 } 220 221 /** 222 * Initial size to use when creating a non system terminal, 223 * i.e. when the builder has been given the input and 224 * outut streams using the {@link #streams(InputStream, OutputStream)} method 225 * or when {@link #system(boolean)} has been explicitely called with 226 * <code>false</code>. 227 * 228 * @param size the initial size 229 * @return The builder 230 * @see #attributes(Attributes) 231 * @see #system(boolean) 232 */ size(Size size)233 public TerminalBuilder size(Size size) { 234 this.size = size; 235 return this; 236 } 237 nativeSignals(boolean nativeSignals)238 public TerminalBuilder nativeSignals(boolean nativeSignals) { 239 this.nativeSignals = nativeSignals; 240 return this; 241 } 242 signalHandler(Terminal.SignalHandler signalHandler)243 public TerminalBuilder signalHandler(Terminal.SignalHandler signalHandler) { 244 this.signalHandler = signalHandler; 245 return this; 246 } 247 248 /** 249 * Initial paused state of the terminal (defaults to false). 250 * By default, the terminal is started, but in some cases, 251 * one might want to make sure the input stream is not consumed 252 * before needed, in which case the terminal needs to be created 253 * in a paused state. 254 * @param paused the initial paused state 255 * @return The builder 256 * @see Terminal#pause() 257 */ paused(boolean paused)258 public TerminalBuilder paused(boolean paused) { 259 this.paused = paused; 260 return this; 261 } 262 inputStreamWrapper(Function<InputStream, InputStream> wrapper)263 public TerminalBuilder inputStreamWrapper(Function<InputStream, InputStream> wrapper) { 264 this.inputStreamWrapper = wrapper; 265 return this; 266 } 267 build()268 public Terminal build() throws IOException { 269 Terminal terminal = doBuild(); 270 Log.debug(() -> "Using terminal " + terminal.getClass().getSimpleName()); 271 if (terminal instanceof AbstractPosixTerminal) { 272 Log.debug(() -> "Using pty " + ((AbstractPosixTerminal) terminal).getPty().getClass().getSimpleName()); 273 } 274 return terminal; 275 } 276 doBuild()277 private Terminal doBuild() throws IOException { 278 String name = this.name; 279 if (name == null) { 280 name = "JLine terminal"; 281 } 282 Charset encoding = this.encoding; 283 if (encoding == null) { 284 String charsetName = System.getProperty(PROP_ENCODING); 285 if (charsetName != null && Charset.isSupported(charsetName)) { 286 encoding = Charset.forName(charsetName); 287 } 288 } 289 int codepage = this.codepage; 290 if (codepage <= 0) { 291 String str = System.getProperty(PROP_CODEPAGE); 292 if (str != null) { 293 codepage = Integer.parseInt(str); 294 } 295 } 296 String type = this.type; 297 if (type == null) { 298 type = System.getProperty(PROP_TYPE); 299 } 300 if (type == null) { 301 type = System.getenv("TERM"); 302 } 303 Boolean jna = this.jna; 304 if (jna == null) { 305 jna = getBoolean(PROP_JNA, true); 306 } 307 Boolean jansi = this.jansi; 308 if (jansi == null) { 309 jansi = getBoolean(PROP_JANSI, true); 310 } 311 Boolean exec = this.exec; 312 if (exec == null) { 313 exec = getBoolean(PROP_EXEC, true); 314 } 315 Boolean dumb = this.dumb; 316 if (dumb == null) { 317 dumb = getBoolean(PROP_DUMB, null); 318 } 319 if ((system != null && system) || (system == null && in == null && out == null)) { 320 if (attributes != null || size != null) { 321 Log.warn("Attributes and size fields are ignored when creating a system terminal"); 322 } 323 IllegalStateException exception = new IllegalStateException("Unable to create a system terminal"); 324 if (OSUtils.IS_WINDOWS) { 325 boolean cygwinTerm = "cygwin".equals(System.getenv("TERM")); 326 boolean ansiPassThrough = OSUtils.IS_CONEMU; 327 // 328 // Cygwin support 329 // 330 if ((OSUtils.IS_CYGWIN || OSUtils.IS_MSYSTEM) && exec && !cygwinTerm) { 331 try { 332 Pty pty = ExecPty.current(); 333 // Cygwin defaults to XTERM, but actually supports 256 colors, 334 // so if the value comes from the environment, change it to xterm-256color 335 if ("xterm".equals(type) && this.type == null && System.getProperty(PROP_TYPE) == null) { 336 type = "xterm-256color"; 337 } 338 return new PosixSysTerminal(name, type, pty, inputStreamWrapper.apply(pty.getSlaveInput()), pty.getSlaveOutput(), encoding, nativeSignals, signalHandler); 339 } catch (IOException e) { 340 // Ignore if not a tty 341 Log.debug("Error creating EXEC based terminal: ", e.getMessage(), e); 342 exception.addSuppressed(e); 343 } 344 } 345 if (jna) { 346 try { 347 return load(JnaSupport.class).winSysTerminal(name, type, ansiPassThrough, encoding, codepage, nativeSignals, signalHandler, paused, inputStreamWrapper); 348 } catch (Throwable t) { 349 Log.debug("Error creating JNA based terminal: ", t.getMessage(), t); 350 exception.addSuppressed(t); 351 } 352 } 353 if (jansi) { 354 try { 355 return load(JansiSupport.class).winSysTerminal(name, type, ansiPassThrough, encoding, codepage, nativeSignals, signalHandler, paused); 356 } catch (Throwable t) { 357 Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t); 358 exception.addSuppressed(t); 359 } 360 } 361 } else { 362 if (jna) { 363 try { 364 Pty pty = load(JnaSupport.class).current(); 365 return new PosixSysTerminal(name, type, pty, inputStreamWrapper.apply(pty.getSlaveInput()), pty.getSlaveOutput(), encoding, nativeSignals, signalHandler); 366 } catch (Throwable t) { 367 // ignore 368 Log.debug("Error creating JNA based terminal: ", t.getMessage(), t); 369 exception.addSuppressed(t); 370 } 371 } 372 if (jansi) { 373 try { 374 Pty pty = load(JansiSupport.class).current(); 375 return new PosixSysTerminal(name, type, pty, inputStreamWrapper.apply(pty.getSlaveInput()), pty.getSlaveOutput(), encoding, nativeSignals, signalHandler); 376 } catch (Throwable t) { 377 Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t); 378 exception.addSuppressed(t); 379 } 380 } 381 if (exec) { 382 try { 383 Pty pty = ExecPty.current(); 384 return new PosixSysTerminal(name, type, pty, inputStreamWrapper.apply(pty.getSlaveInput()), pty.getSlaveOutput(), encoding, nativeSignals, signalHandler); 385 } catch (Throwable t) { 386 // Ignore if not a tty 387 Log.debug("Error creating EXEC based terminal: ", t.getMessage(), t); 388 exception.addSuppressed(t); 389 } 390 } 391 } 392 if (dumb == null || dumb) { 393 // forced colored dumb terminal 394 boolean color = getBoolean(PROP_DUMB_COLOR, false); 395 // detect emacs using the env variable 396 if (!color) { 397 color = System.getenv("INSIDE_EMACS") != null; 398 } 399 // detect Intellij Idea 400 if (!color) { 401 String command = getParentProcessCommand(); 402 color = command != null && command.contains("idea"); 403 } 404 if (!color && dumb == null) { 405 if (Log.isDebugEnabled()) { 406 Log.warn("Creating a dumb terminal", exception); 407 } else { 408 Log.warn("Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)"); 409 } 410 } 411 return new DumbTerminal(name, color ? Terminal.TYPE_DUMB_COLOR : Terminal.TYPE_DUMB, 412 new FileInputStream(FileDescriptor.in), 413 new FileOutputStream(FileDescriptor.out), 414 encoding, signalHandler); 415 } else { 416 throw exception; 417 } 418 } else { 419 if (jna) { 420 try { 421 Pty pty = load(JnaSupport.class).open(attributes, size); 422 return new PosixPtyTerminal(name, type, pty, in, out, encoding, signalHandler, paused); 423 } catch (Throwable t) { 424 Log.debug("Error creating JNA based terminal: ", t.getMessage(), t); 425 } 426 } 427 if (jansi) { 428 try { 429 Pty pty = load(JansiSupport.class).open(attributes, size); 430 return new PosixPtyTerminal(name, type, pty, in, out, encoding, signalHandler, paused); 431 } catch (Throwable t) { 432 Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t); 433 } 434 } 435 Terminal terminal = new ExternalTerminal(name, type, in, out, encoding, signalHandler, paused); 436 if (attributes != null) { 437 terminal.setAttributes(attributes); 438 } 439 if (size != null) { 440 terminal.setSize(size); 441 } 442 return terminal; 443 } 444 } 445 getParentProcessCommand()446 private static String getParentProcessCommand() { 447 try { 448 Class<?> phClass = Class.forName("java.lang.ProcessHandle"); 449 Object current = phClass.getMethod("current").invoke(null); 450 Object parent = ((Optional<?>) phClass.getMethod("parent").invoke(current)).orElse(null); 451 Method infoMethod = phClass.getMethod("info"); 452 Object info = infoMethod.invoke(parent); 453 Object command = ((Optional<?>) infoMethod.getReturnType().getMethod("command").invoke(info)).orElse(null); 454 return (String) command; 455 } catch (Throwable t) { 456 return null; 457 } 458 } 459 getBoolean(String name, Boolean def)460 private static Boolean getBoolean(String name, Boolean def) { 461 try { 462 String str = System.getProperty(name); 463 if (str != null) { 464 return Boolean.parseBoolean(str); 465 } 466 } catch (IllegalArgumentException | NullPointerException e) { 467 } 468 return def; 469 } 470 load(Class<S> clazz)471 private <S> S load(Class<S> clazz) { 472 return ServiceLoader.load(clazz, clazz.getClassLoader()).iterator().next(); 473 } 474 } 475