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 class ECMA48Terminal
implements Runnable
{
67 * The session information.
69 private SessionInfo sessionInfo
;
72 * Getter for sessionInfo.
74 * @return the SessionInfo
76 public final 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 * true if mouse1 was down. Used to report mouse1 on the release event.
133 private boolean mouse1
;
136 * true if mouse2 was down. Used to report mouse2 on the release event.
138 private boolean mouse2
;
141 * true if mouse3 was down. Used to report mouse3 on the release event.
143 private boolean mouse3
;
146 * Cache the cursor visibility value so we only emit the sequence when we
149 private boolean cursorOn
= true;
152 * Cache the last window size to figure out if a TResizeEvent needs to be
155 private TResizeEvent windowResize
= null;
158 * If true, then we changed System.in and need to change it back.
160 private boolean setRawMode
;
163 * The terminal's input. If an InputStream is not specified in the
164 * constructor, then this InputStreamReader will be bound to System.in
165 * with UTF-8 encoding.
167 private Reader input
;
170 * The terminal's raw InputStream. If an InputStream is not specified in
171 * the constructor, then this InputReader will be bound to System.in.
172 * This is used by run() to see if bytes are available() before calling
173 * (Reader)input.read().
175 private InputStream inputStream
;
178 * The terminal's output. If an OutputStream is not specified in the
179 * constructor, then this PrintWriter will be bound to System.out with
182 private PrintWriter output
;
185 * When true, the terminal is sending non-UTF8 bytes when reporting mouse
188 * TODO: Add broken mouse detection back into the reader.
190 private boolean brokenTerminalUTFMouse
= false;
193 * Get the output writer.
197 public PrintWriter
getOutput() {
202 * Check if there are events in the queue.
204 * @return if true, getEvents() has something to return to the backend
206 public boolean hasEvents() {
207 synchronized (eventQueue
) {
208 return (eventQueue
.size() > 0);
213 * Call 'stty' to set cooked mode.
215 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
217 private void sttyCooked() {
222 * Call 'stty' to set raw mode.
224 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
225 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
226 * -parenb cs8 min 1 < /dev/tty'
228 private void sttyRaw() {
233 * Call 'stty' to set raw or cooked mode.
235 * @param mode if true, set raw mode, otherwise set cooked mode
237 private void doStty(final boolean mode
) {
239 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
241 String
[] cmdCooked
= {
242 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
247 process
= Runtime
.getRuntime().exec(cmdRaw
);
249 process
= Runtime
.getRuntime().exec(cmdCooked
);
251 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
252 String line
= in
.readLine();
253 if ((line
!= null) && (line
.length() > 0)) {
254 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
257 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
258 line
= err
.readLine();
259 if ((line
!= null) && (line
.length() > 0)) {
260 System
.err
.println("Error output from stty: " + line
);
265 } catch (InterruptedException e
) {
269 int rc
= process
.exitValue();
271 System
.err
.println("stty returned error code: " + rc
);
273 } catch (IOException e
) {
279 * Constructor sets up state for getEvent().
281 * @param input an InputStream connected to the remote user, or null for
282 * System.in. If System.in is used, then on non-Windows systems it will
283 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
284 * mode. input is always converted to a Reader with UTF-8 encoding.
285 * @param output an OutputStream connected to the remote user, or null
286 * for System.out. output is always converted to a Writer with UTF-8
288 * @throws UnsupportedEncodingException if an exception is thrown when
289 * creating the InputStreamReader
291 public ECMA48Terminal(final InputStream input
,
292 final OutputStream output
) throws UnsupportedEncodingException
{
298 stopReaderThread
= false;
301 // inputStream = System.in;
302 inputStream
= new FileInputStream(FileDescriptor
.in
);
308 this.input
= new InputStreamReader(inputStream
, "UTF-8");
310 // TODO: include TelnetSocket from NIB and have it implement
312 if (input
instanceof SessionInfo
) {
313 sessionInfo
= (SessionInfo
) input
;
315 if (sessionInfo
== null) {
317 // Reading right off the tty
318 sessionInfo
= new TTYSessionInfo();
320 sessionInfo
= new TSessionInfo();
324 if (output
== null) {
325 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
328 this.output
= new PrintWriter(new OutputStreamWriter(output
,
332 // Enable mouse reporting and metaSendsEscape
333 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
335 // Hang onto the window size
336 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
337 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
339 // Spin up the input reader
340 eventQueue
= new LinkedList
<TInputEvent
>();
341 readerThread
= new Thread(this);
342 readerThread
.start();
346 * Restore terminal to normal state.
348 public void shutdown() {
350 // System.err.println("=== shutdown() ==="); System.err.flush();
352 // Tell the reader thread to stop looking at input
353 stopReaderThread
= true;
356 } catch (InterruptedException e
) {
360 // Disable mouse reporting and show cursor
361 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
367 // We don't close System.in/out
369 // Shut down the streams, this should wake up the reader thread
376 if (output
!= null) {
380 } catch (IOException e
) {
389 public void flush() {
394 * Reset keyboard/mouse input parser.
396 private void reset() {
397 state
= ParseState
.GROUND
;
398 params
= new ArrayList
<String
>();
405 * Produce a control character or one of the special ones (ENTER, TAB,
408 * @param ch Unicode code point
409 * @return one TKeypress event, either a control character (e.g. isKey ==
410 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
413 private TKeypressEvent
controlChar(final char ch
) {
414 TKeypressEvent event
= new TKeypressEvent();
416 // System.err.printf("controlChar: %02x\n", ch);
420 // Carriage return --> ENTER
424 // Linefeed --> ENTER
436 // Make all other control characters come back as the alphabetic
437 // character with the ctrl field set. So SOH would be 'A' +
439 event
.key
= new TKeypress(false, 0, (char)(ch
+ 0x40),
447 * Produce special key from CSI Pn ; Pm ; ... ~
449 * @return one KEYPRESS event representing a special key
451 private TInputEvent
csiFnKey() {
454 if (params
.size() > 0) {
455 key
= Integer
.parseInt(params
.get(0));
457 if (params
.size() > 1) {
458 modifier
= Integer
.parseInt(params
.get(1));
460 TKeypressEvent event
= new TKeypressEvent();
518 event
.key
= kbShiftHome
;
521 event
.key
= kbShiftIns
;
524 event
.key
= kbShiftDel
;
527 event
.key
= kbShiftEnd
;
530 event
.key
= kbShiftPgUp
;
533 event
.key
= kbShiftPgDn
;
536 event
.key
= kbShiftF5
;
539 event
.key
= kbShiftF6
;
542 event
.key
= kbShiftF7
;
545 event
.key
= kbShiftF8
;
548 event
.key
= kbShiftF9
;
551 event
.key
= kbShiftF10
;
554 event
.key
= kbShiftF11
;
557 event
.key
= kbShiftF12
;
569 event
.key
= kbAltHome
;
572 event
.key
= kbAltIns
;
575 event
.key
= kbAltDel
;
578 event
.key
= kbAltEnd
;
581 event
.key
= kbAltPgUp
;
584 event
.key
= kbAltPgDn
;
602 event
.key
= kbAltF10
;
605 event
.key
= kbAltF11
;
608 event
.key
= kbAltF12
;
620 event
.key
= kbCtrlHome
;
623 event
.key
= kbCtrlIns
;
626 event
.key
= kbCtrlDel
;
629 event
.key
= kbCtrlEnd
;
632 event
.key
= kbCtrlPgUp
;
635 event
.key
= kbCtrlPgDn
;
638 event
.key
= kbCtrlF5
;
641 event
.key
= kbCtrlF6
;
644 event
.key
= kbCtrlF7
;
647 event
.key
= kbCtrlF8
;
650 event
.key
= kbCtrlF9
;
653 event
.key
= kbCtrlF10
;
656 event
.key
= kbCtrlF11
;
659 event
.key
= kbCtrlF12
;
672 // All OK, return a keypress
677 * Produce mouse events based on "Any event tracking" and UTF-8
679 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
681 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
683 private TInputEvent
parseMouse() {
684 int buttons
= params
.get(0).charAt(0) - 32;
685 int x
= params
.get(0).charAt(1) - 32 - 1;
686 int y
= params
.get(0).charAt(2) - 32 - 1;
688 // Clamp X and Y to the physical screen coordinates.
689 if (x
>= windowResize
.getWidth()) {
690 x
= windowResize
.getWidth() - 1;
692 if (y
>= windowResize
.getHeight()) {
693 y
= windowResize
.getHeight() - 1;
696 TMouseEvent event
= new TMouseEvent(TMouseEvent
.Type
.MOUSE_DOWN
);
702 // System.err.printf("buttons: %04x\r\n", buttons);
719 if (!mouse1
&& !mouse2
&& !mouse3
) {
720 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
722 event
.type
= TMouseEvent
.Type
.MOUSE_UP
;
739 // Dragging with mouse1 down
742 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
746 // Dragging with mouse2 down
749 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
753 // Dragging with mouse3 down
756 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
760 // Dragging with mouse2 down after wheelUp
763 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
767 // Dragging with mouse2 down after wheelDown
770 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
774 event
.mouseWheelUp
= true;
778 event
.mouseWheelDown
= true;
782 // Unknown, just make it motion
783 event
.type
= TMouseEvent
.Type
.MOUSE_MOTION
;
790 * Return any events in the IO queue.
792 * @param queue list to append new events to
794 public void getEvents(final List
<TInputEvent
> queue
) {
795 synchronized (eventQueue
) {
796 if (eventQueue
.size() > 0) {
797 queue
.addAll(eventQueue
);
804 * Return any events in the IO queue due to timeout.
806 * @param queue list to append new events to
808 public void getIdleEvents(final List
<TInputEvent
> queue
) {
810 // Check for new window size
811 sessionInfo
.queryWindowSize();
812 int newWidth
= sessionInfo
.getWindowWidth();
813 int newHeight
= sessionInfo
.getWindowHeight();
814 if ((newWidth
!= windowResize
.getWidth())
815 || (newHeight
!= windowResize
.getHeight())
817 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
818 newWidth
, newHeight
);
819 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
820 newWidth
, newHeight
);
821 synchronized (eventQueue
) {
822 eventQueue
.add(event
);
826 synchronized (eventQueue
) {
827 if (eventQueue
.size() > 0) {
828 queue
.addAll(eventQueue
);
835 * Parses the next character of input to see if an InputEvent is
838 * @param events list to append new events to
839 * @param ch Unicode code point
841 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
843 TKeypressEvent keypress
;
844 Date now
= new Date();
846 // ESCDELAY type timeout
847 if (state
== ParseState
.ESCAPE
) {
848 long escDelay
= now
.getTime() - escapeTime
;
849 if (escDelay
> 250) {
850 // After 0.25 seconds, assume a true escape character
851 events
.add(controlChar((char)0x1B));
856 // System.err.printf("state: %s ch %c\r\n", state, ch);
862 state
= ParseState
.ESCAPE
;
863 escapeTime
= now
.getTime();
869 events
.add(controlChar(ch
));
876 keypress
= new TKeypressEvent();
877 keypress
.key
.isKey
= false;
878 keypress
.key
.ch
= ch
;
879 events
.add(keypress
);
888 // ALT-Control character
889 keypress
= controlChar(ch
);
890 keypress
.key
.alt
= true;
891 events
.add(keypress
);
897 // This will be one of the function keys
898 state
= ParseState
.ESCAPE_INTERMEDIATE
;
902 // '[' goes to CSI_ENTRY
904 state
= ParseState
.CSI_ENTRY
;
908 // Everything else is assumed to be Alt-keystroke
909 keypress
= new TKeypressEvent();
910 keypress
.key
.isKey
= false;
911 keypress
.key
.ch
= ch
;
912 keypress
.key
.alt
= true;
913 if ((ch
>= 'A') && (ch
<= 'Z')) {
914 keypress
.key
.shift
= true;
916 events
.add(keypress
);
920 case ESCAPE_INTERMEDIATE
:
921 if ((ch
>= 'P') && (ch
<= 'S')) {
923 keypress
= new TKeypressEvent();
924 keypress
.key
.isKey
= true;
927 keypress
.key
.fnKey
= TKeypress
.F1
;
930 keypress
.key
.fnKey
= TKeypress
.F2
;
933 keypress
.key
.fnKey
= TKeypress
.F3
;
936 keypress
.key
.fnKey
= TKeypress
.F4
;
941 events
.add(keypress
);
946 // Unknown keystroke, ignore
951 // Numbers - parameter values
952 if ((ch
>= '0') && (ch
<= '9')) {
953 params
.set(paramI
, params
.get(paramI
) + ch
);
954 state
= ParseState
.CSI_PARAM
;
957 // Parameter separator
960 params
.set(paramI
, "");
964 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
968 keypress
= new TKeypressEvent();
969 keypress
.key
.isKey
= true;
970 keypress
.key
.fnKey
= TKeypress
.UP
;
971 if (params
.size() > 1) {
972 if (params
.get(1).equals("2")) {
973 keypress
.key
.shift
= true;
975 if (params
.get(1).equals("5")) {
976 keypress
.key
.ctrl
= true;
978 if (params
.get(1).equals("3")) {
979 keypress
.key
.alt
= true;
982 events
.add(keypress
);
987 keypress
= new TKeypressEvent();
988 keypress
.key
.isKey
= true;
989 keypress
.key
.fnKey
= TKeypress
.DOWN
;
990 if (params
.size() > 1) {
991 if (params
.get(1).equals("2")) {
992 keypress
.key
.shift
= true;
994 if (params
.get(1).equals("5")) {
995 keypress
.key
.ctrl
= true;
997 if (params
.get(1).equals("3")) {
998 keypress
.key
.alt
= true;
1001 events
.add(keypress
);
1006 keypress
= new TKeypressEvent();
1007 keypress
.key
.isKey
= true;
1008 keypress
.key
.fnKey
= TKeypress
.RIGHT
;
1009 if (params
.size() > 1) {
1010 if (params
.get(1).equals("2")) {
1011 keypress
.key
.shift
= true;
1013 if (params
.get(1).equals("5")) {
1014 keypress
.key
.ctrl
= true;
1016 if (params
.get(1).equals("3")) {
1017 keypress
.key
.alt
= true;
1020 events
.add(keypress
);
1025 keypress
= new TKeypressEvent();
1026 keypress
.key
.isKey
= true;
1027 keypress
.key
.fnKey
= TKeypress
.LEFT
;
1028 if (params
.size() > 1) {
1029 if (params
.get(1).equals("2")) {
1030 keypress
.key
.shift
= true;
1032 if (params
.get(1).equals("5")) {
1033 keypress
.key
.ctrl
= true;
1035 if (params
.get(1).equals("3")) {
1036 keypress
.key
.alt
= true;
1039 events
.add(keypress
);
1044 keypress
= new TKeypressEvent();
1045 keypress
.key
.isKey
= true;
1046 keypress
.key
.fnKey
= TKeypress
.HOME
;
1047 events
.add(keypress
);
1052 keypress
= new TKeypressEvent();
1053 keypress
.key
.isKey
= true;
1054 keypress
.key
.fnKey
= TKeypress
.END
;
1055 events
.add(keypress
);
1059 // CBT - Cursor backward X tab stops (default 1)
1060 keypress
= new TKeypressEvent();
1061 keypress
.key
.isKey
= true;
1062 keypress
.key
.fnKey
= TKeypress
.BTAB
;
1063 events
.add(keypress
);
1068 state
= ParseState
.MOUSE
;
1075 // Unknown keystroke, ignore
1080 // Numbers - parameter values
1081 if ((ch
>= '0') && (ch
<= '9')) {
1082 params
.set(paramI
, params
.get(paramI
) + ch
);
1083 state
= ParseState
.CSI_PARAM
;
1086 // Parameter separator
1089 params
.set(paramI
, "");
1094 events
.add(csiFnKey());
1099 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1103 keypress
= new TKeypressEvent();
1104 keypress
.key
.isKey
= true;
1105 keypress
.key
.fnKey
= TKeypress
.UP
;
1106 if (params
.size() > 1) {
1107 if (params
.get(1).equals("2")) {
1108 keypress
.key
.shift
= true;
1110 if (params
.get(1).equals("5")) {
1111 keypress
.key
.ctrl
= true;
1113 if (params
.get(1).equals("3")) {
1114 keypress
.key
.alt
= true;
1117 events
.add(keypress
);
1122 keypress
= new TKeypressEvent();
1123 keypress
.key
.isKey
= true;
1124 keypress
.key
.fnKey
= TKeypress
.DOWN
;
1125 if (params
.size() > 1) {
1126 if (params
.get(1).equals("2")) {
1127 keypress
.key
.shift
= true;
1129 if (params
.get(1).equals("5")) {
1130 keypress
.key
.ctrl
= true;
1132 if (params
.get(1).equals("3")) {
1133 keypress
.key
.alt
= true;
1136 events
.add(keypress
);
1141 keypress
= new TKeypressEvent();
1142 keypress
.key
.isKey
= true;
1143 keypress
.key
.fnKey
= TKeypress
.RIGHT
;
1144 if (params
.size() > 1) {
1145 if (params
.get(1).equals("2")) {
1146 keypress
.key
.shift
= true;
1148 if (params
.get(1).equals("5")) {
1149 keypress
.key
.ctrl
= true;
1151 if (params
.get(1).equals("3")) {
1152 keypress
.key
.alt
= true;
1155 events
.add(keypress
);
1160 keypress
= new TKeypressEvent();
1161 keypress
.key
.isKey
= true;
1162 keypress
.key
.fnKey
= TKeypress
.LEFT
;
1163 if (params
.size() > 1) {
1164 if (params
.get(1).equals("2")) {
1165 keypress
.key
.shift
= true;
1167 if (params
.get(1).equals("5")) {
1168 keypress
.key
.ctrl
= true;
1170 if (params
.get(1).equals("3")) {
1171 keypress
.key
.alt
= true;
1174 events
.add(keypress
);
1182 // Unknown keystroke, ignore
1187 params
.set(0, params
.get(paramI
) + ch
);
1188 if (params
.get(0).length() == 3) {
1189 // We have enough to generate a mouse event
1190 events
.add(parseMouse());
1199 // This "should" be impossible to reach
1204 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1205 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1208 * @param on if true, enable metaSendsEscape
1209 * @return the string to emit to xterm
1211 public String
xtermMetaSendsEscape(final boolean on
) {
1213 return "\033[?1036h\033[?1034l";
1215 return "\033[?1036l";
1219 * Convert a list of SGR parameters into a full escape sequence. This
1220 * also eliminates a trailing ';' which would otherwise reset everything
1221 * to white-on-black not-bold.
1223 * @param str string of parameters, e.g. "31;1;"
1224 * @return the string to emit to an ANSI / ECMA-style terminal,
1227 public String
addHeaderSGR(String str
) {
1228 if (str
.length() > 0) {
1229 // Nix any trailing ';' because that resets all attributes
1230 while (str
.endsWith(":")) {
1231 str
= str
.substring(0, str
.length() - 1);
1234 return "\033[" + str
+ "m";
1238 * Create a SGR parameter sequence for a single color change.
1240 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1241 * @param foreground if true, this is a foreground color
1242 * @return the string to emit to an ANSI / ECMA-style terminal,
1245 public String
color(final Color color
, final boolean foreground
) {
1246 return color(color
, foreground
, true);
1250 * Create a SGR parameter sequence for a single color change.
1252 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1253 * @param foreground if true, this is a foreground color
1254 * @param header if true, make the full header, otherwise just emit the
1255 * color parameter e.g. "42;"
1256 * @return the string to emit to an ANSI / ECMA-style terminal,
1259 public String
color(final Color color
, final boolean foreground
,
1260 final boolean header
) {
1262 int ecmaColor
= color
.getValue();
1264 // Convert Color.* values to SGR numerics
1272 return String
.format("\033[%dm", ecmaColor
);
1274 return String
.format("%d;", ecmaColor
);
1279 * Create a SGR parameter sequence for both foreground and
1280 * background color change.
1282 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1283 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1284 * @return the string to emit to an ANSI / ECMA-style terminal,
1285 * e.g. "\033[31;42m"
1287 public String
color(final Color foreColor
, final Color backColor
) {
1288 return color(foreColor
, backColor
, true);
1292 * Create a SGR parameter sequence for both foreground and
1293 * background color change.
1295 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1296 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1297 * @param header if true, make the full header, otherwise just emit the
1298 * color parameter e.g. "31;42;"
1299 * @return the string to emit to an ANSI / ECMA-style terminal,
1300 * e.g. "\033[31;42m"
1302 public String
color(final Color foreColor
, final Color backColor
,
1303 final boolean header
) {
1305 int ecmaForeColor
= foreColor
.getValue();
1306 int ecmaBackColor
= backColor
.getValue();
1308 // Convert Color.* values to SGR numerics
1309 ecmaBackColor
+= 40;
1310 ecmaForeColor
+= 30;
1313 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1315 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1320 * Create a SGR parameter sequence for foreground, background, and
1321 * several attributes. This sequence first resets all attributes to
1322 * default, then sets attributes as per the parameters.
1324 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1325 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1326 * @param bold if true, set bold
1327 * @param reverse if true, set reverse
1328 * @param blink if true, set blink
1329 * @param underline if true, set underline
1330 * @return the string to emit to an ANSI / ECMA-style terminal,
1331 * e.g. "\033[0;1;31;42m"
1333 public String
color(final Color foreColor
, final Color backColor
,
1334 final boolean bold
, final boolean reverse
, final boolean blink
,
1335 final boolean underline
) {
1337 int ecmaForeColor
= foreColor
.getValue();
1338 int ecmaBackColor
= backColor
.getValue();
1340 // Convert Color.* values to SGR numerics
1341 ecmaBackColor
+= 40;
1342 ecmaForeColor
+= 30;
1344 StringBuilder sb
= new StringBuilder();
1345 if ( bold
&& reverse
&& blink
&& !underline
) {
1346 sb
.append("\033[0;1;7;5;");
1347 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1348 sb
.append("\033[0;1;7;");
1349 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1350 sb
.append("\033[0;7;5;");
1351 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1352 sb
.append("\033[0;1;5;");
1353 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1354 sb
.append("\033[0;1;");
1355 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1356 sb
.append("\033[0;7;");
1357 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1358 sb
.append("\033[0;5;");
1359 } else if ( bold
&& reverse
&& blink
&& underline
) {
1360 sb
.append("\033[0;1;7;5;4;");
1361 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1362 sb
.append("\033[0;1;7;4;");
1363 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1364 sb
.append("\033[0;7;5;4;");
1365 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1366 sb
.append("\033[0;1;5;4;");
1367 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1368 sb
.append("\033[0;1;4;");
1369 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1370 sb
.append("\033[0;7;4;");
1371 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1372 sb
.append("\033[0;5;4;");
1373 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1374 sb
.append("\033[0;4;");
1376 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1377 sb
.append("\033[0;");
1379 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1380 return sb
.toString();
1384 * Create a SGR parameter sequence for enabling reverse color.
1386 * @param on if true, turn on reverse
1387 * @return the string to emit to an ANSI / ECMA-style terminal,
1390 public String
reverse(final boolean on
) {
1398 * Create a SGR parameter sequence to reset to defaults.
1400 * @return the string to emit to an ANSI / ECMA-style terminal,
1403 public String
normal() {
1404 return normal(true);
1408 * Create a SGR parameter sequence to reset to defaults.
1410 * @param header if true, make the full header, otherwise just emit the
1411 * bare parameter e.g. "0;"
1412 * @return the string to emit to an ANSI / ECMA-style terminal,
1415 public String
normal(final boolean header
) {
1417 return "\033[0;37;40m";
1423 * Create a SGR parameter sequence for enabling boldface.
1425 * @param on if true, turn on bold
1426 * @return the string to emit to an ANSI / ECMA-style terminal,
1429 public String
bold(final boolean on
) {
1430 return bold(on
, true);
1434 * Create a SGR parameter sequence for enabling boldface.
1436 * @param on if true, turn on bold
1437 * @param header if true, make the full header, otherwise just emit the
1438 * bare parameter e.g. "1;"
1439 * @return the string to emit to an ANSI / ECMA-style terminal,
1442 public String
bold(final boolean on
, final boolean header
) {
1456 * Create a SGR parameter sequence for enabling blinking text.
1458 * @param on if true, turn on blink
1459 * @return the string to emit to an ANSI / ECMA-style terminal,
1462 public String
blink(final boolean on
) {
1463 return blink(on
, true);
1467 * Create a SGR parameter sequence for enabling blinking text.
1469 * @param on if true, turn on blink
1470 * @param header if true, make the full header, otherwise just emit the
1471 * bare parameter e.g. "5;"
1472 * @return the string to emit to an ANSI / ECMA-style terminal,
1475 public String
blink(final boolean on
, final boolean header
) {
1489 * Create a SGR parameter sequence for enabling underline / underscored
1492 * @param on if true, turn on underline
1493 * @return the string to emit to an ANSI / ECMA-style terminal,
1496 public String
underline(final boolean on
) {
1504 * Create a SGR parameter sequence for enabling the visible cursor.
1506 * @param on if true, turn on cursor
1507 * @return the string to emit to an ANSI / ECMA-style terminal
1509 public String
cursor(final boolean on
) {
1510 if (on
&& !cursorOn
) {
1514 if (!on
&& cursorOn
) {
1522 * Clear the entire screen. Because some terminals use back-color-erase,
1523 * set the color to white-on-black beforehand.
1525 * @return the string to emit to an ANSI / ECMA-style terminal
1527 public String
clearAll() {
1528 return "\033[0;37;40m\033[2J";
1532 * Clear the line from the cursor (inclusive) to the end of the screen.
1533 * Because some terminals use back-color-erase, set the color to
1534 * white-on-black beforehand.
1536 * @return the string to emit to an ANSI / ECMA-style terminal
1538 public String
clearRemainingLine() {
1539 return "\033[0;37;40m\033[K";
1543 * Clear the line up the cursor (inclusive). Because some terminals use
1544 * back-color-erase, set the color to white-on-black beforehand.
1546 * @return the string to emit to an ANSI / ECMA-style terminal
1548 public String
clearPreceedingLine() {
1549 return "\033[0;37;40m\033[1K";
1553 * Clear the line. Because some terminals use back-color-erase, set the
1554 * color to white-on-black beforehand.
1556 * @return the string to emit to an ANSI / ECMA-style terminal
1558 public String
clearLine() {
1559 return "\033[0;37;40m\033[2K";
1563 * Move the cursor to the top-left corner.
1565 * @return the string to emit to an ANSI / ECMA-style terminal
1567 public String
home() {
1572 * Move the cursor to (x, y).
1574 * @param x column coordinate. 0 is the left-most column.
1575 * @param y row coordinate. 0 is the top-most row.
1576 * @return the string to emit to an ANSI / ECMA-style terminal
1578 public String
gotoXY(final int x
, final int y
) {
1579 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1583 * Tell (u)xterm that we want to receive mouse events based on "Any event
1584 * tracking" and UTF-8 coordinates. See
1585 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1587 * Note that this also sets the alternate/primary screen buffer.
1589 * @param on If true, enable mouse report and use the alternate screen
1590 * buffer. If false disable mouse reporting and use the primary screen
1592 * @return the string to emit to xterm
1594 public String
mouse(final boolean on
) {
1596 return "\033[?1003;1005h\033[?1049h";
1598 return "\033[?1003;1005l\033[?1049l";
1602 * Read function runs on a separate thread.
1605 boolean done
= false;
1606 // available() will often return > 1, so we need to read in chunks to
1608 char [] readBuffer
= new char[128];
1609 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1611 while (!done
&& !stopReaderThread
) {
1613 // We assume that if inputStream has bytes available, then
1614 // input won't block on read().
1615 int n
= inputStream
.available();
1617 if (readBuffer
.length
< n
) {
1618 // The buffer wasn't big enough, make it huger
1619 readBuffer
= new char[readBuffer
.length
* 2];
1622 int rc
= input
.read(readBuffer
, 0, n
);
1623 // System.err.printf("read() %d", rc); System.err.flush();
1628 for (int i
= 0; i
< rc
; i
++) {
1629 int ch
= readBuffer
[i
];
1630 processChar(events
, (char)ch
);
1631 if (events
.size() > 0) {
1632 // Add to the queue for the backend thread to
1633 // be able to obtain.
1634 synchronized (eventQueue
) {
1635 eventQueue
.addAll(events
);
1637 // Now wake up the backend
1638 synchronized (this) {
1646 // Wait 5 millis for more data
1649 // System.err.println("end while loop"); System.err.flush();
1650 } catch (InterruptedException e
) {
1652 } catch (IOException e
) {
1653 e
.printStackTrace();
1656 } // while ((done == false) && (stopReaderThread == false))
1657 // System.err.println("*** run() exiting..."); System.err.flush();