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
.bits
.Color
;
50 import jexer
.event
.TInputEvent
;
51 import jexer
.event
.TKeypressEvent
;
52 import jexer
.event
.TMouseEvent
;
53 import jexer
.event
.TResizeEvent
;
54 import jexer
.session
.SessionInfo
;
55 import jexer
.session
.TSessionInfo
;
56 import jexer
.session
.TTYSessionInfo
;
57 import static jexer
.TKeypress
.*;
60 * This class reads keystrokes and mouse events and emits output to ANSI
61 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
63 public final class ECMA48Terminal
implements Runnable
{
66 * The session information.
68 private SessionInfo sessionInfo
;
71 * Getter for sessionInfo.
73 * @return the SessionInfo
75 public SessionInfo
getSessionInfo() {
80 * The event queue, filled up by a thread reading on input.
82 private List
<TInputEvent
> eventQueue
;
85 * If true, we want the reader thread to exit gracefully.
87 private boolean stopReaderThread
;
92 private Thread readerThread
;
95 * Parameters being collected. E.g. if the string is \033[1;3m, then
96 * params[0] will be 1 and params[1] will be 3.
98 private ArrayList
<String
> params
;
101 * States in the input parser.
103 private enum ParseState
{
115 * Current parsing state.
117 private ParseState state
;
120 * The time we entered ESCAPE. If we get a bare escape without a code
121 * following it, this is used to return that bare escape.
123 private long escapeTime
;
126 * The time we last checked the window size. We try not to spawn stty
127 * more than once per second.
129 private long windowSizeTime
;
132 * true if mouse1 was down. Used to report mouse1 on the release event.
134 private boolean mouse1
;
137 * true if mouse2 was down. Used to report mouse2 on the release event.
139 private boolean mouse2
;
142 * true if mouse3 was down. Used to report mouse3 on the release event.
144 private boolean mouse3
;
147 * Cache the cursor visibility value so we only emit the sequence when we
150 private boolean cursorOn
= true;
153 * Cache the last window size to figure out if a TResizeEvent needs to be
156 private TResizeEvent windowResize
= null;
159 * If true, then we changed System.in and need to change it back.
161 private boolean setRawMode
;
164 * The terminal's input. If an InputStream is not specified in the
165 * constructor, then this InputStreamReader will be bound to System.in
166 * with UTF-8 encoding.
168 private Reader input
;
171 * The terminal's raw InputStream. If an InputStream is not specified in
172 * the constructor, then this InputReader will be bound to System.in.
173 * This is used by run() to see if bytes are available() before calling
174 * (Reader)input.read().
176 private InputStream inputStream
;
179 * The terminal's output. If an OutputStream is not specified in the
180 * constructor, then this PrintWriter will be bound to System.out with
183 private PrintWriter output
;
186 * The listening object that run() wakes up on new input.
188 private Object listener
;
191 * Get the output writer.
195 public PrintWriter
getOutput() {
200 * Check if there are events in the queue.
202 * @return if true, getEvents() has something to return to the backend
204 public boolean hasEvents() {
205 synchronized (eventQueue
) {
206 return (eventQueue
.size() > 0);
211 * Call 'stty' to set cooked mode.
213 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
215 private void sttyCooked() {
220 * Call 'stty' to set raw mode.
222 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
223 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
224 * -parenb cs8 min 1 < /dev/tty'
226 private void sttyRaw() {
231 * Call 'stty' to set raw or cooked mode.
233 * @param mode if true, set raw mode, otherwise set cooked mode
235 private void doStty(final boolean mode
) {
237 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
239 String
[] cmdCooked
= {
240 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
245 process
= Runtime
.getRuntime().exec(cmdRaw
);
247 process
= Runtime
.getRuntime().exec(cmdCooked
);
249 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
250 String line
= in
.readLine();
251 if ((line
!= null) && (line
.length() > 0)) {
252 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
255 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
256 line
= err
.readLine();
257 if ((line
!= null) && (line
.length() > 0)) {
258 System
.err
.println("Error output from stty: " + line
);
263 } catch (InterruptedException e
) {
267 int rc
= process
.exitValue();
269 System
.err
.println("stty returned error code: " + rc
);
271 } catch (IOException e
) {
277 * Constructor sets up state for getEvent().
279 * @param listener the object this backend needs to wake up when new
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 Object listener
, final InputStream input
,
292 final OutputStream output
) throws UnsupportedEncodingException
{
298 stopReaderThread
= false;
299 this.listener
= listener
;
302 // inputStream = System.in;
303 inputStream
= new FileInputStream(FileDescriptor
.in
);
309 this.input
= new InputStreamReader(inputStream
, "UTF-8");
311 // TODO: include TelnetSocket from NIB and have it implement
313 if (input
instanceof SessionInfo
) {
314 sessionInfo
= (SessionInfo
) input
;
316 if (sessionInfo
== null) {
318 // Reading right off the tty
319 sessionInfo
= new TTYSessionInfo();
321 sessionInfo
= new TSessionInfo();
325 if (output
== null) {
326 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
329 this.output
= new PrintWriter(new OutputStreamWriter(output
,
333 // Enable mouse reporting and metaSendsEscape
334 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
336 // Hang onto the window size
337 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
338 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
340 // Spin up the input reader
341 eventQueue
= new LinkedList
<TInputEvent
>();
342 readerThread
= new Thread(this);
343 readerThread
.start();
347 * Restore terminal to normal state.
349 public void shutdown() {
351 // System.err.println("=== shutdown() ==="); System.err.flush();
353 // Tell the reader thread to stop looking at input
354 stopReaderThread
= true;
357 } catch (InterruptedException e
) {
361 // Disable mouse reporting and show cursor
362 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
368 // We don't close System.in/out
370 // Shut down the streams, this should wake up the reader thread
377 if (output
!= null) {
381 } catch (IOException e
) {
390 public void flush() {
395 * Reset keyboard/mouse input parser.
397 private void reset() {
398 state
= ParseState
.GROUND
;
399 params
= new ArrayList
<String
>();
405 * Produce a control character or one of the special ones (ENTER, TAB,
408 * @param ch Unicode code point
409 * @param alt if true, set alt on the TKeypress
410 * @return one TKeypress event, either a control character (e.g. isKey ==
411 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
414 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
415 // System.err.printf("controlChar: %02x\n", ch);
419 // Carriage return --> ENTER
420 return new TKeypressEvent(kbEnter
, alt
, false, false);
422 // Linefeed --> ENTER
423 return new TKeypressEvent(kbEnter
, alt
, false, false);
426 return new TKeypressEvent(kbEsc
, alt
, false, false);
429 return new TKeypressEvent(kbTab
, alt
, false, false);
431 // Make all other control characters come back as the alphabetic
432 // character with the ctrl field set. So SOH would be 'A' +
434 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
440 * Produce special key from CSI Pn ; Pm ; ... ~
442 * @return one KEYPRESS event representing a special key
444 private TInputEvent
csiFnKey() {
447 if (params
.size() > 0) {
448 key
= Integer
.parseInt(params
.get(0));
450 if (params
.size() > 1) {
451 modifier
= Integer
.parseInt(params
.get(1));
454 boolean ctrl
= false;
455 boolean shift
= false;
474 // Unknown modifier, bail out
480 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
482 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
484 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
486 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
488 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
490 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
492 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
494 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
496 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
498 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
500 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
502 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
504 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
506 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
514 * Produce mouse events based on "Any event tracking" and UTF-8
516 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
518 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
520 private TInputEvent
parseMouse() {
521 int buttons
= params
.get(0).charAt(0) - 32;
522 int x
= params
.get(0).charAt(1) - 32 - 1;
523 int y
= params
.get(0).charAt(2) - 32 - 1;
525 // Clamp X and Y to the physical screen coordinates.
526 if (x
>= windowResize
.getWidth()) {
527 x
= windowResize
.getWidth() - 1;
529 if (y
>= windowResize
.getHeight()) {
530 y
= windowResize
.getHeight() - 1;
533 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
534 boolean eventMouse1
= false;
535 boolean eventMouse2
= false;
536 boolean eventMouse3
= false;
537 boolean eventMouseWheelUp
= false;
538 boolean eventMouseWheelDown
= false;
540 // System.err.printf("buttons: %04x\r\n", buttons);
557 if (!mouse1
&& !mouse2
&& !mouse3
) {
558 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
560 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
577 // Dragging with mouse1 down
580 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
584 // Dragging with mouse2 down
587 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
591 // Dragging with mouse3 down
594 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
598 // Dragging with mouse2 down after wheelUp
601 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
605 // Dragging with mouse2 down after wheelDown
608 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
612 eventMouseWheelUp
= true;
616 eventMouseWheelDown
= true;
620 // Unknown, just make it motion
621 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
624 return new TMouseEvent(eventType
, x
, y
, x
, y
,
625 eventMouse1
, eventMouse2
, eventMouse3
,
626 eventMouseWheelUp
, eventMouseWheelDown
);
630 * Produce mouse events based on "Any event tracking" and SGR
632 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
634 * @param release if true, this was a release ('m')
635 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
637 private TInputEvent
parseMouseSGR(final boolean release
) {
638 // SGR extended coordinates - mode 1006
639 if (params
.size() < 3) {
640 // Invalid position, bail out.
643 int buttons
= Integer
.parseInt(params
.get(0));
644 int x
= Integer
.parseInt(params
.get(1)) - 1;
645 int y
= Integer
.parseInt(params
.get(2)) - 1;
647 // Clamp X and Y to the physical screen coordinates.
648 if (x
>= windowResize
.getWidth()) {
649 x
= windowResize
.getWidth() - 1;
651 if (y
>= windowResize
.getHeight()) {
652 y
= windowResize
.getHeight() - 1;
655 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
656 boolean eventMouse1
= false;
657 boolean eventMouse2
= false;
658 boolean eventMouse3
= false;
659 boolean eventMouseWheelUp
= false;
660 boolean eventMouseWheelDown
= false;
663 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
677 // Motion only, no buttons down
678 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
682 // Dragging with mouse1 down
684 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
688 // Dragging with mouse2 down
690 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
694 // Dragging with mouse3 down
696 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
700 // Dragging with mouse2 down after wheelUp
702 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
706 // Dragging with mouse2 down after wheelDown
708 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
712 eventMouseWheelUp
= true;
716 eventMouseWheelDown
= true;
723 return new TMouseEvent(eventType
, x
, y
, x
, y
,
724 eventMouse1
, eventMouse2
, eventMouse3
,
725 eventMouseWheelUp
, eventMouseWheelDown
);
729 * Return any events in the IO queue.
731 * @param queue list to append new events to
733 public void getEvents(final List
<TInputEvent
> queue
) {
734 synchronized (eventQueue
) {
735 if (eventQueue
.size() > 0) {
736 synchronized (queue
) {
737 queue
.addAll(eventQueue
);
745 * Return any events in the IO queue due to timeout.
747 * @param queue list to append new events to
749 private void getIdleEvents(final List
<TInputEvent
> queue
) {
750 Date now
= new Date();
752 // Check for new window size
753 long windowSizeDelay
= now
.getTime() - windowSizeTime
;
754 if (windowSizeDelay
> 1000) {
755 sessionInfo
.queryWindowSize();
756 int newWidth
= sessionInfo
.getWindowWidth();
757 int newHeight
= sessionInfo
.getWindowHeight();
758 if ((newWidth
!= windowResize
.getWidth())
759 || (newHeight
!= windowResize
.getHeight())
761 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
762 newWidth
, newHeight
);
763 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
764 newWidth
, newHeight
);
767 windowSizeTime
= now
.getTime();
770 // ESCDELAY type timeout
771 if (state
== ParseState
.ESCAPE
) {
772 long escDelay
= now
.getTime() - escapeTime
;
773 if (escDelay
> 100) {
774 // After 0.1 seconds, assume a true escape character
775 queue
.add(controlChar((char)0x1B, false));
782 * Parses the next character of input to see if an InputEvent is
785 * @param events list to append new events to
786 * @param ch Unicode code point
788 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
790 // ESCDELAY type timeout
791 Date now
= new Date();
792 if (state
== ParseState
.ESCAPE
) {
793 long escDelay
= now
.getTime() - escapeTime
;
794 if (escDelay
> 250) {
795 // After 0.25 seconds, assume a true escape character
796 events
.add(controlChar((char)0x1B, false));
802 boolean ctrl
= false;
804 boolean shift
= false;
806 // System.err.printf("state: %s ch %c\r\n", state, ch);
812 state
= ParseState
.ESCAPE
;
813 escapeTime
= now
.getTime();
819 events
.add(controlChar(ch
, false));
826 events
.add(new TKeypressEvent(false, 0, ch
,
827 false, false, false));
836 // ALT-Control character
837 events
.add(controlChar(ch
, true));
843 // This will be one of the function keys
844 state
= ParseState
.ESCAPE_INTERMEDIATE
;
848 // '[' goes to CSI_ENTRY
850 state
= ParseState
.CSI_ENTRY
;
854 // Everything else is assumed to be Alt-keystroke
855 if ((ch
>= 'A') && (ch
<= 'Z')) {
859 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
863 case ESCAPE_INTERMEDIATE
:
864 if ((ch
>= 'P') && (ch
<= 'S')) {
868 events
.add(new TKeypressEvent(kbF1
));
871 events
.add(new TKeypressEvent(kbF2
));
874 events
.add(new TKeypressEvent(kbF3
));
877 events
.add(new TKeypressEvent(kbF4
));
886 // Unknown keystroke, ignore
891 // Numbers - parameter values
892 if ((ch
>= '0') && (ch
<= '9')) {
893 params
.set(params
.size() - 1,
894 params
.get(params
.size() - 1) + ch
);
895 state
= ParseState
.CSI_PARAM
;
898 // Parameter separator
904 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
908 if (params
.size() > 1) {
909 if (params
.get(1).equals("2")) {
912 if (params
.get(1).equals("5")) {
915 if (params
.get(1).equals("3")) {
919 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
924 if (params
.size() > 1) {
925 if (params
.get(1).equals("2")) {
928 if (params
.get(1).equals("5")) {
931 if (params
.get(1).equals("3")) {
935 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
940 if (params
.size() > 1) {
941 if (params
.get(1).equals("2")) {
944 if (params
.get(1).equals("5")) {
947 if (params
.get(1).equals("3")) {
951 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
956 if (params
.size() > 1) {
957 if (params
.get(1).equals("2")) {
960 if (params
.get(1).equals("5")) {
963 if (params
.get(1).equals("3")) {
967 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
972 events
.add(new TKeypressEvent(kbHome
));
977 events
.add(new TKeypressEvent(kbEnd
));
981 // CBT - Cursor backward X tab stops (default 1)
982 events
.add(new TKeypressEvent(kbBackTab
));
987 state
= ParseState
.MOUSE
;
990 // Mouse position, SGR (1006) coordinates
991 state
= ParseState
.MOUSE_SGR
;
998 // Unknown keystroke, ignore
1003 // Numbers - parameter values
1004 if ((ch
>= '0') && (ch
<= '9')) {
1005 params
.set(params
.size() - 1,
1006 params
.get(params
.size() - 1) + ch
);
1009 // Parameter separator
1017 // Generate a mouse press event
1018 TInputEvent event
= parseMouseSGR(false);
1019 if (event
!= null) {
1025 // Generate a mouse release event
1026 event
= parseMouseSGR(true);
1027 if (event
!= null) {
1036 // Unknown keystroke, ignore
1041 // Numbers - parameter values
1042 if ((ch
>= '0') && (ch
<= '9')) {
1043 params
.set(params
.size() - 1,
1044 params
.get(params
.size() - 1) + ch
);
1045 state
= ParseState
.CSI_PARAM
;
1048 // Parameter separator
1055 events
.add(csiFnKey());
1060 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1064 if (params
.size() > 1) {
1065 if (params
.get(1).equals("2")) {
1068 if (params
.get(1).equals("5")) {
1071 if (params
.get(1).equals("3")) {
1075 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1080 if (params
.size() > 1) {
1081 if (params
.get(1).equals("2")) {
1084 if (params
.get(1).equals("5")) {
1087 if (params
.get(1).equals("3")) {
1091 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1096 if (params
.size() > 1) {
1097 if (params
.get(1).equals("2")) {
1100 if (params
.get(1).equals("5")) {
1103 if (params
.get(1).equals("3")) {
1107 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1112 if (params
.size() > 1) {
1113 if (params
.get(1).equals("2")) {
1116 if (params
.get(1).equals("5")) {
1119 if (params
.get(1).equals("3")) {
1123 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1131 // Unknown keystroke, ignore
1136 params
.set(0, params
.get(params
.size() - 1) + ch
);
1137 if (params
.get(0).length() == 3) {
1138 // We have enough to generate a mouse event
1139 events
.add(parseMouse());
1148 // This "should" be impossible to reach
1153 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1154 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1157 * @param on if true, enable metaSendsEscape
1158 * @return the string to emit to xterm
1160 private String
xtermMetaSendsEscape(final boolean on
) {
1162 return "\033[?1036h\033[?1034l";
1164 return "\033[?1036l";
1168 * Convert a list of SGR parameters into a full escape sequence. This
1169 * also eliminates a trailing ';' which would otherwise reset everything
1170 * to white-on-black not-bold.
1172 * @param str string of parameters, e.g. "31;1;"
1173 * @return the string to emit to an ANSI / ECMA-style terminal,
1176 private String
addHeaderSGR(String str
) {
1177 if (str
.length() > 0) {
1178 // Nix any trailing ';' because that resets all attributes
1179 while (str
.endsWith(":")) {
1180 str
= str
.substring(0, str
.length() - 1);
1183 return "\033[" + str
+ "m";
1187 * Create a SGR parameter sequence for a single color change. Note
1188 * package private access.
1190 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1191 * @param foreground if true, this is a foreground color
1192 * @return the string to emit to an ANSI / ECMA-style terminal,
1195 String
color(final Color color
, final boolean foreground
) {
1196 return color(color
, foreground
, true);
1200 * Create a SGR parameter sequence for a single color change.
1202 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1203 * @param foreground if true, this is a foreground color
1204 * @param header if true, make the full header, otherwise just emit the
1205 * color parameter e.g. "42;"
1206 * @return the string to emit to an ANSI / ECMA-style terminal,
1209 private String
color(final Color color
, final boolean foreground
,
1210 final boolean header
) {
1212 int ecmaColor
= color
.getValue();
1214 // Convert Color.* values to SGR numerics
1222 return String
.format("\033[%dm", ecmaColor
);
1224 return String
.format("%d;", ecmaColor
);
1229 * Create a SGR parameter sequence for both foreground and background
1230 * color change. Note package private access.
1232 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1233 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1234 * @return the string to emit to an ANSI / ECMA-style terminal,
1235 * e.g. "\033[31;42m"
1237 String
color(final Color foreColor
, final Color backColor
) {
1238 return color(foreColor
, backColor
, true);
1242 * Create a SGR parameter sequence for both foreground and
1243 * background color change.
1245 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1246 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1247 * @param header if true, make the full header, otherwise just emit the
1248 * color parameter e.g. "31;42;"
1249 * @return the string to emit to an ANSI / ECMA-style terminal,
1250 * e.g. "\033[31;42m"
1252 private String
color(final Color foreColor
, final Color backColor
,
1253 final boolean header
) {
1255 int ecmaForeColor
= foreColor
.getValue();
1256 int ecmaBackColor
= backColor
.getValue();
1258 // Convert Color.* values to SGR numerics
1259 ecmaBackColor
+= 40;
1260 ecmaForeColor
+= 30;
1263 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1265 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1270 * Create a SGR parameter sequence for foreground, background, and
1271 * several attributes. This sequence first resets all attributes to
1272 * default, then sets attributes as per the parameters. Note package
1275 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1276 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1277 * @param bold if true, set bold
1278 * @param reverse if true, set reverse
1279 * @param blink if true, set blink
1280 * @param underline if true, set underline
1281 * @return the string to emit to an ANSI / ECMA-style terminal,
1282 * e.g. "\033[0;1;31;42m"
1284 String
color(final Color foreColor
, final Color backColor
,
1285 final boolean bold
, final boolean reverse
, final boolean blink
,
1286 final boolean underline
) {
1288 int ecmaForeColor
= foreColor
.getValue();
1289 int ecmaBackColor
= backColor
.getValue();
1291 // Convert Color.* values to SGR numerics
1292 ecmaBackColor
+= 40;
1293 ecmaForeColor
+= 30;
1295 StringBuilder sb
= new StringBuilder();
1296 if ( bold
&& reverse
&& blink
&& !underline
) {
1297 sb
.append("\033[0;1;7;5;");
1298 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1299 sb
.append("\033[0;1;7;");
1300 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1301 sb
.append("\033[0;7;5;");
1302 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1303 sb
.append("\033[0;1;5;");
1304 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1305 sb
.append("\033[0;1;");
1306 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1307 sb
.append("\033[0;7;");
1308 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1309 sb
.append("\033[0;5;");
1310 } else if ( bold
&& reverse
&& blink
&& underline
) {
1311 sb
.append("\033[0;1;7;5;4;");
1312 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1313 sb
.append("\033[0;1;7;4;");
1314 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1315 sb
.append("\033[0;7;5;4;");
1316 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1317 sb
.append("\033[0;1;5;4;");
1318 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1319 sb
.append("\033[0;1;4;");
1320 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1321 sb
.append("\033[0;7;4;");
1322 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1323 sb
.append("\033[0;5;4;");
1324 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1325 sb
.append("\033[0;4;");
1327 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1328 sb
.append("\033[0;");
1330 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1331 return sb
.toString();
1335 * Create a SGR parameter sequence for enabling reverse color.
1337 * @param on if true, turn on reverse
1338 * @return the string to emit to an ANSI / ECMA-style terminal,
1341 private String
reverse(final boolean on
) {
1349 * Create a SGR parameter sequence to reset to defaults. Note package
1352 * @return the string to emit to an ANSI / ECMA-style terminal,
1356 return normal(true);
1360 * Create a SGR parameter sequence to reset to defaults.
1362 * @param header if true, make the full header, otherwise just emit the
1363 * bare parameter e.g. "0;"
1364 * @return the string to emit to an ANSI / ECMA-style terminal,
1367 private String
normal(final boolean header
) {
1369 return "\033[0;37;40m";
1375 * Create a SGR parameter sequence for enabling boldface.
1377 * @param on if true, turn on bold
1378 * @return the string to emit to an ANSI / ECMA-style terminal,
1381 private String
bold(final boolean on
) {
1382 return bold(on
, true);
1386 * Create a SGR parameter sequence for enabling boldface.
1388 * @param on if true, turn on bold
1389 * @param header if true, make the full header, otherwise just emit the
1390 * bare parameter e.g. "1;"
1391 * @return the string to emit to an ANSI / ECMA-style terminal,
1394 private String
bold(final boolean on
, final boolean header
) {
1408 * Create a SGR parameter sequence for enabling blinking text.
1410 * @param on if true, turn on blink
1411 * @return the string to emit to an ANSI / ECMA-style terminal,
1414 private String
blink(final boolean on
) {
1415 return blink(on
, true);
1419 * Create a SGR parameter sequence for enabling blinking text.
1421 * @param on if true, turn on blink
1422 * @param header if true, make the full header, otherwise just emit the
1423 * bare parameter e.g. "5;"
1424 * @return the string to emit to an ANSI / ECMA-style terminal,
1427 private String
blink(final boolean on
, final boolean header
) {
1441 * Create a SGR parameter sequence for enabling underline / underscored
1444 * @param on if true, turn on underline
1445 * @return the string to emit to an ANSI / ECMA-style terminal,
1448 private String
underline(final boolean on
) {
1456 * Create a SGR parameter sequence for enabling the visible cursor. Note
1457 * package private access.
1459 * @param on if true, turn on cursor
1460 * @return the string to emit to an ANSI / ECMA-style terminal
1462 String
cursor(final boolean on
) {
1463 if (on
&& !cursorOn
) {
1467 if (!on
&& cursorOn
) {
1475 * Clear the entire screen. Because some terminals use back-color-erase,
1476 * set the color to white-on-black beforehand.
1478 * @return the string to emit to an ANSI / ECMA-style terminal
1480 public String
clearAll() {
1481 return "\033[0;37;40m\033[2J";
1485 * Clear the line from the cursor (inclusive) to the end of the screen.
1486 * Because some terminals use back-color-erase, set the color to
1487 * white-on-black beforehand. Note package private access.
1489 * @return the string to emit to an ANSI / ECMA-style terminal
1491 String
clearRemainingLine() {
1492 return "\033[0;37;40m\033[K";
1496 * Clear the line up the cursor (inclusive). Because some terminals use
1497 * back-color-erase, set the color to white-on-black beforehand.
1499 * @return the string to emit to an ANSI / ECMA-style terminal
1501 private String
clearPreceedingLine() {
1502 return "\033[0;37;40m\033[1K";
1506 * Clear the line. Because some terminals use back-color-erase, set the
1507 * color to white-on-black beforehand.
1509 * @return the string to emit to an ANSI / ECMA-style terminal
1511 private String
clearLine() {
1512 return "\033[0;37;40m\033[2K";
1516 * Move the cursor to the top-left corner.
1518 * @return the string to emit to an ANSI / ECMA-style terminal
1520 private String
home() {
1525 * Move the cursor to (x, y). Note package private access.
1527 * @param x column coordinate. 0 is the left-most column.
1528 * @param y row coordinate. 0 is the top-most row.
1529 * @return the string to emit to an ANSI / ECMA-style terminal
1531 String
gotoXY(final int x
, final int y
) {
1532 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1536 * Tell (u)xterm that we want to receive mouse events based on "Any event
1537 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1538 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1540 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1542 * Note that this also sets the alternate/primary screen buffer.
1544 * @param on If true, enable mouse report and use the alternate screen
1545 * buffer. If false disable mouse reporting and use the primary screen
1547 * @return the string to emit to xterm
1549 private String
mouse(final boolean on
) {
1551 return "\033[?1003;1005;1006h\033[?1049h";
1553 return "\033[?1003;1006;1005l\033[?1049l";
1557 * Read function runs on a separate thread.
1560 boolean done
= false;
1561 // available() will often return > 1, so we need to read in chunks to
1563 char [] readBuffer
= new char[128];
1564 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1566 while (!done
&& !stopReaderThread
) {
1568 // We assume that if inputStream has bytes available, then
1569 // input won't block on read().
1570 int n
= inputStream
.available();
1572 if (readBuffer
.length
< n
) {
1573 // The buffer wasn't big enough, make it huger
1574 readBuffer
= new char[readBuffer
.length
* 2];
1577 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1578 // System.err.printf("read() %d", rc); System.err.flush();
1583 for (int i
= 0; i
< rc
; i
++) {
1584 int ch
= readBuffer
[i
];
1585 processChar(events
, (char)ch
);
1586 if (events
.size() > 0) {
1587 // Add to the queue for the backend thread to
1588 // be able to obtain.
1589 synchronized (eventQueue
) {
1590 eventQueue
.addAll(events
);
1592 synchronized (listener
) {
1593 listener
.notifyAll();
1600 getIdleEvents(events
);
1601 if (events
.size() > 0) {
1602 synchronized (eventQueue
) {
1603 eventQueue
.addAll(events
);
1606 synchronized (listener
) {
1607 listener
.notifyAll();
1611 // Wait 10 millis for more data
1614 // System.err.println("end while loop"); System.err.flush();
1615 } catch (InterruptedException e
) {
1617 } catch (IOException e
) {
1618 e
.printStackTrace();
1621 } // while ((done == false) && (stopReaderThread == false))
1622 // System.err.println("*** run() exiting..."); System.err.flush();