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