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