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