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