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