2 * Jexer - Java Text User Interface
4 * License: LGPLv3 or later
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
10 * Copyright (C) 2015 Kevin Lamonte
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
33 import java
.io
.BufferedReader
;
34 import java
.io
.FileDescriptor
;
35 import java
.io
.FileInputStream
;
36 import java
.io
.InputStream
;
37 import java
.io
.InputStreamReader
;
38 import java
.io
.IOException
;
39 import java
.io
.OutputStream
;
40 import java
.io
.OutputStreamWriter
;
41 import java
.io
.PrintWriter
;
42 import java
.io
.Reader
;
43 import java
.io
.UnsupportedEncodingException
;
44 import java
.util
.ArrayList
;
45 import java
.util
.Date
;
46 import java
.util
.List
;
47 import java
.util
.LinkedList
;
49 import jexer
.TKeypress
;
50 import jexer
.bits
.Color
;
51 import jexer
.event
.TInputEvent
;
52 import jexer
.event
.TKeypressEvent
;
53 import jexer
.event
.TMouseEvent
;
54 import jexer
.event
.TResizeEvent
;
55 import jexer
.session
.SessionInfo
;
56 import jexer
.session
.TSessionInfo
;
57 import jexer
.session
.TTYSessionInfo
;
58 import static jexer
.TKeypress
.*;
61 * This class reads keystrokes and mouse events and emits output to ANSI
62 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
64 public final class ECMA48Terminal
implements Runnable
{
67 * The session information.
69 private SessionInfo sessionInfo
;
72 * Getter for sessionInfo.
74 * @return the SessionInfo
76 public SessionInfo
getSessionInfo() {
81 * The event queue, filled up by a thread reading on input.
83 private List
<TInputEvent
> eventQueue
;
86 * If true, we want the reader thread to exit gracefully.
88 private boolean stopReaderThread
;
93 private Thread readerThread
;
96 * Parameters being collected. E.g. if the string is \033[1;3m, then
97 * params[0] will be 1 and params[1] will be 3.
99 private ArrayList
<String
> params
;
102 * params[paramI] is being appended to.
107 * States in the input parser.
109 private enum ParseState
{
120 * Current parsing state.
122 private ParseState state
;
125 * The time we entered ESCAPE. If we get a bare escape without a code
126 * following it, this is used to return that bare escape.
128 private long escapeTime
;
131 * The time we last checked the window size. We try not to spawn stty
132 * more than once per second.
134 private long windowSizeTime
;
137 * true if mouse1 was down. Used to report mouse1 on the release event.
139 private boolean mouse1
;
142 * true if mouse2 was down. Used to report mouse2 on the release event.
144 private boolean mouse2
;
147 * true if mouse3 was down. Used to report mouse3 on the release event.
149 private boolean mouse3
;
152 * Cache the cursor visibility value so we only emit the sequence when we
155 private boolean cursorOn
= true;
158 * Cache the last window size to figure out if a TResizeEvent needs to be
161 private TResizeEvent windowResize
= null;
164 * If true, then we changed System.in and need to change it back.
166 private boolean setRawMode
;
169 * The terminal's input. If an InputStream is not specified in the
170 * constructor, then this InputStreamReader will be bound to System.in
171 * with UTF-8 encoding.
173 private Reader input
;
176 * The terminal's raw InputStream. If an InputStream is not specified in
177 * the constructor, then this InputReader will be bound to System.in.
178 * This is used by run() to see if bytes are available() before calling
179 * (Reader)input.read().
181 private InputStream inputStream
;
184 * The terminal's output. If an OutputStream is not specified in the
185 * constructor, then this PrintWriter will be bound to System.out with
188 private PrintWriter output
;
191 * When true, the terminal is sending non-UTF8 bytes when reporting mouse
194 * TODO: Add broken mouse detection back into the reader.
196 private boolean brokenTerminalUTFMouse
= false;
199 * Get the output writer.
203 public PrintWriter
getOutput() {
208 * Check if there are events in the queue.
210 * @return if true, getEvents() has something to return to the backend
212 public boolean hasEvents() {
213 synchronized (eventQueue
) {
214 return (eventQueue
.size() > 0);
219 * Call 'stty' to set cooked mode.
221 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
223 private void sttyCooked() {
228 * Call 'stty' to set raw mode.
230 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
231 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
232 * -parenb cs8 min 1 < /dev/tty'
234 private void sttyRaw() {
239 * Call 'stty' to set raw or cooked mode.
241 * @param mode if true, set raw mode, otherwise set cooked mode
243 private void doStty(final boolean mode
) {
245 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
247 String
[] cmdCooked
= {
248 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
253 process
= Runtime
.getRuntime().exec(cmdRaw
);
255 process
= Runtime
.getRuntime().exec(cmdCooked
);
257 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
258 String line
= in
.readLine();
259 if ((line
!= null) && (line
.length() > 0)) {
260 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
263 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
264 line
= err
.readLine();
265 if ((line
!= null) && (line
.length() > 0)) {
266 System
.err
.println("Error output from stty: " + line
);
271 } catch (InterruptedException e
) {
275 int rc
= process
.exitValue();
277 System
.err
.println("stty returned error code: " + rc
);
279 } catch (IOException e
) {
285 * Constructor sets up state for getEvent().
287 * @param input an InputStream connected to the remote user, or null for
288 * System.in. If System.in is used, then on non-Windows systems it will
289 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
290 * mode. input is always converted to a Reader with UTF-8 encoding.
291 * @param output an OutputStream connected to the remote user, or null
292 * for System.out. output is always converted to a Writer with UTF-8
294 * @throws UnsupportedEncodingException if an exception is thrown when
295 * creating the InputStreamReader
297 public ECMA48Terminal(final InputStream input
,
298 final OutputStream output
) throws UnsupportedEncodingException
{
304 stopReaderThread
= false;
307 // inputStream = System.in;
308 inputStream
= new FileInputStream(FileDescriptor
.in
);
314 this.input
= new InputStreamReader(inputStream
, "UTF-8");
316 // TODO: include TelnetSocket from NIB and have it implement
318 if (input
instanceof SessionInfo
) {
319 sessionInfo
= (SessionInfo
) input
;
321 if (sessionInfo
== null) {
323 // Reading right off the tty
324 sessionInfo
= new TTYSessionInfo();
326 sessionInfo
= new TSessionInfo();
330 if (output
== null) {
331 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
334 this.output
= new PrintWriter(new OutputStreamWriter(output
,
338 // Enable mouse reporting and metaSendsEscape
339 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
341 // Hang onto the window size
342 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
343 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
345 // Spin up the input reader
346 eventQueue
= new LinkedList
<TInputEvent
>();
347 readerThread
= new Thread(this);
348 readerThread
.start();
352 * Restore terminal to normal state.
354 public void shutdown() {
356 // System.err.println("=== shutdown() ==="); System.err.flush();
358 // Tell the reader thread to stop looking at input
359 stopReaderThread
= true;
362 } catch (InterruptedException e
) {
366 // Disable mouse reporting and show cursor
367 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
373 // We don't close System.in/out
375 // Shut down the streams, this should wake up the reader thread
382 if (output
!= null) {
386 } catch (IOException e
) {
395 public void flush() {
400 * Reset keyboard/mouse input parser.
402 private void reset() {
403 state
= ParseState
.GROUND
;
404 params
= new ArrayList
<String
>();
411 * Produce a control character or one of the special ones (ENTER, TAB,
414 * @param ch Unicode code point
415 * @param alt if true, set alt on the TKeypress
416 * @return one TKeypress event, either a control character (e.g. isKey ==
417 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
420 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
421 // System.err.printf("controlChar: %02x\n", ch);
425 // Carriage return --> ENTER
426 return new TKeypressEvent(kbEnter
, alt
, false, false);
428 // Linefeed --> ENTER
429 return new TKeypressEvent(kbEnter
, alt
, false, false);
432 return new TKeypressEvent(kbEsc
, alt
, false, false);
435 return new TKeypressEvent(kbTab
, alt
, false, false);
437 // Make all other control characters come back as the alphabetic
438 // character with the ctrl field set. So SOH would be 'A' +
440 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
446 * Produce special key from CSI Pn ; Pm ; ... ~
448 * @return one KEYPRESS event representing a special key
450 private TInputEvent
csiFnKey() {
453 if (params
.size() > 0) {
454 key
= Integer
.parseInt(params
.get(0));
456 if (params
.size() > 1) {
457 modifier
= Integer
.parseInt(params
.get(1));
460 boolean ctrl
= false;
461 boolean shift
= false;
480 // Unknown modifier, bail out
486 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
488 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
490 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
492 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
494 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
496 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
498 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
500 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
502 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
504 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
506 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
508 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
510 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
512 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
520 * Produce mouse events based on "Any event tracking" and UTF-8
522 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
524 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
526 private TInputEvent
parseMouse() {
527 int buttons
= params
.get(0).charAt(0) - 32;
528 int x
= params
.get(0).charAt(1) - 32 - 1;
529 int y
= params
.get(0).charAt(2) - 32 - 1;
531 // Clamp X and Y to the physical screen coordinates.
532 if (x
>= windowResize
.getWidth()) {
533 x
= windowResize
.getWidth() - 1;
535 if (y
>= windowResize
.getHeight()) {
536 y
= windowResize
.getHeight() - 1;
539 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
540 boolean eventMouse1
= false;
541 boolean eventMouse2
= false;
542 boolean eventMouse3
= false;
543 boolean eventMouseWheelUp
= false;
544 boolean eventMouseWheelDown
= false;
546 // System.err.printf("buttons: %04x\r\n", buttons);
563 if (!mouse1
&& !mouse2
&& !mouse3
) {
564 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
566 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
583 // Dragging with mouse1 down
586 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
590 // Dragging with mouse2 down
593 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
597 // Dragging with mouse3 down
600 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
604 // Dragging with mouse2 down after wheelUp
607 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
611 // Dragging with mouse2 down after wheelDown
614 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
618 eventMouseWheelUp
= true;
622 eventMouseWheelDown
= true;
626 // Unknown, just make it motion
627 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
630 return new TMouseEvent(eventType
, x
, y
, x
, y
,
631 eventMouse1
, eventMouse2
, eventMouse3
,
632 eventMouseWheelUp
, eventMouseWheelDown
);
636 * Return any events in the IO queue.
638 * @param queue list to append new events to
640 public void getEvents(final List
<TInputEvent
> queue
) {
641 synchronized (eventQueue
) {
642 if (eventQueue
.size() > 0) {
643 synchronized (queue
) {
644 queue
.addAll(eventQueue
);
652 * Return any events in the IO queue due to timeout.
654 * @param queue list to append new events to
656 private void getIdleEvents(final List
<TInputEvent
> queue
) {
657 Date now
= new Date();
659 // Check for new window size
660 long windowSizeDelay
= now
.getTime() - windowSizeTime
;
661 if (windowSizeDelay
> 1000) {
662 sessionInfo
.queryWindowSize();
663 int newWidth
= sessionInfo
.getWindowWidth();
664 int newHeight
= sessionInfo
.getWindowHeight();
665 if ((newWidth
!= windowResize
.getWidth())
666 || (newHeight
!= windowResize
.getHeight())
668 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
669 newWidth
, newHeight
);
670 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
671 newWidth
, newHeight
);
674 windowSizeTime
= now
.getTime();
677 // ESCDELAY type timeout
678 if (state
== ParseState
.ESCAPE
) {
679 long escDelay
= now
.getTime() - escapeTime
;
680 if (escDelay
> 100) {
681 // After 0.1 seconds, assume a true escape character
682 queue
.add(controlChar((char)0x1B, false));
689 * Parses the next character of input to see if an InputEvent is
692 * @param events list to append new events to
693 * @param ch Unicode code point
695 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
697 // ESCDELAY type timeout
698 Date now
= new Date();
699 if (state
== ParseState
.ESCAPE
) {
700 long escDelay
= now
.getTime() - escapeTime
;
701 if (escDelay
> 250) {
702 // After 0.25 seconds, assume a true escape character
703 events
.add(controlChar((char)0x1B, false));
709 boolean ctrl
= false;
711 boolean shift
= false;
715 // System.err.printf("state: %s ch %c\r\n", state, ch);
721 state
= ParseState
.ESCAPE
;
722 escapeTime
= now
.getTime();
728 events
.add(controlChar(ch
, false));
735 events
.add(new TKeypressEvent(false, 0, ch
,
736 false, false, false));
745 // ALT-Control character
746 events
.add(controlChar(ch
, true));
752 // This will be one of the function keys
753 state
= ParseState
.ESCAPE_INTERMEDIATE
;
757 // '[' goes to CSI_ENTRY
759 state
= ParseState
.CSI_ENTRY
;
763 // Everything else is assumed to be Alt-keystroke
764 if ((ch
>= 'A') && (ch
<= 'Z')) {
768 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
772 case ESCAPE_INTERMEDIATE
:
773 if ((ch
>= 'P') && (ch
<= 'S')) {
777 events
.add(new TKeypressEvent(kbF1
));
780 events
.add(new TKeypressEvent(kbF2
));
783 events
.add(new TKeypressEvent(kbF3
));
786 events
.add(new TKeypressEvent(kbF4
));
795 // Unknown keystroke, ignore
800 // Numbers - parameter values
801 if ((ch
>= '0') && (ch
<= '9')) {
802 params
.set(paramI
, params
.get(paramI
) + ch
);
803 state
= ParseState
.CSI_PARAM
;
806 // Parameter separator
813 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
817 if (params
.size() > 1) {
818 if (params
.get(1).equals("2")) {
821 if (params
.get(1).equals("5")) {
824 if (params
.get(1).equals("3")) {
828 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
833 if (params
.size() > 1) {
834 if (params
.get(1).equals("2")) {
837 if (params
.get(1).equals("5")) {
840 if (params
.get(1).equals("3")) {
844 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
849 if (params
.size() > 1) {
850 if (params
.get(1).equals("2")) {
853 if (params
.get(1).equals("5")) {
856 if (params
.get(1).equals("3")) {
860 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
865 if (params
.size() > 1) {
866 if (params
.get(1).equals("2")) {
869 if (params
.get(1).equals("5")) {
872 if (params
.get(1).equals("3")) {
876 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
881 events
.add(new TKeypressEvent(kbHome
));
886 events
.add(new TKeypressEvent(kbEnd
));
890 // CBT - Cursor backward X tab stops (default 1)
891 events
.add(new TKeypressEvent(kbBackTab
));
896 state
= ParseState
.MOUSE
;
903 // Unknown keystroke, ignore
908 // Numbers - parameter values
909 if ((ch
>= '0') && (ch
<= '9')) {
910 params
.set(paramI
, params
.get(paramI
) + ch
);
911 state
= ParseState
.CSI_PARAM
;
914 // Parameter separator
917 params
.add(paramI
, "");
922 events
.add(csiFnKey());
927 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
931 if (params
.size() > 1) {
932 if (params
.get(1).equals("2")) {
935 if (params
.get(1).equals("5")) {
938 if (params
.get(1).equals("3")) {
942 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
947 if (params
.size() > 1) {
948 if (params
.get(1).equals("2")) {
951 if (params
.get(1).equals("5")) {
954 if (params
.get(1).equals("3")) {
958 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
963 if (params
.size() > 1) {
964 if (params
.get(1).equals("2")) {
967 if (params
.get(1).equals("5")) {
970 if (params
.get(1).equals("3")) {
974 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
979 if (params
.size() > 1) {
980 if (params
.get(1).equals("2")) {
983 if (params
.get(1).equals("5")) {
986 if (params
.get(1).equals("3")) {
990 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
998 // Unknown keystroke, ignore
1003 params
.set(0, params
.get(paramI
) + ch
);
1004 if (params
.get(0).length() == 3) {
1005 // We have enough to generate a mouse event
1006 events
.add(parseMouse());
1015 // This "should" be impossible to reach
1020 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1021 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1024 * @param on if true, enable metaSendsEscape
1025 * @return the string to emit to xterm
1027 public String
xtermMetaSendsEscape(final boolean on
) {
1029 return "\033[?1036h\033[?1034l";
1031 return "\033[?1036l";
1035 * Convert a list of SGR parameters into a full escape sequence. This
1036 * also eliminates a trailing ';' which would otherwise reset everything
1037 * to white-on-black not-bold.
1039 * @param str string of parameters, e.g. "31;1;"
1040 * @return the string to emit to an ANSI / ECMA-style terminal,
1043 public String
addHeaderSGR(String str
) {
1044 if (str
.length() > 0) {
1045 // Nix any trailing ';' because that resets all attributes
1046 while (str
.endsWith(":")) {
1047 str
= str
.substring(0, str
.length() - 1);
1050 return "\033[" + str
+ "m";
1054 * Create a SGR parameter sequence for a single color change.
1056 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1057 * @param foreground if true, this is a foreground color
1058 * @return the string to emit to an ANSI / ECMA-style terminal,
1061 public String
color(final Color color
, final boolean foreground
) {
1062 return color(color
, foreground
, true);
1066 * Create a SGR parameter sequence for a single color change.
1068 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1069 * @param foreground if true, this is a foreground color
1070 * @param header if true, make the full header, otherwise just emit the
1071 * color parameter e.g. "42;"
1072 * @return the string to emit to an ANSI / ECMA-style terminal,
1075 public String
color(final Color color
, final boolean foreground
,
1076 final boolean header
) {
1078 int ecmaColor
= color
.getValue();
1080 // Convert Color.* values to SGR numerics
1088 return String
.format("\033[%dm", ecmaColor
);
1090 return String
.format("%d;", ecmaColor
);
1095 * Create a SGR parameter sequence for both foreground and
1096 * background color change.
1098 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1099 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1100 * @return the string to emit to an ANSI / ECMA-style terminal,
1101 * e.g. "\033[31;42m"
1103 public String
color(final Color foreColor
, final Color backColor
) {
1104 return color(foreColor
, backColor
, true);
1108 * Create a SGR parameter sequence for both foreground and
1109 * background color change.
1111 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1112 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1113 * @param header if true, make the full header, otherwise just emit the
1114 * color parameter e.g. "31;42;"
1115 * @return the string to emit to an ANSI / ECMA-style terminal,
1116 * e.g. "\033[31;42m"
1118 public String
color(final Color foreColor
, final Color backColor
,
1119 final boolean header
) {
1121 int ecmaForeColor
= foreColor
.getValue();
1122 int ecmaBackColor
= backColor
.getValue();
1124 // Convert Color.* values to SGR numerics
1125 ecmaBackColor
+= 40;
1126 ecmaForeColor
+= 30;
1129 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1131 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1136 * Create a SGR parameter sequence for foreground, background, and
1137 * several attributes. This sequence first resets all attributes to
1138 * default, then sets attributes as per the parameters.
1140 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1141 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1142 * @param bold if true, set bold
1143 * @param reverse if true, set reverse
1144 * @param blink if true, set blink
1145 * @param underline if true, set underline
1146 * @return the string to emit to an ANSI / ECMA-style terminal,
1147 * e.g. "\033[0;1;31;42m"
1149 public String
color(final Color foreColor
, final Color backColor
,
1150 final boolean bold
, final boolean reverse
, final boolean blink
,
1151 final boolean underline
) {
1153 int ecmaForeColor
= foreColor
.getValue();
1154 int ecmaBackColor
= backColor
.getValue();
1156 // Convert Color.* values to SGR numerics
1157 ecmaBackColor
+= 40;
1158 ecmaForeColor
+= 30;
1160 StringBuilder sb
= new StringBuilder();
1161 if ( bold
&& reverse
&& blink
&& !underline
) {
1162 sb
.append("\033[0;1;7;5;");
1163 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1164 sb
.append("\033[0;1;7;");
1165 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1166 sb
.append("\033[0;7;5;");
1167 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1168 sb
.append("\033[0;1;5;");
1169 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1170 sb
.append("\033[0;1;");
1171 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1172 sb
.append("\033[0;7;");
1173 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1174 sb
.append("\033[0;5;");
1175 } else if ( bold
&& reverse
&& blink
&& underline
) {
1176 sb
.append("\033[0;1;7;5;4;");
1177 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1178 sb
.append("\033[0;1;7;4;");
1179 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1180 sb
.append("\033[0;7;5;4;");
1181 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1182 sb
.append("\033[0;1;5;4;");
1183 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1184 sb
.append("\033[0;1;4;");
1185 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1186 sb
.append("\033[0;7;4;");
1187 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1188 sb
.append("\033[0;5;4;");
1189 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1190 sb
.append("\033[0;4;");
1192 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1193 sb
.append("\033[0;");
1195 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1196 return sb
.toString();
1200 * Create a SGR parameter sequence for enabling reverse color.
1202 * @param on if true, turn on reverse
1203 * @return the string to emit to an ANSI / ECMA-style terminal,
1206 public String
reverse(final boolean on
) {
1214 * Create a SGR parameter sequence to reset to defaults.
1216 * @return the string to emit to an ANSI / ECMA-style terminal,
1219 public String
normal() {
1220 return normal(true);
1224 * Create a SGR parameter sequence to reset to defaults.
1226 * @param header if true, make the full header, otherwise just emit the
1227 * bare parameter e.g. "0;"
1228 * @return the string to emit to an ANSI / ECMA-style terminal,
1231 public String
normal(final boolean header
) {
1233 return "\033[0;37;40m";
1239 * Create a SGR parameter sequence for enabling boldface.
1241 * @param on if true, turn on bold
1242 * @return the string to emit to an ANSI / ECMA-style terminal,
1245 public String
bold(final boolean on
) {
1246 return bold(on
, true);
1250 * Create a SGR parameter sequence for enabling boldface.
1252 * @param on if true, turn on bold
1253 * @param header if true, make the full header, otherwise just emit the
1254 * bare parameter e.g. "1;"
1255 * @return the string to emit to an ANSI / ECMA-style terminal,
1258 public String
bold(final boolean on
, final boolean header
) {
1272 * Create a SGR parameter sequence for enabling blinking text.
1274 * @param on if true, turn on blink
1275 * @return the string to emit to an ANSI / ECMA-style terminal,
1278 public String
blink(final boolean on
) {
1279 return blink(on
, true);
1283 * Create a SGR parameter sequence for enabling blinking text.
1285 * @param on if true, turn on blink
1286 * @param header if true, make the full header, otherwise just emit the
1287 * bare parameter e.g. "5;"
1288 * @return the string to emit to an ANSI / ECMA-style terminal,
1291 public String
blink(final boolean on
, final boolean header
) {
1305 * Create a SGR parameter sequence for enabling underline / underscored
1308 * @param on if true, turn on underline
1309 * @return the string to emit to an ANSI / ECMA-style terminal,
1312 public String
underline(final boolean on
) {
1320 * Create a SGR parameter sequence for enabling the visible cursor.
1322 * @param on if true, turn on cursor
1323 * @return the string to emit to an ANSI / ECMA-style terminal
1325 public String
cursor(final boolean on
) {
1326 if (on
&& !cursorOn
) {
1330 if (!on
&& cursorOn
) {
1338 * Clear the entire screen. Because some terminals use back-color-erase,
1339 * set the color to white-on-black beforehand.
1341 * @return the string to emit to an ANSI / ECMA-style terminal
1343 public String
clearAll() {
1344 return "\033[0;37;40m\033[2J";
1348 * Clear the line from the cursor (inclusive) to the end of the screen.
1349 * Because some terminals use back-color-erase, set the color to
1350 * white-on-black beforehand.
1352 * @return the string to emit to an ANSI / ECMA-style terminal
1354 public String
clearRemainingLine() {
1355 return "\033[0;37;40m\033[K";
1359 * Clear the line up the cursor (inclusive). Because some terminals use
1360 * back-color-erase, set the color to white-on-black beforehand.
1362 * @return the string to emit to an ANSI / ECMA-style terminal
1364 public String
clearPreceedingLine() {
1365 return "\033[0;37;40m\033[1K";
1369 * Clear the line. Because some terminals use back-color-erase, set the
1370 * color to white-on-black beforehand.
1372 * @return the string to emit to an ANSI / ECMA-style terminal
1374 public String
clearLine() {
1375 return "\033[0;37;40m\033[2K";
1379 * Move the cursor to the top-left corner.
1381 * @return the string to emit to an ANSI / ECMA-style terminal
1383 public String
home() {
1388 * Move the cursor to (x, y).
1390 * @param x column coordinate. 0 is the left-most column.
1391 * @param y row coordinate. 0 is the top-most row.
1392 * @return the string to emit to an ANSI / ECMA-style terminal
1394 public String
gotoXY(final int x
, final int y
) {
1395 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1399 * Tell (u)xterm that we want to receive mouse events based on "Any event
1400 * tracking" and UTF-8 coordinates. See
1401 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1403 * Note that this also sets the alternate/primary screen buffer.
1405 * @param on If true, enable mouse report and use the alternate screen
1406 * buffer. If false disable mouse reporting and use the primary screen
1408 * @return the string to emit to xterm
1410 public String
mouse(final boolean on
) {
1412 return "\033[?1003;1005h\033[?1049h";
1414 return "\033[?1003;1005l\033[?1049l";
1418 * Read function runs on a separate thread.
1421 boolean done
= false;
1422 // available() will often return > 1, so we need to read in chunks to
1424 char [] readBuffer
= new char[128];
1425 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1427 while (!done
&& !stopReaderThread
) {
1429 // We assume that if inputStream has bytes available, then
1430 // input won't block on read().
1431 int n
= inputStream
.available();
1433 if (readBuffer
.length
< n
) {
1434 // The buffer wasn't big enough, make it huger
1435 readBuffer
= new char[readBuffer
.length
* 2];
1438 int rc
= input
.read(readBuffer
, 0, n
);
1439 // System.err.printf("read() %d", rc); System.err.flush();
1444 for (int i
= 0; i
< rc
; i
++) {
1445 int ch
= readBuffer
[i
];
1446 processChar(events
, (char)ch
);
1447 if (events
.size() > 0) {
1448 // Add to the queue for the backend thread to
1449 // be able to obtain.
1450 synchronized (eventQueue
) {
1451 eventQueue
.addAll(events
);
1453 // Now wake up the backend
1454 synchronized (this) {
1462 getIdleEvents(events
);
1463 if (events
.size() > 0) {
1464 synchronized (eventQueue
) {
1465 eventQueue
.addAll(events
);
1470 // Wait 10 millis for more data
1473 // System.err.println("end while loop"); System.err.flush();
1474 } catch (InterruptedException e
) {
1476 } catch (IOException e
) {
1477 e
.printStackTrace();
1480 } // while ((done == false) && (stopReaderThread == false))
1481 // System.err.println("*** run() exiting..."); System.err.flush();