e232176be224d925c9305e10f6eb122c3cd33046
[nikiroo-utils.git] / src / jexer / io / 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 * Create a SGR parameter sequence for a single color change. Note
1170 * package private access.
1171 *
1172 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1173 * @param foreground if true, this is a foreground color
1174 * @return the string to emit to an ANSI / ECMA-style terminal,
1175 * e.g. "\033[42m"
1176 */
1177 String color(final Color color, final boolean foreground) {
1178 return color(color, foreground, true);
1179 }
1180
1181 /**
1182 * Create a SGR parameter sequence for a single color change.
1183 *
1184 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1185 * @param foreground if true, this is a foreground color
1186 * @param header if true, make the full header, otherwise just emit the
1187 * color parameter e.g. "42;"
1188 * @return the string to emit to an ANSI / ECMA-style terminal,
1189 * e.g. "\033[42m"
1190 */
1191 private String color(final Color color, final boolean foreground,
1192 final boolean header) {
1193
1194 int ecmaColor = color.getValue();
1195
1196 // Convert Color.* values to SGR numerics
1197 if (foreground) {
1198 ecmaColor += 30;
1199 } else {
1200 ecmaColor += 40;
1201 }
1202
1203 if (header) {
1204 return String.format("\033[%dm", ecmaColor);
1205 } else {
1206 return String.format("%d;", ecmaColor);
1207 }
1208 }
1209
1210 /**
1211 * Create a SGR parameter sequence for both foreground and background
1212 * color change. Note package private access.
1213 *
1214 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1215 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1216 * @return the string to emit to an ANSI / ECMA-style terminal,
1217 * e.g. "\033[31;42m"
1218 */
1219 String color(final Color foreColor, final Color backColor) {
1220 return color(foreColor, backColor, true);
1221 }
1222
1223 /**
1224 * Create a SGR parameter sequence for both foreground and
1225 * background color change.
1226 *
1227 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1228 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1229 * @param header if true, make the full header, otherwise just emit the
1230 * color parameter e.g. "31;42;"
1231 * @return the string to emit to an ANSI / ECMA-style terminal,
1232 * e.g. "\033[31;42m"
1233 */
1234 private String color(final Color foreColor, final Color backColor,
1235 final boolean header) {
1236
1237 int ecmaForeColor = foreColor.getValue();
1238 int ecmaBackColor = backColor.getValue();
1239
1240 // Convert Color.* values to SGR numerics
1241 ecmaBackColor += 40;
1242 ecmaForeColor += 30;
1243
1244 if (header) {
1245 return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
1246 } else {
1247 return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
1248 }
1249 }
1250
1251 /**
1252 * Create a SGR parameter sequence for foreground, background, and
1253 * several attributes. This sequence first resets all attributes to
1254 * default, then sets attributes as per the parameters. Note package
1255 * private access.
1256 *
1257 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1258 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1259 * @param bold if true, set bold
1260 * @param reverse if true, set reverse
1261 * @param blink if true, set blink
1262 * @param underline if true, set underline
1263 * @return the string to emit to an ANSI / ECMA-style terminal,
1264 * e.g. "\033[0;1;31;42m"
1265 */
1266 String color(final Color foreColor, final Color backColor,
1267 final boolean bold, final boolean reverse, final boolean blink,
1268 final boolean underline) {
1269
1270 int ecmaForeColor = foreColor.getValue();
1271 int ecmaBackColor = backColor.getValue();
1272
1273 // Convert Color.* values to SGR numerics
1274 ecmaBackColor += 40;
1275 ecmaForeColor += 30;
1276
1277 StringBuilder sb = new StringBuilder();
1278 if ( bold && reverse && blink && !underline ) {
1279 sb.append("\033[0;1;7;5;");
1280 } else if ( bold && reverse && !blink && !underline ) {
1281 sb.append("\033[0;1;7;");
1282 } else if ( !bold && reverse && blink && !underline ) {
1283 sb.append("\033[0;7;5;");
1284 } else if ( bold && !reverse && blink && !underline ) {
1285 sb.append("\033[0;1;5;");
1286 } else if ( bold && !reverse && !blink && !underline ) {
1287 sb.append("\033[0;1;");
1288 } else if ( !bold && reverse && !blink && !underline ) {
1289 sb.append("\033[0;7;");
1290 } else if ( !bold && !reverse && blink && !underline) {
1291 sb.append("\033[0;5;");
1292 } else if ( bold && reverse && blink && underline ) {
1293 sb.append("\033[0;1;7;5;4;");
1294 } else if ( bold && reverse && !blink && underline ) {
1295 sb.append("\033[0;1;7;4;");
1296 } else if ( !bold && reverse && blink && underline ) {
1297 sb.append("\033[0;7;5;4;");
1298 } else if ( bold && !reverse && blink && underline ) {
1299 sb.append("\033[0;1;5;4;");
1300 } else if ( bold && !reverse && !blink && underline ) {
1301 sb.append("\033[0;1;4;");
1302 } else if ( !bold && reverse && !blink && underline ) {
1303 sb.append("\033[0;7;4;");
1304 } else if ( !bold && !reverse && blink && underline) {
1305 sb.append("\033[0;5;4;");
1306 } else if ( !bold && !reverse && !blink && underline) {
1307 sb.append("\033[0;4;");
1308 } else {
1309 assert (!bold && !reverse && !blink && !underline);
1310 sb.append("\033[0;");
1311 }
1312 sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
1313 return sb.toString();
1314 }
1315
1316 /**
1317 * Create a SGR parameter sequence to reset to defaults. Note package
1318 * private access.
1319 *
1320 * @return the string to emit to an ANSI / ECMA-style terminal,
1321 * e.g. "\033[0m"
1322 */
1323 String normal() {
1324 return normal(true);
1325 }
1326
1327 /**
1328 * Create a SGR parameter sequence to reset to defaults.
1329 *
1330 * @param header if true, make the full header, otherwise just emit the
1331 * bare parameter e.g. "0;"
1332 * @return the string to emit to an ANSI / ECMA-style terminal,
1333 * e.g. "\033[0m"
1334 */
1335 private String normal(final boolean header) {
1336 if (header) {
1337 return "\033[0;37;40m";
1338 }
1339 return "0;37;40";
1340 }
1341
1342 /**
1343 * Create a SGR parameter sequence for enabling the visible cursor. Note
1344 * package private access.
1345 *
1346 * @param on if true, turn on cursor
1347 * @return the string to emit to an ANSI / ECMA-style terminal
1348 */
1349 String cursor(final boolean on) {
1350 if (on && !cursorOn) {
1351 cursorOn = true;
1352 return "\033[?25h";
1353 }
1354 if (!on && cursorOn) {
1355 cursorOn = false;
1356 return "\033[?25l";
1357 }
1358 return "";
1359 }
1360
1361 /**
1362 * Clear the entire screen. Because some terminals use back-color-erase,
1363 * set the color to white-on-black beforehand.
1364 *
1365 * @return the string to emit to an ANSI / ECMA-style terminal
1366 */
1367 public String clearAll() {
1368 return "\033[0;37;40m\033[2J";
1369 }
1370
1371 /**
1372 * Clear the line from the cursor (inclusive) to the end of the screen.
1373 * Because some terminals use back-color-erase, set the color to
1374 * white-on-black beforehand. Note package private access.
1375 *
1376 * @return the string to emit to an ANSI / ECMA-style terminal
1377 */
1378 String clearRemainingLine() {
1379 return "\033[0;37;40m\033[K";
1380 }
1381
1382 /**
1383 * Move the cursor to (x, y). Note package private access.
1384 *
1385 * @param x column coordinate. 0 is the left-most column.
1386 * @param y row coordinate. 0 is the top-most row.
1387 * @return the string to emit to an ANSI / ECMA-style terminal
1388 */
1389 String gotoXY(final int x, final int y) {
1390 return String.format("\033[%d;%dH", y + 1, x + 1);
1391 }
1392
1393 /**
1394 * Tell (u)xterm that we want to receive mouse events based on "Any event
1395 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1396 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1397 * See
1398 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1399 *
1400 * Note that this also sets the alternate/primary screen buffer.
1401 *
1402 * @param on If true, enable mouse report and use the alternate screen
1403 * buffer. If false disable mouse reporting and use the primary screen
1404 * buffer.
1405 * @return the string to emit to xterm
1406 */
1407 private String mouse(final boolean on) {
1408 if (on) {
1409 return "\033[?1003;1005;1006h\033[?1049h";
1410 }
1411 return "\033[?1003;1006;1005l\033[?1049l";
1412 }
1413
1414 /**
1415 * Read function runs on a separate thread.
1416 */
1417 public void run() {
1418 boolean done = false;
1419 // available() will often return > 1, so we need to read in chunks to
1420 // stay caught up.
1421 char [] readBuffer = new char[128];
1422 List<TInputEvent> events = new LinkedList<TInputEvent>();
1423
1424 while (!done && !stopReaderThread) {
1425 try {
1426 // We assume that if inputStream has bytes available, then
1427 // input won't block on read().
1428 int n = inputStream.available();
1429 if (n > 0) {
1430 if (readBuffer.length < n) {
1431 // The buffer wasn't big enough, make it huger
1432 readBuffer = new char[readBuffer.length * 2];
1433 }
1434
1435 int rc = input.read(readBuffer, 0, readBuffer.length);
1436 // System.err.printf("read() %d", rc); System.err.flush();
1437 if (rc == -1) {
1438 // This is EOF
1439 done = true;
1440 } else {
1441 for (int i = 0; i < rc; i++) {
1442 int ch = readBuffer[i];
1443 processChar(events, (char)ch);
1444 }
1445 getIdleEvents(events);
1446 if (events.size() > 0) {
1447 // Add to the queue for the backend thread to
1448 // be able to obtain.
1449 synchronized (eventQueue) {
1450 eventQueue.addAll(events);
1451 }
1452 synchronized (listener) {
1453 listener.notifyAll();
1454 }
1455 events.clear();
1456 }
1457 }
1458 } else {
1459 getIdleEvents(events);
1460 if (events.size() > 0) {
1461 synchronized (eventQueue) {
1462 eventQueue.addAll(events);
1463 }
1464 events.clear();
1465 synchronized (listener) {
1466 listener.notifyAll();
1467 }
1468 }
1469
1470 // Wait 10 millis for more data
1471 Thread.sleep(10);
1472 }
1473 // System.err.println("end while loop"); System.err.flush();
1474 } catch (InterruptedException e) {
1475 // SQUASH
1476 } catch (IOException e) {
1477 e.printStackTrace();
1478 done = true;
1479 }
1480 } // while ((done == false) && (stopReaderThread == false))
1481 // System.err.println("*** run() exiting..."); System.err.flush();
1482 }
1483
1484 }