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 close()217 public void close() throws IOException { 218 super.close(); 219 closing = true; 220 pump.interrupt(); 221 ShutdownHooks.remove(closer); 222 for (Map.Entry<Signal, Object> entry : nativeHandlers.entrySet()) { 223 Signals.unregister(entry.getKey().name(), entry.getValue()); 224 } 225 reader.close(); 226 writer.close(); 227 setConsoleMode(originalConsoleMode); 228 } 229 230 static final int SHIFT_FLAG = 0x01; 231 static final int ALT_FLAG = 0x02; 232 static final int CTRL_FLAG = 0x04; 233 234 static final int RIGHT_ALT_PRESSED = 0x0001; 235 static final int LEFT_ALT_PRESSED = 0x0002; 236 static final int RIGHT_CTRL_PRESSED = 0x0004; 237 static final int LEFT_CTRL_PRESSED = 0x0008; 238 static final int SHIFT_PRESSED = 0x0010; 239 static final int NUMLOCK_ON = 0x0020; 240 static final int SCROLLLOCK_ON = 0x0040; 241 static final int CAPSLOCK_ON = 0x0080; 242 processKeyEvent(final boolean isKeyDown, final short virtualKeyCode, char ch, final int controlKeyState)243 protected void processKeyEvent(final boolean isKeyDown, final short virtualKeyCode, char ch, final int controlKeyState) throws IOException { 244 final boolean isCtrl = (controlKeyState & (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) > 0; 245 final boolean isAlt = (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED)) > 0; 246 final boolean isShift = (controlKeyState & SHIFT_PRESSED) > 0; 247 // key down event 248 if (isKeyDown && ch != '\3') { 249 // Pressing "Alt Gr" is translated to Alt-Ctrl, hence it has to be checked that Ctrl is _not_ pressed, 250 // otherwise inserting of "Alt Gr" codes on non-US keyboards would yield errors 251 if (ch != 0 252 && (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED)) 253 == (RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED)) { 254 processInputChar(ch); 255 } else { 256 final String keySeq = getEscapeSequence(virtualKeyCode, (isCtrl ? CTRL_FLAG : 0) + (isAlt ? ALT_FLAG : 0) + (isShift ? SHIFT_FLAG : 0)); 257 if (keySeq != null) { 258 for (char c : keySeq.toCharArray()) { 259 processInputChar(c); 260 } 261 return; 262 } 263 /* uchar value in Windows when CTRL is pressed: 264 * 1). Ctrl + <0x41 to 0x5e> : uchar=<keyCode> - 'A' + 1 265 * 2). Ctrl + Backspace(0x08) : uchar=0x7f 266 * 3). Ctrl + Enter(0x0d) : uchar=0x0a 267 * 4). Ctrl + Space(0x20) : uchar=0x20 268 * 5). Ctrl + <Other key> : uchar=0 269 * 6). Ctrl + Alt + <Any key> : uchar=0 270 */ 271 if (ch > 0) { 272 if (isAlt) { 273 processInputChar('\033'); 274 } 275 if (isCtrl && ch != ' ' && ch != '\n' && ch != 0x7f) { 276 processInputChar((char) (ch == '?' ? 0x7f : Character.toUpperCase(ch) & 0x1f)); 277 } else if (isCtrl && ch == '\n') { 278 //simulate Alt-Enter: 279 processInputChar('\033'); 280 processInputChar('\r'); 281 } else { 282 processInputChar(ch); 283 } 284 } else if (isCtrl) { //Handles the ctrl key events(uchar=0) 285 if (virtualKeyCode >= 'A' && virtualKeyCode <= 'Z') { 286 ch = (char) (virtualKeyCode - 0x40); 287 } else if (virtualKeyCode == 191) { //? 288 ch = 127; 289 } 290 if (ch > 0) { 291 if (isAlt) { 292 processInputChar('\033'); 293 } 294 processInputChar(ch); 295 } 296 } 297 } 298 } else if (isKeyDown && ch == '\3') { 299 processInputChar('\3'); 300 } 301 // key up event 302 else { 303 // support ALT+NumPad input method 304 if (virtualKeyCode == 0x12 /*VK_MENU ALT key*/ && ch > 0) { 305 processInputChar(ch); // no such combination in Windows 306 } 307 } 308 } 309 getEscapeSequence(short keyCode, int keyState)310 protected String getEscapeSequence(short keyCode, int keyState) { 311 // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx 312 // TODO: numpad keys, modifiers 313 String escapeSequence = null; 314 switch (keyCode) { 315 case 0x08: // VK_BACK BackSpace 316 escapeSequence = (keyState & ALT_FLAG) > 0 ? "\\E^H" : getRawSequence(InfoCmp.Capability.key_backspace); 317 break; 318 case 0x09: 319 escapeSequence = (keyState & SHIFT_FLAG) > 0 ? getRawSequence(InfoCmp.Capability.key_btab) : null; 320 break; 321 case 0x21: // VK_PRIOR PageUp 322 escapeSequence = getRawSequence(InfoCmp.Capability.key_ppage); 323 break; 324 case 0x22: // VK_NEXT PageDown 325 escapeSequence = getRawSequence(InfoCmp.Capability.key_npage); 326 break; 327 case 0x23: // VK_END 328 escapeSequence = keyState > 0 ? "\\E[1;%p1%dF" : getRawSequence(InfoCmp.Capability.key_end); 329 break; 330 case 0x24: // VK_HOME 331 escapeSequence = keyState > 0 ? "\\E[1;%p1%dH" : getRawSequence(InfoCmp.Capability.key_home); 332 break; 333 case 0x25: // VK_LEFT 334 escapeSequence = keyState > 0 ? "\\E[1;%p1%dD" : getRawSequence(InfoCmp.Capability.key_left); 335 break; 336 case 0x26: // VK_UP 337 escapeSequence = keyState > 0 ? "\\E[1;%p1%dA" : getRawSequence(InfoCmp.Capability.key_up); 338 break; 339 case 0x27: // VK_RIGHT 340 escapeSequence = keyState > 0 ? "\\E[1;%p1%dC" : getRawSequence(InfoCmp.Capability.key_right); 341 break; 342 case 0x28: // VK_DOWN 343 escapeSequence = keyState > 0 ? "\\E[1;%p1%dB" : getRawSequence(InfoCmp.Capability.key_down); 344 break; 345 case 0x2D: // VK_INSERT 346 escapeSequence = getRawSequence(InfoCmp.Capability.key_ic); 347 break; 348 case 0x2E: // VK_DELETE 349 escapeSequence = getRawSequence(InfoCmp.Capability.key_dc); 350 break; 351 case 0x70: // VK_F1 352 escapeSequence = keyState > 0 ? "\\E[1;%p1%dP" : getRawSequence(InfoCmp.Capability.key_f1); 353 break; 354 case 0x71: // VK_F2 355 escapeSequence = keyState > 0 ? "\\E[1;%p1%dQ" : getRawSequence(InfoCmp.Capability.key_f2); 356 break; 357 case 0x72: // VK_F3 358 escapeSequence = keyState > 0 ? "\\E[1;%p1%dR" : getRawSequence(InfoCmp.Capability.key_f3); 359 break; 360 case 0x73: // VK_F4 361 escapeSequence = keyState > 0 ? "\\E[1;%p1%dS" : getRawSequence(InfoCmp.Capability.key_f4); 362 break; 363 case 0x74: // VK_F5 364 escapeSequence = keyState > 0 ? "\\E[15;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f5); 365 break; 366 case 0x75: // VK_F6 367 escapeSequence = keyState > 0 ? "\\E[17;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f6); 368 break; 369 case 0x76: // VK_F7 370 escapeSequence = keyState > 0 ? "\\E[18;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f7); 371 break; 372 case 0x77: // VK_F8 373 escapeSequence = keyState > 0 ? "\\E[19;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f8); 374 break; 375 case 0x78: // VK_F9 376 escapeSequence = keyState > 0 ? "\\E[20;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f9); 377 break; 378 case 0x79: // VK_F10 379 escapeSequence = keyState > 0 ? "\\E[21;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f10); 380 break; 381 case 0x7A: // VK_F11 382 escapeSequence = keyState > 0 ? "\\E[23;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f11); 383 break; 384 case 0x7B: // VK_F12 385 escapeSequence = keyState > 0 ? "\\E[24;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f12); 386 break; 387 case 0x5D: // VK_CLOSE_BRACKET(Menu key) 388 case 0x5B: // VK_OPEN_BRACKET(Window key) 389 default: 390 return null; 391 } 392 return Curses.tputs(escapeSequence, keyState + 1); 393 } 394 getRawSequence(InfoCmp.Capability cap)395 protected String getRawSequence(InfoCmp.Capability cap) { 396 return strings.get(cap); 397 } 398 399 @Override hasFocusSupport()400 public boolean hasFocusSupport() { 401 return true; 402 } 403 404 @Override trackFocus(boolean tracking)405 public boolean trackFocus(boolean tracking) { 406 focusTracking = tracking; 407 return true; 408 } 409 410 @Override canPauseResume()411 public boolean canPauseResume() { 412 return true; 413 } 414 415 @Override pause()416 public void pause() { 417 synchronized (lock) { 418 paused = true; 419 } 420 } 421 422 @Override pause(boolean wait)423 public void pause(boolean wait) throws InterruptedException { 424 Thread p; 425 synchronized (lock) { 426 paused = true; 427 p = pump; 428 } 429 if (p != null) { 430 p.interrupt(); 431 p.join(); 432 } 433 } 434 435 @Override resume()436 public void resume() { 437 synchronized (lock) { 438 paused = false; 439 if (pump == null) { 440 pump = new Thread(this::pump, "WindowsStreamPump"); 441 pump.setDaemon(true); 442 pump.start(); 443 } 444 } 445 } 446 447 @Override paused()448 public boolean paused() { 449 synchronized (lock) { 450 return paused; 451 } 452 } 453 pump()454 protected void pump() { 455 try { 456 while (!closing) { 457 synchronized (lock) { 458 if (paused) { 459 pump = null; 460 break; 461 } 462 } 463 if (processConsoleInput()) { 464 slaveInputPipe.flush(); 465 } 466 } 467 } catch (IOException e) { 468 if (!closing) { 469 Log.warn("Error in WindowsStreamPump", e); 470 try { 471 close(); 472 } catch (IOException e1) { 473 Log.warn("Error closing terminal", e); 474 } 475 } 476 } finally { 477 synchronized (lock) { 478 pump = null; 479 } 480 } 481 } 482 processInputChar(char c)483 public void processInputChar(char c) throws IOException { 484 if (attributes.getLocalFlag(Attributes.LocalFlag.ISIG)) { 485 if (c == attributes.getControlChar(Attributes.ControlChar.VINTR)) { 486 raise(Signal.INT); 487 return; 488 } else if (c == attributes.getControlChar(Attributes.ControlChar.VQUIT)) { 489 raise(Signal.QUIT); 490 return; 491 } else if (c == attributes.getControlChar(Attributes.ControlChar.VSUSP)) { 492 raise(Signal.TSTP); 493 return; 494 } else if (c == attributes.getControlChar(Attributes.ControlChar.VSTATUS)) { 495 raise(Signal.INFO); 496 } 497 } 498 if (c == '\r') { 499 if (attributes.getInputFlag(Attributes.InputFlag.IGNCR)) { 500 return; 501 } 502 if (attributes.getInputFlag(Attributes.InputFlag.ICRNL)) { 503 c = '\n'; 504 } 505 } else if (c == '\n' && attributes.getInputFlag(Attributes.InputFlag.INLCR)) { 506 c = '\r'; 507 } 508 // if (attributes.getLocalFlag(Attributes.LocalFlag.ECHO)) { 509 // processOutputByte(c); 510 // masterOutput.flush(); 511 // } 512 slaveInputPipe.write(c); 513 } 514 515 @Override trackMouse(MouseTracking tracking)516 public boolean trackMouse(MouseTracking tracking) { 517 this.tracking = tracking; 518 updateConsoleMode(); 519 return true; 520 } 521 getConsoleOutputCP()522 protected abstract int getConsoleOutputCP(); 523 getConsoleMode()524 protected abstract int getConsoleMode(); 525 setConsoleMode(int mode)526 protected abstract void setConsoleMode(int mode); 527 528 /** 529 * Read a single input event from the input buffer and process it. 530 * 531 * @return true if new input was generated from the event 532 * @throws IOException if anything wrong happens 533 */ processConsoleInput()534 protected abstract boolean processConsoleInput() throws IOException; 535 536 } 537 538