1 /* 2 * Copyright (c) 2002-2019, 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 * https://opensource.org/licenses/BSD-3-Clause 8 */ 9 package jdk.internal.org.jline.terminal.impl; 10 11 import jdk.internal.org.jline.terminal.Attributes; 12 import jdk.internal.org.jline.terminal.Size; 13 import jdk.internal.org.jline.utils.Curses; 14 import jdk.internal.org.jline.utils.InfoCmp; 15 import jdk.internal.org.jline.utils.Log; 16 import jdk.internal.org.jline.utils.NonBlocking; 17 import jdk.internal.org.jline.utils.NonBlockingInputStream; 18 import jdk.internal.org.jline.utils.NonBlockingPumpReader; 19 import jdk.internal.org.jline.utils.NonBlockingReader; 20 import jdk.internal.org.jline.utils.ShutdownHooks; 21 import jdk.internal.org.jline.utils.Signals; 22 import jdk.internal.org.jline.utils.WriterOutputStream; 23 24 import java.io.IOException; 25 import java.io.InputStream; 26 import java.io.OutputStream; 27 import java.io.PrintWriter; 28 import java.io.Writer; 29 import java.nio.charset.Charset; 30 import java.nio.charset.StandardCharsets; 31 import java.util.HashMap; 32 import java.util.Map; 33 import java.util.function.Function; 34 35 /** 36 * The AbstractWindowsTerminal is used as the base class for windows terminal. 37 * Due to windows limitations, mostly the missing support for ansi sequences, 38 * the only way to create a correct terminal is to use the windows api to set 39 * character attributes, move the cursor, erasing, etc... 40 * 41 * UTF-8 support is also lacking in windows and the code page supposed to 42 * emulate UTF-8 is a bit broken. In order to work around this broken 43 * code page, windows api WriteConsoleW is used directly. This means that 44 * the writer() becomes the primary output, while the output() is bridged 45 * to the writer() using a WriterOutputStream wrapper. 46 */ 47 public abstract class AbstractWindowsTerminal extends AbstractTerminal { 48 49 public static final String TYPE_WINDOWS = "windows"; 50 public static final String TYPE_WINDOWS_256_COLOR = "windows-256color"; 51 public static final String TYPE_WINDOWS_CONEMU = "windows-conemu"; 52 public static final String TYPE_WINDOWS_VTP = "windows-vtp"; 53 54 public static final int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; 55 56 private static final int UTF8_CODE_PAGE = 65001; 57 58 protected static final int ENABLE_PROCESSED_INPUT = 0x0001; 59 protected static final int ENABLE_LINE_INPUT = 0x0002; 60 protected static final int ENABLE_ECHO_INPUT = 0x0004; 61 protected static final int ENABLE_WINDOW_INPUT = 0x0008; 62 protected static final int ENABLE_MOUSE_INPUT = 0x0010; 63 protected static final int ENABLE_INSERT_MODE = 0x0020; 64 protected static final int ENABLE_QUICK_EDIT_MODE = 0x0040; 65 66 protected final Writer slaveInputPipe; 67 protected final InputStream input; 68 protected final OutputStream output; 69 protected final NonBlockingReader reader; 70 protected final PrintWriter writer; 71 protected final Map<Signal, Object> nativeHandlers = new HashMap<>(); 72 protected final ShutdownHooks.Task closer; 73 protected final Attributes attributes = new Attributes(); 74 protected final int originalConsoleMode; 75 76 protected final Object lock = new Object(); 77 protected boolean paused = true; 78 protected Thread pump; 79 80 protected MouseTracking tracking = MouseTracking.Off; 81 protected boolean focusTracking = false; 82 private volatile boolean closing; 83 AbstractWindowsTerminal(Writer writer, String name, String type, Charset encoding, int codepage, boolean nativeSignals, SignalHandler signalHandler, Function<InputStream, InputStream> inputStreamWrapper)84 public AbstractWindowsTerminal(Writer writer, String name, String type, Charset encoding, int codepage, boolean nativeSignals, SignalHandler signalHandler, Function<InputStream, InputStream> inputStreamWrapper) throws IOException { 85 super(name, type, selectCharset(encoding, codepage), signalHandler); 86 NonBlockingPumpReader reader = NonBlocking.nonBlockingPumpReader(); 87 this.slaveInputPipe = reader.getWriter(); 88 this.input = inputStreamWrapper.apply(NonBlocking.nonBlockingStream(reader, encoding())); 89 this.reader = NonBlocking.nonBlocking(name, input, encoding()); 90 this.writer = new PrintWriter(writer); 91 this.output = new WriterOutputStream(writer, encoding()); 92 parseInfoCmp(); 93 // Attributes 94 originalConsoleMode = getConsoleMode(); 95 attributes.setLocalFlag(Attributes.LocalFlag.ISIG, true); 96 attributes.setControlChar(Attributes.ControlChar.VINTR, ctrl('C')); 97 attributes.setControlChar(Attributes.ControlChar.VEOF, ctrl('D')); 98 attributes.setControlChar(Attributes.ControlChar.VSUSP, ctrl('Z')); 99 // Handle signals 100 if (nativeSignals) { 101 for (final Signal signal : Signal.values()) { 102 if (signalHandler == SignalHandler.SIG_DFL) { 103 nativeHandlers.put(signal, Signals.registerDefault(signal.name())); 104 } else { 105 nativeHandlers.put(signal, Signals.register(signal.name(), () -> raise(signal))); 106 } 107 } 108 } 109 closer = this::close; 110 ShutdownHooks.add(closer); 111 // ConEMU extended fonts support 112 if (TYPE_WINDOWS_CONEMU.equals(getType()) 113 && !Boolean.getBoolean("org.jline.terminal.conemu.disable-activate")) { 114 writer.write("\u001b[9999E"); 115 writer.flush(); 116 } 117 } 118 selectCharset(Charset encoding, int codepage)119 private static Charset selectCharset(Charset encoding, int codepage) { 120 if (encoding != null) { 121 return encoding; 122 } 123 124 if (codepage >= 0) { 125 return getCodepageCharset(codepage); 126 } 127 128 // Use UTF-8 as default 129 return StandardCharsets.UTF_8; 130 } 131 getCodepageCharset(int codepage)132 private static Charset getCodepageCharset(int codepage) { 133 //http://docs.oracle.com/javase/6/docs/technotes/guides/intl/encoding.doc.html 134 if (codepage == UTF8_CODE_PAGE) { 135 return StandardCharsets.UTF_8; 136 } 137 String charsetMS = "ms" + codepage; 138 if (Charset.isSupported(charsetMS)) { 139 return Charset.forName(charsetMS); 140 } 141 String charsetCP = "cp" + codepage; 142 if (Charset.isSupported(charsetCP)) { 143 return Charset.forName(charsetCP); 144 } 145 return Charset.defaultCharset(); 146 } 147 148 @Override handle(Signal signal, SignalHandler handler)149 public SignalHandler handle(Signal signal, SignalHandler handler) { 150 SignalHandler prev = super.handle(signal, handler); 151 if (prev != handler) { 152 if (handler == SignalHandler.SIG_DFL) { 153 Signals.registerDefault(signal.name()); 154 } else { 155 Signals.register(signal.name(), () -> raise(signal)); 156 } 157 } 158 return prev; 159 } 160 reader()161 public NonBlockingReader reader() { 162 return reader; 163 } 164 writer()165 public PrintWriter writer() { 166 return writer; 167 } 168 169 @Override input()170 public InputStream input() { 171 return input; 172 } 173 174 @Override output()175 public OutputStream output() { 176 return output; 177 } 178 getAttributes()179 public Attributes getAttributes() { 180 int mode = getConsoleMode(); 181 if ((mode & ENABLE_ECHO_INPUT) != 0) { 182 attributes.setLocalFlag(Attributes.LocalFlag.ECHO, true); 183 } 184 if ((mode & ENABLE_LINE_INPUT) != 0) { 185 attributes.setLocalFlag(Attributes.LocalFlag.ICANON, true); 186 } 187 return new Attributes(attributes); 188 } 189 setAttributes(Attributes attr)190 public void setAttributes(Attributes attr) { 191 attributes.copy(attr); 192 updateConsoleMode(); 193 } 194 updateConsoleMode()195 protected void updateConsoleMode() { 196 int mode = ENABLE_WINDOW_INPUT; 197 if (attributes.getLocalFlag(Attributes.LocalFlag.ECHO)) { 198 mode |= ENABLE_ECHO_INPUT; 199 } 200 if (attributes.getLocalFlag(Attributes.LocalFlag.ICANON)) { 201 mode |= ENABLE_LINE_INPUT; 202 } 203 if (tracking != MouseTracking.Off) { 204 mode |= ENABLE_MOUSE_INPUT; 205 } 206 setConsoleMode(mode); 207 } 208 ctrl(char key)209 protected int ctrl(char key) { 210 return (Character.toUpperCase(key) & 0x1f); 211 } 212 setSize(Size size)213 public void setSize(Size size) { 214 throw new UnsupportedOperationException("Can not resize windows terminal"); 215 } 216 doClose()217 protected void doClose() throws IOException { 218 super.doClose(); 219 closing = true; 220 if (pump != null) { 221 pump.interrupt(); 222 } 223 ShutdownHooks.remove(closer); 224 for (Map.Entry<Signal, Object> entry : nativeHandlers.entrySet()) { 225 Signals.unregister(entry.getKey().name(), entry.getValue()); 226 } 227 reader.close(); 228 writer.close(); 229 setConsoleMode(originalConsoleMode); 230 } 231 232 static final int SHIFT_FLAG = 0x01; 233 static final int ALT_FLAG = 0x02; 234 static final int CTRL_FLAG = 0x04; 235 236 static final int RIGHT_ALT_PRESSED = 0x0001; 237 static final int LEFT_ALT_PRESSED = 0x0002; 238 static final int RIGHT_CTRL_PRESSED = 0x0004; 239 static final int LEFT_CTRL_PRESSED = 0x0008; 240 static final int SHIFT_PRESSED = 0x0010; 241 static final int NUMLOCK_ON = 0x0020; 242 static final int SCROLLLOCK_ON = 0x0040; 243 static final int CAPSLOCK_ON = 0x0080; 244 processKeyEvent(final boolean isKeyDown, final short virtualKeyCode, char ch, final int controlKeyState)245 protected void processKeyEvent(final boolean isKeyDown, final short virtualKeyCode, char ch, final int controlKeyState) throws IOException { 246 final boolean isCtrl = (controlKeyState & (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) > 0; 247 final boolean isAlt = (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED)) > 0; 248 final boolean isShift = (controlKeyState & SHIFT_PRESSED) > 0; 249 // key down event 250 if (isKeyDown && ch != '\3') { 251 // Pressing "Alt Gr" is translated to Alt-Ctrl, hence it has to be checked that Ctrl is _not_ pressed, 252 // otherwise inserting of "Alt Gr" codes on non-US keyboards would yield errors 253 if (ch != 0 254 && (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED)) 255 == (RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED)) { 256 processInputChar(ch); 257 } else { 258 final String keySeq = getEscapeSequence(virtualKeyCode, (isCtrl ? CTRL_FLAG : 0) + (isAlt ? ALT_FLAG : 0) + (isShift ? SHIFT_FLAG : 0)); 259 if (keySeq != null) { 260 for (char c : keySeq.toCharArray()) { 261 processInputChar(c); 262 } 263 return; 264 } 265 /* uchar value in Windows when CTRL is pressed: 266 * 1). Ctrl + <0x41 to 0x5e> : uchar=<keyCode> - 'A' + 1 267 * 2). Ctrl + Backspace(0x08) : uchar=0x7f 268 * 3). Ctrl + Enter(0x0d) : uchar=0x0a 269 * 4). Ctrl + Space(0x20) : uchar=0x20 270 * 5). Ctrl + <Other key> : uchar=0 271 * 6). Ctrl + Alt + <Any key> : uchar=0 272 */ 273 if (ch > 0) { 274 if (isAlt) { 275 processInputChar('\033'); 276 } 277 if (isCtrl && ch != ' ' && ch != '\n' && ch != 0x7f) { 278 processInputChar((char) (ch == '?' ? 0x7f : Character.toUpperCase(ch) & 0x1f)); 279 } else if (isCtrl && ch == '\n') { 280 //simulate Alt-Enter: 281 processInputChar('\033'); 282 processInputChar('\r'); 283 } else { 284 processInputChar(ch); 285 } 286 } else if (isCtrl) { //Handles the ctrl key events(uchar=0) 287 if (virtualKeyCode >= 'A' && virtualKeyCode <= 'Z') { 288 ch = (char) (virtualKeyCode - 0x40); 289 } else if (virtualKeyCode == 191) { //? 290 ch = 127; 291 } 292 if (ch > 0) { 293 if (isAlt) { 294 processInputChar('\033'); 295 } 296 processInputChar(ch); 297 } 298 } 299 } 300 } else if (isKeyDown && ch == '\3') { 301 processInputChar('\3'); 302 } 303 // key up event 304 else { 305 // support ALT+NumPad input method 306 if (virtualKeyCode == 0x12 /*VK_MENU ALT key*/ && ch > 0) { 307 processInputChar(ch); // no such combination in Windows 308 } 309 } 310 } 311 getEscapeSequence(short keyCode, int keyState)312 protected String getEscapeSequence(short keyCode, int keyState) { 313 // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx 314 // TODO: numpad keys, modifiers 315 String escapeSequence = null; 316 switch (keyCode) { 317 case 0x08: // VK_BACK BackSpace 318 escapeSequence = (keyState & ALT_FLAG) > 0 ? "\\E^H" : getRawSequence(InfoCmp.Capability.key_backspace); 319 break; 320 case 0x09: 321 escapeSequence = (keyState & SHIFT_FLAG) > 0 ? getRawSequence(InfoCmp.Capability.key_btab) : null; 322 break; 323 case 0x21: // VK_PRIOR PageUp 324 escapeSequence = getRawSequence(InfoCmp.Capability.key_ppage); 325 break; 326 case 0x22: // VK_NEXT PageDown 327 escapeSequence = getRawSequence(InfoCmp.Capability.key_npage); 328 break; 329 case 0x23: // VK_END 330 escapeSequence = keyState > 0 ? "\\E[1;%p1%dF" : getRawSequence(InfoCmp.Capability.key_end); 331 break; 332 case 0x24: // VK_HOME 333 escapeSequence = keyState > 0 ? "\\E[1;%p1%dH" : getRawSequence(InfoCmp.Capability.key_home); 334 break; 335 case 0x25: // VK_LEFT 336 escapeSequence = keyState > 0 ? "\\E[1;%p1%dD" : getRawSequence(InfoCmp.Capability.key_left); 337 break; 338 case 0x26: // VK_UP 339 escapeSequence = keyState > 0 ? "\\E[1;%p1%dA" : getRawSequence(InfoCmp.Capability.key_up); 340 break; 341 case 0x27: // VK_RIGHT 342 escapeSequence = keyState > 0 ? "\\E[1;%p1%dC" : getRawSequence(InfoCmp.Capability.key_right); 343 break; 344 case 0x28: // VK_DOWN 345 escapeSequence = keyState > 0 ? "\\E[1;%p1%dB" : getRawSequence(InfoCmp.Capability.key_down); 346 break; 347 case 0x2D: // VK_INSERT 348 escapeSequence = getRawSequence(InfoCmp.Capability.key_ic); 349 break; 350 case 0x2E: // VK_DELETE 351 escapeSequence = getRawSequence(InfoCmp.Capability.key_dc); 352 break; 353 case 0x70: // VK_F1 354 escapeSequence = keyState > 0 ? "\\E[1;%p1%dP" : getRawSequence(InfoCmp.Capability.key_f1); 355 break; 356 case 0x71: // VK_F2 357 escapeSequence = keyState > 0 ? "\\E[1;%p1%dQ" : getRawSequence(InfoCmp.Capability.key_f2); 358 break; 359 case 0x72: // VK_F3 360 escapeSequence = keyState > 0 ? "\\E[1;%p1%dR" : getRawSequence(InfoCmp.Capability.key_f3); 361 break; 362 case 0x73: // VK_F4 363 escapeSequence = keyState > 0 ? "\\E[1;%p1%dS" : getRawSequence(InfoCmp.Capability.key_f4); 364 break; 365 case 0x74: // VK_F5 366 escapeSequence = keyState > 0 ? "\\E[15;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f5); 367 break; 368 case 0x75: // VK_F6 369 escapeSequence = keyState > 0 ? "\\E[17;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f6); 370 break; 371 case 0x76: // VK_F7 372 escapeSequence = keyState > 0 ? "\\E[18;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f7); 373 break; 374 case 0x77: // VK_F8 375 escapeSequence = keyState > 0 ? "\\E[19;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f8); 376 break; 377 case 0x78: // VK_F9 378 escapeSequence = keyState > 0 ? "\\E[20;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f9); 379 break; 380 case 0x79: // VK_F10 381 escapeSequence = keyState > 0 ? "\\E[21;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f10); 382 break; 383 case 0x7A: // VK_F11 384 escapeSequence = keyState > 0 ? "\\E[23;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f11); 385 break; 386 case 0x7B: // VK_F12 387 escapeSequence = keyState > 0 ? "\\E[24;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f12); 388 break; 389 case 0x5D: // VK_CLOSE_BRACKET(Menu key) 390 case 0x5B: // VK_OPEN_BRACKET(Window key) 391 default: 392 return null; 393 } 394 return Curses.tputs(escapeSequence, keyState + 1); 395 } 396 getRawSequence(InfoCmp.Capability cap)397 protected String getRawSequence(InfoCmp.Capability cap) { 398 return strings.get(cap); 399 } 400 401 @Override hasFocusSupport()402 public boolean hasFocusSupport() { 403 return true; 404 } 405 406 @Override trackFocus(boolean tracking)407 public boolean trackFocus(boolean tracking) { 408 focusTracking = tracking; 409 return true; 410 } 411 412 @Override canPauseResume()413 public boolean canPauseResume() { 414 return true; 415 } 416 417 @Override pause()418 public void pause() { 419 synchronized (lock) { 420 paused = true; 421 } 422 } 423 424 @Override pause(boolean wait)425 public void pause(boolean wait) throws InterruptedException { 426 Thread p; 427 synchronized (lock) { 428 paused = true; 429 p = pump; 430 } 431 if (p != null) { 432 p.interrupt(); 433 p.join(); 434 } 435 } 436 437 @Override resume()438 public void resume() { 439 synchronized (lock) { 440 paused = false; 441 if (pump == null) { 442 pump = new Thread(this::pump, "WindowsStreamPump"); 443 pump.setDaemon(true); 444 pump.start(); 445 } 446 } 447 } 448 449 @Override paused()450 public boolean paused() { 451 synchronized (lock) { 452 return paused; 453 } 454 } 455 pump()456 protected void pump() { 457 try { 458 while (!closing) { 459 synchronized (lock) { 460 if (paused) { 461 pump = null; 462 break; 463 } 464 } 465 if (processConsoleInput()) { 466 slaveInputPipe.flush(); 467 } 468 } 469 } catch (IOException e) { 470 if (!closing) { 471 Log.warn("Error in WindowsStreamPump", e); 472 try { 473 close(); 474 } catch (IOException e1) { 475 Log.warn("Error closing terminal", e); 476 } 477 } 478 } finally { 479 synchronized (lock) { 480 pump = null; 481 } 482 } 483 } 484 processInputChar(char c)485 public void processInputChar(char c) throws IOException { 486 if (attributes.getLocalFlag(Attributes.LocalFlag.ISIG)) { 487 if (c == attributes.getControlChar(Attributes.ControlChar.VINTR)) { 488 raise(Signal.INT); 489 return; 490 } else if (c == attributes.getControlChar(Attributes.ControlChar.VQUIT)) { 491 raise(Signal.QUIT); 492 return; 493 } else if (c == attributes.getControlChar(Attributes.ControlChar.VSUSP)) { 494 raise(Signal.TSTP); 495 return; 496 } else if (c == attributes.getControlChar(Attributes.ControlChar.VSTATUS)) { 497 raise(Signal.INFO); 498 } 499 } 500 if (c == '\r') { 501 if (attributes.getInputFlag(Attributes.InputFlag.IGNCR)) { 502 return; 503 } 504 if (attributes.getInputFlag(Attributes.InputFlag.ICRNL)) { 505 c = '\n'; 506 } 507 } else if (c == '\n' && attributes.getInputFlag(Attributes.InputFlag.INLCR)) { 508 c = '\r'; 509 } 510 // if (attributes.getLocalFlag(Attributes.LocalFlag.ECHO)) { 511 // processOutputByte(c); 512 // masterOutput.flush(); 513 // } 514 slaveInputPipe.write(c); 515 } 516 517 @Override trackMouse(MouseTracking tracking)518 public boolean trackMouse(MouseTracking tracking) { 519 this.tracking = tracking; 520 updateConsoleMode(); 521 return true; 522 } 523 getConsoleOutputCP()524 protected abstract int getConsoleOutputCP(); 525 getConsoleMode()526 protected abstract int getConsoleMode(); 527 setConsoleMode(int mode)528 protected abstract void setConsoleMode(int mode); 529 530 /** 531 * Read a single input event from the input buffer and process it. 532 * 533 * @return true if new input was generated from the event 534 * @throws IOException if anything wrong happens 535 */ processConsoleInput()536 protected abstract boolean processConsoleInput() throws IOException; 537 538 } 539 540