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 * Call 'stty cooked' to set cooked mode.
199 private void sttyCooked() {
204 * Call 'stty raw' to set raw mode.
206 private void sttyRaw() {
211 * Call 'stty' to set raw or cooked mode.
213 * @param mode if true, set raw mode, otherwise set cooked mode
215 private void doStty(boolean mode
) {
217 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
219 String
[] cmdCooked
= {
220 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
225 process
= Runtime
.getRuntime().exec(cmdRaw
);
227 process
= Runtime
.getRuntime().exec(cmdCooked
);
229 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
230 String line
= in
.readLine();
231 if ((line
!= null) && (line
.length() > 0)) {
232 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
235 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
236 line
= err
.readLine();
237 if ((line
!= null) && (line
.length() > 0)) {
238 System
.err
.println("Error output from stty: " + line
);
243 } catch (InterruptedException e
) {
247 int rc
= process
.exitValue();
249 System
.err
.println("stty returned error code: " + rc
);
251 } catch (IOException e
) {
257 * Constructor sets up state for getEvent()
259 * @param input an InputStream connected to the remote user, or null for
260 * System.in. If System.in is used, then on non-Windows systems it will
261 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
262 * mode. input is always converted to a Reader with UTF-8 encoding.
263 * @param output an OutputStream connected to the remote user, or null
264 * for System.out. output is always converted to a Writer with UTF-8
267 public ECMA48Terminal(InputStream input
, OutputStream output
) throws UnsupportedEncodingException
{
273 stopReaderThread
= false;
276 // inputStream = System.in;
277 inputStream
= new FileInputStream(FileDescriptor
.in
);
283 this.input
= new InputStreamReader(inputStream
, "UTF-8");
285 // TODO: include TelnetSocket from NIB and have it implement
287 if (input
instanceof SessionInfo
) {
288 session
= (SessionInfo
)input
;
290 if (session
== null) {
292 // Reading right off the tty
293 session
= new TTYSessionInfo();
295 session
= new TSessionInfo();
299 if (output
== null) {
300 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
303 this.output
= new PrintWriter(new OutputStreamWriter(output
,
307 // Enable mouse reporting and metaSendsEscape
308 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
310 // Hang onto the window size
311 windowResize
= new TResizeEvent(TResizeEvent
.Type
.Screen
,
312 session
.getWindowWidth(), session
.getWindowHeight());
314 // Spin up the input reader
315 eventQueue
= new LinkedList
<TInputEvent
>();
316 readerThread
= new Thread(this);
317 readerThread
.start();
321 * Restore terminal to normal state
323 public void shutdown() {
325 // System.err.println("=== shutdown() ==="); System.err.flush();
327 // Tell the reader thread to stop looking at input
328 stopReaderThread
= true;
331 } catch (InterruptedException e
) {
335 // Disable mouse reporting and show cursor
336 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
342 // We don't close System.in/out
344 // Shut down the streams, this should wake up the reader thread
351 if (output
!= null) {
355 } catch (IOException e
) {
364 public void flush() {
369 * Reset keyboard/mouse input parser
371 private void reset() {
372 state
= ParseState
.GROUND
;
373 params
= new ArrayList
<String
>();
380 * Produce a control character or one of the special ones (ENTER, TAB,
383 * @param ch Unicode code point
384 * @return one KEYPRESS event, either a control character (e.g. isKey ==
385 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
388 private TKeypressEvent
controlChar(char ch
) {
389 TKeypressEvent event
= new TKeypressEvent();
391 // System.err.printf("controlChar: %02x\n", ch);
395 // Carriage return --> ENTER
399 // Linefeed --> ENTER
411 // Make all other control characters come back as the alphabetic
412 // character with the ctrl field set. So SOH would be 'A' +
414 event
.key
= new TKeypress(false, 0, (char)(ch
+ 0x40),
422 * Produce special key from CSI Pn ; Pm ; ... ~
424 * @return one KEYPRESS event representing a special key
426 private TInputEvent
csiFnKey() {
429 if (params
.size() > 0) {
430 key
= Integer
.parseInt(params
.get(0));
432 if (params
.size() > 1) {
433 modifier
= Integer
.parseInt(params
.get(1));
435 TKeypressEvent event
= new TKeypressEvent();
493 event
.key
= kbShiftHome
;
496 event
.key
= kbShiftIns
;
499 event
.key
= kbShiftDel
;
502 event
.key
= kbShiftEnd
;
505 event
.key
= kbShiftPgUp
;
508 event
.key
= kbShiftPgDn
;
511 event
.key
= kbShiftF5
;
514 event
.key
= kbShiftF6
;
517 event
.key
= kbShiftF7
;
520 event
.key
= kbShiftF8
;
523 event
.key
= kbShiftF9
;
526 event
.key
= kbShiftF10
;
529 event
.key
= kbShiftF11
;
532 event
.key
= kbShiftF12
;
544 event
.key
= kbAltHome
;
547 event
.key
= kbAltIns
;
550 event
.key
= kbAltDel
;
553 event
.key
= kbAltEnd
;
556 event
.key
= kbAltPgUp
;
559 event
.key
= kbAltPgDn
;
577 event
.key
= kbAltF10
;
580 event
.key
= kbAltF11
;
583 event
.key
= kbAltF12
;
595 event
.key
= kbCtrlHome
;
598 event
.key
= kbCtrlIns
;
601 event
.key
= kbCtrlDel
;
604 event
.key
= kbCtrlEnd
;
607 event
.key
= kbCtrlPgUp
;
610 event
.key
= kbCtrlPgDn
;
613 event
.key
= kbCtrlF5
;
616 event
.key
= kbCtrlF6
;
619 event
.key
= kbCtrlF7
;
622 event
.key
= kbCtrlF8
;
625 event
.key
= kbCtrlF9
;
628 event
.key
= kbCtrlF10
;
631 event
.key
= kbCtrlF11
;
634 event
.key
= kbCtrlF12
;
647 // All OK, return a keypress
652 * Produce mouse events based on "Any event tracking" and UTF-8
654 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
656 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
658 private TInputEvent
parseMouse() {
659 int buttons
= params
.get(0).charAt(0) - 32;
660 int x
= params
.get(0).charAt(1) - 32 - 1;
661 int y
= params
.get(0).charAt(2) - 32 - 1;
663 // Clamp X and Y to the physical screen coordinates.
664 if (x
>= windowResize
.width
) {
665 x
= windowResize
.width
- 1;
667 if (y
>= windowResize
.height
) {
668 y
= windowResize
.height
- 1;
671 TMouseEvent event
= new TMouseEvent(TMouseEvent
.Type
.MOUSE_DOWN
);
677 // System.err.printf("buttons: %04x\r\n", buttons);
694 if (!mouse1
&& !mouse2
&& !mouse3
) {
695 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
697 event
.type
= TMouseEvent
.Type
.MOUSE_UP
;
714 // Dragging with mouse1 down
717 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
721 // Dragging with mouse2 down
724 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
728 // Dragging with mouse3 down
731 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
735 // Dragging with mouse2 down after wheelUp
738 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
742 // Dragging with mouse2 down after wheelDown
745 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
749 event
.mouseWheelUp
= true;
753 event
.mouseWheelDown
= true;
757 // Unknown, just make it motion
758 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
765 * Return any events in the IO queue.
767 * @return list of new events (which may be empty)
769 public List
<TInputEvent
> getEvents() {
770 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
773 if (eventQueue
.size() > 0) {
774 events
.addAll(eventQueue
);
779 // TEST: drop a cmAbort
780 // events.add(new jexer.event.TCommandEvent(jexer.TCommand.cmAbort));
781 // events.add(new jexer.event.TKeypressEvent(kbAltX));
787 * Parses the next character of input to see if an InputEvent is fully
790 * @param ch Unicode code point
791 * @return list of new events (which may be empty)
793 public List
<TInputEvent
> getEvents(char ch
) {
794 return getEvents(ch
, false);
798 * Parses the next character of input to see if an InputEvent is
801 * @param ch Unicode code point
802 * @param noChar if true, ignore ch. This is currently used to return a
803 * bare ESC and RESIZE events.
804 * @return list of new events (which may be empty)
806 public List
<TInputEvent
> getEvents(char ch
, boolean noChar
) {
807 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
809 TKeypressEvent keypress
;
810 Date now
= new Date();
813 // ESCDELAY type timeout
814 if (state == ParseState.ESCAPE) {
815 long escDelay = now.getTime() - escapeTime;
816 if (escDelay > 250) {
817 // After 0.25 seconds, assume a true escape character
818 events.add(controlChar((char)0x1B));
824 if (noChar
== true) {
825 int newWidth
= session
.getWindowWidth();
826 int newHeight
= session
.getWindowHeight();
827 if ((newWidth
!= windowResize
.width
) ||
828 (newHeight
!= windowResize
.height
)) {
829 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.Screen
,
830 newWidth
, newHeight
);
831 windowResize
.width
= newWidth
;
832 windowResize
.height
= newHeight
;
836 // Nothing else to do, bail out
840 // System.err.printf("state: %s ch %c\r\n", state, ch);
846 state
= ParseState
.ESCAPE
;
847 escapeTime
= now
.getTime();
853 events
.add(controlChar(ch
));
860 keypress
= new TKeypressEvent();
861 keypress
.key
.isKey
= false;
862 keypress
.key
.ch
= ch
;
863 events
.add(keypress
);
872 // ALT-Control character
873 keypress
= controlChar(ch
);
874 keypress
.key
.alt
= true;
875 events
.add(keypress
);
881 // This will be one of the function keys
882 state
= ParseState
.ESCAPE_INTERMEDIATE
;
886 // '[' goes to CSI_ENTRY
888 state
= ParseState
.CSI_ENTRY
;
892 // Everything else is assumed to be Alt-keystroke
893 keypress
= new TKeypressEvent();
894 keypress
.key
.isKey
= false;
895 keypress
.key
.ch
= ch
;
896 keypress
.key
.alt
= true;
897 if ((ch
>= 'A') && (ch
<= 'Z')) {
898 keypress
.key
.shift
= true;
900 events
.add(keypress
);
904 case ESCAPE_INTERMEDIATE
:
905 if ((ch
>= 'P') && (ch
<= 'S')) {
907 keypress
= new TKeypressEvent();
908 keypress
.key
.isKey
= true;
911 keypress
.key
.fnKey
= TKeypress
.F1
;
914 keypress
.key
.fnKey
= TKeypress
.F2
;
917 keypress
.key
.fnKey
= TKeypress
.F3
;
920 keypress
.key
.fnKey
= TKeypress
.F4
;
925 events
.add(keypress
);
930 // Unknown keystroke, ignore
935 // Numbers - parameter values
936 if ((ch
>= '0') && (ch
<= '9')) {
937 params
.set(paramI
, params
.get(paramI
) + ch
);
938 state
= ParseState
.CSI_PARAM
;
941 // Parameter separator
944 params
.set(paramI
, "");
948 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
952 keypress
= new TKeypressEvent();
953 keypress
.key
.isKey
= true;
954 keypress
.key
.fnKey
= TKeypress
.UP
;
955 if (params
.size() > 1) {
956 if (params
.get(1).equals("2")) {
957 keypress
.key
.shift
= true;
959 if (params
.get(1).equals("5")) {
960 keypress
.key
.ctrl
= true;
962 if (params
.get(1).equals("3")) {
963 keypress
.key
.alt
= true;
966 events
.add(keypress
);
971 keypress
= new TKeypressEvent();
972 keypress
.key
.isKey
= true;
973 keypress
.key
.fnKey
= TKeypress
.DOWN
;
974 if (params
.size() > 1) {
975 if (params
.get(1).equals("2")) {
976 keypress
.key
.shift
= true;
978 if (params
.get(1).equals("5")) {
979 keypress
.key
.ctrl
= true;
981 if (params
.get(1).equals("3")) {
982 keypress
.key
.alt
= true;
985 events
.add(keypress
);
990 keypress
= new TKeypressEvent();
991 keypress
.key
.isKey
= true;
992 keypress
.key
.fnKey
= TKeypress
.RIGHT
;
993 if (params
.size() > 1) {
994 if (params
.get(1).equals("2")) {
995 keypress
.key
.shift
= true;
997 if (params
.get(1).equals("5")) {
998 keypress
.key
.ctrl
= true;
1000 if (params
.get(1).equals("3")) {
1001 keypress
.key
.alt
= true;
1004 events
.add(keypress
);
1009 keypress
= new TKeypressEvent();
1010 keypress
.key
.isKey
= true;
1011 keypress
.key
.fnKey
= TKeypress
.LEFT
;
1012 if (params
.size() > 1) {
1013 if (params
.get(1).equals("2")) {
1014 keypress
.key
.shift
= true;
1016 if (params
.get(1).equals("5")) {
1017 keypress
.key
.ctrl
= true;
1019 if (params
.get(1).equals("3")) {
1020 keypress
.key
.alt
= true;
1023 events
.add(keypress
);
1028 keypress
= new TKeypressEvent();
1029 keypress
.key
.isKey
= true;
1030 keypress
.key
.fnKey
= TKeypress
.HOME
;
1031 events
.add(keypress
);
1036 keypress
= new TKeypressEvent();
1037 keypress
.key
.isKey
= true;
1038 keypress
.key
.fnKey
= TKeypress
.END
;
1039 events
.add(keypress
);
1043 // CBT - Cursor backward X tab stops (default 1)
1044 keypress
= new TKeypressEvent();
1045 keypress
.key
.isKey
= true;
1046 keypress
.key
.fnKey
= TKeypress
.BTAB
;
1047 events
.add(keypress
);
1052 state
= ParseState
.MOUSE
;
1059 // Unknown keystroke, ignore
1064 // Numbers - parameter values
1065 if ((ch
>= '0') && (ch
<= '9')) {
1066 params
.set(paramI
, params
.get(paramI
) + ch
);
1067 state
= ParseState
.CSI_PARAM
;
1070 // Parameter separator
1073 params
.set(paramI
, "");
1078 events
.add(csiFnKey());
1083 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1087 keypress
= new TKeypressEvent();
1088 keypress
.key
.isKey
= true;
1089 keypress
.key
.fnKey
= TKeypress
.UP
;
1090 if (params
.size() > 1) {
1091 if (params
.get(1).equals("2")) {
1092 keypress
.key
.shift
= true;
1094 if (params
.get(1).equals("5")) {
1095 keypress
.key
.ctrl
= true;
1097 if (params
.get(1).equals("3")) {
1098 keypress
.key
.alt
= true;
1101 events
.add(keypress
);
1106 keypress
= new TKeypressEvent();
1107 keypress
.key
.isKey
= true;
1108 keypress
.key
.fnKey
= TKeypress
.DOWN
;
1109 if (params
.size() > 1) {
1110 if (params
.get(1).equals("2")) {
1111 keypress
.key
.shift
= true;
1113 if (params
.get(1).equals("5")) {
1114 keypress
.key
.ctrl
= true;
1116 if (params
.get(1).equals("3")) {
1117 keypress
.key
.alt
= true;
1120 events
.add(keypress
);
1125 keypress
= new TKeypressEvent();
1126 keypress
.key
.isKey
= true;
1127 keypress
.key
.fnKey
= TKeypress
.RIGHT
;
1128 if (params
.size() > 1) {
1129 if (params
.get(1).equals("2")) {
1130 keypress
.key
.shift
= true;
1132 if (params
.get(1).equals("5")) {
1133 keypress
.key
.ctrl
= true;
1135 if (params
.get(1).equals("3")) {
1136 keypress
.key
.alt
= true;
1139 events
.add(keypress
);
1144 keypress
= new TKeypressEvent();
1145 keypress
.key
.isKey
= true;
1146 keypress
.key
.fnKey
= TKeypress
.LEFT
;
1147 if (params
.size() > 1) {
1148 if (params
.get(1).equals("2")) {
1149 keypress
.key
.shift
= true;
1151 if (params
.get(1).equals("5")) {
1152 keypress
.key
.ctrl
= true;
1154 if (params
.get(1).equals("3")) {
1155 keypress
.key
.alt
= true;
1158 events
.add(keypress
);
1166 // Unknown keystroke, ignore
1171 params
.set(0, params
.get(paramI
) + ch
);
1172 if (params
.get(0).length() == 3) {
1173 // We have enough to generate a mouse event
1174 events
.add(parseMouse());
1183 // This "should" be impossible to reach
1188 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1189 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1192 * @param on if true, enable metaSendsEscape
1193 * @return the string to emit to xterm
1195 static public String
xtermMetaSendsEscape(boolean on
) {
1197 return "\033[?1036h\033[?1034l";
1199 return "\033[?1036l";
1203 * Convert a list of SGR parameters into a full escape sequence. This
1204 * also eliminates a trailing ';' which would otherwise reset everything
1205 * to white-on-black not-bold.
1207 * @param str string of parameters, e.g. "31;1;"
1208 * @return the string to emit to an ANSI / ECMA-style terminal,
1211 static public String
addHeaderSGR(String str
) {
1212 if (str
.length() > 0) {
1213 // Nix any trailing ';' because that resets all attributes
1214 while (str
.endsWith(":")) {
1215 str
= str
.substring(0, str
.length() - 1);
1218 return "\033[" + str
+ "m";
1222 * Create a SGR parameter sequence for a single color change.
1224 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1225 * @param foreground if true, this is a foreground color
1226 * @return the string to emit to an ANSI / ECMA-style terminal,
1229 static public String
color(Color color
, boolean foreground
) {
1230 return color(color
, foreground
, true);
1234 * Create a SGR parameter sequence for a single color change.
1236 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1237 * @param foreground if true, this is a foreground color
1238 * @param header if true, make the full header, otherwise just emit the
1239 * color parameter e.g. "42;"
1240 * @return the string to emit to an ANSI / ECMA-style terminal,
1243 static public String
color(Color color
, boolean foreground
,
1246 int ecmaColor
= color
.value
;
1248 // Convert Color.* values to SGR numerics
1249 if (foreground
== true) {
1256 return String
.format("\033[%dm", ecmaColor
);
1258 return String
.format("%d;", ecmaColor
);
1263 * Create a SGR parameter sequence for both foreground and
1264 * background color change.
1266 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1267 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1268 * @return the string to emit to an ANSI / ECMA-style terminal,
1269 * e.g. "\033[31;42m"
1271 static public String
color(Color foreColor
, Color backColor
) {
1272 return color(foreColor
, backColor
, true);
1276 * Create a SGR parameter sequence for both foreground and
1277 * background color change.
1279 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1280 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1281 * @param header if true, make the full header, otherwise just emit the
1282 * color parameter e.g. "31;42;"
1283 * @return the string to emit to an ANSI / ECMA-style terminal,
1284 * e.g. "\033[31;42m"
1286 static public String
color(Color foreColor
, Color backColor
,
1289 int ecmaForeColor
= foreColor
.value
;
1290 int ecmaBackColor
= backColor
.value
;
1292 // Convert Color.* values to SGR numerics
1293 ecmaBackColor
+= 40;
1294 ecmaForeColor
+= 30;
1297 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1299 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1304 * Create a SGR parameter sequence for foreground, background, and
1305 * several attributes. This sequence first resets all attributes to
1306 * default, then sets attributes as per the parameters.
1308 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1309 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1310 * @param bold if true, set bold
1311 * @param reverse if true, set reverse
1312 * @param blink if true, set blink
1313 * @param underline if true, set underline
1314 * @return the string to emit to an ANSI / ECMA-style terminal,
1315 * e.g. "\033[0;1;31;42m"
1317 static public String
color(Color foreColor
, Color backColor
, boolean bold
,
1318 boolean reverse
, boolean blink
, boolean underline
) {
1320 int ecmaForeColor
= foreColor
.value
;
1321 int ecmaBackColor
= backColor
.value
;
1323 // Convert Color.* values to SGR numerics
1324 ecmaBackColor
+= 40;
1325 ecmaForeColor
+= 30;
1327 StringBuilder sb
= new StringBuilder();
1328 if ( bold
&& reverse
&& blink
&& !underline
) {
1329 sb
.append("\033[0;1;7;5;");
1330 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1331 sb
.append("\033[0;1;7;");
1332 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1333 sb
.append("\033[0;7;5;");
1334 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1335 sb
.append("\033[0;1;5;");
1336 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1337 sb
.append("\033[0;1;");
1338 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1339 sb
.append("\033[0;7;");
1340 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1341 sb
.append("\033[0;5;");
1342 } else if ( bold
&& reverse
&& blink
&& underline
) {
1343 sb
.append("\033[0;1;7;5;4;");
1344 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1345 sb
.append("\033[0;1;7;4;");
1346 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1347 sb
.append("\033[0;7;5;4;");
1348 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1349 sb
.append("\033[0;1;5;4;");
1350 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1351 sb
.append("\033[0;1;4;");
1352 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1353 sb
.append("\033[0;7;4;");
1354 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1355 sb
.append("\033[0;5;4;");
1356 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1357 sb
.append("\033[0;4;");
1359 assert(!bold
&& !reverse
&& !blink
&& !underline
);
1360 sb
.append("\033[0;");
1362 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1363 return sb
.toString();
1367 * Create a SGR parameter sequence for enabling reverse color.
1369 * @param on if true, turn on reverse
1370 * @return the string to emit to an ANSI / ECMA-style terminal,
1373 static public String
reverse(boolean on
) {
1381 * Create a SGR parameter sequence to reset to defaults.
1383 * @return the string to emit to an ANSI / ECMA-style terminal,
1386 static public String
normal() {
1387 return normal(true);
1391 * Create a SGR parameter sequence to reset to defaults.
1393 * @param header if true, make the full header, otherwise just emit the
1394 * bare parameter e.g. "0;"
1395 * @return the string to emit to an ANSI / ECMA-style terminal,
1398 static public String
normal(boolean header
) {
1400 return "\033[0;37;40m";
1406 * Create a SGR parameter sequence for enabling boldface.
1408 * @param on if true, turn on bold
1409 * @return the string to emit to an ANSI / ECMA-style terminal,
1412 static public String
bold(boolean on
) {
1413 return bold(on
, true);
1417 * Create a SGR parameter sequence for enabling boldface.
1419 * @param on if true, turn on bold
1420 * @param header if true, make the full header, otherwise just emit the
1421 * bare parameter e.g. "1;"
1422 * @return the string to emit to an ANSI / ECMA-style terminal,
1425 static public String
bold(boolean on
, boolean header
) {
1439 * Create a SGR parameter sequence for enabling blinking text.
1441 * @param on if true, turn on blink
1442 * @return the string to emit to an ANSI / ECMA-style terminal,
1445 static public String
blink(boolean on
) {
1446 return blink(on
, true);
1450 * Create a SGR parameter sequence for enabling blinking text.
1452 * @param on if true, turn on blink
1453 * @param header if true, make the full header, otherwise just emit the
1454 * bare parameter e.g. "5;"
1455 * @return the string to emit to an ANSI / ECMA-style terminal,
1458 static public String
blink(boolean on
, boolean header
) {
1472 * Create a SGR parameter sequence for enabling underline / underscored
1475 * @param on if true, turn on underline
1476 * @return the string to emit to an ANSI / ECMA-style terminal,
1479 static public String
underline(boolean on
) {
1487 * Create a SGR parameter sequence for enabling the visible cursor.
1489 * @param on if true, turn on cursor
1490 * @return the string to emit to an ANSI / ECMA-style terminal
1492 public String
cursor(boolean on
) {
1493 if (on
&& (cursorOn
== false)) {
1497 if (!on
&& (cursorOn
== true)) {
1505 * Clear the entire screen. Because some terminals use back-color-erase,
1506 * set the color to white-on-black beforehand.
1508 * @return the string to emit to an ANSI / ECMA-style terminal
1510 static public String
clearAll() {
1511 return "\033[0;37;40m\033[2J";
1515 * Clear the line from the cursor (inclusive) to the end of the screen.
1516 * Because some terminals use back-color-erase, set the color to
1517 * white-on-black beforehand.
1519 * @return the string to emit to an ANSI / ECMA-style terminal
1521 static public String
clearRemainingLine() {
1522 return "\033[0;37;40m\033[K";
1526 * Clear the line up the cursor (inclusive). Because some terminals use
1527 * back-color-erase, set the color to white-on-black beforehand.
1529 * @return the string to emit to an ANSI / ECMA-style terminal
1531 static public String
clearPreceedingLine() {
1532 return "\033[0;37;40m\033[1K";
1536 * Clear the line. Because some terminals use back-color-erase, set the
1537 * color to white-on-black beforehand.
1539 * @return the string to emit to an ANSI / ECMA-style terminal
1541 static public String
clearLine() {
1542 return "\033[0;37;40m\033[2K";
1546 * Move the cursor to the top-left corner.
1548 * @return the string to emit to an ANSI / ECMA-style terminal
1550 static public String
home() {
1555 * Move the cursor to (x, y).
1557 * @param x column coordinate. 0 is the left-most column.
1558 * @param y row coordinate. 0 is the top-most row.
1559 * @return the string to emit to an ANSI / ECMA-style terminal
1561 static public String
gotoXY(int x
, int y
) {
1562 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1566 * Tell (u)xterm that we want to receive mouse events based on "Any event
1567 * tracking" and UTF-8 coordinates. See
1568 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1570 * Note that this also sets the alternate/primary screen buffer.
1572 * @param on If true, enable mouse report and use the alternate screen
1573 * buffer. If false disable mouse reporting and use the primary screen
1575 * @return the string to emit to xterm
1577 static public String
mouse(boolean on
) {
1579 return "\033[?1003;1005h\033[?1049h";
1581 return "\033[?1003;1005l\033[?1049l";
1585 * Read function runs on a separate thread.
1588 boolean done
= false;
1589 // available() will often return > 1, so we need to read in chunks to
1591 char [] readBuffer
= new char[128];
1593 while ((done
== false) && (stopReaderThread
== false)) {
1595 // We assume that if inputStream has bytes available, then
1596 // input won't block on read().
1597 int n
= inputStream
.available();
1599 if (readBuffer
.length
< n
) {
1600 // The buffer wasn't big enough, make it huger
1601 readBuffer
= new char[readBuffer
.length
* 2];
1604 int rc
= input
.read(readBuffer
, 0, n
);
1605 // System.err.printf("read() %d", rc); System.err.flush();
1610 for (int i
= 0; i
< rc
; i
++) {
1611 int ch
= readBuffer
[i
];
1613 // System.err.printf("** READ 0x%x '%c'", ch, ch);
1614 List
<TInputEvent
> events
= getEvents((char)ch
);
1615 synchronized (this) {
1617 System.err.printf("adding %d events\n",
1620 eventQueue
.addAll(events
);
1625 // Wait 5 millis for more data
1628 // System.err.println("end while loop"); System.err.flush();
1629 } catch (InterruptedException e
) {
1631 } catch (IOException e
) {
1632 e
.printStackTrace();
1635 } // while ((done == false) && (stopReaderThread == false))
1636 // System.err.println("*** run() exiting..."); System.err.flush();