ECMA48Screen 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 /**
699 * Parses the next character of input to see if an InputEvent is
700 * fully here.
701 *
702 * Params:
703 * ch = Unicode code point
704 * noChar = if true, ignore ch. This is currently used to
705 * return a bare ESC and RESIZE events.
706 *
707 * Returns:
708 * list of new events (which may be empty)
709 */
710 public List<TInputEvent> getEvents(char ch) {
711 return getEvents(ch, false);
712 }
713
714 /**
715 * Parses the next character of input to see if an InputEvent is
716 * fully here.
717 *
718 * Params:
719 * ch = Unicode code point
720 * noChar = if true, ignore ch. This is currently used to
721 * return a bare ESC and RESIZE events.
722 *
723 * Returns:
724 * list of new events (which may be empty)
725 */
726 public List<TInputEvent> getEvents(char ch, boolean noChar) {
727 List<TInputEvent> events = new LinkedList<TInputEvent>();
728
729 TKeypressEvent keypress;
730 Date now = new Date();
731
732 // ESCDELAY type timeout
733 if (state == ParseState.ESCAPE) {
734 long escDelay = now.getTime() - escapeTime;
735 if (escDelay > 250) {
736 // After 0.25 seconds, assume a true escape character
737 events.add(controlChar((char)0x1B));
738 reset();
739 }
740 }
741
742 if (noChar == true) {
743 int newWidth = session.getWindowWidth();
744 int newHeight = session.getWindowHeight();
745 if ((newWidth != windowResize.width) ||
746 (newHeight != windowResize.height)) {
747 TResizeEvent event = new TResizeEvent(TResizeEvent.Type.Screen,
748 newWidth, newHeight);
749 windowResize.width = newWidth;
750 windowResize.height = newHeight;
751 events.add(event);
752 }
753
754 // Nothing else to do, bail out
755 return events;
756 }
757
758 // System.err.printf("state: %s ch %c\r\n", state, ch);
759
760 switch (state) {
761 case GROUND:
762
763 if (ch == 0x1B) {
764 state = ParseState.ESCAPE;
765 escapeTime = now.getTime();
766 return events;
767 }
768
769 if (ch <= 0x1F) {
770 // Control character
771 events.add(controlChar(ch));
772 reset();
773 return events;
774 }
775
776 if (ch >= 0x20) {
777 // Normal character
778 keypress = new TKeypressEvent();
779 keypress.key.isKey = false;
780 keypress.key.ch = ch;
781 events.add(keypress);
782 reset();
783 return events;
784 }
785
786 break;
787
788 case ESCAPE:
789 if (ch <= 0x1F) {
790 // ALT-Control character
791 keypress = controlChar(ch);
792 keypress.key.alt = true;
793 events.add(keypress);
794 reset();
795 return events;
796 }
797
798 if (ch == 'O') {
799 // This will be one of the function keys
800 state = ParseState.ESCAPE_INTERMEDIATE;
801 return events;
802 }
803
804 // '[' goes to CSI_ENTRY
805 if (ch == '[') {
806 state = ParseState.CSI_ENTRY;
807 return events;
808 }
809
810 // Everything else is assumed to be Alt-keystroke
811 keypress = new TKeypressEvent();
812 keypress.key.isKey = false;
813 keypress.key.ch = ch;
814 keypress.key.alt = true;
815 if ((ch >= 'A') && (ch <= 'Z')) {
816 keypress.key.shift = true;
817 }
818 events.add(keypress);
819 reset();
820 return events;
821
822 case ESCAPE_INTERMEDIATE:
823 if ((ch >= 'P') && (ch <= 'S')) {
824 // Function key
825 keypress = new TKeypressEvent();
826 keypress.key.isKey = true;
827 switch (ch) {
828 case 'P':
829 keypress.key.fnKey = TKeypress.F1;
830 break;
831 case 'Q':
832 keypress.key.fnKey = TKeypress.F2;
833 break;
834 case 'R':
835 keypress.key.fnKey = TKeypress.F3;
836 break;
837 case 'S':
838 keypress.key.fnKey = TKeypress.F4;
839 break;
840 default:
841 break;
842 }
843 events.add(keypress);
844 reset();
845 return events;
846 }
847
848 // Unknown keystroke, ignore
849 reset();
850 return events;
851
852 case CSI_ENTRY:
853 // Numbers - parameter values
854 if ((ch >= '0') && (ch <= '9')) {
855 params.set(paramI, params.get(paramI) + ch);
856 state = ParseState.CSI_PARAM;
857 return events;
858 }
859 // Parameter separator
860 if (ch == ';') {
861 paramI++;
862 params.set(paramI, "");
863 return events;
864 }
865
866 if ((ch >= 0x30) && (ch <= 0x7E)) {
867 switch (ch) {
868 case 'A':
869 // Up
870 keypress = new TKeypressEvent();
871 keypress.key.isKey = true;
872 keypress.key.fnKey = TKeypress.UP;
873 if (params.size() > 1) {
874 if (params.get(1).equals("2")) {
875 keypress.key.shift = true;
876 }
877 if (params.get(1).equals("5")) {
878 keypress.key.ctrl = true;
879 }
880 if (params.get(1).equals("3")) {
881 keypress.key.alt = true;
882 }
883 }
884 events.add(keypress);
885 reset();
886 return events;
887 case 'B':
888 // Down
889 keypress = new TKeypressEvent();
890 keypress.key.isKey = true;
891 keypress.key.fnKey = TKeypress.DOWN;
892 if (params.size() > 1) {
893 if (params.get(1).equals("2")) {
894 keypress.key.shift = true;
895 }
896 if (params.get(1).equals("5")) {
897 keypress.key.ctrl = true;
898 }
899 if (params.get(1).equals("3")) {
900 keypress.key.alt = true;
901 }
902 }
903 events.add(keypress);
904 reset();
905 return events;
906 case 'C':
907 // Right
908 keypress = new TKeypressEvent();
909 keypress.key.isKey = true;
910 keypress.key.fnKey = TKeypress.RIGHT;
911 if (params.size() > 1) {
912 if (params.get(1).equals("2")) {
913 keypress.key.shift = true;
914 }
915 if (params.get(1).equals("5")) {
916 keypress.key.ctrl = true;
917 }
918 if (params.get(1).equals("3")) {
919 keypress.key.alt = true;
920 }
921 }
922 events.add(keypress);
923 reset();
924 return events;
925 case 'D':
926 // Left
927 keypress = new TKeypressEvent();
928 keypress.key.isKey = true;
929 keypress.key.fnKey = TKeypress.LEFT;
930 if (params.size() > 1) {
931 if (params.get(1).equals("2")) {
932 keypress.key.shift = true;
933 }
934 if (params.get(1).equals("5")) {
935 keypress.key.ctrl = true;
936 }
937 if (params.get(1).equals("3")) {
938 keypress.key.alt = true;
939 }
940 }
941 events.add(keypress);
942 reset();
943 return events;
944 case 'H':
945 // Home
946 keypress = new TKeypressEvent();
947 keypress.key.isKey = true;
948 keypress.key.fnKey = TKeypress.HOME;
949 events.add(keypress);
950 reset();
951 return events;
952 case 'F':
953 // End
954 keypress = new TKeypressEvent();
955 keypress.key.isKey = true;
956 keypress.key.fnKey = TKeypress.END;
957 events.add(keypress);
958 reset();
959 return events;
960 case 'Z':
961 // CBT - Cursor backward X tab stops (default 1)
962 keypress = new TKeypressEvent();
963 keypress.key.isKey = true;
964 keypress.key.fnKey = TKeypress.BTAB;
965 events.add(keypress);
966 reset();
967 return events;
968 case 'M':
969 // Mouse position
970 state = ParseState.MOUSE;
971 return events;
972 default:
973 break;
974 }
975 }
976
977 // Unknown keystroke, ignore
978 reset();
979 return events;
980
981 case CSI_PARAM:
982 // Numbers - parameter values
983 if ((ch >= '0') && (ch <= '9')) {
984 params.set(paramI, params.get(paramI) + ch);
985 state = ParseState.CSI_PARAM;
986 return events;
987 }
988 // Parameter separator
989 if (ch == ';') {
990 paramI++;
991 params.set(paramI, "");
992 return events;
993 }
994
995 if (ch == '~') {
996 events.add(csiFnKey());
997 reset();
998 return events;
999 }
1000
1001 if ((ch >= 0x30) && (ch <= 0x7E)) {
1002 switch (ch) {
1003 case 'A':
1004 // Up
1005 keypress = new TKeypressEvent();
1006 keypress.key.isKey = true;
1007 keypress.key.fnKey = TKeypress.UP;
1008 if (params.size() > 1) {
1009 if (params.get(1).equals("2")) {
1010 keypress.key.shift = true;
1011 }
1012 if (params.get(1).equals("5")) {
1013 keypress.key.ctrl = true;
1014 }
1015 if (params.get(1).equals("3")) {
1016 keypress.key.alt = true;
1017 }
1018 }
1019 events.add(keypress);
1020 reset();
1021 return events;
1022 case 'B':
1023 // Down
1024 keypress = new TKeypressEvent();
1025 keypress.key.isKey = true;
1026 keypress.key.fnKey = TKeypress.DOWN;
1027 if (params.size() > 1) {
1028 if (params.get(1).equals("2")) {
1029 keypress.key.shift = true;
1030 }
1031 if (params.get(1).equals("5")) {
1032 keypress.key.ctrl = true;
1033 }
1034 if (params.get(1).equals("3")) {
1035 keypress.key.alt = true;
1036 }
1037 }
1038 events.add(keypress);
1039 reset();
1040 return events;
1041 case 'C':
1042 // Right
1043 keypress = new TKeypressEvent();
1044 keypress.key.isKey = true;
1045 keypress.key.fnKey = TKeypress.RIGHT;
1046 if (params.size() > 1) {
1047 if (params.get(1).equals("2")) {
1048 keypress.key.shift = true;
1049 }
1050 if (params.get(1).equals("5")) {
1051 keypress.key.ctrl = true;
1052 }
1053 if (params.get(1).equals("3")) {
1054 keypress.key.alt = true;
1055 }
1056 }
1057 events.add(keypress);
1058 reset();
1059 return events;
1060 case 'D':
1061 // Left
1062 keypress = new TKeypressEvent();
1063 keypress.key.isKey = true;
1064 keypress.key.fnKey = TKeypress.LEFT;
1065 if (params.size() > 1) {
1066 if (params.get(1).equals("2")) {
1067 keypress.key.shift = true;
1068 }
1069 if (params.get(1).equals("5")) {
1070 keypress.key.ctrl = true;
1071 }
1072 if (params.get(1).equals("3")) {
1073 keypress.key.alt = true;
1074 }
1075 }
1076 events.add(keypress);
1077 reset();
1078 return events;
1079 default:
1080 break;
1081 }
1082 }
1083
1084 // Unknown keystroke, ignore
1085 reset();
1086 return events;
1087
1088 case MOUSE:
1089 params.set(0, params.get(paramI) + ch);
1090 if (params.get(0).length() == 3) {
1091 // We have enough to generate a mouse event
1092 events.add(parseMouse());
1093 reset();
1094 }
1095 return events;
1096
1097 default:
1098 break;
1099 }
1100
1101 // This "should" be impossible to reach
1102 return events;
1103 }
1104
1105 /**
1106 * Tell (u)xterm that we want alt- keystrokes to send escape +
1107 * character rather than set the 8th bit. Anyone who wants UTF8
1108 * should want this enabled.
1109 *
1110 * Params:
1111 * on = if true, enable metaSendsEscape
1112 *
1113 * Returns:
1114 * the string to emit to xterm
1115 */
1116 static public String xtermMetaSendsEscape(boolean on) {
1117 if (on) {
1118 return "\033[?1036h\033[?1034l";
1119 }
1120 return "\033[?1036l";
1121 }
1122
1123 /**
1124 * Convert a list of SGR parameters into a full escape sequence.
1125 * This also eliminates a trailing ';' which would otherwise reset
1126 * everything to white-on-black not-bold.
1127 *
1128 * Params:
1129 * str = string of parameters, e.g. "31;1;"
1130 *
1131 * Returns:
1132 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[31;1m"
1133 */
1134 static public String addHeaderSGR(String str) {
1135 if (str.length() > 0) {
1136 // Nix any trailing ';' because that resets all attributes
1137 while (str.endsWith(":")) {
1138 str = str.substring(0, str.length() - 1);
1139 }
1140 }
1141 return "\033[" + str + "m";
1142 }
1143
1144 /**
1145 * Create a SGR parameter sequence for a single color change.
1146 *
1147 * Params:
1148 * color = one of the Color.WHITE, Color.BLUE, etc. constants
1149 * foreground = if true, this is a foreground color
1150 *
1151 * Returns:
1152 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[42m"
1153 */
1154 static public String color(Color color, boolean foreground) {
1155 return color(color, foreground, true);
1156 }
1157
1158 /**
1159 * Create a SGR parameter sequence for a single color change.
1160 *
1161 * Params:
1162 * color = one of the Color.WHITE, Color.BLUE, etc. constants
1163 * foreground = if true, this is a foreground color
1164 * header = if true, make the full header, otherwise just emit
1165 * the color parameter e.g. "42;"
1166 *
1167 * Returns:
1168 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[42m"
1169 */
1170 static public String color(Color color, boolean foreground,
1171 boolean header) {
1172
1173 int ecmaColor = color.value;
1174
1175 // Convert Color.* values to SGR numerics
1176 if (foreground == true) {
1177 ecmaColor += 30;
1178 } else {
1179 ecmaColor += 40;
1180 }
1181
1182 if (header) {
1183 return String.format("\033[%dm", ecmaColor);
1184 } else {
1185 return String.format("%d;", ecmaColor);
1186 }
1187 }
1188
1189 /**
1190 * Create a SGR parameter sequence for both foreground and
1191 * background color change.
1192 *
1193 * Params:
1194 * foreColor = one of the Color.WHITE, Color.BLUE, etc. constants
1195 * backColor = one of the Color.WHITE, Color.BLUE, etc. constants
1196 *
1197 * Returns:
1198 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[31;42m"
1199 */
1200 static public String color(Color foreColor, Color backColor) {
1201 return color(foreColor, backColor, true);
1202 }
1203
1204 /**
1205 * Create a SGR parameter sequence for both foreground and
1206 * background color change.
1207 *
1208 * Params:
1209 * foreColor = one of the Color.WHITE, Color.BLUE, etc. constants
1210 * backColor = one of the Color.WHITE, Color.BLUE, etc. constants
1211 * header = if true, make the full header, otherwise just emit
1212 * the color parameter e.g. "31;42;"
1213 *
1214 * Returns:
1215 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[31;42m"
1216 */
1217 static public String color(Color foreColor, Color backColor,
1218 boolean header) {
1219
1220 int ecmaForeColor = foreColor.value;
1221 int ecmaBackColor = backColor.value;
1222
1223 // Convert Color.* values to SGR numerics
1224 ecmaBackColor += 40;
1225 ecmaForeColor += 30;
1226
1227 if (header) {
1228 return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
1229 } else {
1230 return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
1231 }
1232 }
1233
1234 /**
1235 * Create a SGR parameter sequence for foreground, background, and
1236 * several attributes. This sequence first resets all attributes
1237 * to default, then sets attributes as per the parameters.
1238 *
1239 * Params:
1240 * foreColor = one of the Color.WHITE, Color.BLUE, etc. constants
1241 * backColor = one of the Color.WHITE, Color.BLUE, etc. constants
1242 * bold = if true, set bold
1243 * reverse = if true, set reverse
1244 * blink = if true, set blink
1245 * underline = if true, set underline
1246 *
1247 * Returns:
1248 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[0;1;31;42m"
1249 */
1250 static public String color(Color foreColor, Color backColor, boolean bold,
1251 boolean reverse, boolean blink, boolean underline) {
1252
1253 int ecmaForeColor = foreColor.value;
1254 int ecmaBackColor = backColor.value;
1255
1256 // Convert Color.* values to SGR numerics
1257 ecmaBackColor += 40;
1258 ecmaForeColor += 30;
1259
1260 StringBuilder sb = new StringBuilder();
1261 if ( bold && reverse && blink && !underline ) {
1262 sb.append("\033[0;1;7;5;");
1263 } else if ( bold && reverse && !blink && !underline ) {
1264 sb.append("\033[0;1;7;");
1265 } else if ( !bold && reverse && blink && !underline ) {
1266 sb.append("\033[0;7;5;");
1267 } else if ( bold && !reverse && blink && !underline ) {
1268 sb.append("\033[0;1;5;");
1269 } else if ( bold && !reverse && !blink && !underline ) {
1270 sb.append("\033[0;1;");
1271 } else if ( !bold && reverse && !blink && !underline ) {
1272 sb.append("\033[0;7;");
1273 } else if ( !bold && !reverse && blink && !underline) {
1274 sb.append("\033[0;5;");
1275 } else if ( bold && reverse && blink && underline ) {
1276 sb.append("\033[0;1;7;5;4;");
1277 } else if ( bold && reverse && !blink && underline ) {
1278 sb.append("\033[0;1;7;4;");
1279 } else if ( !bold && reverse && blink && underline ) {
1280 sb.append("\033[0;7;5;4;");
1281 } else if ( bold && !reverse && blink && underline ) {
1282 sb.append("\033[0;1;5;4;");
1283 } else if ( bold && !reverse && !blink && underline ) {
1284 sb.append("\033[0;1;4;");
1285 } else if ( !bold && reverse && !blink && underline ) {
1286 sb.append("\033[0;7;4;");
1287 } else if ( !bold && !reverse && blink && underline) {
1288 sb.append("\033[0;5;4;");
1289 } else if ( !bold && !reverse && !blink && underline) {
1290 sb.append("\033[0;4;");
1291 } else {
1292 assert(!bold && !reverse && !blink && !underline);
1293 sb.append("\033[0;");
1294 }
1295 sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
1296 return sb.toString();
1297 }
1298
1299 /**
1300 * Create a SGR parameter sequence for enabling reverse color.
1301 *
1302 * Params:
1303 * on = if true, turn on reverse
1304 *
1305 * Returns:
1306 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[7m"
1307 */
1308 static public String reverse(boolean on) {
1309 if (on) {
1310 return "\033[7m";
1311 }
1312 return "\033[27m";
1313 }
1314
1315 /**
1316 * Create a SGR parameter sequence to reset to defaults.
1317 *
1318 * Returns:
1319 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[0m"
1320 */
1321 static public String normal() {
1322 return normal(true);
1323 }
1324
1325 /**
1326 * Create a SGR parameter sequence to reset to defaults.
1327 *
1328 * Params:
1329 * header = if true, make the full header, otherwise just emit
1330 * the bare parameter e.g. "0;"
1331 *
1332 * Returns:
1333 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[0m"
1334 */
1335 static public String normal(boolean header) {
1336 if (header) {
1337 return "\033[0;37;40m";
1338 }
1339 return "0;37;40";
1340 }
1341
1342 /**
1343 * Create a SGR parameter sequence for enabling boldface.
1344 *
1345 * Params:
1346 * on = if true, turn on bold
1347 *
1348 * Returns:
1349 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[1m"
1350 */
1351 static public String bold(boolean on) {
1352 return bold(on, true);
1353 }
1354
1355 /**
1356 * Create a SGR parameter sequence for enabling boldface.
1357 *
1358 * Params:
1359 * on = if true, turn on bold
1360 * header = if true, make the full header, otherwise just emit
1361 * the bare parameter e.g. "1;"
1362 *
1363 * Returns:
1364 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[1m"
1365 */
1366 static public String bold(boolean on, boolean header) {
1367 if (header) {
1368 if (on) {
1369 return "\033[1m";
1370 }
1371 return "\033[22m";
1372 }
1373 if (on) {
1374 return "1;";
1375 }
1376 return "22;";
1377 }
1378
1379 /**
1380 * Create a SGR parameter sequence for enabling blinking text.
1381 *
1382 * Params:
1383 * on = if true, turn on blink
1384 *
1385 * Returns:
1386 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[5m"
1387 */
1388 static public String blink(boolean on) {
1389 return blink(on, true);
1390 }
1391
1392 /**
1393 * Create a SGR parameter sequence for enabling blinking text.
1394 *
1395 * Params:
1396 * on = if true, turn on blink
1397 * header = if true, make the full header, otherwise just emit
1398 * the bare parameter e.g. "5;"
1399 *
1400 * Returns:
1401 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[5m"
1402 */
1403 static public String blink(boolean on, boolean header) {
1404 if (header) {
1405 if (on) {
1406 return "\033[5m";
1407 }
1408 return "\033[25m";
1409 }
1410 if (on) {
1411 return "5;";
1412 }
1413 return "25;";
1414 }
1415
1416 /**
1417 * Create a SGR parameter sequence for enabling underline /
1418 * underscored text.
1419 *
1420 * Params:
1421 * on = if true, turn on underline
1422 *
1423 * Returns:
1424 * the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[4m"
1425 */
1426 static public String underline(boolean on) {
1427 if (on) {
1428 return "\033[4m";
1429 }
1430 return "\033[24m";
1431 }
1432
1433 /**
1434 * Create a SGR parameter sequence for enabling the visible cursor.
1435 *
1436 * Params:
1437 * on = if true, turn on cursor
1438 *
1439 * Returns:
1440 * the string to emit to an ANSI / ECMA-style terminal
1441 */
1442 public String cursor(boolean on) {
1443 if (on && (cursorOn == false)) {
1444 cursorOn = true;
1445 return "\033[?25h";
1446 }
1447 if (!on && (cursorOn == true)) {
1448 cursorOn = false;
1449 return "\033[?25l";
1450 }
1451 return "";
1452 }
1453
1454 /**
1455 * Clear the entire screen. Because some terminals use back-color-erase,
1456 * set the color to white-on-black beforehand.
1457 *
1458 * Returns:
1459 * the string to emit to an ANSI / ECMA-style terminal
1460 */
1461 static public String clearAll() {
1462 return "\033[0;37;40m\033[2J";
1463 }
1464
1465 /**
1466 * Clear the line from the cursor (inclusive) to the end of the screen.
1467 * Because some terminals use back-color-erase, set the color to
1468 * white-on-black beforehand.
1469 *
1470 * Returns:
1471 * the string to emit to an ANSI / ECMA-style terminal
1472 */
1473 static public String clearRemainingLine() {
1474 return "\033[0;37;40m\033[K";
1475 }
1476
1477 /**
1478 * Clear the line up the cursor (inclusive). Because some terminals use
1479 * back-color-erase, set the color to white-on-black beforehand.
1480 *
1481 * Returns:
1482 * the string to emit to an ANSI / ECMA-style terminal
1483 */
1484 static public String clearPreceedingLine() {
1485 return "\033[0;37;40m\033[1K";
1486 }
1487
1488 /**
1489 * Clear the line. Because some terminals use back-color-erase, set the
1490 * color to white-on-black beforehand.
1491 *
1492 * Returns:
1493 * the string to emit to an ANSI / ECMA-style terminal
1494 */
1495 static public String clearLine() {
1496 return "\033[0;37;40m\033[2K";
1497 }
1498
1499 /**
1500 * Move the cursor to the top-left corner.
1501 *
1502 * Returns:
1503 * the string to emit to an ANSI / ECMA-style terminal
1504 */
1505 static public String home() {
1506 return "\033[H";
1507 }
1508
1509 /**
1510 * Move the cursor to (x, y).
1511 *
1512 * Params:
1513 * x = column coordinate. 0 is the left-most column.
1514 * y = row coordinate. 0 is the top-most row.
1515 *
1516 * Returns:
1517 * the string to emit to an ANSI / ECMA-style terminal
1518 */
1519 static public String gotoXY(int x, int y) {
1520 return String.format("\033[%d;%dH", y + 1, x + 1);
1521 }
1522
1523 /**
1524 * Tell (u)xterm that we want to receive mouse events based on
1525 * "Any event tracking" and UTF-8 coordinates. See
1526 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1527 *
1528 * Finally, this sets the alternate screen buffer.
1529 *
1530 * Params:
1531 * on = if true, enable mouse report
1532 *
1533 * Returns:
1534 * the string to emit to xterm
1535 */
1536 static public String mouse(boolean on) {
1537 if (on) {
1538 return "\033[?1003;1005h\033[?1049h";
1539 }
1540 return "\033[?1003;1005l\033[?1049l";
1541 }
1542
1543}