2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2016 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
31 import java
.io
.BufferedReader
;
32 import java
.io
.FileDescriptor
;
33 import java
.io
.FileInputStream
;
34 import java
.io
.InputStream
;
35 import java
.io
.InputStreamReader
;
36 import java
.io
.IOException
;
37 import java
.io
.OutputStream
;
38 import java
.io
.OutputStreamWriter
;
39 import java
.io
.PrintWriter
;
40 import java
.io
.Reader
;
41 import java
.io
.UnsupportedEncodingException
;
42 import java
.util
.ArrayList
;
43 import java
.util
.Date
;
44 import java
.util
.List
;
45 import java
.util
.LinkedList
;
47 import jexer
.bits
.Color
;
48 import jexer
.event
.TInputEvent
;
49 import jexer
.event
.TKeypressEvent
;
50 import jexer
.event
.TMouseEvent
;
51 import jexer
.event
.TResizeEvent
;
52 import jexer
.session
.SessionInfo
;
53 import jexer
.session
.TSessionInfo
;
54 import jexer
.session
.TTYSessionInfo
;
55 import static jexer
.TKeypress
.*;
58 * This class reads keystrokes and mouse events and emits output to ANSI
59 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
61 public final class ECMA48Terminal
implements Runnable
{
64 * The session information.
66 private SessionInfo sessionInfo
;
69 * Getter for sessionInfo.
71 * @return the SessionInfo
73 public SessionInfo
getSessionInfo() {
78 * The event queue, filled up by a thread reading on input.
80 private List
<TInputEvent
> eventQueue
;
83 * If true, we want the reader thread to exit gracefully.
85 private boolean stopReaderThread
;
90 private Thread readerThread
;
93 * Parameters being collected. E.g. if the string is \033[1;3m, then
94 * params[0] will be 1 and params[1] will be 3.
96 private ArrayList
<String
> params
;
99 * States in the input parser.
101 private enum ParseState
{
113 * Current parsing state.
115 private ParseState state
;
118 * The time we entered ESCAPE. If we get a bare escape without a code
119 * following it, this is used to return that bare escape.
121 private long escapeTime
;
124 * The time we last checked the window size. We try not to spawn stty
125 * more than once per second.
127 private long windowSizeTime
;
130 * true if mouse1 was down. Used to report mouse1 on the release event.
132 private boolean mouse1
;
135 * true if mouse2 was down. Used to report mouse2 on the release event.
137 private boolean mouse2
;
140 * true if mouse3 was down. Used to report mouse3 on the release event.
142 private boolean mouse3
;
145 * Cache the cursor visibility value so we only emit the sequence when we
148 private boolean cursorOn
= true;
151 * Cache the last window size to figure out if a TResizeEvent needs to be
154 private TResizeEvent windowResize
= null;
157 * If true, then we changed System.in and need to change it back.
159 private boolean setRawMode
;
162 * The terminal's input. If an InputStream is not specified in the
163 * constructor, then this InputStreamReader will be bound to System.in
164 * with UTF-8 encoding.
166 private Reader input
;
169 * The terminal's raw InputStream. If an InputStream is not specified in
170 * the constructor, then this InputReader will be bound to System.in.
171 * This is used by run() to see if bytes are available() before calling
172 * (Reader)input.read().
174 private InputStream inputStream
;
177 * The terminal's output. If an OutputStream is not specified in the
178 * constructor, then this PrintWriter will be bound to System.out with
181 private PrintWriter output
;
184 * The listening object that run() wakes up on new input.
186 private Object listener
;
189 * Get the output writer.
193 public PrintWriter
getOutput() {
198 * Check if there are events in the queue.
200 * @return if true, getEvents() has something to return to the backend
202 public boolean hasEvents() {
203 synchronized (eventQueue
) {
204 return (eventQueue
.size() > 0);
209 * Call 'stty' to set cooked mode.
211 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
213 private void sttyCooked() {
218 * Call 'stty' to set raw mode.
220 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
221 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
222 * -parenb cs8 min 1 < /dev/tty'
224 private void sttyRaw() {
229 * Call 'stty' to set raw or cooked mode.
231 * @param mode if true, set raw mode, otherwise set cooked mode
233 private void doStty(final boolean mode
) {
235 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
237 String
[] cmdCooked
= {
238 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
243 process
= Runtime
.getRuntime().exec(cmdRaw
);
245 process
= Runtime
.getRuntime().exec(cmdCooked
);
247 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
248 String line
= in
.readLine();
249 if ((line
!= null) && (line
.length() > 0)) {
250 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
253 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
254 line
= err
.readLine();
255 if ((line
!= null) && (line
.length() > 0)) {
256 System
.err
.println("Error output from stty: " + line
);
261 } catch (InterruptedException e
) {
265 int rc
= process
.exitValue();
267 System
.err
.println("stty returned error code: " + rc
);
269 } catch (IOException e
) {
275 * Constructor sets up state for getEvent().
277 * @param listener the object this backend needs to wake up when new
279 * @param input an InputStream connected to the remote user, or null for
280 * System.in. If System.in is used, then on non-Windows systems it will
281 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
282 * mode. input is always converted to a Reader with UTF-8 encoding.
283 * @param output an OutputStream connected to the remote user, or null
284 * for System.out. output is always converted to a Writer with UTF-8
286 * @throws UnsupportedEncodingException if an exception is thrown when
287 * creating the InputStreamReader
289 public ECMA48Terminal(final Object listener
, final InputStream input
,
290 final OutputStream output
) throws UnsupportedEncodingException
{
296 stopReaderThread
= false;
297 this.listener
= listener
;
300 // inputStream = System.in;
301 inputStream
= new FileInputStream(FileDescriptor
.in
);
307 this.input
= new InputStreamReader(inputStream
, "UTF-8");
309 if (input
instanceof SessionInfo
) {
310 // This is a TelnetInputStream that exposes window size and
311 // environment variables from the telnet layer.
312 sessionInfo
= (SessionInfo
) input
;
314 if (sessionInfo
== null) {
316 // Reading right off the tty
317 sessionInfo
= new TTYSessionInfo();
319 sessionInfo
= new TSessionInfo();
323 if (output
== null) {
324 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
327 this.output
= new PrintWriter(new OutputStreamWriter(output
,
331 // Enable mouse reporting and metaSendsEscape
332 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
>();
404 * Produce a control character or one of the special ones (ENTER, TAB,
407 * @param ch Unicode code point
408 * @param alt if true, set alt on the TKeypress
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
, final boolean alt
) {
414 // System.err.printf("controlChar: %02x\n", ch);
418 // Carriage return --> ENTER
419 return new TKeypressEvent(kbEnter
, alt
, false, false);
421 // Linefeed --> ENTER
422 return new TKeypressEvent(kbEnter
, alt
, false, false);
425 return new TKeypressEvent(kbEsc
, alt
, false, false);
428 return new TKeypressEvent(kbTab
, alt
, false, false);
430 // Make all other control characters come back as the alphabetic
431 // character with the ctrl field set. So SOH would be 'A' +
433 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
439 * Produce special key from CSI Pn ; Pm ; ... ~
441 * @return one KEYPRESS event representing a special key
443 private TInputEvent
csiFnKey() {
446 if (params
.size() > 0) {
447 key
= Integer
.parseInt(params
.get(0));
449 if (params
.size() > 1) {
450 modifier
= Integer
.parseInt(params
.get(1));
453 boolean ctrl
= false;
454 boolean shift
= false;
473 // Unknown modifier, bail out
479 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
481 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
483 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
485 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
487 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
489 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
491 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
493 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
495 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
497 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
499 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
501 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
503 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
505 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
513 * Produce mouse events based on "Any event tracking" and UTF-8
515 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
517 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
519 private TInputEvent
parseMouse() {
520 int buttons
= params
.get(0).charAt(0) - 32;
521 int x
= params
.get(0).charAt(1) - 32 - 1;
522 int y
= params
.get(0).charAt(2) - 32 - 1;
524 // Clamp X and Y to the physical screen coordinates.
525 if (x
>= windowResize
.getWidth()) {
526 x
= windowResize
.getWidth() - 1;
528 if (y
>= windowResize
.getHeight()) {
529 y
= windowResize
.getHeight() - 1;
532 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
533 boolean eventMouse1
= false;
534 boolean eventMouse2
= false;
535 boolean eventMouse3
= false;
536 boolean eventMouseWheelUp
= false;
537 boolean eventMouseWheelDown
= false;
539 // System.err.printf("buttons: %04x\r\n", buttons);
556 if (!mouse1
&& !mouse2
&& !mouse3
) {
557 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
559 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
576 // Dragging with mouse1 down
579 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
583 // Dragging with mouse2 down
586 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
590 // Dragging with mouse3 down
593 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
597 // Dragging with mouse2 down after wheelUp
600 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
604 // Dragging with mouse2 down after wheelDown
607 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
611 eventMouseWheelUp
= true;
615 eventMouseWheelDown
= true;
619 // Unknown, just make it motion
620 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
623 return new TMouseEvent(eventType
, x
, y
, x
, y
,
624 eventMouse1
, eventMouse2
, eventMouse3
,
625 eventMouseWheelUp
, eventMouseWheelDown
);
629 * Produce mouse events based on "Any event tracking" and SGR
631 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
633 * @param release if true, this was a release ('m')
634 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
636 private TInputEvent
parseMouseSGR(final boolean release
) {
637 // SGR extended coordinates - mode 1006
638 if (params
.size() < 3) {
639 // Invalid position, bail out.
642 int buttons
= Integer
.parseInt(params
.get(0));
643 int x
= Integer
.parseInt(params
.get(1)) - 1;
644 int y
= Integer
.parseInt(params
.get(2)) - 1;
646 // Clamp X and Y to the physical screen coordinates.
647 if (x
>= windowResize
.getWidth()) {
648 x
= windowResize
.getWidth() - 1;
650 if (y
>= windowResize
.getHeight()) {
651 y
= windowResize
.getHeight() - 1;
654 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
655 boolean eventMouse1
= false;
656 boolean eventMouse2
= false;
657 boolean eventMouse3
= false;
658 boolean eventMouseWheelUp
= false;
659 boolean eventMouseWheelDown
= false;
662 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
676 // Motion only, no buttons down
677 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
681 // Dragging with mouse1 down
683 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
687 // Dragging with mouse2 down
689 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
693 // Dragging with mouse3 down
695 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
699 // Dragging with mouse2 down after wheelUp
701 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
705 // Dragging with mouse2 down after wheelDown
707 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
711 eventMouseWheelUp
= true;
715 eventMouseWheelDown
= true;
722 return new TMouseEvent(eventType
, x
, y
, x
, y
,
723 eventMouse1
, eventMouse2
, eventMouse3
,
724 eventMouseWheelUp
, eventMouseWheelDown
);
728 * Return any events in the IO queue.
730 * @param queue list to append new events to
732 public void getEvents(final List
<TInputEvent
> queue
) {
733 synchronized (eventQueue
) {
734 if (eventQueue
.size() > 0) {
735 synchronized (queue
) {
736 queue
.addAll(eventQueue
);
744 * Return any events in the IO queue due to timeout.
746 * @param queue list to append new events to
748 private void getIdleEvents(final List
<TInputEvent
> queue
) {
749 Date now
= new Date();
751 // Check for new window size
752 long windowSizeDelay
= now
.getTime() - windowSizeTime
;
753 if (windowSizeDelay
> 1000) {
754 sessionInfo
.queryWindowSize();
755 int newWidth
= sessionInfo
.getWindowWidth();
756 int newHeight
= sessionInfo
.getWindowHeight();
757 if ((newWidth
!= windowResize
.getWidth())
758 || (newHeight
!= windowResize
.getHeight())
760 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
761 newWidth
, newHeight
);
762 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
763 newWidth
, newHeight
);
766 windowSizeTime
= now
.getTime();
769 // ESCDELAY type timeout
770 if (state
== ParseState
.ESCAPE
) {
771 long escDelay
= now
.getTime() - escapeTime
;
772 if (escDelay
> 100) {
773 // After 0.1 seconds, assume a true escape character
774 queue
.add(controlChar((char)0x1B, false));
781 * Parses the next character of input to see if an InputEvent is
784 * @param events list to append new events to
785 * @param ch Unicode code point
787 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
789 // ESCDELAY type timeout
790 Date now
= new Date();
791 if (state
== ParseState
.ESCAPE
) {
792 long escDelay
= now
.getTime() - escapeTime
;
793 if (escDelay
> 250) {
794 // After 0.25 seconds, assume a true escape character
795 events
.add(controlChar((char)0x1B, false));
801 boolean ctrl
= false;
803 boolean shift
= false;
805 // System.err.printf("state: %s ch %c\r\n", state, ch);
811 state
= ParseState
.ESCAPE
;
812 escapeTime
= now
.getTime();
818 events
.add(controlChar(ch
, false));
825 events
.add(new TKeypressEvent(false, 0, ch
,
826 false, false, false));
835 // ALT-Control character
836 events
.add(controlChar(ch
, true));
842 // This will be one of the function keys
843 state
= ParseState
.ESCAPE_INTERMEDIATE
;
847 // '[' goes to CSI_ENTRY
849 state
= ParseState
.CSI_ENTRY
;
853 // Everything else is assumed to be Alt-keystroke
854 if ((ch
>= 'A') && (ch
<= 'Z')) {
858 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
862 case ESCAPE_INTERMEDIATE
:
863 if ((ch
>= 'P') && (ch
<= 'S')) {
867 events
.add(new TKeypressEvent(kbF1
));
870 events
.add(new TKeypressEvent(kbF2
));
873 events
.add(new TKeypressEvent(kbF3
));
876 events
.add(new TKeypressEvent(kbF4
));
885 // Unknown keystroke, ignore
890 // Numbers - parameter values
891 if ((ch
>= '0') && (ch
<= '9')) {
892 params
.set(params
.size() - 1,
893 params
.get(params
.size() - 1) + ch
);
894 state
= ParseState
.CSI_PARAM
;
897 // Parameter separator
903 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
907 if (params
.size() > 1) {
908 if (params
.get(1).equals("2")) {
911 if (params
.get(1).equals("5")) {
914 if (params
.get(1).equals("3")) {
918 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
923 if (params
.size() > 1) {
924 if (params
.get(1).equals("2")) {
927 if (params
.get(1).equals("5")) {
930 if (params
.get(1).equals("3")) {
934 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
939 if (params
.size() > 1) {
940 if (params
.get(1).equals("2")) {
943 if (params
.get(1).equals("5")) {
946 if (params
.get(1).equals("3")) {
950 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
955 if (params
.size() > 1) {
956 if (params
.get(1).equals("2")) {
959 if (params
.get(1).equals("5")) {
962 if (params
.get(1).equals("3")) {
966 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
971 events
.add(new TKeypressEvent(kbHome
));
976 events
.add(new TKeypressEvent(kbEnd
));
980 // CBT - Cursor backward X tab stops (default 1)
981 events
.add(new TKeypressEvent(kbBackTab
));
986 state
= ParseState
.MOUSE
;
989 // Mouse position, SGR (1006) coordinates
990 state
= ParseState
.MOUSE_SGR
;
997 // Unknown keystroke, ignore
1002 // Numbers - parameter values
1003 if ((ch
>= '0') && (ch
<= '9')) {
1004 params
.set(params
.size() - 1,
1005 params
.get(params
.size() - 1) + ch
);
1008 // Parameter separator
1016 // Generate a mouse press event
1017 TInputEvent event
= parseMouseSGR(false);
1018 if (event
!= null) {
1024 // Generate a mouse release event
1025 event
= parseMouseSGR(true);
1026 if (event
!= null) {
1035 // Unknown keystroke, ignore
1040 // Numbers - parameter values
1041 if ((ch
>= '0') && (ch
<= '9')) {
1042 params
.set(params
.size() - 1,
1043 params
.get(params
.size() - 1) + ch
);
1044 state
= ParseState
.CSI_PARAM
;
1047 // Parameter separator
1054 events
.add(csiFnKey());
1059 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1063 if (params
.size() > 1) {
1064 if (params
.get(1).equals("2")) {
1067 if (params
.get(1).equals("5")) {
1070 if (params
.get(1).equals("3")) {
1074 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1079 if (params
.size() > 1) {
1080 if (params
.get(1).equals("2")) {
1083 if (params
.get(1).equals("5")) {
1086 if (params
.get(1).equals("3")) {
1090 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1095 if (params
.size() > 1) {
1096 if (params
.get(1).equals("2")) {
1099 if (params
.get(1).equals("5")) {
1102 if (params
.get(1).equals("3")) {
1106 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1111 if (params
.size() > 1) {
1112 if (params
.get(1).equals("2")) {
1115 if (params
.get(1).equals("5")) {
1118 if (params
.get(1).equals("3")) {
1122 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1130 // Unknown keystroke, ignore
1135 params
.set(0, params
.get(params
.size() - 1) + ch
);
1136 if (params
.get(0).length() == 3) {
1137 // We have enough to generate a mouse event
1138 events
.add(parseMouse());
1147 // This "should" be impossible to reach
1152 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1153 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1156 * @param on if true, enable metaSendsEscape
1157 * @return the string to emit to xterm
1159 private String
xtermMetaSendsEscape(final boolean on
) {
1161 return "\033[?1036h\033[?1034l";
1163 return "\033[?1036l";
1167 * Create a SGR parameter sequence for a single color change. Note
1168 * package private access.
1170 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1171 * @param foreground if true, this is a foreground color
1172 * @return the string to emit to an ANSI / ECMA-style terminal,
1175 String
color(final Color color
, final boolean foreground
) {
1176 return color(color
, foreground
, true);
1180 * Create a SGR parameter sequence for a single color change.
1182 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1183 * @param foreground if true, this is a foreground color
1184 * @param header if true, make the full header, otherwise just emit the
1185 * color parameter e.g. "42;"
1186 * @return the string to emit to an ANSI / ECMA-style terminal,
1189 private String
color(final Color color
, final boolean foreground
,
1190 final boolean header
) {
1192 int ecmaColor
= color
.getValue();
1194 // Convert Color.* values to SGR numerics
1202 return String
.format("\033[%dm", ecmaColor
);
1204 return String
.format("%d;", ecmaColor
);
1209 * Create a SGR parameter sequence for both foreground and background
1210 * color change. Note package private access.
1212 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1213 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1214 * @return the string to emit to an ANSI / ECMA-style terminal,
1215 * e.g. "\033[31;42m"
1217 String
color(final Color foreColor
, final Color backColor
) {
1218 return color(foreColor
, backColor
, true);
1222 * Create a SGR parameter sequence for both foreground and
1223 * background color change.
1225 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1226 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1227 * @param header if true, make the full header, otherwise just emit the
1228 * color parameter e.g. "31;42;"
1229 * @return the string to emit to an ANSI / ECMA-style terminal,
1230 * e.g. "\033[31;42m"
1232 private String
color(final Color foreColor
, final Color backColor
,
1233 final boolean header
) {
1235 int ecmaForeColor
= foreColor
.getValue();
1236 int ecmaBackColor
= backColor
.getValue();
1238 // Convert Color.* values to SGR numerics
1239 ecmaBackColor
+= 40;
1240 ecmaForeColor
+= 30;
1243 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1245 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1250 * Create a SGR parameter sequence for foreground, background, and
1251 * several attributes. This sequence first resets all attributes to
1252 * default, then sets attributes as per the parameters. Note package
1255 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1256 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1257 * @param bold if true, set bold
1258 * @param reverse if true, set reverse
1259 * @param blink if true, set blink
1260 * @param underline if true, set underline
1261 * @return the string to emit to an ANSI / ECMA-style terminal,
1262 * e.g. "\033[0;1;31;42m"
1264 String
color(final Color foreColor
, final Color backColor
,
1265 final boolean bold
, final boolean reverse
, final boolean blink
,
1266 final boolean underline
) {
1268 int ecmaForeColor
= foreColor
.getValue();
1269 int ecmaBackColor
= backColor
.getValue();
1271 // Convert Color.* values to SGR numerics
1272 ecmaBackColor
+= 40;
1273 ecmaForeColor
+= 30;
1275 StringBuilder sb
= new StringBuilder();
1276 if ( bold
&& reverse
&& blink
&& !underline
) {
1277 sb
.append("\033[0;1;7;5;");
1278 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1279 sb
.append("\033[0;1;7;");
1280 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1281 sb
.append("\033[0;7;5;");
1282 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1283 sb
.append("\033[0;1;5;");
1284 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1285 sb
.append("\033[0;1;");
1286 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1287 sb
.append("\033[0;7;");
1288 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1289 sb
.append("\033[0;5;");
1290 } else if ( bold
&& reverse
&& blink
&& underline
) {
1291 sb
.append("\033[0;1;7;5;4;");
1292 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1293 sb
.append("\033[0;1;7;4;");
1294 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1295 sb
.append("\033[0;7;5;4;");
1296 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1297 sb
.append("\033[0;1;5;4;");
1298 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1299 sb
.append("\033[0;1;4;");
1300 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1301 sb
.append("\033[0;7;4;");
1302 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1303 sb
.append("\033[0;5;4;");
1304 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1305 sb
.append("\033[0;4;");
1307 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1308 sb
.append("\033[0;");
1310 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1311 return sb
.toString();
1315 * Create a SGR parameter sequence to reset to defaults. Note package
1318 * @return the string to emit to an ANSI / ECMA-style terminal,
1322 return normal(true);
1326 * Create a SGR parameter sequence to reset to defaults.
1328 * @param header if true, make the full header, otherwise just emit the
1329 * bare parameter e.g. "0;"
1330 * @return the string to emit to an ANSI / ECMA-style terminal,
1333 private String
normal(final boolean header
) {
1335 return "\033[0;37;40m";
1341 * Create a SGR parameter sequence for enabling the visible cursor. Note
1342 * package private access.
1344 * @param on if true, turn on cursor
1345 * @return the string to emit to an ANSI / ECMA-style terminal
1347 String
cursor(final boolean on
) {
1348 if (on
&& !cursorOn
) {
1352 if (!on
&& cursorOn
) {
1360 * Clear the entire screen. Because some terminals use back-color-erase,
1361 * set the color to white-on-black beforehand.
1363 * @return the string to emit to an ANSI / ECMA-style terminal
1365 public String
clearAll() {
1366 return "\033[0;37;40m\033[2J";
1370 * Clear the line from the cursor (inclusive) to the end of the screen.
1371 * Because some terminals use back-color-erase, set the color to
1372 * white-on-black beforehand. Note package private access.
1374 * @return the string to emit to an ANSI / ECMA-style terminal
1376 String
clearRemainingLine() {
1377 return "\033[0;37;40m\033[K";
1381 * Move the cursor to (x, y). Note package private access.
1383 * @param x column coordinate. 0 is the left-most column.
1384 * @param y row coordinate. 0 is the top-most row.
1385 * @return the string to emit to an ANSI / ECMA-style terminal
1387 String
gotoXY(final int x
, final int y
) {
1388 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1392 * Tell (u)xterm that we want to receive mouse events based on "Any event
1393 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1394 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1396 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1398 * Note that this also sets the alternate/primary screen buffer.
1400 * @param on If true, enable mouse report and use the alternate screen
1401 * buffer. If false disable mouse reporting and use the primary screen
1403 * @return the string to emit to xterm
1405 private String
mouse(final boolean on
) {
1407 return "\033[?1003;1005;1006h\033[?1049h";
1409 return "\033[?1003;1006;1005l\033[?1049l";
1413 * Read function runs on a separate thread.
1416 boolean done
= false;
1417 // available() will often return > 1, so we need to read in chunks to
1419 char [] readBuffer
= new char[128];
1420 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1422 while (!done
&& !stopReaderThread
) {
1424 // We assume that if inputStream has bytes available, then
1425 // input won't block on read().
1426 int n
= inputStream
.available();
1428 if (readBuffer
.length
< n
) {
1429 // The buffer wasn't big enough, make it huger
1430 readBuffer
= new char[readBuffer
.length
* 2];
1433 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1434 // System.err.printf("read() %d", rc); System.err.flush();
1439 for (int i
= 0; i
< rc
; i
++) {
1440 int ch
= readBuffer
[i
];
1441 processChar(events
, (char)ch
);
1443 getIdleEvents(events
);
1444 if (events
.size() > 0) {
1445 // Add to the queue for the backend thread to
1446 // be able to obtain.
1447 synchronized (eventQueue
) {
1448 eventQueue
.addAll(events
);
1450 synchronized (listener
) {
1451 listener
.notifyAll();
1457 getIdleEvents(events
);
1458 if (events
.size() > 0) {
1459 synchronized (eventQueue
) {
1460 eventQueue
.addAll(events
);
1463 synchronized (listener
) {
1464 listener
.notifyAll();
1468 // Wait 10 millis for more data
1471 // System.err.println("end while loop"); System.err.flush();
1472 } catch (InterruptedException e
) {
1474 } catch (IOException e
) {
1475 e
.printStackTrace();
1478 } // while ((done == false) && (stopReaderThread == false))
1479 // System.err.println("*** run() exiting..."); System.err.flush();