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 if (input
instanceof SessionInfo
) {
312 // This is a TelnetInputStream that exposes window size and
313 // environment variables from the telnet layer.
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));
337 // Hang onto the window size
338 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
339 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
341 // Spin up the input reader
342 eventQueue
= new LinkedList
<TInputEvent
>();
343 readerThread
= new Thread(this);
344 readerThread
.start();
348 * Restore terminal to normal state.
350 public void shutdown() {
352 // System.err.println("=== shutdown() ==="); System.err.flush();
354 // Tell the reader thread to stop looking at input
355 stopReaderThread
= true;
358 } catch (InterruptedException e
) {
362 // Disable mouse reporting and show cursor
363 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
369 // We don't close System.in/out
371 // Shut down the streams, this should wake up the reader thread
378 if (output
!= null) {
382 } catch (IOException e
) {
391 public void flush() {
396 * Reset keyboard/mouse input parser.
398 private void reset() {
399 state
= ParseState
.GROUND
;
400 params
= new ArrayList
<String
>();
406 * Produce a control character or one of the special ones (ENTER, TAB,
409 * @param ch Unicode code point
410 * @param alt if true, set alt on the TKeypress
411 * @return one TKeypress event, either a control character (e.g. isKey ==
412 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
415 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
416 // System.err.printf("controlChar: %02x\n", ch);
420 // Carriage return --> ENTER
421 return new TKeypressEvent(kbEnter
, alt
, false, false);
423 // Linefeed --> ENTER
424 return new TKeypressEvent(kbEnter
, alt
, false, false);
427 return new TKeypressEvent(kbEsc
, alt
, false, false);
430 return new TKeypressEvent(kbTab
, alt
, false, false);
432 // Make all other control characters come back as the alphabetic
433 // character with the ctrl field set. So SOH would be 'A' +
435 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
441 * Produce special key from CSI Pn ; Pm ; ... ~
443 * @return one KEYPRESS event representing a special key
445 private TInputEvent
csiFnKey() {
448 if (params
.size() > 0) {
449 key
= Integer
.parseInt(params
.get(0));
451 if (params
.size() > 1) {
452 modifier
= Integer
.parseInt(params
.get(1));
455 boolean ctrl
= false;
456 boolean shift
= false;
475 // Unknown modifier, bail out
481 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
483 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
485 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
487 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
489 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
491 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
493 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
495 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
497 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
499 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
501 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
503 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
505 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
507 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
515 * Produce mouse events based on "Any event tracking" and UTF-8
517 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
519 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
521 private TInputEvent
parseMouse() {
522 int buttons
= params
.get(0).charAt(0) - 32;
523 int x
= params
.get(0).charAt(1) - 32 - 1;
524 int y
= params
.get(0).charAt(2) - 32 - 1;
526 // Clamp X and Y to the physical screen coordinates.
527 if (x
>= windowResize
.getWidth()) {
528 x
= windowResize
.getWidth() - 1;
530 if (y
>= windowResize
.getHeight()) {
531 y
= windowResize
.getHeight() - 1;
534 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
535 boolean eventMouse1
= false;
536 boolean eventMouse2
= false;
537 boolean eventMouse3
= false;
538 boolean eventMouseWheelUp
= false;
539 boolean eventMouseWheelDown
= false;
541 // System.err.printf("buttons: %04x\r\n", buttons);
558 if (!mouse1
&& !mouse2
&& !mouse3
) {
559 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
561 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
578 // Dragging with mouse1 down
581 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
585 // Dragging with mouse2 down
588 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
592 // Dragging with mouse3 down
595 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
599 // Dragging with mouse2 down after wheelUp
602 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
606 // Dragging with mouse2 down after wheelDown
609 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
613 eventMouseWheelUp
= true;
617 eventMouseWheelDown
= true;
621 // Unknown, just make it motion
622 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
625 return new TMouseEvent(eventType
, x
, y
, x
, y
,
626 eventMouse1
, eventMouse2
, eventMouse3
,
627 eventMouseWheelUp
, eventMouseWheelDown
);
631 * Produce mouse events based on "Any event tracking" and SGR
633 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
635 * @param release if true, this was a release ('m')
636 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
638 private TInputEvent
parseMouseSGR(final boolean release
) {
639 // SGR extended coordinates - mode 1006
640 if (params
.size() < 3) {
641 // Invalid position, bail out.
644 int buttons
= Integer
.parseInt(params
.get(0));
645 int x
= Integer
.parseInt(params
.get(1)) - 1;
646 int y
= Integer
.parseInt(params
.get(2)) - 1;
648 // Clamp X and Y to the physical screen coordinates.
649 if (x
>= windowResize
.getWidth()) {
650 x
= windowResize
.getWidth() - 1;
652 if (y
>= windowResize
.getHeight()) {
653 y
= windowResize
.getHeight() - 1;
656 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
657 boolean eventMouse1
= false;
658 boolean eventMouse2
= false;
659 boolean eventMouse3
= false;
660 boolean eventMouseWheelUp
= false;
661 boolean eventMouseWheelDown
= false;
664 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
678 // Motion only, no buttons down
679 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
683 // Dragging with mouse1 down
685 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
689 // Dragging with mouse2 down
691 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
695 // Dragging with mouse3 down
697 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
701 // Dragging with mouse2 down after wheelUp
703 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
707 // Dragging with mouse2 down after wheelDown
709 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
713 eventMouseWheelUp
= true;
717 eventMouseWheelDown
= true;
724 return new TMouseEvent(eventType
, x
, y
, x
, y
,
725 eventMouse1
, eventMouse2
, eventMouse3
,
726 eventMouseWheelUp
, eventMouseWheelDown
);
730 * Return any events in the IO queue.
732 * @param queue list to append new events to
734 public void getEvents(final List
<TInputEvent
> queue
) {
735 synchronized (eventQueue
) {
736 if (eventQueue
.size() > 0) {
737 synchronized (queue
) {
738 queue
.addAll(eventQueue
);
746 * Return any events in the IO queue due to timeout.
748 * @param queue list to append new events to
750 private void getIdleEvents(final List
<TInputEvent
> queue
) {
751 Date now
= new Date();
753 // Check for new window size
754 long windowSizeDelay
= now
.getTime() - windowSizeTime
;
755 if (windowSizeDelay
> 1000) {
756 sessionInfo
.queryWindowSize();
757 int newWidth
= sessionInfo
.getWindowWidth();
758 int newHeight
= sessionInfo
.getWindowHeight();
759 if ((newWidth
!= windowResize
.getWidth())
760 || (newHeight
!= windowResize
.getHeight())
762 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
763 newWidth
, newHeight
);
764 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
765 newWidth
, newHeight
);
768 windowSizeTime
= now
.getTime();
771 // ESCDELAY type timeout
772 if (state
== ParseState
.ESCAPE
) {
773 long escDelay
= now
.getTime() - escapeTime
;
774 if (escDelay
> 100) {
775 // After 0.1 seconds, assume a true escape character
776 queue
.add(controlChar((char)0x1B, false));
783 * Parses the next character of input to see if an InputEvent is
786 * @param events list to append new events to
787 * @param ch Unicode code point
789 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
791 // ESCDELAY type timeout
792 Date now
= new Date();
793 if (state
== ParseState
.ESCAPE
) {
794 long escDelay
= now
.getTime() - escapeTime
;
795 if (escDelay
> 250) {
796 // After 0.25 seconds, assume a true escape character
797 events
.add(controlChar((char)0x1B, false));
803 boolean ctrl
= false;
805 boolean shift
= false;
807 // System.err.printf("state: %s ch %c\r\n", state, ch);
813 state
= ParseState
.ESCAPE
;
814 escapeTime
= now
.getTime();
820 events
.add(controlChar(ch
, false));
827 events
.add(new TKeypressEvent(false, 0, ch
,
828 false, false, false));
837 // ALT-Control character
838 events
.add(controlChar(ch
, true));
844 // This will be one of the function keys
845 state
= ParseState
.ESCAPE_INTERMEDIATE
;
849 // '[' goes to CSI_ENTRY
851 state
= ParseState
.CSI_ENTRY
;
855 // Everything else is assumed to be Alt-keystroke
856 if ((ch
>= 'A') && (ch
<= 'Z')) {
860 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
864 case ESCAPE_INTERMEDIATE
:
865 if ((ch
>= 'P') && (ch
<= 'S')) {
869 events
.add(new TKeypressEvent(kbF1
));
872 events
.add(new TKeypressEvent(kbF2
));
875 events
.add(new TKeypressEvent(kbF3
));
878 events
.add(new TKeypressEvent(kbF4
));
887 // Unknown keystroke, ignore
892 // Numbers - parameter values
893 if ((ch
>= '0') && (ch
<= '9')) {
894 params
.set(params
.size() - 1,
895 params
.get(params
.size() - 1) + ch
);
896 state
= ParseState
.CSI_PARAM
;
899 // Parameter separator
905 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
909 if (params
.size() > 1) {
910 if (params
.get(1).equals("2")) {
913 if (params
.get(1).equals("5")) {
916 if (params
.get(1).equals("3")) {
920 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
925 if (params
.size() > 1) {
926 if (params
.get(1).equals("2")) {
929 if (params
.get(1).equals("5")) {
932 if (params
.get(1).equals("3")) {
936 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
941 if (params
.size() > 1) {
942 if (params
.get(1).equals("2")) {
945 if (params
.get(1).equals("5")) {
948 if (params
.get(1).equals("3")) {
952 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
957 if (params
.size() > 1) {
958 if (params
.get(1).equals("2")) {
961 if (params
.get(1).equals("5")) {
964 if (params
.get(1).equals("3")) {
968 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
973 events
.add(new TKeypressEvent(kbHome
));
978 events
.add(new TKeypressEvent(kbEnd
));
982 // CBT - Cursor backward X tab stops (default 1)
983 events
.add(new TKeypressEvent(kbBackTab
));
988 state
= ParseState
.MOUSE
;
991 // Mouse position, SGR (1006) coordinates
992 state
= ParseState
.MOUSE_SGR
;
999 // Unknown keystroke, ignore
1004 // Numbers - parameter values
1005 if ((ch
>= '0') && (ch
<= '9')) {
1006 params
.set(params
.size() - 1,
1007 params
.get(params
.size() - 1) + ch
);
1010 // Parameter separator
1018 // Generate a mouse press event
1019 TInputEvent event
= parseMouseSGR(false);
1020 if (event
!= null) {
1026 // Generate a mouse release event
1027 event
= parseMouseSGR(true);
1028 if (event
!= null) {
1037 // Unknown keystroke, ignore
1042 // Numbers - parameter values
1043 if ((ch
>= '0') && (ch
<= '9')) {
1044 params
.set(params
.size() - 1,
1045 params
.get(params
.size() - 1) + ch
);
1046 state
= ParseState
.CSI_PARAM
;
1049 // Parameter separator
1056 events
.add(csiFnKey());
1061 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1065 if (params
.size() > 1) {
1066 if (params
.get(1).equals("2")) {
1069 if (params
.get(1).equals("5")) {
1072 if (params
.get(1).equals("3")) {
1076 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1081 if (params
.size() > 1) {
1082 if (params
.get(1).equals("2")) {
1085 if (params
.get(1).equals("5")) {
1088 if (params
.get(1).equals("3")) {
1092 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1097 if (params
.size() > 1) {
1098 if (params
.get(1).equals("2")) {
1101 if (params
.get(1).equals("5")) {
1104 if (params
.get(1).equals("3")) {
1108 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1113 if (params
.size() > 1) {
1114 if (params
.get(1).equals("2")) {
1117 if (params
.get(1).equals("5")) {
1120 if (params
.get(1).equals("3")) {
1124 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1132 // Unknown keystroke, ignore
1137 params
.set(0, params
.get(params
.size() - 1) + ch
);
1138 if (params
.get(0).length() == 3) {
1139 // We have enough to generate a mouse event
1140 events
.add(parseMouse());
1149 // This "should" be impossible to reach
1154 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1155 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1158 * @param on if true, enable metaSendsEscape
1159 * @return the string to emit to xterm
1161 private String
xtermMetaSendsEscape(final boolean on
) {
1163 return "\033[?1036h\033[?1034l";
1165 return "\033[?1036l";
1169 * Convert a list of SGR parameters into a full escape sequence. This
1170 * also eliminates a trailing ';' which would otherwise reset everything
1171 * to white-on-black not-bold.
1173 * @param str string of parameters, e.g. "31;1;"
1174 * @return the string to emit to an ANSI / ECMA-style terminal,
1177 private String
addHeaderSGR(String str
) {
1178 if (str
.length() > 0) {
1179 // Nix any trailing ';' because that resets all attributes
1180 while (str
.endsWith(":")) {
1181 str
= str
.substring(0, str
.length() - 1);
1184 return "\033[" + str
+ "m";
1188 * Create a SGR parameter sequence for a single color change. Note
1189 * package private access.
1191 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1192 * @param foreground if true, this is a foreground color
1193 * @return the string to emit to an ANSI / ECMA-style terminal,
1196 String
color(final Color color
, final boolean foreground
) {
1197 return color(color
, foreground
, true);
1201 * Create a SGR parameter sequence for a single color change.
1203 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1204 * @param foreground if true, this is a foreground color
1205 * @param header if true, make the full header, otherwise just emit the
1206 * color parameter e.g. "42;"
1207 * @return the string to emit to an ANSI / ECMA-style terminal,
1210 private String
color(final Color color
, final boolean foreground
,
1211 final boolean header
) {
1213 int ecmaColor
= color
.getValue();
1215 // Convert Color.* values to SGR numerics
1223 return String
.format("\033[%dm", ecmaColor
);
1225 return String
.format("%d;", ecmaColor
);
1230 * Create a SGR parameter sequence for both foreground and background
1231 * color change. Note package private access.
1233 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1234 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1235 * @return the string to emit to an ANSI / ECMA-style terminal,
1236 * e.g. "\033[31;42m"
1238 String
color(final Color foreColor
, final Color backColor
) {
1239 return color(foreColor
, backColor
, true);
1243 * Create a SGR parameter sequence for both foreground and
1244 * background color change.
1246 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1247 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1248 * @param header if true, make the full header, otherwise just emit the
1249 * color parameter e.g. "31;42;"
1250 * @return the string to emit to an ANSI / ECMA-style terminal,
1251 * e.g. "\033[31;42m"
1253 private String
color(final Color foreColor
, final Color backColor
,
1254 final boolean header
) {
1256 int ecmaForeColor
= foreColor
.getValue();
1257 int ecmaBackColor
= backColor
.getValue();
1259 // Convert Color.* values to SGR numerics
1260 ecmaBackColor
+= 40;
1261 ecmaForeColor
+= 30;
1264 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1266 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1271 * Create a SGR parameter sequence for foreground, background, and
1272 * several attributes. This sequence first resets all attributes to
1273 * default, then sets attributes as per the parameters. Note package
1276 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1277 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1278 * @param bold if true, set bold
1279 * @param reverse if true, set reverse
1280 * @param blink if true, set blink
1281 * @param underline if true, set underline
1282 * @return the string to emit to an ANSI / ECMA-style terminal,
1283 * e.g. "\033[0;1;31;42m"
1285 String
color(final Color foreColor
, final Color backColor
,
1286 final boolean bold
, final boolean reverse
, final boolean blink
,
1287 final boolean underline
) {
1289 int ecmaForeColor
= foreColor
.getValue();
1290 int ecmaBackColor
= backColor
.getValue();
1292 // Convert Color.* values to SGR numerics
1293 ecmaBackColor
+= 40;
1294 ecmaForeColor
+= 30;
1296 StringBuilder sb
= new StringBuilder();
1297 if ( bold
&& reverse
&& blink
&& !underline
) {
1298 sb
.append("\033[0;1;7;5;");
1299 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1300 sb
.append("\033[0;1;7;");
1301 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1302 sb
.append("\033[0;7;5;");
1303 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1304 sb
.append("\033[0;1;5;");
1305 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1306 sb
.append("\033[0;1;");
1307 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1308 sb
.append("\033[0;7;");
1309 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1310 sb
.append("\033[0;5;");
1311 } else if ( bold
&& reverse
&& blink
&& underline
) {
1312 sb
.append("\033[0;1;7;5;4;");
1313 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1314 sb
.append("\033[0;1;7;4;");
1315 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1316 sb
.append("\033[0;7;5;4;");
1317 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1318 sb
.append("\033[0;1;5;4;");
1319 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1320 sb
.append("\033[0;1;4;");
1321 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1322 sb
.append("\033[0;7;4;");
1323 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1324 sb
.append("\033[0;5;4;");
1325 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1326 sb
.append("\033[0;4;");
1328 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1329 sb
.append("\033[0;");
1331 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1332 return sb
.toString();
1336 * Create a SGR parameter sequence for enabling reverse color.
1338 * @param on if true, turn on reverse
1339 * @return the string to emit to an ANSI / ECMA-style terminal,
1342 private String
reverse(final boolean on
) {
1350 * Create a SGR parameter sequence to reset to defaults. Note package
1353 * @return the string to emit to an ANSI / ECMA-style terminal,
1357 return normal(true);
1361 * Create a SGR parameter sequence to reset to defaults.
1363 * @param header if true, make the full header, otherwise just emit the
1364 * bare parameter e.g. "0;"
1365 * @return the string to emit to an ANSI / ECMA-style terminal,
1368 private String
normal(final boolean header
) {
1370 return "\033[0;37;40m";
1376 * Create a SGR parameter sequence for enabling boldface.
1378 * @param on if true, turn on bold
1379 * @return the string to emit to an ANSI / ECMA-style terminal,
1382 private String
bold(final boolean on
) {
1383 return bold(on
, true);
1387 * Create a SGR parameter sequence for enabling boldface.
1389 * @param on if true, turn on bold
1390 * @param header if true, make the full header, otherwise just emit the
1391 * bare parameter e.g. "1;"
1392 * @return the string to emit to an ANSI / ECMA-style terminal,
1395 private String
bold(final boolean on
, final boolean header
) {
1409 * Create a SGR parameter sequence for enabling blinking text.
1411 * @param on if true, turn on blink
1412 * @return the string to emit to an ANSI / ECMA-style terminal,
1415 private String
blink(final boolean on
) {
1416 return blink(on
, true);
1420 * Create a SGR parameter sequence for enabling blinking text.
1422 * @param on if true, turn on blink
1423 * @param header if true, make the full header, otherwise just emit the
1424 * bare parameter e.g. "5;"
1425 * @return the string to emit to an ANSI / ECMA-style terminal,
1428 private String
blink(final boolean on
, final boolean header
) {
1442 * Create a SGR parameter sequence for enabling underline / underscored
1445 * @param on if true, turn on underline
1446 * @return the string to emit to an ANSI / ECMA-style terminal,
1449 private String
underline(final boolean on
) {
1457 * Create a SGR parameter sequence for enabling the visible cursor. Note
1458 * package private access.
1460 * @param on if true, turn on cursor
1461 * @return the string to emit to an ANSI / ECMA-style terminal
1463 String
cursor(final boolean on
) {
1464 if (on
&& !cursorOn
) {
1468 if (!on
&& cursorOn
) {
1476 * Clear the entire screen. Because some terminals use back-color-erase,
1477 * set the color to white-on-black beforehand.
1479 * @return the string to emit to an ANSI / ECMA-style terminal
1481 public String
clearAll() {
1482 return "\033[0;37;40m\033[2J";
1486 * Clear the line from the cursor (inclusive) to the end of the screen.
1487 * Because some terminals use back-color-erase, set the color to
1488 * white-on-black beforehand. Note package private access.
1490 * @return the string to emit to an ANSI / ECMA-style terminal
1492 String
clearRemainingLine() {
1493 return "\033[0;37;40m\033[K";
1497 * Clear the line up the cursor (inclusive). Because some terminals use
1498 * back-color-erase, set the color to white-on-black beforehand.
1500 * @return the string to emit to an ANSI / ECMA-style terminal
1502 private String
clearPreceedingLine() {
1503 return "\033[0;37;40m\033[1K";
1507 * Clear the line. Because some terminals use back-color-erase, set the
1508 * color to white-on-black beforehand.
1510 * @return the string to emit to an ANSI / ECMA-style terminal
1512 private String
clearLine() {
1513 return "\033[0;37;40m\033[2K";
1517 * Move the cursor to the top-left corner.
1519 * @return the string to emit to an ANSI / ECMA-style terminal
1521 private String
home() {
1526 * Move the cursor to (x, y). Note package private access.
1528 * @param x column coordinate. 0 is the left-most column.
1529 * @param y row coordinate. 0 is the top-most row.
1530 * @return the string to emit to an ANSI / ECMA-style terminal
1532 String
gotoXY(final int x
, final int y
) {
1533 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1537 * Tell (u)xterm that we want to receive mouse events based on "Any event
1538 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1539 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1541 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1543 * Note that this also sets the alternate/primary screen buffer.
1545 * @param on If true, enable mouse report and use the alternate screen
1546 * buffer. If false disable mouse reporting and use the primary screen
1548 * @return the string to emit to xterm
1550 private String
mouse(final boolean on
) {
1552 return "\033[?1003;1005;1006h\033[?1049h";
1554 return "\033[?1003;1006;1005l\033[?1049l";
1558 * Read function runs on a separate thread.
1561 boolean done
= false;
1562 // available() will often return > 1, so we need to read in chunks to
1564 char [] readBuffer
= new char[128];
1565 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1567 while (!done
&& !stopReaderThread
) {
1569 // We assume that if inputStream has bytes available, then
1570 // input won't block on read().
1571 int n
= inputStream
.available();
1573 if (readBuffer
.length
< n
) {
1574 // The buffer wasn't big enough, make it huger
1575 readBuffer
= new char[readBuffer
.length
* 2];
1578 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1579 // System.err.printf("read() %d", rc); System.err.flush();
1584 for (int i
= 0; i
< rc
; i
++) {
1585 int ch
= readBuffer
[i
];
1586 processChar(events
, (char)ch
);
1588 getIdleEvents(events
);
1589 if (events
.size() > 0) {
1590 // Add to the queue for the backend thread to
1591 // be able to obtain.
1592 synchronized (eventQueue
) {
1593 eventQueue
.addAll(events
);
1595 synchronized (listener
) {
1596 listener
.notifyAll();
1602 getIdleEvents(events
);
1603 if (events
.size() > 0) {
1604 synchronized (eventQueue
) {
1605 eventQueue
.addAll(events
);
1608 synchronized (listener
) {
1609 listener
.notifyAll();
1613 // Wait 10 millis for more data
1616 // System.err.println("end while loop"); System.err.flush();
1617 } catch (InterruptedException e
) {
1619 } catch (IOException e
) {
1620 e
.printStackTrace();
1623 } // while ((done == false) && (stopReaderThread == false))
1624 // System.err.println("*** run() exiting..."); System.err.flush();