2 * Jexer - Java Text User Interface
6 * Author: Kevin Lamonte, <a href="mailto:kevin.lamonte@gmail.com">kevin.lamonte@gmail.com</a>
8 * License: LGPLv3 or later
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
15 * Copyright (C) 2015 Kevin Lamonte
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.
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.
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
35 import java
.io
.BufferedReader
;
36 import java
.io
.FileDescriptor
;
37 import java
.io
.FileInputStream
;
38 import java
.io
.InputStream
;
39 import java
.io
.InputStreamReader
;
40 import java
.io
.IOException
;
41 import java
.io
.OutputStream
;
42 import java
.io
.OutputStreamWriter
;
43 import java
.io
.PrintWriter
;
44 import java
.io
.Reader
;
45 import java
.io
.UnsupportedEncodingException
;
46 import java
.util
.ArrayList
;
47 import java
.util
.Date
;
48 import java
.util
.List
;
49 import java
.util
.LinkedList
;
51 import jexer
.TKeypress
;
52 import jexer
.bits
.Color
;
53 import jexer
.event
.TInputEvent
;
54 import jexer
.event
.TKeypressEvent
;
55 import jexer
.event
.TMouseEvent
;
56 import jexer
.event
.TResizeEvent
;
57 import jexer
.session
.SessionInfo
;
58 import jexer
.session
.TSessionInfo
;
59 import jexer
.session
.TTYSessionInfo
;
60 import static jexer
.TKeypress
.*;
63 * This class has convenience methods for emitting output to ANSI
64 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys,
67 public class ECMA48Terminal
implements Runnable
{
70 * The session information
72 public SessionInfo session
;
75 * The event queue, filled up by a thread reading on input
77 private List
<TInputEvent
> eventQueue
;
80 * If true, we want the reader thread to exit gracefully.
82 private boolean stopReaderThread
;
87 private Thread readerThread
;
90 * Parameters being collected. E.g. if the string is \033[1;3m, then
91 * params[0] will be 1 and params[1] will be 3.
93 private ArrayList
<String
> params
;
96 * params[paramI] is being appended to.
101 * States in the input parser
103 private enum ParseState
{
114 * Current parsing state
116 private ParseState state
;
119 * The time we entered ESCAPE. If we get a bare escape
120 * without a code following it, this is used to return that bare
123 private long escapeTime
;
126 * true if mouse1 was down. Used to report mouse1 on the release event.
128 private boolean mouse1
;
131 * true if mouse2 was down. Used to report mouse2 on the release event.
133 private boolean mouse2
;
136 * true if mouse3 was down. Used to report mouse3 on the release event.
138 private boolean mouse3
;
141 * Cache the cursor visibility value so we only emit the sequence when we
144 private boolean cursorOn
= true;
147 * Cache the last window size to figure out if a TResizeEvent needs to be
150 private TResizeEvent windowResize
= null;
153 * If true, then we changed System.in and need to change it back.
155 private boolean setRawMode
;
158 * The terminal's input. If an InputStream is not specified in the
159 * constructor, then this InputStreamReader will be bound to System.in
160 * with UTF-8 encoding.
162 private Reader input
;
165 * The terminal's raw InputStream. If an InputStream is not specified in
166 * the constructor, then this InputReader will be bound to System.in.
167 * This is used by run() to see if bytes are available() before calling
168 * (Reader)input.read().
170 private InputStream inputStream
;
173 * The terminal's output. If an OutputStream is not specified in the
174 * constructor, then this PrintWriter will be bound to System.out with
177 private PrintWriter output
;
180 * When true, the terminal is sending non-UTF8 bytes when reporting mouse
183 * TODO: Add broken mouse detection back into the reader.
185 private boolean brokenTerminalUTFMouse
= false;
188 * Get the output writer.
192 public PrintWriter
getOutput() {
197 * Check if there are events in the queue.
199 * @return if true, getEvents() has something to return to the backend
201 public boolean hasEvents() {
202 synchronized (eventQueue
) {
203 return (eventQueue
.size() > 0);
208 * Call 'stty cooked' to set cooked mode.
210 private void sttyCooked() {
215 * Call 'stty raw' to set raw mode.
217 private void sttyRaw() {
222 * Call 'stty' to set raw or cooked mode.
224 * @param mode if true, set raw mode, otherwise set cooked mode
226 private void doStty(boolean mode
) {
228 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
230 String
[] cmdCooked
= {
231 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
236 process
= Runtime
.getRuntime().exec(cmdRaw
);
238 process
= Runtime
.getRuntime().exec(cmdCooked
);
240 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
241 String line
= in
.readLine();
242 if ((line
!= null) && (line
.length() > 0)) {
243 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
246 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
247 line
= err
.readLine();
248 if ((line
!= null) && (line
.length() > 0)) {
249 System
.err
.println("Error output from stty: " + line
);
254 } catch (InterruptedException e
) {
258 int rc
= process
.exitValue();
260 System
.err
.println("stty returned error code: " + rc
);
262 } catch (IOException e
) {
268 * Constructor sets up state for getEvent()
270 * @param input an InputStream connected to the remote user, or null for
271 * System.in. If System.in is used, then on non-Windows systems it will
272 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
273 * mode. input is always converted to a Reader with UTF-8 encoding.
274 * @param output an OutputStream connected to the remote user, or null
275 * for System.out. output is always converted to a Writer with UTF-8
278 public ECMA48Terminal(InputStream input
, OutputStream output
) throws UnsupportedEncodingException
{
284 stopReaderThread
= false;
287 // inputStream = System.in;
288 inputStream
= new FileInputStream(FileDescriptor
.in
);
294 this.input
= new InputStreamReader(inputStream
, "UTF-8");
296 // TODO: include TelnetSocket from NIB and have it implement
298 if (input
instanceof SessionInfo
) {
299 session
= (SessionInfo
)input
;
301 if (session
== null) {
303 // Reading right off the tty
304 session
= new TTYSessionInfo();
306 session
= new TSessionInfo();
310 if (output
== null) {
311 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
314 this.output
= new PrintWriter(new OutputStreamWriter(output
,
318 // Enable mouse reporting and metaSendsEscape
319 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
321 // Hang onto the window size
322 windowResize
= new TResizeEvent(TResizeEvent
.Type
.Screen
,
323 session
.getWindowWidth(), session
.getWindowHeight());
325 // Spin up the input reader
326 eventQueue
= new LinkedList
<TInputEvent
>();
327 readerThread
= new Thread(this);
328 readerThread
.start();
332 * Restore terminal to normal state
334 public void shutdown() {
336 // System.err.println("=== shutdown() ==="); System.err.flush();
338 // Tell the reader thread to stop looking at input
339 stopReaderThread
= true;
342 } catch (InterruptedException e
) {
346 // Disable mouse reporting and show cursor
347 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
353 // We don't close System.in/out
355 // Shut down the streams, this should wake up the reader thread
362 if (output
!= null) {
366 } catch (IOException e
) {
375 public void flush() {
380 * Reset keyboard/mouse input parser
382 private void reset() {
383 state
= ParseState
.GROUND
;
384 params
= new ArrayList
<String
>();
391 * Produce a control character or one of the special ones (ENTER, TAB,
394 * @param ch Unicode code point
395 * @return one KEYPRESS event, either a control character (e.g. isKey ==
396 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
399 private TKeypressEvent
controlChar(char ch
) {
400 TKeypressEvent event
= new TKeypressEvent();
402 // System.err.printf("controlChar: %02x\n", ch);
406 // Carriage return --> ENTER
410 // Linefeed --> ENTER
422 // Make all other control characters come back as the alphabetic
423 // character with the ctrl field set. So SOH would be 'A' +
425 event
.key
= new TKeypress(false, 0, (char)(ch
+ 0x40),
433 * Produce special key from CSI Pn ; Pm ; ... ~
435 * @return one KEYPRESS event representing a special key
437 private TInputEvent
csiFnKey() {
440 if (params
.size() > 0) {
441 key
= Integer
.parseInt(params
.get(0));
443 if (params
.size() > 1) {
444 modifier
= Integer
.parseInt(params
.get(1));
446 TKeypressEvent event
= new TKeypressEvent();
504 event
.key
= kbShiftHome
;
507 event
.key
= kbShiftIns
;
510 event
.key
= kbShiftDel
;
513 event
.key
= kbShiftEnd
;
516 event
.key
= kbShiftPgUp
;
519 event
.key
= kbShiftPgDn
;
522 event
.key
= kbShiftF5
;
525 event
.key
= kbShiftF6
;
528 event
.key
= kbShiftF7
;
531 event
.key
= kbShiftF8
;
534 event
.key
= kbShiftF9
;
537 event
.key
= kbShiftF10
;
540 event
.key
= kbShiftF11
;
543 event
.key
= kbShiftF12
;
555 event
.key
= kbAltHome
;
558 event
.key
= kbAltIns
;
561 event
.key
= kbAltDel
;
564 event
.key
= kbAltEnd
;
567 event
.key
= kbAltPgUp
;
570 event
.key
= kbAltPgDn
;
588 event
.key
= kbAltF10
;
591 event
.key
= kbAltF11
;
594 event
.key
= kbAltF12
;
606 event
.key
= kbCtrlHome
;
609 event
.key
= kbCtrlIns
;
612 event
.key
= kbCtrlDel
;
615 event
.key
= kbCtrlEnd
;
618 event
.key
= kbCtrlPgUp
;
621 event
.key
= kbCtrlPgDn
;
624 event
.key
= kbCtrlF5
;
627 event
.key
= kbCtrlF6
;
630 event
.key
= kbCtrlF7
;
633 event
.key
= kbCtrlF8
;
636 event
.key
= kbCtrlF9
;
639 event
.key
= kbCtrlF10
;
642 event
.key
= kbCtrlF11
;
645 event
.key
= kbCtrlF12
;
658 // All OK, return a keypress
663 * Produce mouse events based on "Any event tracking" and UTF-8
665 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
667 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
669 private TInputEvent
parseMouse() {
670 int buttons
= params
.get(0).charAt(0) - 32;
671 int x
= params
.get(0).charAt(1) - 32 - 1;
672 int y
= params
.get(0).charAt(2) - 32 - 1;
674 // Clamp X and Y to the physical screen coordinates.
675 if (x
>= windowResize
.width
) {
676 x
= windowResize
.width
- 1;
678 if (y
>= windowResize
.height
) {
679 y
= windowResize
.height
- 1;
682 TMouseEvent event
= new TMouseEvent(TMouseEvent
.Type
.MOUSE_DOWN
);
688 // System.err.printf("buttons: %04x\r\n", buttons);
705 if (!mouse1
&& !mouse2
&& !mouse3
) {
706 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
708 event
.type
= TMouseEvent
.Type
.MOUSE_UP
;
725 // Dragging with mouse1 down
728 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
732 // Dragging with mouse2 down
735 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
739 // Dragging with mouse3 down
742 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
746 // Dragging with mouse2 down after wheelUp
749 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
753 // Dragging with mouse2 down after wheelDown
756 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
760 event
.mouseWheelUp
= true;
764 event
.mouseWheelDown
= true;
768 // Unknown, just make it motion
769 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
776 * Return any events in the IO queue.
778 * @param queue list to append new events to
780 public void getEvents(List
<TInputEvent
> queue
) {
781 synchronized (eventQueue
) {
782 if (eventQueue
.size() > 0) {
783 queue
.addAll(eventQueue
);
790 * Return any events in the IO queue due to timeout.
792 * @param queue list to append new events to
794 public void getIdleEvents(List
<TInputEvent
> queue
) {
796 // Check for new window size
797 session
.queryWindowSize();
798 int newWidth
= session
.getWindowWidth();
799 int newHeight
= session
.getWindowHeight();
800 if ((newWidth
!= windowResize
.width
) ||
801 (newHeight
!= windowResize
.height
)) {
802 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.Screen
,
803 newWidth
, newHeight
);
804 windowResize
.width
= newWidth
;
805 windowResize
.height
= newHeight
;
806 synchronized (eventQueue
) {
807 eventQueue
.add(event
);
811 synchronized (eventQueue
) {
812 if (eventQueue
.size() > 0) {
813 queue
.addAll(eventQueue
);
820 * Parses the next character of input to see if an InputEvent is
823 * @param events list to append new events to
824 * @param ch Unicode code point
826 private void processChar(List
<TInputEvent
> events
, char ch
) {
828 TKeypressEvent keypress
;
829 Date now
= new Date();
831 // ESCDELAY type timeout
832 if (state
== ParseState
.ESCAPE
) {
833 long escDelay
= now
.getTime() - escapeTime
;
834 if (escDelay
> 250) {
835 // After 0.25 seconds, assume a true escape character
836 events
.add(controlChar((char)0x1B));
841 // System.err.printf("state: %s ch %c\r\n", state, ch);
847 state
= ParseState
.ESCAPE
;
848 escapeTime
= now
.getTime();
854 events
.add(controlChar(ch
));
861 keypress
= new TKeypressEvent();
862 keypress
.key
.isKey
= false;
863 keypress
.key
.ch
= ch
;
864 events
.add(keypress
);
873 // ALT-Control character
874 keypress
= controlChar(ch
);
875 keypress
.key
.alt
= true;
876 events
.add(keypress
);
882 // This will be one of the function keys
883 state
= ParseState
.ESCAPE_INTERMEDIATE
;
887 // '[' goes to CSI_ENTRY
889 state
= ParseState
.CSI_ENTRY
;
893 // Everything else is assumed to be Alt-keystroke
894 keypress
= new TKeypressEvent();
895 keypress
.key
.isKey
= false;
896 keypress
.key
.ch
= ch
;
897 keypress
.key
.alt
= true;
898 if ((ch
>= 'A') && (ch
<= 'Z')) {
899 keypress
.key
.shift
= true;
901 events
.add(keypress
);
905 case ESCAPE_INTERMEDIATE
:
906 if ((ch
>= 'P') && (ch
<= 'S')) {
908 keypress
= new TKeypressEvent();
909 keypress
.key
.isKey
= true;
912 keypress
.key
.fnKey
= TKeypress
.F1
;
915 keypress
.key
.fnKey
= TKeypress
.F2
;
918 keypress
.key
.fnKey
= TKeypress
.F3
;
921 keypress
.key
.fnKey
= TKeypress
.F4
;
926 events
.add(keypress
);
931 // Unknown keystroke, ignore
936 // Numbers - parameter values
937 if ((ch
>= '0') && (ch
<= '9')) {
938 params
.set(paramI
, params
.get(paramI
) + ch
);
939 state
= ParseState
.CSI_PARAM
;
942 // Parameter separator
945 params
.set(paramI
, "");
949 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
953 keypress
= new TKeypressEvent();
954 keypress
.key
.isKey
= true;
955 keypress
.key
.fnKey
= TKeypress
.UP
;
956 if (params
.size() > 1) {
957 if (params
.get(1).equals("2")) {
958 keypress
.key
.shift
= true;
960 if (params
.get(1).equals("5")) {
961 keypress
.key
.ctrl
= true;
963 if (params
.get(1).equals("3")) {
964 keypress
.key
.alt
= true;
967 events
.add(keypress
);
972 keypress
= new TKeypressEvent();
973 keypress
.key
.isKey
= true;
974 keypress
.key
.fnKey
= TKeypress
.DOWN
;
975 if (params
.size() > 1) {
976 if (params
.get(1).equals("2")) {
977 keypress
.key
.shift
= true;
979 if (params
.get(1).equals("5")) {
980 keypress
.key
.ctrl
= true;
982 if (params
.get(1).equals("3")) {
983 keypress
.key
.alt
= true;
986 events
.add(keypress
);
991 keypress
= new TKeypressEvent();
992 keypress
.key
.isKey
= true;
993 keypress
.key
.fnKey
= TKeypress
.RIGHT
;
994 if (params
.size() > 1) {
995 if (params
.get(1).equals("2")) {
996 keypress
.key
.shift
= true;
998 if (params
.get(1).equals("5")) {
999 keypress
.key
.ctrl
= true;
1001 if (params
.get(1).equals("3")) {
1002 keypress
.key
.alt
= true;
1005 events
.add(keypress
);
1010 keypress
= new TKeypressEvent();
1011 keypress
.key
.isKey
= true;
1012 keypress
.key
.fnKey
= TKeypress
.LEFT
;
1013 if (params
.size() > 1) {
1014 if (params
.get(1).equals("2")) {
1015 keypress
.key
.shift
= true;
1017 if (params
.get(1).equals("5")) {
1018 keypress
.key
.ctrl
= true;
1020 if (params
.get(1).equals("3")) {
1021 keypress
.key
.alt
= true;
1024 events
.add(keypress
);
1029 keypress
= new TKeypressEvent();
1030 keypress
.key
.isKey
= true;
1031 keypress
.key
.fnKey
= TKeypress
.HOME
;
1032 events
.add(keypress
);
1037 keypress
= new TKeypressEvent();
1038 keypress
.key
.isKey
= true;
1039 keypress
.key
.fnKey
= TKeypress
.END
;
1040 events
.add(keypress
);
1044 // CBT - Cursor backward X tab stops (default 1)
1045 keypress
= new TKeypressEvent();
1046 keypress
.key
.isKey
= true;
1047 keypress
.key
.fnKey
= TKeypress
.BTAB
;
1048 events
.add(keypress
);
1053 state
= ParseState
.MOUSE
;
1060 // Unknown keystroke, ignore
1065 // Numbers - parameter values
1066 if ((ch
>= '0') && (ch
<= '9')) {
1067 params
.set(paramI
, params
.get(paramI
) + ch
);
1068 state
= ParseState
.CSI_PARAM
;
1071 // Parameter separator
1074 params
.set(paramI
, "");
1079 events
.add(csiFnKey());
1084 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1088 keypress
= new TKeypressEvent();
1089 keypress
.key
.isKey
= true;
1090 keypress
.key
.fnKey
= TKeypress
.UP
;
1091 if (params
.size() > 1) {
1092 if (params
.get(1).equals("2")) {
1093 keypress
.key
.shift
= true;
1095 if (params
.get(1).equals("5")) {
1096 keypress
.key
.ctrl
= true;
1098 if (params
.get(1).equals("3")) {
1099 keypress
.key
.alt
= true;
1102 events
.add(keypress
);
1107 keypress
= new TKeypressEvent();
1108 keypress
.key
.isKey
= true;
1109 keypress
.key
.fnKey
= TKeypress
.DOWN
;
1110 if (params
.size() > 1) {
1111 if (params
.get(1).equals("2")) {
1112 keypress
.key
.shift
= true;
1114 if (params
.get(1).equals("5")) {
1115 keypress
.key
.ctrl
= true;
1117 if (params
.get(1).equals("3")) {
1118 keypress
.key
.alt
= true;
1121 events
.add(keypress
);
1126 keypress
= new TKeypressEvent();
1127 keypress
.key
.isKey
= true;
1128 keypress
.key
.fnKey
= TKeypress
.RIGHT
;
1129 if (params
.size() > 1) {
1130 if (params
.get(1).equals("2")) {
1131 keypress
.key
.shift
= true;
1133 if (params
.get(1).equals("5")) {
1134 keypress
.key
.ctrl
= true;
1136 if (params
.get(1).equals("3")) {
1137 keypress
.key
.alt
= true;
1140 events
.add(keypress
);
1145 keypress
= new TKeypressEvent();
1146 keypress
.key
.isKey
= true;
1147 keypress
.key
.fnKey
= TKeypress
.LEFT
;
1148 if (params
.size() > 1) {
1149 if (params
.get(1).equals("2")) {
1150 keypress
.key
.shift
= true;
1152 if (params
.get(1).equals("5")) {
1153 keypress
.key
.ctrl
= true;
1155 if (params
.get(1).equals("3")) {
1156 keypress
.key
.alt
= true;
1159 events
.add(keypress
);
1167 // Unknown keystroke, ignore
1172 params
.set(0, params
.get(paramI
) + ch
);
1173 if (params
.get(0).length() == 3) {
1174 // We have enough to generate a mouse event
1175 events
.add(parseMouse());
1184 // This "should" be impossible to reach
1189 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1190 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1193 * @param on if true, enable metaSendsEscape
1194 * @return the string to emit to xterm
1196 static public String
xtermMetaSendsEscape(boolean on
) {
1198 return "\033[?1036h\033[?1034l";
1200 return "\033[?1036l";
1204 * Convert a list of SGR parameters into a full escape sequence. This
1205 * also eliminates a trailing ';' which would otherwise reset everything
1206 * to white-on-black not-bold.
1208 * @param str string of parameters, e.g. "31;1;"
1209 * @return the string to emit to an ANSI / ECMA-style terminal,
1212 static public String
addHeaderSGR(String str
) {
1213 if (str
.length() > 0) {
1214 // Nix any trailing ';' because that resets all attributes
1215 while (str
.endsWith(":")) {
1216 str
= str
.substring(0, str
.length() - 1);
1219 return "\033[" + str
+ "m";
1223 * Create a SGR parameter sequence for a single color change.
1225 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1226 * @param foreground if true, this is a foreground color
1227 * @return the string to emit to an ANSI / ECMA-style terminal,
1230 static public String
color(Color color
, boolean foreground
) {
1231 return color(color
, foreground
, true);
1235 * Create a SGR parameter sequence for a single color change.
1237 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1238 * @param foreground if true, this is a foreground color
1239 * @param header if true, make the full header, otherwise just emit the
1240 * color parameter e.g. "42;"
1241 * @return the string to emit to an ANSI / ECMA-style terminal,
1244 static public String
color(Color color
, boolean foreground
,
1247 int ecmaColor
= color
.value
;
1249 // Convert Color.* values to SGR numerics
1250 if (foreground
== true) {
1257 return String
.format("\033[%dm", ecmaColor
);
1259 return String
.format("%d;", ecmaColor
);
1264 * Create a SGR parameter sequence for both foreground and
1265 * background color change.
1267 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1268 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1269 * @return the string to emit to an ANSI / ECMA-style terminal,
1270 * e.g. "\033[31;42m"
1272 static public String
color(Color foreColor
, Color backColor
) {
1273 return color(foreColor
, backColor
, true);
1277 * Create a SGR parameter sequence for both foreground and
1278 * background color change.
1280 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1281 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1282 * @param header if true, make the full header, otherwise just emit the
1283 * color parameter e.g. "31;42;"
1284 * @return the string to emit to an ANSI / ECMA-style terminal,
1285 * e.g. "\033[31;42m"
1287 static public String
color(Color foreColor
, Color backColor
,
1290 int ecmaForeColor
= foreColor
.value
;
1291 int ecmaBackColor
= backColor
.value
;
1293 // Convert Color.* values to SGR numerics
1294 ecmaBackColor
+= 40;
1295 ecmaForeColor
+= 30;
1298 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1300 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1305 * Create a SGR parameter sequence for foreground, background, and
1306 * several attributes. This sequence first resets all attributes to
1307 * default, then sets attributes as per the parameters.
1309 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1310 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1311 * @param bold if true, set bold
1312 * @param reverse if true, set reverse
1313 * @param blink if true, set blink
1314 * @param underline if true, set underline
1315 * @return the string to emit to an ANSI / ECMA-style terminal,
1316 * e.g. "\033[0;1;31;42m"
1318 static public String
color(Color foreColor
, Color backColor
, boolean bold
,
1319 boolean reverse
, boolean blink
, boolean underline
) {
1321 int ecmaForeColor
= foreColor
.value
;
1322 int ecmaBackColor
= backColor
.value
;
1324 // Convert Color.* values to SGR numerics
1325 ecmaBackColor
+= 40;
1326 ecmaForeColor
+= 30;
1328 StringBuilder sb
= new StringBuilder();
1329 if ( bold
&& reverse
&& blink
&& !underline
) {
1330 sb
.append("\033[0;1;7;5;");
1331 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1332 sb
.append("\033[0;1;7;");
1333 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1334 sb
.append("\033[0;7;5;");
1335 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1336 sb
.append("\033[0;1;5;");
1337 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1338 sb
.append("\033[0;1;");
1339 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1340 sb
.append("\033[0;7;");
1341 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1342 sb
.append("\033[0;5;");
1343 } else if ( bold
&& reverse
&& blink
&& underline
) {
1344 sb
.append("\033[0;1;7;5;4;");
1345 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1346 sb
.append("\033[0;1;7;4;");
1347 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1348 sb
.append("\033[0;7;5;4;");
1349 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1350 sb
.append("\033[0;1;5;4;");
1351 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1352 sb
.append("\033[0;1;4;");
1353 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1354 sb
.append("\033[0;7;4;");
1355 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1356 sb
.append("\033[0;5;4;");
1357 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1358 sb
.append("\033[0;4;");
1360 assert(!bold
&& !reverse
&& !blink
&& !underline
);
1361 sb
.append("\033[0;");
1363 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1364 return sb
.toString();
1368 * Create a SGR parameter sequence for enabling reverse color.
1370 * @param on if true, turn on reverse
1371 * @return the string to emit to an ANSI / ECMA-style terminal,
1374 static public String
reverse(boolean on
) {
1382 * Create a SGR parameter sequence to reset to defaults.
1384 * @return the string to emit to an ANSI / ECMA-style terminal,
1387 static public String
normal() {
1388 return normal(true);
1392 * Create a SGR parameter sequence to reset to defaults.
1394 * @param header if true, make the full header, otherwise just emit the
1395 * bare parameter e.g. "0;"
1396 * @return the string to emit to an ANSI / ECMA-style terminal,
1399 static public String
normal(boolean header
) {
1401 return "\033[0;37;40m";
1407 * Create a SGR parameter sequence for enabling boldface.
1409 * @param on if true, turn on bold
1410 * @return the string to emit to an ANSI / ECMA-style terminal,
1413 static public String
bold(boolean on
) {
1414 return bold(on
, true);
1418 * Create a SGR parameter sequence for enabling boldface.
1420 * @param on if true, turn on bold
1421 * @param header if true, make the full header, otherwise just emit the
1422 * bare parameter e.g. "1;"
1423 * @return the string to emit to an ANSI / ECMA-style terminal,
1426 static public String
bold(boolean on
, boolean header
) {
1440 * Create a SGR parameter sequence for enabling blinking text.
1442 * @param on if true, turn on blink
1443 * @return the string to emit to an ANSI / ECMA-style terminal,
1446 static public String
blink(boolean on
) {
1447 return blink(on
, true);
1451 * Create a SGR parameter sequence for enabling blinking text.
1453 * @param on if true, turn on blink
1454 * @param header if true, make the full header, otherwise just emit the
1455 * bare parameter e.g. "5;"
1456 * @return the string to emit to an ANSI / ECMA-style terminal,
1459 static public String
blink(boolean on
, boolean header
) {
1473 * Create a SGR parameter sequence for enabling underline / underscored
1476 * @param on if true, turn on underline
1477 * @return the string to emit to an ANSI / ECMA-style terminal,
1480 static public String
underline(boolean on
) {
1488 * Create a SGR parameter sequence for enabling the visible cursor.
1490 * @param on if true, turn on cursor
1491 * @return the string to emit to an ANSI / ECMA-style terminal
1493 public String
cursor(boolean on
) {
1494 if (on
&& (cursorOn
== false)) {
1498 if (!on
&& (cursorOn
== true)) {
1506 * Clear the entire screen. Because some terminals use back-color-erase,
1507 * set the color to white-on-black beforehand.
1509 * @return the string to emit to an ANSI / ECMA-style terminal
1511 static public String
clearAll() {
1512 return "\033[0;37;40m\033[2J";
1516 * Clear the line from the cursor (inclusive) to the end of the screen.
1517 * Because some terminals use back-color-erase, set the color to
1518 * white-on-black beforehand.
1520 * @return the string to emit to an ANSI / ECMA-style terminal
1522 static public String
clearRemainingLine() {
1523 return "\033[0;37;40m\033[K";
1527 * Clear the line up the cursor (inclusive). Because some terminals use
1528 * back-color-erase, set the color to white-on-black beforehand.
1530 * @return the string to emit to an ANSI / ECMA-style terminal
1532 static public String
clearPreceedingLine() {
1533 return "\033[0;37;40m\033[1K";
1537 * Clear the line. Because some terminals use back-color-erase, set the
1538 * color to white-on-black beforehand.
1540 * @return the string to emit to an ANSI / ECMA-style terminal
1542 static public String
clearLine() {
1543 return "\033[0;37;40m\033[2K";
1547 * Move the cursor to the top-left corner.
1549 * @return the string to emit to an ANSI / ECMA-style terminal
1551 static public String
home() {
1556 * Move the cursor to (x, y).
1558 * @param x column coordinate. 0 is the left-most column.
1559 * @param y row coordinate. 0 is the top-most row.
1560 * @return the string to emit to an ANSI / ECMA-style terminal
1562 static public String
gotoXY(int x
, int y
) {
1563 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1567 * Tell (u)xterm that we want to receive mouse events based on "Any event
1568 * tracking" and UTF-8 coordinates. See
1569 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1571 * Note that this also sets the alternate/primary screen buffer.
1573 * @param on If true, enable mouse report and use the alternate screen
1574 * buffer. If false disable mouse reporting and use the primary screen
1576 * @return the string to emit to xterm
1578 static public String
mouse(boolean on
) {
1580 return "\033[?1003;1005h\033[?1049h";
1582 return "\033[?1003;1005l\033[?1049l";
1586 * Read function runs on a separate thread.
1589 boolean done
= false;
1590 // available() will often return > 1, so we need to read in chunks to
1592 char [] readBuffer
= new char[128];
1593 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1595 while ((done
== false) && (stopReaderThread
== false)) {
1597 // We assume that if inputStream has bytes available, then
1598 // input won't block on read().
1599 int n
= inputStream
.available();
1601 if (readBuffer
.length
< n
) {
1602 // The buffer wasn't big enough, make it huger
1603 readBuffer
= new char[readBuffer
.length
* 2];
1606 int rc
= input
.read(readBuffer
, 0, n
);
1607 // System.err.printf("read() %d", rc); System.err.flush();
1612 for (int i
= 0; i
< rc
; i
++) {
1613 int ch
= readBuffer
[i
];
1614 processChar(events
, (char)ch
);
1615 if (events
.size() > 0) {
1616 // Add to the queue for the backend thread to
1617 // be able to obtain.
1618 synchronized (eventQueue
) {
1619 eventQueue
.addAll(events
);
1621 // Now wake up the backend
1622 synchronized (this) {
1630 // Wait 5 millis for more data
1633 // System.err.println("end while loop"); System.err.flush();
1634 } catch (InterruptedException e
) {
1636 } catch (IOException e
) {
1637 e
.printStackTrace();
1640 } // while ((done == false) && (stopReaderThread == false))
1641 // System.err.println("*** run() exiting..."); System.err.flush();