| /* |
| * Copyright (c) 2002-2019, the original author or authors. |
| * |
| * This software is distributable under the BSD license. See the terms of the |
| * BSD license in the documentation provided with this software. |
| * |
| * https://opensource.org/licenses/BSD-3-Clause |
| */ |
| package jdk.internal.org.jline.terminal.impl; |
| |
| import jdk.internal.org.jline.terminal.Attributes; |
| import jdk.internal.org.jline.terminal.Size; |
| import jdk.internal.org.jline.utils.Curses; |
| import jdk.internal.org.jline.utils.InfoCmp; |
| import jdk.internal.org.jline.utils.Log; |
| import jdk.internal.org.jline.utils.NonBlocking; |
| import jdk.internal.org.jline.utils.NonBlockingInputStream; |
| import jdk.internal.org.jline.utils.NonBlockingPumpReader; |
| import jdk.internal.org.jline.utils.NonBlockingReader; |
| import jdk.internal.org.jline.utils.ShutdownHooks; |
| import jdk.internal.org.jline.utils.Signals; |
| import jdk.internal.org.jline.utils.WriterOutputStream; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.io.Writer; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.function.Function; |
| |
| /** |
| * The AbstractWindowsTerminal is used as the base class for windows terminal. |
| * Due to windows limitations, mostly the missing support for ansi sequences, |
| * the only way to create a correct terminal is to use the windows api to set |
| * character attributes, move the cursor, erasing, etc... |
| * |
| * UTF-8 support is also lacking in windows and the code page supposed to |
| * emulate UTF-8 is a bit broken. In order to work around this broken |
| * code page, windows api WriteConsoleW is used directly. This means that |
| * the writer() becomes the primary output, while the output() is bridged |
| * to the writer() using a WriterOutputStream wrapper. |
| */ |
| public abstract class AbstractWindowsTerminal extends AbstractTerminal { |
| |
| public static final String TYPE_WINDOWS = "windows"; |
| public static final String TYPE_WINDOWS_256_COLOR = "windows-256color"; |
| public static final String TYPE_WINDOWS_CONEMU = "windows-conemu"; |
| public static final String TYPE_WINDOWS_VTP = "windows-vtp"; |
| |
| public static final int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; |
| |
| private static final int UTF8_CODE_PAGE = 65001; |
| |
| protected static final int ENABLE_PROCESSED_INPUT = 0x0001; |
| protected static final int ENABLE_LINE_INPUT = 0x0002; |
| protected static final int ENABLE_ECHO_INPUT = 0x0004; |
| protected static final int ENABLE_WINDOW_INPUT = 0x0008; |
| protected static final int ENABLE_MOUSE_INPUT = 0x0010; |
| protected static final int ENABLE_INSERT_MODE = 0x0020; |
| protected static final int ENABLE_QUICK_EDIT_MODE = 0x0040; |
| |
| protected final Writer slaveInputPipe; |
| protected final InputStream input; |
| protected final OutputStream output; |
| protected final NonBlockingReader reader; |
| protected final PrintWriter writer; |
| protected final Map<Signal, Object> nativeHandlers = new HashMap<>(); |
| protected final ShutdownHooks.Task closer; |
| protected final Attributes attributes = new Attributes(); |
| protected final int originalConsoleMode; |
| |
| protected final Object lock = new Object(); |
| protected boolean paused = true; |
| protected Thread pump; |
| |
| protected MouseTracking tracking = MouseTracking.Off; |
| protected boolean focusTracking = false; |
| private volatile boolean closing; |
| |
| public AbstractWindowsTerminal(Writer writer, String name, String type, Charset encoding, int codepage, boolean nativeSignals, SignalHandler signalHandler, Function<InputStream, InputStream> inputStreamWrapper) throws IOException { |
| super(name, type, selectCharset(encoding, codepage), signalHandler); |
| NonBlockingPumpReader reader = NonBlocking.nonBlockingPumpReader(); |
| this.slaveInputPipe = reader.getWriter(); |
| this.input = inputStreamWrapper.apply(NonBlocking.nonBlockingStream(reader, encoding())); |
| this.reader = NonBlocking.nonBlocking(name, input, encoding()); |
| this.writer = new PrintWriter(writer); |
| this.output = new WriterOutputStream(writer, encoding()); |
| parseInfoCmp(); |
| // Attributes |
| originalConsoleMode = getConsoleMode(); |
| attributes.setLocalFlag(Attributes.LocalFlag.ISIG, true); |
| attributes.setControlChar(Attributes.ControlChar.VINTR, ctrl('C')); |
| attributes.setControlChar(Attributes.ControlChar.VEOF, ctrl('D')); |
| attributes.setControlChar(Attributes.ControlChar.VSUSP, ctrl('Z')); |
| // Handle signals |
| if (nativeSignals) { |
| for (final Signal signal : Signal.values()) { |
| if (signalHandler == SignalHandler.SIG_DFL) { |
| nativeHandlers.put(signal, Signals.registerDefault(signal.name())); |
| } else { |
| nativeHandlers.put(signal, Signals.register(signal.name(), () -> raise(signal))); |
| } |
| } |
| } |
| closer = this::close; |
| ShutdownHooks.add(closer); |
| // ConEMU extended fonts support |
| if (TYPE_WINDOWS_CONEMU.equals(getType()) |
| && !Boolean.getBoolean("org.jline.terminal.conemu.disable-activate")) { |
| writer.write("\u001b[9999E"); |
| writer.flush(); |
| } |
| } |
| |
| private static Charset selectCharset(Charset encoding, int codepage) { |
| if (encoding != null) { |
| return encoding; |
| } |
| |
| if (codepage >= 0) { |
| return getCodepageCharset(codepage); |
| } |
| |
| // Use UTF-8 as default |
| return StandardCharsets.UTF_8; |
| } |
| |
| private static Charset getCodepageCharset(int codepage) { |
| //http://docs.oracle.com/javase/6/docs/technotes/guides/intl/encoding.doc.html |
| if (codepage == UTF8_CODE_PAGE) { |
| return StandardCharsets.UTF_8; |
| } |
| String charsetMS = "ms" + codepage; |
| if (Charset.isSupported(charsetMS)) { |
| return Charset.forName(charsetMS); |
| } |
| String charsetCP = "cp" + codepage; |
| if (Charset.isSupported(charsetCP)) { |
| return Charset.forName(charsetCP); |
| } |
| return Charset.defaultCharset(); |
| } |
| |
| @Override |
| public SignalHandler handle(Signal signal, SignalHandler handler) { |
| SignalHandler prev = super.handle(signal, handler); |
| if (prev != handler) { |
| if (handler == SignalHandler.SIG_DFL) { |
| Signals.registerDefault(signal.name()); |
| } else { |
| Signals.register(signal.name(), () -> raise(signal)); |
| } |
| } |
| return prev; |
| } |
| |
| public NonBlockingReader reader() { |
| return reader; |
| } |
| |
| public PrintWriter writer() { |
| return writer; |
| } |
| |
| @Override |
| public InputStream input() { |
| return input; |
| } |
| |
| @Override |
| public OutputStream output() { |
| return output; |
| } |
| |
| public Attributes getAttributes() { |
| int mode = getConsoleMode(); |
| if ((mode & ENABLE_ECHO_INPUT) != 0) { |
| attributes.setLocalFlag(Attributes.LocalFlag.ECHO, true); |
| } |
| if ((mode & ENABLE_LINE_INPUT) != 0) { |
| attributes.setLocalFlag(Attributes.LocalFlag.ICANON, true); |
| } |
| return new Attributes(attributes); |
| } |
| |
| public void setAttributes(Attributes attr) { |
| attributes.copy(attr); |
| updateConsoleMode(); |
| } |
| |
| protected void updateConsoleMode() { |
| int mode = ENABLE_WINDOW_INPUT; |
| if (attributes.getLocalFlag(Attributes.LocalFlag.ECHO)) { |
| mode |= ENABLE_ECHO_INPUT; |
| } |
| if (attributes.getLocalFlag(Attributes.LocalFlag.ICANON)) { |
| mode |= ENABLE_LINE_INPUT; |
| } |
| if (tracking != MouseTracking.Off) { |
| mode |= ENABLE_MOUSE_INPUT; |
| } |
| setConsoleMode(mode); |
| } |
| |
| protected int ctrl(char key) { |
| return (Character.toUpperCase(key) & 0x1f); |
| } |
| |
| public void setSize(Size size) { |
| throw new UnsupportedOperationException("Can not resize windows terminal"); |
| } |
| |
| protected void doClose() throws IOException { |
| super.doClose(); |
| closing = true; |
| if (pump != null) { |
| pump.interrupt(); |
| } |
| ShutdownHooks.remove(closer); |
| for (Map.Entry<Signal, Object> entry : nativeHandlers.entrySet()) { |
| Signals.unregister(entry.getKey().name(), entry.getValue()); |
| } |
| reader.close(); |
| writer.close(); |
| setConsoleMode(originalConsoleMode); |
| } |
| |
| static final int SHIFT_FLAG = 0x01; |
| static final int ALT_FLAG = 0x02; |
| static final int CTRL_FLAG = 0x04; |
| |
| static final int RIGHT_ALT_PRESSED = 0x0001; |
| static final int LEFT_ALT_PRESSED = 0x0002; |
| static final int RIGHT_CTRL_PRESSED = 0x0004; |
| static final int LEFT_CTRL_PRESSED = 0x0008; |
| static final int SHIFT_PRESSED = 0x0010; |
| static final int NUMLOCK_ON = 0x0020; |
| static final int SCROLLLOCK_ON = 0x0040; |
| static final int CAPSLOCK_ON = 0x0080; |
| |
| protected void processKeyEvent(final boolean isKeyDown, final short virtualKeyCode, char ch, final int controlKeyState) throws IOException { |
| final boolean isCtrl = (controlKeyState & (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) > 0; |
| final boolean isAlt = (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED)) > 0; |
| final boolean isShift = (controlKeyState & SHIFT_PRESSED) > 0; |
| // key down event |
| if (isKeyDown && ch != '\3') { |
| // Pressing "Alt Gr" is translated to Alt-Ctrl, hence it has to be checked that Ctrl is _not_ pressed, |
| // otherwise inserting of "Alt Gr" codes on non-US keyboards would yield errors |
| if (ch != 0 |
| && (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED)) |
| == (RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED)) { |
| processInputChar(ch); |
| } else { |
| final String keySeq = getEscapeSequence(virtualKeyCode, (isCtrl ? CTRL_FLAG : 0) + (isAlt ? ALT_FLAG : 0) + (isShift ? SHIFT_FLAG : 0)); |
| if (keySeq != null) { |
| for (char c : keySeq.toCharArray()) { |
| processInputChar(c); |
| } |
| return; |
| } |
| /* uchar value in Windows when CTRL is pressed: |
| * 1). Ctrl + <0x41 to 0x5e> : uchar=<keyCode> - 'A' + 1 |
| * 2). Ctrl + Backspace(0x08) : uchar=0x7f |
| * 3). Ctrl + Enter(0x0d) : uchar=0x0a |
| * 4). Ctrl + Space(0x20) : uchar=0x20 |
| * 5). Ctrl + <Other key> : uchar=0 |
| * 6). Ctrl + Alt + <Any key> : uchar=0 |
| */ |
| if (ch > 0) { |
| if (isAlt) { |
| processInputChar('\033'); |
| } |
| if (isCtrl && ch != ' ' && ch != '\n' && ch != 0x7f) { |
| processInputChar((char) (ch == '?' ? 0x7f : Character.toUpperCase(ch) & 0x1f)); |
| } else if (isCtrl && ch == '\n') { |
| //simulate Alt-Enter: |
| processInputChar('\033'); |
| processInputChar('\r'); |
| } else { |
| processInputChar(ch); |
| } |
| } else if (isCtrl) { //Handles the ctrl key events(uchar=0) |
| if (virtualKeyCode >= 'A' && virtualKeyCode <= 'Z') { |
| ch = (char) (virtualKeyCode - 0x40); |
| } else if (virtualKeyCode == 191) { //? |
| ch = 127; |
| } |
| if (ch > 0) { |
| if (isAlt) { |
| processInputChar('\033'); |
| } |
| processInputChar(ch); |
| } |
| } |
| } |
| } else if (isKeyDown && ch == '\3') { |
| processInputChar('\3'); |
| } |
| // key up event |
| else { |
| // support ALT+NumPad input method |
| if (virtualKeyCode == 0x12 /*VK_MENU ALT key*/ && ch > 0) { |
| processInputChar(ch); // no such combination in Windows |
| } |
| } |
| } |
| |
| protected String getEscapeSequence(short keyCode, int keyState) { |
| // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx |
| // TODO: numpad keys, modifiers |
| String escapeSequence = null; |
| switch (keyCode) { |
| case 0x08: // VK_BACK BackSpace |
| escapeSequence = (keyState & ALT_FLAG) > 0 ? "\\E^H" : getRawSequence(InfoCmp.Capability.key_backspace); |
| break; |
| case 0x09: |
| escapeSequence = (keyState & SHIFT_FLAG) > 0 ? getRawSequence(InfoCmp.Capability.key_btab) : null; |
| break; |
| case 0x21: // VK_PRIOR PageUp |
| escapeSequence = getRawSequence(InfoCmp.Capability.key_ppage); |
| break; |
| case 0x22: // VK_NEXT PageDown |
| escapeSequence = getRawSequence(InfoCmp.Capability.key_npage); |
| break; |
| case 0x23: // VK_END |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dF" : getRawSequence(InfoCmp.Capability.key_end); |
| break; |
| case 0x24: // VK_HOME |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dH" : getRawSequence(InfoCmp.Capability.key_home); |
| break; |
| case 0x25: // VK_LEFT |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dD" : getRawSequence(InfoCmp.Capability.key_left); |
| break; |
| case 0x26: // VK_UP |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dA" : getRawSequence(InfoCmp.Capability.key_up); |
| break; |
| case 0x27: // VK_RIGHT |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dC" : getRawSequence(InfoCmp.Capability.key_right); |
| break; |
| case 0x28: // VK_DOWN |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dB" : getRawSequence(InfoCmp.Capability.key_down); |
| break; |
| case 0x2D: // VK_INSERT |
| escapeSequence = getRawSequence(InfoCmp.Capability.key_ic); |
| break; |
| case 0x2E: // VK_DELETE |
| escapeSequence = getRawSequence(InfoCmp.Capability.key_dc); |
| break; |
| case 0x70: // VK_F1 |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dP" : getRawSequence(InfoCmp.Capability.key_f1); |
| break; |
| case 0x71: // VK_F2 |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dQ" : getRawSequence(InfoCmp.Capability.key_f2); |
| break; |
| case 0x72: // VK_F3 |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dR" : getRawSequence(InfoCmp.Capability.key_f3); |
| break; |
| case 0x73: // VK_F4 |
| escapeSequence = keyState > 0 ? "\\E[1;%p1%dS" : getRawSequence(InfoCmp.Capability.key_f4); |
| break; |
| case 0x74: // VK_F5 |
| escapeSequence = keyState > 0 ? "\\E[15;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f5); |
| break; |
| case 0x75: // VK_F6 |
| escapeSequence = keyState > 0 ? "\\E[17;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f6); |
| break; |
| case 0x76: // VK_F7 |
| escapeSequence = keyState > 0 ? "\\E[18;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f7); |
| break; |
| case 0x77: // VK_F8 |
| escapeSequence = keyState > 0 ? "\\E[19;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f8); |
| break; |
| case 0x78: // VK_F9 |
| escapeSequence = keyState > 0 ? "\\E[20;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f9); |
| break; |
| case 0x79: // VK_F10 |
| escapeSequence = keyState > 0 ? "\\E[21;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f10); |
| break; |
| case 0x7A: // VK_F11 |
| escapeSequence = keyState > 0 ? "\\E[23;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f11); |
| break; |
| case 0x7B: // VK_F12 |
| escapeSequence = keyState > 0 ? "\\E[24;%p1%d~" : getRawSequence(InfoCmp.Capability.key_f12); |
| break; |
| case 0x5D: // VK_CLOSE_BRACKET(Menu key) |
| case 0x5B: // VK_OPEN_BRACKET(Window key) |
| default: |
| return null; |
| } |
| return Curses.tputs(escapeSequence, keyState + 1); |
| } |
| |
| protected String getRawSequence(InfoCmp.Capability cap) { |
| return strings.get(cap); |
| } |
| |
| @Override |
| public boolean hasFocusSupport() { |
| return true; |
| } |
| |
| @Override |
| public boolean trackFocus(boolean tracking) { |
| focusTracking = tracking; |
| return true; |
| } |
| |
| @Override |
| public boolean canPauseResume() { |
| return true; |
| } |
| |
| @Override |
| public void pause() { |
| synchronized (lock) { |
| paused = true; |
| } |
| } |
| |
| @Override |
| public void pause(boolean wait) throws InterruptedException { |
| Thread p; |
| synchronized (lock) { |
| paused = true; |
| p = pump; |
| } |
| if (p != null) { |
| p.interrupt(); |
| p.join(); |
| } |
| } |
| |
| @Override |
| public void resume() { |
| synchronized (lock) { |
| paused = false; |
| if (pump == null) { |
| pump = new Thread(this::pump, "WindowsStreamPump"); |
| pump.setDaemon(true); |
| pump.start(); |
| } |
| } |
| } |
| |
| @Override |
| public boolean paused() { |
| synchronized (lock) { |
| return paused; |
| } |
| } |
| |
| protected void pump() { |
| try { |
| while (!closing) { |
| synchronized (lock) { |
| if (paused) { |
| pump = null; |
| break; |
| } |
| } |
| if (processConsoleInput()) { |
| slaveInputPipe.flush(); |
| } |
| } |
| } catch (IOException e) { |
| if (!closing) { |
| Log.warn("Error in WindowsStreamPump", e); |
| try { |
| close(); |
| } catch (IOException e1) { |
| Log.warn("Error closing terminal", e); |
| } |
| } |
| } finally { |
| synchronized (lock) { |
| pump = null; |
| } |
| } |
| } |
| |
| public void processInputChar(char c) throws IOException { |
| if (attributes.getLocalFlag(Attributes.LocalFlag.ISIG)) { |
| if (c == attributes.getControlChar(Attributes.ControlChar.VINTR)) { |
| raise(Signal.INT); |
| return; |
| } else if (c == attributes.getControlChar(Attributes.ControlChar.VQUIT)) { |
| raise(Signal.QUIT); |
| return; |
| } else if (c == attributes.getControlChar(Attributes.ControlChar.VSUSP)) { |
| raise(Signal.TSTP); |
| return; |
| } else if (c == attributes.getControlChar(Attributes.ControlChar.VSTATUS)) { |
| raise(Signal.INFO); |
| } |
| } |
| if (c == '\r') { |
| if (attributes.getInputFlag(Attributes.InputFlag.IGNCR)) { |
| return; |
| } |
| if (attributes.getInputFlag(Attributes.InputFlag.ICRNL)) { |
| c = '\n'; |
| } |
| } else if (c == '\n' && attributes.getInputFlag(Attributes.InputFlag.INLCR)) { |
| c = '\r'; |
| } |
| // if (attributes.getLocalFlag(Attributes.LocalFlag.ECHO)) { |
| // processOutputByte(c); |
| // masterOutput.flush(); |
| // } |
| slaveInputPipe.write(c); |
| } |
| |
| @Override |
| public boolean trackMouse(MouseTracking tracking) { |
| this.tracking = tracking; |
| updateConsoleMode(); |
| return true; |
| } |
| |
| protected abstract int getConsoleOutputCP(); |
| |
| protected abstract int getConsoleMode(); |
| |
| protected abstract void setConsoleMode(int mode); |
| |
| /** |
| * Read a single input event from the input buffer and process it. |
| * |
| * @return true if new input was generated from the event |
| * @throws IOException if anything wrong happens |
| */ |
| protected abstract boolean processConsoleInput() throws IOException; |
| |
| } |
| |