Support for Xterm ctrl/alt/shift function keys
[nikiroo-utils.git] / src / jexer / io / ECMA48Terminal.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2016 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.io;
30
31 import java.io.BufferedReader;
32 import java.io.FileDescriptor;
33 import java.io.FileInputStream;
34 import java.io.InputStream;
35 import java.io.InputStreamReader;
36 import java.io.IOException;
37 import java.io.OutputStream;
38 import java.io.OutputStreamWriter;
39 import java.io.PrintWriter;
40 import java.io.Reader;
41 import java.io.UnsupportedEncodingException;
42 import java.util.ArrayList;
43 import java.util.Date;
44 import java.util.List;
45 import java.util.LinkedList;
46
47 import jexer.bits.Color;
48 import jexer.event.TInputEvent;
49 import jexer.event.TKeypressEvent;
50 import jexer.event.TMouseEvent;
51 import jexer.event.TResizeEvent;
52 import jexer.session.SessionInfo;
53 import jexer.session.TSessionInfo;
54 import jexer.session.TTYSessionInfo;
55 import static jexer.TKeypress.*;
56
57 /**
58 * This class reads keystrokes and mouse events and emits output to ANSI
59 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
60 */
61 public final class ECMA48Terminal implements Runnable {
62
63 /**
64 * The session information.
65 */
66 private SessionInfo sessionInfo;
67
68 /**
69 * Getter for sessionInfo.
70 *
71 * @return the SessionInfo
72 */
73 public SessionInfo getSessionInfo() {
74 return sessionInfo;
75 }
76
77 /**
78 * The event queue, filled up by a thread reading on input.
79 */
80 private List<TInputEvent> eventQueue;
81
82 /**
83 * If true, we want the reader thread to exit gracefully.
84 */
85 private boolean stopReaderThread;
86
87 /**
88 * The reader thread.
89 */
90 private Thread readerThread;
91
92 /**
93 * Parameters being collected. E.g. if the string is \033[1;3m, then
94 * params[0] will be 1 and params[1] will be 3.
95 */
96 private ArrayList<String> params;
97
98 /**
99 * States in the input parser.
100 */
101 private enum ParseState {
102 GROUND,
103 ESCAPE,
104 ESCAPE_INTERMEDIATE,
105 CSI_ENTRY,
106 CSI_PARAM,
107 MOUSE,
108 MOUSE_SGR,
109 }
110
111 /**
112 * Current parsing state.
113 */
114 private ParseState state;
115
116 /**
117 * The time we entered ESCAPE. If we get a bare escape without a code
118 * following it, this is used to return that bare escape.
119 */
120 private long escapeTime;
121
122 /**
123 * The time we last checked the window size. We try not to spawn stty
124 * more than once per second.
125 */
126 private long windowSizeTime;
127
128 /**
129 * true if mouse1 was down. Used to report mouse1 on the release event.
130 */
131 private boolean mouse1;
132
133 /**
134 * true if mouse2 was down. Used to report mouse2 on the release event.
135 */
136 private boolean mouse2;
137
138 /**
139 * true if mouse3 was down. Used to report mouse3 on the release event.
140 */
141 private boolean mouse3;
142
143 /**
144 * Cache the cursor visibility value so we only emit the sequence when we
145 * need to.
146 */
147 private boolean cursorOn = true;
148
149 /**
150 * Cache the last window size to figure out if a TResizeEvent needs to be
151 * generated.
152 */
153 private TResizeEvent windowResize = null;
154
155 /**
156 * If true, then we changed System.in and need to change it back.
157 */
158 private boolean setRawMode;
159
160 /**
161 * The terminal's input. If an InputStream is not specified in the
162 * constructor, then this InputStreamReader will be bound to System.in
163 * with UTF-8 encoding.
164 */
165 private Reader input;
166
167 /**
168 * The terminal's raw InputStream. If an InputStream is not specified in
169 * the constructor, then this InputReader will be bound to System.in.
170 * This is used by run() to see if bytes are available() before calling
171 * (Reader)input.read().
172 */
173 private InputStream inputStream;
174
175 /**
176 * The terminal's output. If an OutputStream is not specified in the
177 * constructor, then this PrintWriter will be bound to System.out with
178 * UTF-8 encoding.
179 */
180 private PrintWriter output;
181
182 /**
183 * The listening object that run() wakes up on new input.
184 */
185 private Object listener;
186
187 /**
188 * Get the output writer.
189 *
190 * @return the Writer
191 */
192 public PrintWriter getOutput() {
193 return output;
194 }
195
196 /**
197 * Check if there are events in the queue.
198 *
199 * @return if true, getEvents() has something to return to the backend
200 */
201 public boolean hasEvents() {
202 synchronized (eventQueue) {
203 return (eventQueue.size() > 0);
204 }
205 }
206
207 /**
208 * Call 'stty' to set cooked mode.
209 *
210 * <p>Actually executes '/bin/sh -c stty sane cooked &lt; /dev/tty'
211 */
212 private void sttyCooked() {
213 doStty(false);
214 }
215
216 /**
217 * Call 'stty' to set raw mode.
218 *
219 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
220 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
221 * -parenb cs8 min 1 &lt; /dev/tty'
222 */
223 private void sttyRaw() {
224 doStty(true);
225 }
226
227 /**
228 * Call 'stty' to set raw or cooked mode.
229 *
230 * @param mode if true, set raw mode, otherwise set cooked mode
231 */
232 private void doStty(final boolean mode) {
233 String [] cmdRaw = {
234 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
235 };
236 String [] cmdCooked = {
237 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
238 };
239 try {
240 Process process;
241 if (mode) {
242 process = Runtime.getRuntime().exec(cmdRaw);
243 } else {
244 process = Runtime.getRuntime().exec(cmdCooked);
245 }
246 BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
247 String line = in.readLine();
248 if ((line != null) && (line.length() > 0)) {
249 System.err.println("WEIRD?! Normal output from stty: " + line);
250 }
251 while (true) {
252 BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
253 line = err.readLine();
254 if ((line != null) && (line.length() > 0)) {
255 System.err.println("Error output from stty: " + line);
256 }
257 try {
258 process.waitFor();
259 break;
260 } catch (InterruptedException e) {
261 e.printStackTrace();
262 }
263 }
264 int rc = process.exitValue();
265 if (rc != 0) {
266 System.err.println("stty returned error code: " + rc);
267 }
268 } catch (IOException e) {
269 e.printStackTrace();
270 }
271 }
272
273 /**
274 * Constructor sets up state for getEvent().
275 *
276 * @param listener the object this backend needs to wake up when new
277 * input comes in
278 * @param input an InputStream connected to the remote user, or null for
279 * System.in. If System.in is used, then on non-Windows systems it will
280 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
281 * mode. input is always converted to a Reader with UTF-8 encoding.
282 * @param output an OutputStream connected to the remote user, or null
283 * for System.out. output is always converted to a Writer with UTF-8
284 * encoding.
285 * @throws UnsupportedEncodingException if an exception is thrown when
286 * creating the InputStreamReader
287 */
288 public ECMA48Terminal(final Object listener, final InputStream input,
289 final OutputStream output) throws UnsupportedEncodingException {
290
291 reset();
292 mouse1 = false;
293 mouse2 = false;
294 mouse3 = false;
295 stopReaderThread = false;
296 this.listener = listener;
297
298 if (input == null) {
299 // inputStream = System.in;
300 inputStream = new FileInputStream(FileDescriptor.in);
301 sttyRaw();
302 setRawMode = true;
303 } else {
304 inputStream = input;
305 }
306 this.input = new InputStreamReader(inputStream, "UTF-8");
307
308 if (input instanceof SessionInfo) {
309 // This is a TelnetInputStream that exposes window size and
310 // environment variables from the telnet layer.
311 sessionInfo = (SessionInfo) input;
312 }
313 if (sessionInfo == null) {
314 if (input == null) {
315 // Reading right off the tty
316 sessionInfo = new TTYSessionInfo();
317 } else {
318 sessionInfo = new TSessionInfo();
319 }
320 }
321
322 if (output == null) {
323 this.output = new PrintWriter(new OutputStreamWriter(System.out,
324 "UTF-8"));
325 } else {
326 this.output = new PrintWriter(new OutputStreamWriter(output,
327 "UTF-8"));
328 }
329
330 // Enable mouse reporting and metaSendsEscape
331 this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
332 this.output.flush();
333
334 // Hang onto the window size
335 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
336 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
337
338 // Spin up the input reader
339 eventQueue = new LinkedList<TInputEvent>();
340 readerThread = new Thread(this);
341 readerThread.start();
342 }
343
344 /**
345 * Restore terminal to normal state.
346 */
347 public void shutdown() {
348
349 // System.err.println("=== shutdown() ==="); System.err.flush();
350
351 // Tell the reader thread to stop looking at input
352 stopReaderThread = true;
353 try {
354 readerThread.join();
355 } catch (InterruptedException e) {
356 e.printStackTrace();
357 }
358
359 // Disable mouse reporting and show cursor
360 output.printf("%s%s%s", mouse(false), cursor(true), normal());
361 output.flush();
362
363 if (setRawMode) {
364 sttyCooked();
365 setRawMode = false;
366 // We don't close System.in/out
367 } else {
368 // Shut down the streams, this should wake up the reader thread
369 // and make it exit.
370 try {
371 if (input != null) {
372 input.close();
373 input = null;
374 }
375 if (output != null) {
376 output.close();
377 output = null;
378 }
379 } catch (IOException e) {
380 e.printStackTrace();
381 }
382 }
383 }
384
385 /**
386 * Flush output.
387 */
388 public void flush() {
389 output.flush();
390 }
391
392 /**
393 * Reset keyboard/mouse input parser.
394 */
395 private void reset() {
396 state = ParseState.GROUND;
397 params = new ArrayList<String>();
398 params.clear();
399 params.add("");
400 }
401
402 /**
403 * Produce a control character or one of the special ones (ENTER, TAB,
404 * etc.).
405 *
406 * @param ch Unicode code point
407 * @param alt if true, set alt on the TKeypress
408 * @return one TKeypress event, either a control character (e.g. isKey ==
409 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
410 * fnKey == ESC)
411 */
412 private TKeypressEvent controlChar(final char ch, final boolean alt) {
413 // System.err.printf("controlChar: %02x\n", ch);
414
415 switch (ch) {
416 case 0x0D:
417 // Carriage return --> ENTER
418 return new TKeypressEvent(kbEnter, alt, false, false);
419 case 0x0A:
420 // Linefeed --> ENTER
421 return new TKeypressEvent(kbEnter, alt, false, false);
422 case 0x1B:
423 // ESC
424 return new TKeypressEvent(kbEsc, alt, false, false);
425 case '\t':
426 // TAB
427 return new TKeypressEvent(kbTab, alt, false, false);
428 default:
429 // Make all other control characters come back as the alphabetic
430 // character with the ctrl field set. So SOH would be 'A' +
431 // ctrl.
432 return new TKeypressEvent(false, 0, (char)(ch + 0x40),
433 alt, true, false);
434 }
435 }
436
437 /**
438 * Produce special key from CSI Pn ; Pm ; ... ~
439 *
440 * @return one KEYPRESS event representing a special key
441 */
442 private TInputEvent csiFnKey() {
443 int key = 0;
444 if (params.size() > 0) {
445 key = Integer.parseInt(params.get(0));
446 }
447 boolean alt = false;
448 boolean ctrl = false;
449 boolean shift = false;
450 if (params.size() > 1) {
451 shift = csiIsShift(params.get(1));
452 alt = csiIsAlt(params.get(1));
453 ctrl = csiIsCtrl(params.get(1));
454 }
455
456 switch (key) {
457 case 1:
458 return new TKeypressEvent(kbHome, alt, ctrl, shift);
459 case 2:
460 return new TKeypressEvent(kbIns, alt, ctrl, shift);
461 case 3:
462 return new TKeypressEvent(kbDel, alt, ctrl, shift);
463 case 4:
464 return new TKeypressEvent(kbEnd, alt, ctrl, shift);
465 case 5:
466 return new TKeypressEvent(kbPgUp, alt, ctrl, shift);
467 case 6:
468 return new TKeypressEvent(kbPgDn, alt, ctrl, shift);
469 case 15:
470 return new TKeypressEvent(kbF5, alt, ctrl, shift);
471 case 17:
472 return new TKeypressEvent(kbF6, alt, ctrl, shift);
473 case 18:
474 return new TKeypressEvent(kbF7, alt, ctrl, shift);
475 case 19:
476 return new TKeypressEvent(kbF8, alt, ctrl, shift);
477 case 20:
478 return new TKeypressEvent(kbF9, alt, ctrl, shift);
479 case 21:
480 return new TKeypressEvent(kbF10, alt, ctrl, shift);
481 case 23:
482 return new TKeypressEvent(kbF11, alt, ctrl, shift);
483 case 24:
484 return new TKeypressEvent(kbF12, alt, ctrl, shift);
485 default:
486 // Unknown
487 return null;
488 }
489 }
490
491 /**
492 * Produce mouse events based on "Any event tracking" and UTF-8
493 * coordinates. See
494 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
495 *
496 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
497 */
498 private TInputEvent parseMouse() {
499 int buttons = params.get(0).charAt(0) - 32;
500 int x = params.get(0).charAt(1) - 32 - 1;
501 int y = params.get(0).charAt(2) - 32 - 1;
502
503 // Clamp X and Y to the physical screen coordinates.
504 if (x >= windowResize.getWidth()) {
505 x = windowResize.getWidth() - 1;
506 }
507 if (y >= windowResize.getHeight()) {
508 y = windowResize.getHeight() - 1;
509 }
510
511 TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
512 boolean eventMouse1 = false;
513 boolean eventMouse2 = false;
514 boolean eventMouse3 = false;
515 boolean eventMouseWheelUp = false;
516 boolean eventMouseWheelDown = false;
517
518 // System.err.printf("buttons: %04x\r\n", buttons);
519
520 switch (buttons) {
521 case 0:
522 eventMouse1 = true;
523 mouse1 = true;
524 break;
525 case 1:
526 eventMouse2 = true;
527 mouse2 = true;
528 break;
529 case 2:
530 eventMouse3 = true;
531 mouse3 = true;
532 break;
533 case 3:
534 // Release or Move
535 if (!mouse1 && !mouse2 && !mouse3) {
536 eventType = TMouseEvent.Type.MOUSE_MOTION;
537 } else {
538 eventType = TMouseEvent.Type.MOUSE_UP;
539 }
540 if (mouse1) {
541 mouse1 = false;
542 eventMouse1 = true;
543 }
544 if (mouse2) {
545 mouse2 = false;
546 eventMouse2 = true;
547 }
548 if (mouse3) {
549 mouse3 = false;
550 eventMouse3 = true;
551 }
552 break;
553
554 case 32:
555 // Dragging with mouse1 down
556 eventMouse1 = true;
557 mouse1 = true;
558 eventType = TMouseEvent.Type.MOUSE_MOTION;
559 break;
560
561 case 33:
562 // Dragging with mouse2 down
563 eventMouse2 = true;
564 mouse2 = true;
565 eventType = TMouseEvent.Type.MOUSE_MOTION;
566 break;
567
568 case 34:
569 // Dragging with mouse3 down
570 eventMouse3 = true;
571 mouse3 = true;
572 eventType = TMouseEvent.Type.MOUSE_MOTION;
573 break;
574
575 case 96:
576 // Dragging with mouse2 down after wheelUp
577 eventMouse2 = true;
578 mouse2 = true;
579 eventType = TMouseEvent.Type.MOUSE_MOTION;
580 break;
581
582 case 97:
583 // Dragging with mouse2 down after wheelDown
584 eventMouse2 = true;
585 mouse2 = true;
586 eventType = TMouseEvent.Type.MOUSE_MOTION;
587 break;
588
589 case 64:
590 eventMouseWheelUp = true;
591 break;
592
593 case 65:
594 eventMouseWheelDown = true;
595 break;
596
597 default:
598 // Unknown, just make it motion
599 eventType = TMouseEvent.Type.MOUSE_MOTION;
600 break;
601 }
602 return new TMouseEvent(eventType, x, y, x, y,
603 eventMouse1, eventMouse2, eventMouse3,
604 eventMouseWheelUp, eventMouseWheelDown);
605 }
606
607 /**
608 * Produce mouse events based on "Any event tracking" and SGR
609 * coordinates. See
610 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
611 *
612 * @param release if true, this was a release ('m')
613 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
614 */
615 private TInputEvent parseMouseSGR(final boolean release) {
616 // SGR extended coordinates - mode 1006
617 if (params.size() < 3) {
618 // Invalid position, bail out.
619 return null;
620 }
621 int buttons = Integer.parseInt(params.get(0));
622 int x = Integer.parseInt(params.get(1)) - 1;
623 int y = Integer.parseInt(params.get(2)) - 1;
624
625 // Clamp X and Y to the physical screen coordinates.
626 if (x >= windowResize.getWidth()) {
627 x = windowResize.getWidth() - 1;
628 }
629 if (y >= windowResize.getHeight()) {
630 y = windowResize.getHeight() - 1;
631 }
632
633 TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
634 boolean eventMouse1 = false;
635 boolean eventMouse2 = false;
636 boolean eventMouse3 = false;
637 boolean eventMouseWheelUp = false;
638 boolean eventMouseWheelDown = false;
639
640 if (release) {
641 eventType = TMouseEvent.Type.MOUSE_UP;
642 }
643
644 switch (buttons) {
645 case 0:
646 eventMouse1 = true;
647 break;
648 case 1:
649 eventMouse2 = true;
650 break;
651 case 2:
652 eventMouse3 = true;
653 break;
654 case 35:
655 // Motion only, no buttons down
656 eventType = TMouseEvent.Type.MOUSE_MOTION;
657 break;
658
659 case 32:
660 // Dragging with mouse1 down
661 eventMouse1 = true;
662 eventType = TMouseEvent.Type.MOUSE_MOTION;
663 break;
664
665 case 33:
666 // Dragging with mouse2 down
667 eventMouse2 = true;
668 eventType = TMouseEvent.Type.MOUSE_MOTION;
669 break;
670
671 case 34:
672 // Dragging with mouse3 down
673 eventMouse3 = true;
674 eventType = TMouseEvent.Type.MOUSE_MOTION;
675 break;
676
677 case 96:
678 // Dragging with mouse2 down after wheelUp
679 eventMouse2 = true;
680 eventType = TMouseEvent.Type.MOUSE_MOTION;
681 break;
682
683 case 97:
684 // Dragging with mouse2 down after wheelDown
685 eventMouse2 = true;
686 eventType = TMouseEvent.Type.MOUSE_MOTION;
687 break;
688
689 case 64:
690 eventMouseWheelUp = true;
691 break;
692
693 case 65:
694 eventMouseWheelDown = true;
695 break;
696
697 default:
698 // Unknown, bail out
699 return null;
700 }
701 return new TMouseEvent(eventType, x, y, x, y,
702 eventMouse1, eventMouse2, eventMouse3,
703 eventMouseWheelUp, eventMouseWheelDown);
704 }
705
706 /**
707 * Return any events in the IO queue.
708 *
709 * @param queue list to append new events to
710 */
711 public void getEvents(final List<TInputEvent> queue) {
712 synchronized (eventQueue) {
713 if (eventQueue.size() > 0) {
714 synchronized (queue) {
715 queue.addAll(eventQueue);
716 }
717 eventQueue.clear();
718 }
719 }
720 }
721
722 /**
723 * Return any events in the IO queue due to timeout.
724 *
725 * @param queue list to append new events to
726 */
727 private void getIdleEvents(final List<TInputEvent> queue) {
728 Date now = new Date();
729
730 // Check for new window size
731 long windowSizeDelay = now.getTime() - windowSizeTime;
732 if (windowSizeDelay > 1000) {
733 sessionInfo.queryWindowSize();
734 int newWidth = sessionInfo.getWindowWidth();
735 int newHeight = sessionInfo.getWindowHeight();
736 if ((newWidth != windowResize.getWidth())
737 || (newHeight != windowResize.getHeight())
738 ) {
739 TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
740 newWidth, newHeight);
741 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
742 newWidth, newHeight);
743 queue.add(event);
744 }
745 windowSizeTime = now.getTime();
746 }
747
748 // ESCDELAY type timeout
749 if (state == ParseState.ESCAPE) {
750 long escDelay = now.getTime() - escapeTime;
751 if (escDelay > 100) {
752 // After 0.1 seconds, assume a true escape character
753 queue.add(controlChar((char)0x1B, false));
754 reset();
755 }
756 }
757 }
758
759 /**
760 * Returns true if the CSI parameter for a keyboard command means that
761 * shift was down.
762 */
763 private boolean csiIsShift(final String x) {
764 if ((x.equals("2"))
765 || (x.equals("4"))
766 || (x.equals("6"))
767 || (x.equals("8"))
768 ) {
769 return true;
770 }
771 return false;
772 }
773
774 /**
775 * Returns true if the CSI parameter for a keyboard command means that
776 * alt was down.
777 */
778 private boolean csiIsAlt(final String x) {
779 if ((x.equals("3"))
780 || (x.equals("4"))
781 || (x.equals("7"))
782 || (x.equals("8"))
783 ) {
784 return true;
785 }
786 return false;
787 }
788
789 /**
790 * Returns true if the CSI parameter for a keyboard command means that
791 * ctrl was down.
792 */
793 private boolean csiIsCtrl(final String x) {
794 if ((x.equals("5"))
795 || (x.equals("6"))
796 || (x.equals("7"))
797 || (x.equals("8"))
798 ) {
799 return true;
800 }
801 return false;
802 }
803
804 /**
805 * Parses the next character of input to see if an InputEvent is
806 * fully here.
807 *
808 * @param events list to append new events to
809 * @param ch Unicode code point
810 */
811 private void processChar(final List<TInputEvent> events, final char ch) {
812
813 // ESCDELAY type timeout
814 Date now = new Date();
815 if (state == ParseState.ESCAPE) {
816 long escDelay = now.getTime() - escapeTime;
817 if (escDelay > 250) {
818 // After 0.25 seconds, assume a true escape character
819 events.add(controlChar((char)0x1B, false));
820 reset();
821 }
822 }
823
824 // TKeypress fields
825 boolean ctrl = false;
826 boolean alt = false;
827 boolean shift = false;
828
829 // System.err.printf("state: %s ch %c\r\n", state, ch);
830
831 switch (state) {
832 case GROUND:
833
834 if (ch == 0x1B) {
835 state = ParseState.ESCAPE;
836 escapeTime = now.getTime();
837 return;
838 }
839
840 if (ch <= 0x1F) {
841 // Control character
842 events.add(controlChar(ch, false));
843 reset();
844 return;
845 }
846
847 if (ch >= 0x20) {
848 // Normal character
849 events.add(new TKeypressEvent(false, 0, ch,
850 false, false, false));
851 reset();
852 return;
853 }
854
855 break;
856
857 case ESCAPE:
858 if (ch <= 0x1F) {
859 // ALT-Control character
860 events.add(controlChar(ch, true));
861 reset();
862 return;
863 }
864
865 if (ch == 'O') {
866 // This will be one of the function keys
867 state = ParseState.ESCAPE_INTERMEDIATE;
868 return;
869 }
870
871 // '[' goes to CSI_ENTRY
872 if (ch == '[') {
873 state = ParseState.CSI_ENTRY;
874 return;
875 }
876
877 // Everything else is assumed to be Alt-keystroke
878 if ((ch >= 'A') && (ch <= 'Z')) {
879 shift = true;
880 }
881 alt = true;
882 events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift));
883 reset();
884 return;
885
886 case ESCAPE_INTERMEDIATE:
887 if ((ch >= 'P') && (ch <= 'S')) {
888 // Function key
889 switch (ch) {
890 case 'P':
891 events.add(new TKeypressEvent(kbF1));
892 break;
893 case 'Q':
894 events.add(new TKeypressEvent(kbF2));
895 break;
896 case 'R':
897 events.add(new TKeypressEvent(kbF3));
898 break;
899 case 'S':
900 events.add(new TKeypressEvent(kbF4));
901 break;
902 default:
903 break;
904 }
905 reset();
906 return;
907 }
908
909 // Unknown keystroke, ignore
910 reset();
911 return;
912
913 case CSI_ENTRY:
914 // Numbers - parameter values
915 if ((ch >= '0') && (ch <= '9')) {
916 params.set(params.size() - 1,
917 params.get(params.size() - 1) + ch);
918 state = ParseState.CSI_PARAM;
919 return;
920 }
921 // Parameter separator
922 if (ch == ';') {
923 params.add("");
924 return;
925 }
926
927 if ((ch >= 0x30) && (ch <= 0x7E)) {
928 switch (ch) {
929 case 'A':
930 // Up
931 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
932 reset();
933 return;
934 case 'B':
935 // Down
936 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
937 reset();
938 return;
939 case 'C':
940 // Right
941 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
942 reset();
943 return;
944 case 'D':
945 // Left
946 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
947 reset();
948 return;
949 case 'H':
950 // Home
951 events.add(new TKeypressEvent(kbHome));
952 reset();
953 return;
954 case 'F':
955 // End
956 events.add(new TKeypressEvent(kbEnd));
957 reset();
958 return;
959 case 'Z':
960 // CBT - Cursor backward X tab stops (default 1)
961 events.add(new TKeypressEvent(kbBackTab));
962 reset();
963 return;
964 case 'M':
965 // Mouse position
966 state = ParseState.MOUSE;
967 return;
968 case '<':
969 // Mouse position, SGR (1006) coordinates
970 state = ParseState.MOUSE_SGR;
971 return;
972 default:
973 break;
974 }
975 }
976
977 // Unknown keystroke, ignore
978 reset();
979 return;
980
981 case MOUSE_SGR:
982 // Numbers - parameter values
983 if ((ch >= '0') && (ch <= '9')) {
984 params.set(params.size() - 1,
985 params.get(params.size() - 1) + ch);
986 return;
987 }
988 // Parameter separator
989 if (ch == ';') {
990 params.add("");
991 return;
992 }
993
994 switch (ch) {
995 case 'M':
996 // Generate a mouse press event
997 TInputEvent event = parseMouseSGR(false);
998 if (event != null) {
999 events.add(event);
1000 }
1001 reset();
1002 return;
1003 case 'm':
1004 // Generate a mouse release event
1005 event = parseMouseSGR(true);
1006 if (event != null) {
1007 events.add(event);
1008 }
1009 reset();
1010 return;
1011 default:
1012 break;
1013 }
1014
1015 // Unknown keystroke, ignore
1016 reset();
1017 return;
1018
1019 case CSI_PARAM:
1020 // Numbers - parameter values
1021 if ((ch >= '0') && (ch <= '9')) {
1022 params.set(params.size() - 1,
1023 params.get(params.size() - 1) + ch);
1024 state = ParseState.CSI_PARAM;
1025 return;
1026 }
1027 // Parameter separator
1028 if (ch == ';') {
1029 params.add("");
1030 return;
1031 }
1032
1033 if (ch == '~') {
1034 events.add(csiFnKey());
1035 reset();
1036 return;
1037 }
1038
1039 if ((ch >= 0x30) && (ch <= 0x7E)) {
1040 switch (ch) {
1041 case 'A':
1042 // Up
1043 if (params.size() > 1) {
1044 shift = csiIsShift(params.get(1));
1045 alt = csiIsAlt(params.get(1));
1046 ctrl = csiIsCtrl(params.get(1));
1047 }
1048 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
1049 reset();
1050 return;
1051 case 'B':
1052 // Down
1053 if (params.size() > 1) {
1054 shift = csiIsShift(params.get(1));
1055 alt = csiIsAlt(params.get(1));
1056 ctrl = csiIsCtrl(params.get(1));
1057 }
1058 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
1059 reset();
1060 return;
1061 case 'C':
1062 // Right
1063 if (params.size() > 1) {
1064 shift = csiIsShift(params.get(1));
1065 alt = csiIsAlt(params.get(1));
1066 ctrl = csiIsCtrl(params.get(1));
1067 }
1068 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
1069 reset();
1070 return;
1071 case 'D':
1072 // Left
1073 if (params.size() > 1) {
1074 shift = csiIsShift(params.get(1));
1075 alt = csiIsAlt(params.get(1));
1076 ctrl = csiIsCtrl(params.get(1));
1077 }
1078 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
1079 reset();
1080 return;
1081 case 'H':
1082 // Home
1083 if (params.size() > 1) {
1084 shift = csiIsShift(params.get(1));
1085 alt = csiIsAlt(params.get(1));
1086 ctrl = csiIsCtrl(params.get(1));
1087 }
1088 events.add(new TKeypressEvent(kbHome, alt, ctrl, shift));
1089 reset();
1090 return;
1091 case 'F':
1092 // End
1093 if (params.size() > 1) {
1094 shift = csiIsShift(params.get(1));
1095 alt = csiIsAlt(params.get(1));
1096 ctrl = csiIsCtrl(params.get(1));
1097 }
1098 events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift));
1099 reset();
1100 return;
1101 default:
1102 break;
1103 }
1104 }
1105
1106 // Unknown keystroke, ignore
1107 reset();
1108 return;
1109
1110 case MOUSE:
1111 params.set(0, params.get(params.size() - 1) + ch);
1112 if (params.get(0).length() == 3) {
1113 // We have enough to generate a mouse event
1114 events.add(parseMouse());
1115 reset();
1116 }
1117 return;
1118
1119 default:
1120 break;
1121 }
1122
1123 // This "should" be impossible to reach
1124 return;
1125 }
1126
1127 /**
1128 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1129 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1130 * enabled.
1131 *
1132 * @param on if true, enable metaSendsEscape
1133 * @return the string to emit to xterm
1134 */
1135 private String xtermMetaSendsEscape(final boolean on) {
1136 if (on) {
1137 return "\033[?1036h\033[?1034l";
1138 }
1139 return "\033[?1036l";
1140 }
1141
1142 /**
1143 * Create a SGR parameter sequence for a single color change. Note
1144 * package private access.
1145 *
1146 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1147 * @param foreground if true, this is a foreground color
1148 * @return the string to emit to an ANSI / ECMA-style terminal,
1149 * e.g. "\033[42m"
1150 */
1151 String color(final Color color, final boolean foreground) {
1152 return color(color, foreground, true);
1153 }
1154
1155 /**
1156 * Create a SGR parameter sequence for a single color change.
1157 *
1158 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1159 * @param foreground if true, this is a foreground color
1160 * @param header if true, make the full header, otherwise just emit the
1161 * color parameter e.g. "42;"
1162 * @return the string to emit to an ANSI / ECMA-style terminal,
1163 * e.g. "\033[42m"
1164 */
1165 private String color(final Color color, final boolean foreground,
1166 final boolean header) {
1167
1168 int ecmaColor = color.getValue();
1169
1170 // Convert Color.* values to SGR numerics
1171 if (foreground) {
1172 ecmaColor += 30;
1173 } else {
1174 ecmaColor += 40;
1175 }
1176
1177 if (header) {
1178 return String.format("\033[%dm", ecmaColor);
1179 } else {
1180 return String.format("%d;", ecmaColor);
1181 }
1182 }
1183
1184 /**
1185 * Create a SGR parameter sequence for both foreground and background
1186 * color change. Note package private access.
1187 *
1188 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1189 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1190 * @return the string to emit to an ANSI / ECMA-style terminal,
1191 * e.g. "\033[31;42m"
1192 */
1193 String color(final Color foreColor, final Color backColor) {
1194 return color(foreColor, backColor, true);
1195 }
1196
1197 /**
1198 * Create a SGR parameter sequence for both foreground and
1199 * background color change.
1200 *
1201 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1202 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1203 * @param header if true, make the full header, otherwise just emit the
1204 * color parameter e.g. "31;42;"
1205 * @return the string to emit to an ANSI / ECMA-style terminal,
1206 * e.g. "\033[31;42m"
1207 */
1208 private String color(final Color foreColor, final Color backColor,
1209 final boolean header) {
1210
1211 int ecmaForeColor = foreColor.getValue();
1212 int ecmaBackColor = backColor.getValue();
1213
1214 // Convert Color.* values to SGR numerics
1215 ecmaBackColor += 40;
1216 ecmaForeColor += 30;
1217
1218 if (header) {
1219 return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
1220 } else {
1221 return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
1222 }
1223 }
1224
1225 /**
1226 * Create a SGR parameter sequence for foreground, background, and
1227 * several attributes. This sequence first resets all attributes to
1228 * default, then sets attributes as per the parameters. Note package
1229 * private access.
1230 *
1231 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1232 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1233 * @param bold if true, set bold
1234 * @param reverse if true, set reverse
1235 * @param blink if true, set blink
1236 * @param underline if true, set underline
1237 * @return the string to emit to an ANSI / ECMA-style terminal,
1238 * e.g. "\033[0;1;31;42m"
1239 */
1240 String color(final Color foreColor, final Color backColor,
1241 final boolean bold, final boolean reverse, final boolean blink,
1242 final boolean underline) {
1243
1244 int ecmaForeColor = foreColor.getValue();
1245 int ecmaBackColor = backColor.getValue();
1246
1247 // Convert Color.* values to SGR numerics
1248 ecmaBackColor += 40;
1249 ecmaForeColor += 30;
1250
1251 StringBuilder sb = new StringBuilder();
1252 if ( bold && reverse && blink && !underline ) {
1253 sb.append("\033[0;1;7;5;");
1254 } else if ( bold && reverse && !blink && !underline ) {
1255 sb.append("\033[0;1;7;");
1256 } else if ( !bold && reverse && blink && !underline ) {
1257 sb.append("\033[0;7;5;");
1258 } else if ( bold && !reverse && blink && !underline ) {
1259 sb.append("\033[0;1;5;");
1260 } else if ( bold && !reverse && !blink && !underline ) {
1261 sb.append("\033[0;1;");
1262 } else if ( !bold && reverse && !blink && !underline ) {
1263 sb.append("\033[0;7;");
1264 } else if ( !bold && !reverse && blink && !underline) {
1265 sb.append("\033[0;5;");
1266 } else if ( bold && reverse && blink && underline ) {
1267 sb.append("\033[0;1;7;5;4;");
1268 } else if ( bold && reverse && !blink && underline ) {
1269 sb.append("\033[0;1;7;4;");
1270 } else if ( !bold && reverse && blink && underline ) {
1271 sb.append("\033[0;7;5;4;");
1272 } else if ( bold && !reverse && blink && underline ) {
1273 sb.append("\033[0;1;5;4;");
1274 } else if ( bold && !reverse && !blink && underline ) {
1275 sb.append("\033[0;1;4;");
1276 } else if ( !bold && reverse && !blink && underline ) {
1277 sb.append("\033[0;7;4;");
1278 } else if ( !bold && !reverse && blink && underline) {
1279 sb.append("\033[0;5;4;");
1280 } else if ( !bold && !reverse && !blink && underline) {
1281 sb.append("\033[0;4;");
1282 } else {
1283 assert (!bold && !reverse && !blink && !underline);
1284 sb.append("\033[0;");
1285 }
1286 sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
1287 return sb.toString();
1288 }
1289
1290 /**
1291 * Create a SGR parameter sequence to reset to defaults. Note package
1292 * private access.
1293 *
1294 * @return the string to emit to an ANSI / ECMA-style terminal,
1295 * e.g. "\033[0m"
1296 */
1297 String normal() {
1298 return normal(true);
1299 }
1300
1301 /**
1302 * Create a SGR parameter sequence to reset to defaults.
1303 *
1304 * @param header if true, make the full header, otherwise just emit the
1305 * bare parameter e.g. "0;"
1306 * @return the string to emit to an ANSI / ECMA-style terminal,
1307 * e.g. "\033[0m"
1308 */
1309 private String normal(final boolean header) {
1310 if (header) {
1311 return "\033[0;37;40m";
1312 }
1313 return "0;37;40";
1314 }
1315
1316 /**
1317 * Create a SGR parameter sequence for enabling the visible cursor. Note
1318 * package private access.
1319 *
1320 * @param on if true, turn on cursor
1321 * @return the string to emit to an ANSI / ECMA-style terminal
1322 */
1323 String cursor(final boolean on) {
1324 if (on && !cursorOn) {
1325 cursorOn = true;
1326 return "\033[?25h";
1327 }
1328 if (!on && cursorOn) {
1329 cursorOn = false;
1330 return "\033[?25l";
1331 }
1332 return "";
1333 }
1334
1335 /**
1336 * Clear the entire screen. Because some terminals use back-color-erase,
1337 * set the color to white-on-black beforehand.
1338 *
1339 * @return the string to emit to an ANSI / ECMA-style terminal
1340 */
1341 public String clearAll() {
1342 return "\033[0;37;40m\033[2J";
1343 }
1344
1345 /**
1346 * Clear the line from the cursor (inclusive) to the end of the screen.
1347 * Because some terminals use back-color-erase, set the color to
1348 * white-on-black beforehand. Note package private access.
1349 *
1350 * @return the string to emit to an ANSI / ECMA-style terminal
1351 */
1352 String clearRemainingLine() {
1353 return "\033[0;37;40m\033[K";
1354 }
1355
1356 /**
1357 * Move the cursor to (x, y). Note package private access.
1358 *
1359 * @param x column coordinate. 0 is the left-most column.
1360 * @param y row coordinate. 0 is the top-most row.
1361 * @return the string to emit to an ANSI / ECMA-style terminal
1362 */
1363 String gotoXY(final int x, final int y) {
1364 return String.format("\033[%d;%dH", y + 1, x + 1);
1365 }
1366
1367 /**
1368 * Tell (u)xterm that we want to receive mouse events based on "Any event
1369 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1370 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1371 * See
1372 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1373 *
1374 * Note that this also sets the alternate/primary screen buffer.
1375 *
1376 * @param on If true, enable mouse report and use the alternate screen
1377 * buffer. If false disable mouse reporting and use the primary screen
1378 * buffer.
1379 * @return the string to emit to xterm
1380 */
1381 private String mouse(final boolean on) {
1382 if (on) {
1383 return "\033[?1002;1003;1005;1006h\033[?1049h";
1384 }
1385 return "\033[?1002;1003;1006;1005l\033[?1049l";
1386 }
1387
1388 /**
1389 * Read function runs on a separate thread.
1390 */
1391 public void run() {
1392 boolean done = false;
1393 // available() will often return > 1, so we need to read in chunks to
1394 // stay caught up.
1395 char [] readBuffer = new char[128];
1396 List<TInputEvent> events = new LinkedList<TInputEvent>();
1397
1398 while (!done && !stopReaderThread) {
1399 try {
1400 // We assume that if inputStream has bytes available, then
1401 // input won't block on read().
1402 int n = inputStream.available();
1403 if (n > 0) {
1404 if (readBuffer.length < n) {
1405 // The buffer wasn't big enough, make it huger
1406 readBuffer = new char[readBuffer.length * 2];
1407 }
1408
1409 int rc = input.read(readBuffer, 0, readBuffer.length);
1410 // System.err.printf("read() %d", rc); System.err.flush();
1411 if (rc == -1) {
1412 // This is EOF
1413 done = true;
1414 } else {
1415 for (int i = 0; i < rc; i++) {
1416 int ch = readBuffer[i];
1417 processChar(events, (char)ch);
1418 }
1419 getIdleEvents(events);
1420 if (events.size() > 0) {
1421 // Add to the queue for the backend thread to
1422 // be able to obtain.
1423 synchronized (eventQueue) {
1424 eventQueue.addAll(events);
1425 }
1426 synchronized (listener) {
1427 listener.notifyAll();
1428 }
1429 events.clear();
1430 }
1431 }
1432 } else {
1433 getIdleEvents(events);
1434 if (events.size() > 0) {
1435 synchronized (eventQueue) {
1436 eventQueue.addAll(events);
1437 }
1438 events.clear();
1439 synchronized (listener) {
1440 listener.notifyAll();
1441 }
1442 }
1443
1444 // Wait 10 millis for more data
1445 Thread.sleep(10);
1446 }
1447 // System.err.println("end while loop"); System.err.flush();
1448 } catch (InterruptedException e) {
1449 // SQUASH
1450 } catch (IOException e) {
1451 e.printStackTrace();
1452 done = true;
1453 }
1454 } // while ((done == false) && (stopReaderThread == false))
1455 // System.err.println("*** run() exiting..."); System.err.flush();
1456 }
1457
1458 }