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