checkstyle'd
[nikiroo-utils.git] / src / jexer / io / ECMA48Terminal.java
CommitLineData
b1589621
KL
1/**
2 * Jexer - Java Text User Interface
3 *
b1589621
KL
4 * License: LGPLv3 or later
5 *
7b5261bc
KL
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.
b1589621
KL
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
7b5261bc
KL
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
b1589621
KL
30 */
31package jexer.io;
32
33import java.io.BufferedReader;
4328bb42
KL
34import java.io.FileDescriptor;
35import java.io.FileInputStream;
b1589621
KL
36import java.io.InputStream;
37import java.io.InputStreamReader;
38import java.io.IOException;
39import java.io.OutputStream;
40import java.io.OutputStreamWriter;
41import java.io.PrintWriter;
42import java.io.Reader;
43import java.io.UnsupportedEncodingException;
44import java.util.ArrayList;
45import java.util.Date;
46import java.util.List;
47import java.util.LinkedList;
48
49import jexer.TKeypress;
50import jexer.bits.Color;
51import jexer.event.TInputEvent;
52import jexer.event.TKeypressEvent;
53import jexer.event.TMouseEvent;
54import jexer.event.TResizeEvent;
55import jexer.session.SessionInfo;
56import jexer.session.TSessionInfo;
57import jexer.session.TTYSessionInfo;
58import static jexer.TKeypress.*;
59
60/**
7b5261bc
KL
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.
b1589621 63 */
4328bb42 64public class ECMA48Terminal implements Runnable {
b1589621
KL
65
66 /**
7b5261bc
KL
67 * The session information.
68 */
69 private SessionInfo sessionInfo;
70
71 /**
72 * Getter for sessionInfo.
73 *
74 * @return the SessionInfo
b1589621 75 */
7b5261bc
KL
76 public final SessionInfo getSessionInfo() {
77 return sessionInfo;
78 }
b1589621 79
4328bb42 80 /**
7b5261bc 81 * The event queue, filled up by a thread reading on input.
4328bb42
KL
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 /**
7b5261bc 91 * The reader thread.
4328bb42
KL
92 */
93 private Thread readerThread;
94
b1589621
KL
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 /**
7b5261bc 107 * States in the input parser.
b1589621
KL
108 */
109 private enum ParseState {
7b5261bc
KL
110 GROUND,
111 ESCAPE,
112 ESCAPE_INTERMEDIATE,
113 CSI_ENTRY,
114 CSI_PARAM,
115 // CSI_INTERMEDIATE,
116 MOUSE
b1589621
KL
117 }
118
119 /**
7b5261bc 120 * Current parsing state.
b1589621
KL
121 */
122 private ParseState state;
123
124 /**
7b5261bc
KL
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.
b1589621
KL
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
4328bb42
KL
164 * constructor, then this InputStreamReader will be bound to System.in
165 * with UTF-8 encoding.
b1589621
KL
166 */
167 private Reader input;
168
4328bb42
KL
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
b1589621
KL
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
217c6107
KL
192 /**
193 * Get the output writer.
194 *
195 * @return the Writer
196 */
197 public PrintWriter getOutput() {
7b5261bc 198 return output;
217c6107
KL
199 }
200
623a1bd1
KL
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() {
7b5261bc
KL
207 synchronized (eventQueue) {
208 return (eventQueue.size() > 0);
209 }
623a1bd1
KL
210 }
211
b1589621 212 /**
7b5261bc
KL
213 * Call 'stty' to set cooked mode.
214 *
215 * <p>Actually executes '/bin/sh -c stty sane cooked &lt; /dev/tty'
b1589621
KL
216 */
217 private void sttyCooked() {
7b5261bc 218 doStty(false);
b1589621
KL
219 }
220
221 /**
7b5261bc
KL
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'
b1589621
KL
227 */
228 private void sttyRaw() {
7b5261bc 229 doStty(true);
b1589621
KL
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 */
7b5261bc
KL
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 == true) {
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 }
b1589621
KL
276 }
277
278 /**
7b5261bc 279 * Constructor sets up state for getEvent().
b1589621
KL
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.
7b5261bc
KL
288 * @throws UnsupportedEncodingException if an exception is thrown when
289 * creating the InputStreamReader
b1589621 290 */
7b5261bc
KL
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();
b1589621
KL
343 }
344
345 /**
7b5261bc 346 * Restore terminal to normal state.
b1589621
KL
347 */
348 public void shutdown() {
4328bb42 349
7b5261bc
KL
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 }
b1589621
KL
384 }
385
386 /**
7b5261bc 387 * Flush output.
b1589621
KL
388 */
389 public void flush() {
7b5261bc 390 output.flush();
b1589621
KL
391 }
392
393 /**
7b5261bc 394 * Reset keyboard/mouse input parser.
b1589621
KL
395 */
396 private void reset() {
7b5261bc
KL
397 state = ParseState.GROUND;
398 params = new ArrayList<String>();
399 paramI = 0;
400 params.clear();
401 params.add("");
b1589621
KL
402 }
403
404 /**
405 * Produce a control character or one of the special ones (ENTER, TAB,
7b5261bc 406 * etc.).
b1589621
KL
407 *
408 * @param ch Unicode code point
7b5261bc 409 * @return one TKeypress event, either a control character (e.g. isKey ==
b1589621
KL
410 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
411 * fnKey == ESC)
412 */
7b5261bc
KL
413 private TKeypressEvent controlChar(final char ch) {
414 TKeypressEvent event = new TKeypressEvent();
415
416 // System.err.printf("controlChar: %02x\n", ch);
417
418 switch (ch) {
419 case 0x0D:
420 // Carriage return --> ENTER
421 event.key = kbEnter;
422 break;
423 case 0x0A:
424 // Linefeed --> ENTER
425 event.key = kbEnter;
426 break;
427 case 0x1B:
428 // ESC
429 event.key = kbEsc;
430 break;
431 case '\t':
432 // TAB
433 event.key = kbTab;
434 break;
435 default:
436 // Make all other control characters come back as the alphabetic
437 // character with the ctrl field set. So SOH would be 'A' +
438 // ctrl.
439 event.key = new TKeypress(false, 0, (char)(ch + 0x40),
440 false, true, false);
441 break;
442 }
443 return event;
b1589621
KL
444 }
445
446 /**
447 * Produce special key from CSI Pn ; Pm ; ... ~
448 *
449 * @return one KEYPRESS event representing a special key
450 */
451 private TInputEvent csiFnKey() {
7b5261bc
KL
452 int key = 0;
453 int modifier = 0;
454 if (params.size() > 0) {
455 key = Integer.parseInt(params.get(0));
456 }
457 if (params.size() > 1) {
458 modifier = Integer.parseInt(params.get(1));
459 }
460 TKeypressEvent event = new TKeypressEvent();
461
462 switch (modifier) {
463 case 0:
464 // No modifier
465 switch (key) {
466 case 1:
467 event.key = kbHome;
468 break;
469 case 2:
470 event.key = kbIns;
471 break;
472 case 3:
473 event.key = kbDel;
474 break;
475 case 4:
476 event.key = kbEnd;
477 break;
478 case 5:
479 event.key = kbPgUp;
480 break;
481 case 6:
482 event.key = kbPgDn;
483 break;
484 case 15:
485 event.key = kbF5;
486 break;
487 case 17:
488 event.key = kbF6;
489 break;
490 case 18:
491 event.key = kbF7;
492 break;
493 case 19:
494 event.key = kbF8;
495 break;
496 case 20:
497 event.key = kbF9;
498 break;
499 case 21:
500 event.key = kbF10;
501 break;
502 case 23:
503 event.key = kbF11;
504 break;
505 case 24:
506 event.key = kbF12;
507 break;
508 default:
509 // Unknown
510 return null;
511 }
512
513 break;
514 case 2:
515 // Shift
516 switch (key) {
517 case 1:
518 event.key = kbShiftHome;
519 break;
520 case 2:
521 event.key = kbShiftIns;
522 break;
523 case 3:
524 event.key = kbShiftDel;
525 break;
526 case 4:
527 event.key = kbShiftEnd;
528 break;
529 case 5:
530 event.key = kbShiftPgUp;
531 break;
532 case 6:
533 event.key = kbShiftPgDn;
534 break;
535 case 15:
536 event.key = kbShiftF5;
537 break;
538 case 17:
539 event.key = kbShiftF6;
540 break;
541 case 18:
542 event.key = kbShiftF7;
543 break;
544 case 19:
545 event.key = kbShiftF8;
546 break;
547 case 20:
548 event.key = kbShiftF9;
549 break;
550 case 21:
551 event.key = kbShiftF10;
552 break;
553 case 23:
554 event.key = kbShiftF11;
555 break;
556 case 24:
557 event.key = kbShiftF12;
558 break;
559 default:
560 // Unknown
561 return null;
562 }
563 break;
564
565 case 3:
566 // Alt
567 switch (key) {
568 case 1:
569 event.key = kbAltHome;
570 break;
571 case 2:
572 event.key = kbAltIns;
573 break;
574 case 3:
575 event.key = kbAltDel;
576 break;
577 case 4:
578 event.key = kbAltEnd;
579 break;
580 case 5:
581 event.key = kbAltPgUp;
582 break;
583 case 6:
584 event.key = kbAltPgDn;
585 break;
586 case 15:
587 event.key = kbAltF5;
588 break;
589 case 17:
590 event.key = kbAltF6;
591 break;
592 case 18:
593 event.key = kbAltF7;
594 break;
595 case 19:
596 event.key = kbAltF8;
597 break;
598 case 20:
599 event.key = kbAltF9;
600 break;
601 case 21:
602 event.key = kbAltF10;
603 break;
604 case 23:
605 event.key = kbAltF11;
606 break;
607 case 24:
608 event.key = kbAltF12;
609 break;
610 default:
611 // Unknown
612 return null;
613 }
614 break;
615
616 case 5:
617 // Ctrl
618 switch (key) {
619 case 1:
620 event.key = kbCtrlHome;
621 break;
622 case 2:
623 event.key = kbCtrlIns;
624 break;
625 case 3:
626 event.key = kbCtrlDel;
627 break;
628 case 4:
629 event.key = kbCtrlEnd;
630 break;
631 case 5:
632 event.key = kbCtrlPgUp;
633 break;
634 case 6:
635 event.key = kbCtrlPgDn;
636 break;
637 case 15:
638 event.key = kbCtrlF5;
639 break;
640 case 17:
641 event.key = kbCtrlF6;
642 break;
643 case 18:
644 event.key = kbCtrlF7;
645 break;
646 case 19:
647 event.key = kbCtrlF8;
648 break;
649 case 20:
650 event.key = kbCtrlF9;
651 break;
652 case 21:
653 event.key = kbCtrlF10;
654 break;
655 case 23:
656 event.key = kbCtrlF11;
657 break;
658 case 24:
659 event.key = kbCtrlF12;
660 break;
661 default:
662 // Unknown
663 return null;
664 }
665 break;
666
667 default:
668 // Unknown
669 return null;
670 }
671
672 // All OK, return a keypress
673 return event;
b1589621
KL
674 }
675
676 /**
677 * Produce mouse events based on "Any event tracking" and UTF-8
678 * coordinates. See
679 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
680 *
681 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
682 */
683 private TInputEvent parseMouse() {
7b5261bc
KL
684 int buttons = params.get(0).charAt(0) - 32;
685 int x = params.get(0).charAt(1) - 32 - 1;
686 int y = params.get(0).charAt(2) - 32 - 1;
687
688 // Clamp X and Y to the physical screen coordinates.
689 if (x >= windowResize.getWidth()) {
690 x = windowResize.getWidth() - 1;
691 }
692 if (y >= windowResize.getHeight()) {
693 y = windowResize.getHeight() - 1;
694 }
695
696 TMouseEvent event = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN);
697 event.x = x;
698 event.y = y;
699 event.absoluteX = x;
700 event.absoluteY = y;
701
702 // System.err.printf("buttons: %04x\r\n", buttons);
703
704 switch (buttons) {
705 case 0:
706 event.mouse1 = true;
707 mouse1 = true;
708 break;
709 case 1:
710 event.mouse2 = true;
711 mouse2 = true;
712 break;
713 case 2:
714 event.mouse3 = true;
715 mouse3 = true;
716 break;
717 case 3:
718 // Release or Move
719 if (!mouse1 && !mouse2 && !mouse3) {
720 event.type = TMouseEvent.Type.MOUSE_MOTION;
721 } else {
722 event.type = TMouseEvent.Type.MOUSE_UP;
723 }
724 if (mouse1) {
725 mouse1 = false;
726 event.mouse1 = true;
727 }
728 if (mouse2) {
729 mouse2 = false;
730 event.mouse2 = true;
731 }
732 if (mouse3) {
733 mouse3 = false;
734 event.mouse3 = true;
735 }
736 break;
737
738 case 32:
739 // Dragging with mouse1 down
740 event.mouse1 = true;
741 mouse1 = true;
742 event.type = TMouseEvent.Type.MOUSE_MOTION;
743 break;
744
745 case 33:
746 // Dragging with mouse2 down
747 event.mouse2 = true;
748 mouse2 = true;
749 event.type = TMouseEvent.Type.MOUSE_MOTION;
750 break;
751
752 case 34:
753 // Dragging with mouse3 down
754 event.mouse3 = true;
755 mouse3 = true;
756 event.type = TMouseEvent.Type.MOUSE_MOTION;
757 break;
758
759 case 96:
760 // Dragging with mouse2 down after wheelUp
761 event.mouse2 = true;
762 mouse2 = true;
763 event.type = TMouseEvent.Type.MOUSE_MOTION;
764 break;
765
766 case 97:
767 // Dragging with mouse2 down after wheelDown
768 event.mouse2 = true;
769 mouse2 = true;
770 event.type = TMouseEvent.Type.MOUSE_MOTION;
771 break;
772
773 case 64:
774 event.mouseWheelUp = true;
775 break;
776
777 case 65:
778 event.mouseWheelDown = true;
779 break;
780
781 default:
782 // Unknown, just make it motion
783 event.type = TMouseEvent.Type.MOUSE_MOTION;
784 break;
785 }
786 return event;
b1589621
KL
787 }
788
789 /**
05dbb28d 790 * Return any events in the IO queue.
b1589621 791 *
623a1bd1 792 * @param queue list to append new events to
05dbb28d 793 */
7b5261bc
KL
794 public void getEvents(final List<TInputEvent> queue) {
795 synchronized (eventQueue) {
796 if (eventQueue.size() > 0) {
797 queue.addAll(eventQueue);
798 eventQueue.clear();
799 }
800 }
05dbb28d
KL
801 }
802
803 /**
623a1bd1 804 * Return any events in the IO queue due to timeout.
b1589621 805 *
623a1bd1 806 * @param queue list to append new events to
b1589621 807 */
7b5261bc
KL
808 public void getIdleEvents(final List<TInputEvent> queue) {
809
810 // Check for new window size
811 sessionInfo.queryWindowSize();
812 int newWidth = sessionInfo.getWindowWidth();
813 int newHeight = sessionInfo.getWindowHeight();
814 if ((newWidth != windowResize.getWidth())
815 || (newHeight != windowResize.getHeight())
816 ) {
817 TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
818 newWidth, newHeight);
819 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
820 newWidth, newHeight);
821 synchronized (eventQueue) {
822 eventQueue.add(event);
823 }
824 }
825
826 synchronized (eventQueue) {
827 if (eventQueue.size() > 0) {
828 queue.addAll(eventQueue);
829 eventQueue.clear();
830 }
831 }
b1589621
KL
832 }
833
834 /**
835 * Parses the next character of input to see if an InputEvent is
836 * fully here.
837 *
623a1bd1 838 * @param events list to append new events to
05dbb28d 839 * @param ch Unicode code point
b1589621 840 */
7b5261bc
KL
841 private void processChar(final List<TInputEvent> events, final char ch) {
842
843 TKeypressEvent keypress;
844 Date now = new Date();
845
846 // ESCDELAY type timeout
847 if (state == ParseState.ESCAPE) {
848 long escDelay = now.getTime() - escapeTime;
849 if (escDelay > 250) {
850 // After 0.25 seconds, assume a true escape character
851 events.add(controlChar((char)0x1B));
852 reset();
853 }
854 }
855
856 // System.err.printf("state: %s ch %c\r\n", state, ch);
857
858 switch (state) {
859 case GROUND:
860
861 if (ch == 0x1B) {
862 state = ParseState.ESCAPE;
863 escapeTime = now.getTime();
864 return;
865 }
866
867 if (ch <= 0x1F) {
868 // Control character
869 events.add(controlChar(ch));
870 reset();
871 return;
872 }
873
874 if (ch >= 0x20) {
875 // Normal character
876 keypress = new TKeypressEvent();
877 keypress.key.isKey = false;
878 keypress.key.ch = ch;
879 events.add(keypress);
880 reset();
881 return;
882 }
883
884 break;
885
886 case ESCAPE:
887 if (ch <= 0x1F) {
888 // ALT-Control character
889 keypress = controlChar(ch);
890 keypress.key.alt = true;
891 events.add(keypress);
892 reset();
893 return;
894 }
895
896 if (ch == 'O') {
897 // This will be one of the function keys
898 state = ParseState.ESCAPE_INTERMEDIATE;
899 return;
900 }
901
902 // '[' goes to CSI_ENTRY
903 if (ch == '[') {
904 state = ParseState.CSI_ENTRY;
905 return;
906 }
907
908 // Everything else is assumed to be Alt-keystroke
909 keypress = new TKeypressEvent();
910 keypress.key.isKey = false;
911 keypress.key.ch = ch;
912 keypress.key.alt = true;
913 if ((ch >= 'A') && (ch <= 'Z')) {
914 keypress.key.shift = true;
915 }
916 events.add(keypress);
917 reset();
918 return;
919
920 case ESCAPE_INTERMEDIATE:
921 if ((ch >= 'P') && (ch <= 'S')) {
922 // Function key
923 keypress = new TKeypressEvent();
924 keypress.key.isKey = true;
925 switch (ch) {
926 case 'P':
927 keypress.key.fnKey = TKeypress.F1;
928 break;
929 case 'Q':
930 keypress.key.fnKey = TKeypress.F2;
931 break;
932 case 'R':
933 keypress.key.fnKey = TKeypress.F3;
934 break;
935 case 'S':
936 keypress.key.fnKey = TKeypress.F4;
937 break;
938 default:
939 break;
940 }
941 events.add(keypress);
942 reset();
943 return;
944 }
945
946 // Unknown keystroke, ignore
947 reset();
948 return;
949
950 case CSI_ENTRY:
951 // Numbers - parameter values
952 if ((ch >= '0') && (ch <= '9')) {
953 params.set(paramI, params.get(paramI) + ch);
954 state = ParseState.CSI_PARAM;
955 return;
956 }
957 // Parameter separator
958 if (ch == ';') {
959 paramI++;
960 params.set(paramI, "");
961 return;
962 }
963
964 if ((ch >= 0x30) && (ch <= 0x7E)) {
965 switch (ch) {
966 case 'A':
967 // Up
968 keypress = new TKeypressEvent();
969 keypress.key.isKey = true;
970 keypress.key.fnKey = TKeypress.UP;
971 if (params.size() > 1) {
972 if (params.get(1).equals("2")) {
973 keypress.key.shift = true;
974 }
975 if (params.get(1).equals("5")) {
976 keypress.key.ctrl = true;
977 }
978 if (params.get(1).equals("3")) {
979 keypress.key.alt = true;
980 }
981 }
982 events.add(keypress);
983 reset();
984 return;
985 case 'B':
986 // Down
987 keypress = new TKeypressEvent();
988 keypress.key.isKey = true;
989 keypress.key.fnKey = TKeypress.DOWN;
990 if (params.size() > 1) {
991 if (params.get(1).equals("2")) {
992 keypress.key.shift = true;
993 }
994 if (params.get(1).equals("5")) {
995 keypress.key.ctrl = true;
996 }
997 if (params.get(1).equals("3")) {
998 keypress.key.alt = true;
999 }
1000 }
1001 events.add(keypress);
1002 reset();
1003 return;
1004 case 'C':
1005 // Right
1006 keypress = new TKeypressEvent();
1007 keypress.key.isKey = true;
1008 keypress.key.fnKey = TKeypress.RIGHT;
1009 if (params.size() > 1) {
1010 if (params.get(1).equals("2")) {
1011 keypress.key.shift = true;
1012 }
1013 if (params.get(1).equals("5")) {
1014 keypress.key.ctrl = true;
1015 }
1016 if (params.get(1).equals("3")) {
1017 keypress.key.alt = true;
1018 }
1019 }
1020 events.add(keypress);
1021 reset();
1022 return;
1023 case 'D':
1024 // Left
1025 keypress = new TKeypressEvent();
1026 keypress.key.isKey = true;
1027 keypress.key.fnKey = TKeypress.LEFT;
1028 if (params.size() > 1) {
1029 if (params.get(1).equals("2")) {
1030 keypress.key.shift = true;
1031 }
1032 if (params.get(1).equals("5")) {
1033 keypress.key.ctrl = true;
1034 }
1035 if (params.get(1).equals("3")) {
1036 keypress.key.alt = true;
1037 }
1038 }
1039 events.add(keypress);
1040 reset();
1041 return;
1042 case 'H':
1043 // Home
1044 keypress = new TKeypressEvent();
1045 keypress.key.isKey = true;
1046 keypress.key.fnKey = TKeypress.HOME;
1047 events.add(keypress);
1048 reset();
1049 return;
1050 case 'F':
1051 // End
1052 keypress = new TKeypressEvent();
1053 keypress.key.isKey = true;
1054 keypress.key.fnKey = TKeypress.END;
1055 events.add(keypress);
1056 reset();
1057 return;
1058 case 'Z':
1059 // CBT - Cursor backward X tab stops (default 1)
1060 keypress = new TKeypressEvent();
1061 keypress.key.isKey = true;
1062 keypress.key.fnKey = TKeypress.BTAB;
1063 events.add(keypress);
1064 reset();
1065 return;
1066 case 'M':
1067 // Mouse position
1068 state = ParseState.MOUSE;
1069 return;
1070 default:
1071 break;
1072 }
1073 }
1074
1075 // Unknown keystroke, ignore
1076 reset();
1077 return;
1078
1079 case CSI_PARAM:
1080 // Numbers - parameter values
1081 if ((ch >= '0') && (ch <= '9')) {
1082 params.set(paramI, params.get(paramI) + ch);
1083 state = ParseState.CSI_PARAM;
1084 return;
1085 }
1086 // Parameter separator
1087 if (ch == ';') {
1088 paramI++;
1089 params.set(paramI, "");
1090 return;
1091 }
1092
1093 if (ch == '~') {
1094 events.add(csiFnKey());
1095 reset();
1096 return;
1097 }
1098
1099 if ((ch >= 0x30) && (ch <= 0x7E)) {
1100 switch (ch) {
1101 case 'A':
1102 // Up
1103 keypress = new TKeypressEvent();
1104 keypress.key.isKey = true;
1105 keypress.key.fnKey = TKeypress.UP;
1106 if (params.size() > 1) {
1107 if (params.get(1).equals("2")) {
1108 keypress.key.shift = true;
1109 }
1110 if (params.get(1).equals("5")) {
1111 keypress.key.ctrl = true;
1112 }
1113 if (params.get(1).equals("3")) {
1114 keypress.key.alt = true;
1115 }
1116 }
1117 events.add(keypress);
1118 reset();
1119 return;
1120 case 'B':
1121 // Down
1122 keypress = new TKeypressEvent();
1123 keypress.key.isKey = true;
1124 keypress.key.fnKey = TKeypress.DOWN;
1125 if (params.size() > 1) {
1126 if (params.get(1).equals("2")) {
1127 keypress.key.shift = true;
1128 }
1129 if (params.get(1).equals("5")) {
1130 keypress.key.ctrl = true;
1131 }
1132 if (params.get(1).equals("3")) {
1133 keypress.key.alt = true;
1134 }
1135 }
1136 events.add(keypress);
1137 reset();
1138 return;
1139 case 'C':
1140 // Right
1141 keypress = new TKeypressEvent();
1142 keypress.key.isKey = true;
1143 keypress.key.fnKey = TKeypress.RIGHT;
1144 if (params.size() > 1) {
1145 if (params.get(1).equals("2")) {
1146 keypress.key.shift = true;
1147 }
1148 if (params.get(1).equals("5")) {
1149 keypress.key.ctrl = true;
1150 }
1151 if (params.get(1).equals("3")) {
1152 keypress.key.alt = true;
1153 }
1154 }
1155 events.add(keypress);
1156 reset();
1157 return;
1158 case 'D':
1159 // Left
1160 keypress = new TKeypressEvent();
1161 keypress.key.isKey = true;
1162 keypress.key.fnKey = TKeypress.LEFT;
1163 if (params.size() > 1) {
1164 if (params.get(1).equals("2")) {
1165 keypress.key.shift = true;
1166 }
1167 if (params.get(1).equals("5")) {
1168 keypress.key.ctrl = true;
1169 }
1170 if (params.get(1).equals("3")) {
1171 keypress.key.alt = true;
1172 }
1173 }
1174 events.add(keypress);
1175 reset();
1176 return;
1177 default:
1178 break;
1179 }
1180 }
1181
1182 // Unknown keystroke, ignore
1183 reset();
1184 return;
1185
1186 case MOUSE:
1187 params.set(0, params.get(paramI) + ch);
1188 if (params.get(0).length() == 3) {
1189 // We have enough to generate a mouse event
1190 events.add(parseMouse());
1191 reset();
1192 }
1193 return;
1194
1195 default:
1196 break;
1197 }
1198
1199 // This "should" be impossible to reach
1200 return;
b1589621
KL
1201 }
1202
1203 /**
05dbb28d
KL
1204 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1205 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1206 * enabled.
b1589621 1207 *
05dbb28d
KL
1208 * @param on if true, enable metaSendsEscape
1209 * @return the string to emit to xterm
b1589621 1210 */
7b5261bc
KL
1211 public String xtermMetaSendsEscape(final boolean on) {
1212 if (on) {
1213 return "\033[?1036h\033[?1034l";
1214 }
1215 return "\033[?1036l";
b1589621
KL
1216 }
1217
1218 /**
05dbb28d
KL
1219 * Convert a list of SGR parameters into a full escape sequence. This
1220 * also eliminates a trailing ';' which would otherwise reset everything
1221 * to white-on-black not-bold.
b1589621 1222 *
05dbb28d
KL
1223 * @param str string of parameters, e.g. "31;1;"
1224 * @return the string to emit to an ANSI / ECMA-style terminal,
1225 * e.g. "\033[31;1m"
b1589621 1226 */
7b5261bc
KL
1227 public String addHeaderSGR(String str) {
1228 if (str.length() > 0) {
1229 // Nix any trailing ';' because that resets all attributes
1230 while (str.endsWith(":")) {
1231 str = str.substring(0, str.length() - 1);
1232 }
1233 }
1234 return "\033[" + str + "m";
b1589621
KL
1235 }
1236
1237 /**
1238 * Create a SGR parameter sequence for a single color change.
1239 *
05dbb28d
KL
1240 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1241 * @param foreground if true, this is a foreground color
1242 * @return the string to emit to an ANSI / ECMA-style terminal,
1243 * e.g. "\033[42m"
b1589621 1244 */
7b5261bc
KL
1245 public String color(final Color color, final boolean foreground) {
1246 return color(color, foreground, true);
b1589621
KL
1247 }
1248
1249 /**
1250 * Create a SGR parameter sequence for a single color change.
1251 *
05dbb28d
KL
1252 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1253 * @param foreground if true, this is a foreground color
1254 * @param header if true, make the full header, otherwise just emit the
1255 * color parameter e.g. "42;"
1256 * @return the string to emit to an ANSI / ECMA-style terminal,
1257 * e.g. "\033[42m"
b1589621 1258 */
7b5261bc
KL
1259 public String color(final Color color, final boolean foreground,
1260 final boolean header) {
1261
1262 int ecmaColor = color.getValue();
1263
1264 // Convert Color.* values to SGR numerics
1265 if (foreground) {
1266 ecmaColor += 30;
1267 } else {
1268 ecmaColor += 40;
1269 }
1270
1271 if (header) {
1272 return String.format("\033[%dm", ecmaColor);
1273 } else {
1274 return String.format("%d;", ecmaColor);
1275 }
b1589621
KL
1276 }
1277
1278 /**
1279 * Create a SGR parameter sequence for both foreground and
1280 * background color change.
1281 *
05dbb28d
KL
1282 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1283 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1284 * @return the string to emit to an ANSI / ECMA-style terminal,
1285 * e.g. "\033[31;42m"
b1589621 1286 */
7b5261bc
KL
1287 public String color(final Color foreColor, final Color backColor) {
1288 return color(foreColor, backColor, true);
b1589621
KL
1289 }
1290
1291 /**
1292 * Create a SGR parameter sequence for both foreground and
1293 * background color change.
1294 *
05dbb28d
KL
1295 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1296 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1297 * @param header if true, make the full header, otherwise just emit the
1298 * color parameter e.g. "31;42;"
1299 * @return the string to emit to an ANSI / ECMA-style terminal,
1300 * e.g. "\033[31;42m"
b1589621 1301 */
7b5261bc
KL
1302 public String color(final Color foreColor, final Color backColor,
1303 final boolean header) {
b1589621 1304
7b5261bc
KL
1305 int ecmaForeColor = foreColor.getValue();
1306 int ecmaBackColor = backColor.getValue();
b1589621 1307
7b5261bc
KL
1308 // Convert Color.* values to SGR numerics
1309 ecmaBackColor += 40;
1310 ecmaForeColor += 30;
b1589621 1311
7b5261bc
KL
1312 if (header) {
1313 return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
1314 } else {
1315 return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
1316 }
b1589621
KL
1317 }
1318
1319 /**
1320 * Create a SGR parameter sequence for foreground, background, and
05dbb28d
KL
1321 * several attributes. This sequence first resets all attributes to
1322 * default, then sets attributes as per the parameters.
b1589621 1323 *
05dbb28d
KL
1324 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1325 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1326 * @param bold if true, set bold
1327 * @param reverse if true, set reverse
1328 * @param blink if true, set blink
1329 * @param underline if true, set underline
1330 * @return the string to emit to an ANSI / ECMA-style terminal,
1331 * e.g. "\033[0;1;31;42m"
b1589621 1332 */
7b5261bc
KL
1333 public String color(final Color foreColor, final Color backColor,
1334 final boolean bold, final boolean reverse, final boolean blink,
1335 final boolean underline) {
1336
1337 int ecmaForeColor = foreColor.getValue();
1338 int ecmaBackColor = backColor.getValue();
1339
1340 // Convert Color.* values to SGR numerics
1341 ecmaBackColor += 40;
1342 ecmaForeColor += 30;
1343
1344 StringBuilder sb = new StringBuilder();
1345 if ( bold && reverse && blink && !underline ) {
1346 sb.append("\033[0;1;7;5;");
1347 } else if ( bold && reverse && !blink && !underline ) {
1348 sb.append("\033[0;1;7;");
1349 } else if ( !bold && reverse && blink && !underline ) {
1350 sb.append("\033[0;7;5;");
1351 } else if ( bold && !reverse && blink && !underline ) {
1352 sb.append("\033[0;1;5;");
1353 } else if ( bold && !reverse && !blink && !underline ) {
1354 sb.append("\033[0;1;");
1355 } else if ( !bold && reverse && !blink && !underline ) {
1356 sb.append("\033[0;7;");
1357 } else if ( !bold && !reverse && blink && !underline) {
1358 sb.append("\033[0;5;");
1359 } else if ( bold && reverse && blink && underline ) {
1360 sb.append("\033[0;1;7;5;4;");
1361 } else if ( bold && reverse && !blink && underline ) {
1362 sb.append("\033[0;1;7;4;");
1363 } else if ( !bold && reverse && blink && underline ) {
1364 sb.append("\033[0;7;5;4;");
1365 } else if ( bold && !reverse && blink && underline ) {
1366 sb.append("\033[0;1;5;4;");
1367 } else if ( bold && !reverse && !blink && underline ) {
1368 sb.append("\033[0;1;4;");
1369 } else if ( !bold && reverse && !blink && underline ) {
1370 sb.append("\033[0;7;4;");
1371 } else if ( !bold && !reverse && blink && underline) {
1372 sb.append("\033[0;5;4;");
1373 } else if ( !bold && !reverse && !blink && underline) {
1374 sb.append("\033[0;4;");
1375 } else {
1376 assert (!bold && !reverse && !blink && !underline);
1377 sb.append("\033[0;");
1378 }
1379 sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
1380 return sb.toString();
b1589621
KL
1381 }
1382
1383 /**
1384 * Create a SGR parameter sequence for enabling reverse color.
1385 *
05dbb28d
KL
1386 * @param on if true, turn on reverse
1387 * @return the string to emit to an ANSI / ECMA-style terminal,
1388 * e.g. "\033[7m"
b1589621 1389 */
7b5261bc
KL
1390 public String reverse(final boolean on) {
1391 if (on) {
1392 return "\033[7m";
1393 }
1394 return "\033[27m";
b1589621
KL
1395 }
1396
1397 /**
1398 * Create a SGR parameter sequence to reset to defaults.
1399 *
05dbb28d
KL
1400 * @return the string to emit to an ANSI / ECMA-style terminal,
1401 * e.g. "\033[0m"
b1589621 1402 */
7b5261bc
KL
1403 public String normal() {
1404 return normal(true);
b1589621
KL
1405 }
1406
1407 /**
1408 * Create a SGR parameter sequence to reset to defaults.
1409 *
05dbb28d
KL
1410 * @param header if true, make the full header, otherwise just emit the
1411 * bare parameter e.g. "0;"
1412 * @return the string to emit to an ANSI / ECMA-style terminal,
1413 * e.g. "\033[0m"
b1589621 1414 */
7b5261bc
KL
1415 public String normal(final boolean header) {
1416 if (header) {
1417 return "\033[0;37;40m";
1418 }
1419 return "0;37;40";
b1589621
KL
1420 }
1421
1422 /**
1423 * Create a SGR parameter sequence for enabling boldface.
1424 *
05dbb28d
KL
1425 * @param on if true, turn on bold
1426 * @return the string to emit to an ANSI / ECMA-style terminal,
1427 * e.g. "\033[1m"
b1589621 1428 */
7b5261bc
KL
1429 public String bold(final boolean on) {
1430 return bold(on, true);
b1589621
KL
1431 }
1432
1433 /**
1434 * Create a SGR parameter sequence for enabling boldface.
1435 *
05dbb28d
KL
1436 * @param on if true, turn on bold
1437 * @param header if true, make the full header, otherwise just emit the
1438 * bare parameter e.g. "1;"
1439 * @return the string to emit to an ANSI / ECMA-style terminal,
1440 * e.g. "\033[1m"
b1589621 1441 */
7b5261bc
KL
1442 public String bold(final boolean on, final boolean header) {
1443 if (header) {
1444 if (on) {
1445 return "\033[1m";
1446 }
1447 return "\033[22m";
1448 }
1449 if (on) {
1450 return "1;";
1451 }
1452 return "22;";
b1589621
KL
1453 }
1454
1455 /**
1456 * Create a SGR parameter sequence for enabling blinking text.
1457 *
05dbb28d
KL
1458 * @param on if true, turn on blink
1459 * @return the string to emit to an ANSI / ECMA-style terminal,
1460 * e.g. "\033[5m"
b1589621 1461 */
7b5261bc
KL
1462 public String blink(final boolean on) {
1463 return blink(on, true);
b1589621
KL
1464 }
1465
1466 /**
1467 * Create a SGR parameter sequence for enabling blinking text.
1468 *
05dbb28d
KL
1469 * @param on if true, turn on blink
1470 * @param header if true, make the full header, otherwise just emit the
1471 * bare parameter e.g. "5;"
1472 * @return the string to emit to an ANSI / ECMA-style terminal,
1473 * e.g. "\033[5m"
b1589621 1474 */
7b5261bc
KL
1475 public String blink(final boolean on, final boolean header) {
1476 if (header) {
1477 if (on) {
1478 return "\033[5m";
1479 }
1480 return "\033[25m";
1481 }
1482 if (on) {
1483 return "5;";
1484 }
1485 return "25;";
b1589621
KL
1486 }
1487
1488 /**
05dbb28d
KL
1489 * Create a SGR parameter sequence for enabling underline / underscored
1490 * text.
b1589621 1491 *
05dbb28d
KL
1492 * @param on if true, turn on underline
1493 * @return the string to emit to an ANSI / ECMA-style terminal,
1494 * e.g. "\033[4m"
b1589621 1495 */
7b5261bc
KL
1496 public String underline(final boolean on) {
1497 if (on) {
1498 return "\033[4m";
1499 }
1500 return "\033[24m";
b1589621
KL
1501 }
1502
1503 /**
1504 * Create a SGR parameter sequence for enabling the visible cursor.
1505 *
05dbb28d
KL
1506 * @param on if true, turn on cursor
1507 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1508 */
7b5261bc
KL
1509 public String cursor(final boolean on) {
1510 if (on && !cursorOn) {
1511 cursorOn = true;
1512 return "\033[?25h";
1513 }
1514 if (!on && cursorOn) {
1515 cursorOn = false;
1516 return "\033[?25l";
1517 }
1518 return "";
b1589621
KL
1519 }
1520
1521 /**
1522 * Clear the entire screen. Because some terminals use back-color-erase,
1523 * set the color to white-on-black beforehand.
1524 *
05dbb28d 1525 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1526 */
7b5261bc
KL
1527 public String clearAll() {
1528 return "\033[0;37;40m\033[2J";
b1589621
KL
1529 }
1530
1531 /**
1532 * Clear the line from the cursor (inclusive) to the end of the screen.
1533 * Because some terminals use back-color-erase, set the color to
1534 * white-on-black beforehand.
1535 *
05dbb28d 1536 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1537 */
7b5261bc
KL
1538 public String clearRemainingLine() {
1539 return "\033[0;37;40m\033[K";
b1589621
KL
1540 }
1541
1542 /**
1543 * Clear the line up the cursor (inclusive). Because some terminals use
1544 * back-color-erase, set the color to white-on-black beforehand.
1545 *
05dbb28d 1546 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1547 */
7b5261bc
KL
1548 public String clearPreceedingLine() {
1549 return "\033[0;37;40m\033[1K";
b1589621
KL
1550 }
1551
1552 /**
1553 * Clear the line. Because some terminals use back-color-erase, set the
1554 * color to white-on-black beforehand.
1555 *
05dbb28d 1556 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1557 */
7b5261bc
KL
1558 public String clearLine() {
1559 return "\033[0;37;40m\033[2K";
b1589621
KL
1560 }
1561
1562 /**
1563 * Move the cursor to the top-left corner.
1564 *
05dbb28d 1565 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1566 */
7b5261bc
KL
1567 public String home() {
1568 return "\033[H";
b1589621
KL
1569 }
1570
1571 /**
1572 * Move the cursor to (x, y).
1573 *
05dbb28d
KL
1574 * @param x column coordinate. 0 is the left-most column.
1575 * @param y row coordinate. 0 is the top-most row.
1576 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1577 */
7b5261bc
KL
1578 public String gotoXY(final int x, final int y) {
1579 return String.format("\033[%d;%dH", y + 1, x + 1);
b1589621
KL
1580 }
1581
1582 /**
05dbb28d
KL
1583 * Tell (u)xterm that we want to receive mouse events based on "Any event
1584 * tracking" and UTF-8 coordinates. See
b1589621
KL
1585 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1586 *
05dbb28d 1587 * Note that this also sets the alternate/primary screen buffer.
b1589621 1588 *
05dbb28d
KL
1589 * @param on If true, enable mouse report and use the alternate screen
1590 * buffer. If false disable mouse reporting and use the primary screen
1591 * buffer.
1592 * @return the string to emit to xterm
b1589621 1593 */
7b5261bc
KL
1594 public String mouse(final boolean on) {
1595 if (on) {
1596 return "\033[?1003;1005h\033[?1049h";
1597 }
1598 return "\033[?1003;1005l\033[?1049l";
b1589621
KL
1599 }
1600
4328bb42
KL
1601 /**
1602 * Read function runs on a separate thread.
1603 */
1604 public void run() {
7b5261bc
KL
1605 boolean done = false;
1606 // available() will often return > 1, so we need to read in chunks to
1607 // stay caught up.
1608 char [] readBuffer = new char[128];
1609 List<TInputEvent> events = new LinkedList<TInputEvent>();
1610
1611 while (!done && !stopReaderThread) {
1612 try {
1613 // We assume that if inputStream has bytes available, then
1614 // input won't block on read().
1615 int n = inputStream.available();
1616 if (n > 0) {
1617 if (readBuffer.length < n) {
1618 // The buffer wasn't big enough, make it huger
1619 readBuffer = new char[readBuffer.length * 2];
1620 }
1621
1622 int rc = input.read(readBuffer, 0, n);
1623 // System.err.printf("read() %d", rc); System.err.flush();
1624 if (rc == -1) {
1625 // This is EOF
1626 done = true;
1627 } else {
1628 for (int i = 0; i < rc; i++) {
1629 int ch = readBuffer[i];
1630 processChar(events, (char)ch);
1631 if (events.size() > 0) {
1632 // Add to the queue for the backend thread to
1633 // be able to obtain.
1634 synchronized (eventQueue) {
1635 eventQueue.addAll(events);
1636 }
1637 // Now wake up the backend
1638 synchronized (this) {
1639 this.notifyAll();
1640 }
1641 events.clear();
1642 }
1643 }
1644 }
1645 } else {
1646 // Wait 5 millis for more data
1647 Thread.sleep(5);
1648 }
1649 // System.err.println("end while loop"); System.err.flush();
1650 } catch (InterruptedException e) {
1651 // SQUASH
1652 } catch (IOException e) {
1653 e.printStackTrace();
1654 done = true;
1655 }
1656 } // while ((done == false) && (stopReaderThread == false))
1657 // System.err.println("*** run() exiting..."); System.err.flush();
4328bb42
KL
1658 }
1659
b1589621 1660}