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