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
{
112 * Current parsing state.
114 private ParseState state
;
117 * The time we entered ESCAPE. If we get a bare escape without a code
118 * following it, this is used to return that bare escape.
120 private long escapeTime
;
123 * The time we last checked the window size. We try not to spawn stty
124 * more than once per second.
126 private long windowSizeTime
;
129 * true if mouse1 was down. Used to report mouse1 on the release event.
131 private boolean mouse1
;
134 * true if mouse2 was down. Used to report mouse2 on the release event.
136 private boolean mouse2
;
139 * true if mouse3 was down. Used to report mouse3 on the release event.
141 private boolean mouse3
;
144 * Cache the cursor visibility value so we only emit the sequence when we
147 private boolean cursorOn
= true;
150 * Cache the last window size to figure out if a TResizeEvent needs to be
153 private TResizeEvent windowResize
= null;
156 * If true, then we changed System.in and need to change it back.
158 private boolean setRawMode
;
161 * The terminal's input. If an InputStream is not specified in the
162 * constructor, then this InputStreamReader will be bound to System.in
163 * with UTF-8 encoding.
165 private Reader input
;
168 * The terminal's raw InputStream. If an InputStream is not specified in
169 * the constructor, then this InputReader will be bound to System.in.
170 * This is used by run() to see if bytes are available() before calling
171 * (Reader)input.read().
173 private InputStream inputStream
;
176 * The terminal's output. If an OutputStream is not specified in the
177 * constructor, then this PrintWriter will be bound to System.out with
180 private PrintWriter output
;
183 * The listening object that run() wakes up on new input.
185 private Object listener
;
188 * Get the output writer.
192 public PrintWriter
getOutput() {
197 * Check if there are events in the queue.
199 * @return if true, getEvents() has something to return to the backend
201 public boolean hasEvents() {
202 synchronized (eventQueue
) {
203 return (eventQueue
.size() > 0);
208 * Call 'stty' to set cooked mode.
210 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
212 private void sttyCooked() {
217 * Call 'stty' to set raw mode.
219 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
220 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
221 * -parenb cs8 min 1 < /dev/tty'
223 private void sttyRaw() {
228 * Call 'stty' to set raw or cooked mode.
230 * @param mode if true, set raw mode, otherwise set cooked mode
232 private void doStty(final boolean mode
) {
234 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
236 String
[] cmdCooked
= {
237 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
242 process
= Runtime
.getRuntime().exec(cmdRaw
);
244 process
= Runtime
.getRuntime().exec(cmdCooked
);
246 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
247 String line
= in
.readLine();
248 if ((line
!= null) && (line
.length() > 0)) {
249 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
252 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
253 line
= err
.readLine();
254 if ((line
!= null) && (line
.length() > 0)) {
255 System
.err
.println("Error output from stty: " + line
);
260 } catch (InterruptedException e
) {
264 int rc
= process
.exitValue();
266 System
.err
.println("stty returned error code: " + rc
);
268 } catch (IOException e
) {
274 * Constructor sets up state for getEvent().
276 * @param listener the object this backend needs to wake up when new
278 * @param input an InputStream connected to the remote user, or null for
279 * System.in. If System.in is used, then on non-Windows systems it will
280 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
281 * mode. input is always converted to a Reader with UTF-8 encoding.
282 * @param output an OutputStream connected to the remote user, or null
283 * for System.out. output is always converted to a Writer with UTF-8
285 * @throws UnsupportedEncodingException if an exception is thrown when
286 * creating the InputStreamReader
288 public ECMA48Terminal(final Object listener
, final InputStream input
,
289 final OutputStream output
) throws UnsupportedEncodingException
{
295 stopReaderThread
= false;
296 this.listener
= listener
;
299 // inputStream = System.in;
300 inputStream
= new FileInputStream(FileDescriptor
.in
);
306 this.input
= new InputStreamReader(inputStream
, "UTF-8");
308 if (input
instanceof SessionInfo
) {
309 // This is a TelnetInputStream that exposes window size and
310 // environment variables from the telnet layer.
311 sessionInfo
= (SessionInfo
) input
;
313 if (sessionInfo
== null) {
315 // Reading right off the tty
316 sessionInfo
= new TTYSessionInfo();
318 sessionInfo
= new TSessionInfo();
322 if (output
== null) {
323 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
326 this.output
= new PrintWriter(new OutputStreamWriter(output
,
330 // Enable mouse reporting and metaSendsEscape
331 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
334 // Hang onto the window size
335 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
336 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
338 // Spin up the input reader
339 eventQueue
= new LinkedList
<TInputEvent
>();
340 readerThread
= new Thread(this);
341 readerThread
.start();
345 * Constructor sets up state for getEvent().
347 * @param listener the object this backend needs to wake up when new
349 * @param input the InputStream underlying 'reader'. Its available()
350 * method is used to determine if reader.read() will block or not.
351 * @param reader a Reader connected to the remote user.
352 * @param writer a PrintWriter connected to the remote user.
353 * @param setRawMode if true, set System.in into raw mode with stty.
354 * This should in general not be used. It is here solely for Demo3,
355 * which uses System.in.
356 * @throws IllegalArgumentException if input, reader, or writer are null.
358 public ECMA48Terminal(final Object listener
, final InputStream input
,
359 final Reader reader
, final PrintWriter writer
,
360 final boolean setRawMode
) {
363 throw new IllegalArgumentException("InputStream must be specified");
365 if (reader
== null) {
366 throw new IllegalArgumentException("Reader must be specified");
368 if (writer
== null) {
369 throw new IllegalArgumentException("Writer must be specified");
375 stopReaderThread
= false;
376 this.listener
= listener
;
381 if (setRawMode
== true) {
384 this.setRawMode
= setRawMode
;
386 if (input
instanceof SessionInfo
) {
387 // This is a TelnetInputStream that exposes window size and
388 // environment variables from the telnet layer.
389 sessionInfo
= (SessionInfo
) input
;
391 if (sessionInfo
== null) {
392 if (setRawMode
== true) {
393 // Reading right off the tty
394 sessionInfo
= new TTYSessionInfo();
396 sessionInfo
= new TSessionInfo();
400 this.output
= writer
;
402 // Enable mouse reporting and metaSendsEscape
403 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
406 // Hang onto the window size
407 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
408 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
410 // Spin up the input reader
411 eventQueue
= new LinkedList
<TInputEvent
>();
412 readerThread
= new Thread(this);
413 readerThread
.start();
417 * Constructor sets up state for getEvent().
419 * @param listener the object this backend needs to wake up when new
421 * @param input the InputStream underlying 'reader'. Its available()
422 * method is used to determine if reader.read() will block or not.
423 * @param reader a Reader connected to the remote user.
424 * @param writer a PrintWriter connected to the remote user.
425 * @throws IllegalArgumentException if input, reader, or writer are null.
427 public ECMA48Terminal(final Object listener
, final InputStream input
,
428 final Reader reader
, final PrintWriter writer
) {
430 this(listener
, input
, reader
, writer
, false);
434 * Restore terminal to normal state.
436 public void shutdown() {
438 // System.err.println("=== shutdown() ==="); System.err.flush();
440 // Tell the reader thread to stop looking at input
441 stopReaderThread
= true;
444 } catch (InterruptedException e
) {
448 // Disable mouse reporting and show cursor
449 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
455 // We don't close System.in/out
457 // Shut down the streams, this should wake up the reader thread
464 if (output
!= null) {
468 } catch (IOException e
) {
477 public void flush() {
482 * Reset keyboard/mouse input parser.
484 private void reset() {
485 state
= ParseState
.GROUND
;
486 params
= new ArrayList
<String
>();
492 * Produce a control character or one of the special ones (ENTER, TAB,
495 * @param ch Unicode code point
496 * @param alt if true, set alt on the TKeypress
497 * @return one TKeypress event, either a control character (e.g. isKey ==
498 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
501 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
502 // System.err.printf("controlChar: %02x\n", ch);
506 // Carriage return --> ENTER
507 return new TKeypressEvent(kbEnter
, alt
, false, false);
509 // Linefeed --> ENTER
510 return new TKeypressEvent(kbEnter
, alt
, false, false);
513 return new TKeypressEvent(kbEsc
, alt
, false, false);
516 return new TKeypressEvent(kbTab
, alt
, false, false);
518 // Make all other control characters come back as the alphabetic
519 // character with the ctrl field set. So SOH would be 'A' +
521 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
527 * Produce special key from CSI Pn ; Pm ; ... ~
529 * @return one KEYPRESS event representing a special key
531 private TInputEvent
csiFnKey() {
533 if (params
.size() > 0) {
534 key
= Integer
.parseInt(params
.get(0));
537 boolean ctrl
= false;
538 boolean shift
= false;
539 if (params
.size() > 1) {
540 shift
= csiIsShift(params
.get(1));
541 alt
= csiIsAlt(params
.get(1));
542 ctrl
= csiIsCtrl(params
.get(1));
547 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
549 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
551 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
553 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
555 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
557 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
559 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
561 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
563 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
565 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
567 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
569 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
571 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
573 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
581 * Produce mouse events based on "Any event tracking" and UTF-8
583 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
585 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
587 private TInputEvent
parseMouse() {
588 int buttons
= params
.get(0).charAt(0) - 32;
589 int x
= params
.get(0).charAt(1) - 32 - 1;
590 int y
= params
.get(0).charAt(2) - 32 - 1;
592 // Clamp X and Y to the physical screen coordinates.
593 if (x
>= windowResize
.getWidth()) {
594 x
= windowResize
.getWidth() - 1;
596 if (y
>= windowResize
.getHeight()) {
597 y
= windowResize
.getHeight() - 1;
600 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
601 boolean eventMouse1
= false;
602 boolean eventMouse2
= false;
603 boolean eventMouse3
= false;
604 boolean eventMouseWheelUp
= false;
605 boolean eventMouseWheelDown
= false;
607 // System.err.printf("buttons: %04x\r\n", buttons);
624 if (!mouse1
&& !mouse2
&& !mouse3
) {
625 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
627 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
644 // Dragging with mouse1 down
647 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
651 // Dragging with mouse2 down
654 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
658 // Dragging with mouse3 down
661 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
665 // Dragging with mouse2 down after wheelUp
668 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
672 // Dragging with mouse2 down after wheelDown
675 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
679 eventMouseWheelUp
= true;
683 eventMouseWheelDown
= true;
687 // Unknown, just make it motion
688 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
691 return new TMouseEvent(eventType
, x
, y
, x
, y
,
692 eventMouse1
, eventMouse2
, eventMouse3
,
693 eventMouseWheelUp
, eventMouseWheelDown
);
697 * Produce mouse events based on "Any event tracking" and SGR
699 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
701 * @param release if true, this was a release ('m')
702 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
704 private TInputEvent
parseMouseSGR(final boolean release
) {
705 // SGR extended coordinates - mode 1006
706 if (params
.size() < 3) {
707 // Invalid position, bail out.
710 int buttons
= Integer
.parseInt(params
.get(0));
711 int x
= Integer
.parseInt(params
.get(1)) - 1;
712 int y
= Integer
.parseInt(params
.get(2)) - 1;
714 // Clamp X and Y to the physical screen coordinates.
715 if (x
>= windowResize
.getWidth()) {
716 x
= windowResize
.getWidth() - 1;
718 if (y
>= windowResize
.getHeight()) {
719 y
= windowResize
.getHeight() - 1;
722 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
723 boolean eventMouse1
= false;
724 boolean eventMouse2
= false;
725 boolean eventMouse3
= false;
726 boolean eventMouseWheelUp
= false;
727 boolean eventMouseWheelDown
= false;
730 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
744 // Motion only, no buttons down
745 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
749 // Dragging with mouse1 down
751 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
755 // Dragging with mouse2 down
757 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
761 // Dragging with mouse3 down
763 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
767 // Dragging with mouse2 down after wheelUp
769 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
773 // Dragging with mouse2 down after wheelDown
775 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
779 eventMouseWheelUp
= true;
783 eventMouseWheelDown
= true;
790 return new TMouseEvent(eventType
, x
, y
, x
, y
,
791 eventMouse1
, eventMouse2
, eventMouse3
,
792 eventMouseWheelUp
, eventMouseWheelDown
);
796 * Return any events in the IO queue.
798 * @param queue list to append new events to
800 public void getEvents(final List
<TInputEvent
> queue
) {
801 synchronized (eventQueue
) {
802 if (eventQueue
.size() > 0) {
803 synchronized (queue
) {
804 queue
.addAll(eventQueue
);
812 * Return any events in the IO queue due to timeout.
814 * @param queue list to append new events to
816 private void getIdleEvents(final List
<TInputEvent
> queue
) {
817 Date now
= new Date();
819 // Check for new window size
820 long windowSizeDelay
= now
.getTime() - windowSizeTime
;
821 if (windowSizeDelay
> 1000) {
822 sessionInfo
.queryWindowSize();
823 int newWidth
= sessionInfo
.getWindowWidth();
824 int newHeight
= sessionInfo
.getWindowHeight();
825 if ((newWidth
!= windowResize
.getWidth())
826 || (newHeight
!= windowResize
.getHeight())
828 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
829 newWidth
, newHeight
);
830 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
831 newWidth
, newHeight
);
834 windowSizeTime
= now
.getTime();
837 // ESCDELAY type timeout
838 if (state
== ParseState
.ESCAPE
) {
839 long escDelay
= now
.getTime() - escapeTime
;
840 if (escDelay
> 100) {
841 // After 0.1 seconds, assume a true escape character
842 queue
.add(controlChar((char)0x1B, false));
849 * Returns true if the CSI parameter for a keyboard command means that
852 private boolean csiIsShift(final String x
) {
864 * Returns true if the CSI parameter for a keyboard command means that
867 private boolean csiIsAlt(final String x
) {
879 * Returns true if the CSI parameter for a keyboard command means that
882 private boolean csiIsCtrl(final String x
) {
894 * Parses the next character of input to see if an InputEvent is
897 * @param events list to append new events to
898 * @param ch Unicode code point
900 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
902 // ESCDELAY type timeout
903 Date now
= new Date();
904 if (state
== ParseState
.ESCAPE
) {
905 long escDelay
= now
.getTime() - escapeTime
;
906 if (escDelay
> 250) {
907 // After 0.25 seconds, assume a true escape character
908 events
.add(controlChar((char)0x1B, false));
914 boolean ctrl
= false;
916 boolean shift
= false;
918 // System.err.printf("state: %s ch %c\r\n", state, ch);
924 state
= ParseState
.ESCAPE
;
925 escapeTime
= now
.getTime();
931 events
.add(controlChar(ch
, false));
938 events
.add(new TKeypressEvent(false, 0, ch
,
939 false, false, false));
948 // ALT-Control character
949 events
.add(controlChar(ch
, true));
955 // This will be one of the function keys
956 state
= ParseState
.ESCAPE_INTERMEDIATE
;
960 // '[' goes to CSI_ENTRY
962 state
= ParseState
.CSI_ENTRY
;
966 // Everything else is assumed to be Alt-keystroke
967 if ((ch
>= 'A') && (ch
<= 'Z')) {
971 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
975 case ESCAPE_INTERMEDIATE
:
976 if ((ch
>= 'P') && (ch
<= 'S')) {
980 events
.add(new TKeypressEvent(kbF1
));
983 events
.add(new TKeypressEvent(kbF2
));
986 events
.add(new TKeypressEvent(kbF3
));
989 events
.add(new TKeypressEvent(kbF4
));
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
);
1007 state
= ParseState
.CSI_PARAM
;
1010 // Parameter separator
1016 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1020 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1025 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1030 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1035 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1040 events
.add(new TKeypressEvent(kbHome
));
1045 events
.add(new TKeypressEvent(kbEnd
));
1049 // CBT - Cursor backward X tab stops (default 1)
1050 events
.add(new TKeypressEvent(kbBackTab
));
1055 state
= ParseState
.MOUSE
;
1058 // Mouse position, SGR (1006) coordinates
1059 state
= ParseState
.MOUSE_SGR
;
1066 // Unknown keystroke, ignore
1071 // Numbers - parameter values
1072 if ((ch
>= '0') && (ch
<= '9')) {
1073 params
.set(params
.size() - 1,
1074 params
.get(params
.size() - 1) + ch
);
1077 // Parameter separator
1085 // Generate a mouse press event
1086 TInputEvent event
= parseMouseSGR(false);
1087 if (event
!= null) {
1093 // Generate a mouse release event
1094 event
= parseMouseSGR(true);
1095 if (event
!= null) {
1104 // Unknown keystroke, ignore
1109 // Numbers - parameter values
1110 if ((ch
>= '0') && (ch
<= '9')) {
1111 params
.set(params
.size() - 1,
1112 params
.get(params
.size() - 1) + ch
);
1113 state
= ParseState
.CSI_PARAM
;
1116 // Parameter separator
1123 events
.add(csiFnKey());
1128 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1132 if (params
.size() > 1) {
1133 shift
= csiIsShift(params
.get(1));
1134 alt
= csiIsAlt(params
.get(1));
1135 ctrl
= csiIsCtrl(params
.get(1));
1137 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1142 if (params
.size() > 1) {
1143 shift
= csiIsShift(params
.get(1));
1144 alt
= csiIsAlt(params
.get(1));
1145 ctrl
= csiIsCtrl(params
.get(1));
1147 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1152 if (params
.size() > 1) {
1153 shift
= csiIsShift(params
.get(1));
1154 alt
= csiIsAlt(params
.get(1));
1155 ctrl
= csiIsCtrl(params
.get(1));
1157 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1162 if (params
.size() > 1) {
1163 shift
= csiIsShift(params
.get(1));
1164 alt
= csiIsAlt(params
.get(1));
1165 ctrl
= csiIsCtrl(params
.get(1));
1167 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1172 if (params
.size() > 1) {
1173 shift
= csiIsShift(params
.get(1));
1174 alt
= csiIsAlt(params
.get(1));
1175 ctrl
= csiIsCtrl(params
.get(1));
1177 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
1182 if (params
.size() > 1) {
1183 shift
= csiIsShift(params
.get(1));
1184 alt
= csiIsAlt(params
.get(1));
1185 ctrl
= csiIsCtrl(params
.get(1));
1187 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
1195 // Unknown keystroke, ignore
1200 params
.set(0, params
.get(params
.size() - 1) + ch
);
1201 if (params
.get(0).length() == 3) {
1202 // We have enough to generate a mouse event
1203 events
.add(parseMouse());
1212 // This "should" be impossible to reach
1217 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1218 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1221 * @param on if true, enable metaSendsEscape
1222 * @return the string to emit to xterm
1224 private String
xtermMetaSendsEscape(final boolean on
) {
1226 return "\033[?1036h\033[?1034l";
1228 return "\033[?1036l";
1232 * Create an xterm OSC sequence to change the window title. Note package
1235 * @param title the new title
1236 * @return the string to emit to xterm
1238 String
setTitle(final String title
) {
1239 return "\033]2;" + title
+ "\007";
1243 * Create a SGR parameter sequence for a single color change. Note
1244 * package private access.
1246 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1247 * @param foreground if true, this is a foreground color
1248 * @return the string to emit to an ANSI / ECMA-style terminal,
1251 String
color(final Color color
, final boolean foreground
) {
1252 return color(color
, foreground
, true);
1256 * Create a SGR parameter sequence for a single color change.
1258 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1259 * @param foreground if true, this is a foreground color
1260 * @param header if true, make the full header, otherwise just emit the
1261 * color parameter e.g. "42;"
1262 * @return the string to emit to an ANSI / ECMA-style terminal,
1265 private String
color(final Color color
, final boolean foreground
,
1266 final boolean header
) {
1268 int ecmaColor
= color
.getValue();
1270 // Convert Color.* values to SGR numerics
1278 return String
.format("\033[%dm", ecmaColor
);
1280 return String
.format("%d;", ecmaColor
);
1285 * Create a SGR parameter sequence for both foreground and background
1286 * color change. Note package private access.
1288 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1289 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1290 * @return the string to emit to an ANSI / ECMA-style terminal,
1291 * e.g. "\033[31;42m"
1293 String
color(final Color foreColor
, final Color backColor
) {
1294 return color(foreColor
, backColor
, true);
1298 * Create a SGR parameter sequence for both foreground and
1299 * background color change.
1301 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1302 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1303 * @param header if true, make the full header, otherwise just emit the
1304 * color parameter e.g. "31;42;"
1305 * @return the string to emit to an ANSI / ECMA-style terminal,
1306 * e.g. "\033[31;42m"
1308 private String
color(final Color foreColor
, final Color backColor
,
1309 final boolean header
) {
1311 int ecmaForeColor
= foreColor
.getValue();
1312 int ecmaBackColor
= backColor
.getValue();
1314 // Convert Color.* values to SGR numerics
1315 ecmaBackColor
+= 40;
1316 ecmaForeColor
+= 30;
1319 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1321 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1326 * Create a SGR parameter sequence for foreground, background, and
1327 * several attributes. This sequence first resets all attributes to
1328 * default, then sets attributes as per the parameters. Note package
1331 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1332 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1333 * @param bold if true, set bold
1334 * @param reverse if true, set reverse
1335 * @param blink if true, set blink
1336 * @param underline if true, set underline
1337 * @return the string to emit to an ANSI / ECMA-style terminal,
1338 * e.g. "\033[0;1;31;42m"
1340 String
color(final Color foreColor
, final Color backColor
,
1341 final boolean bold
, final boolean reverse
, final boolean blink
,
1342 final boolean underline
) {
1344 int ecmaForeColor
= foreColor
.getValue();
1345 int ecmaBackColor
= backColor
.getValue();
1347 // Convert Color.* values to SGR numerics
1348 ecmaBackColor
+= 40;
1349 ecmaForeColor
+= 30;
1351 StringBuilder sb
= new StringBuilder();
1352 if ( bold
&& reverse
&& blink
&& !underline
) {
1353 sb
.append("\033[0;1;7;5;");
1354 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1355 sb
.append("\033[0;1;7;");
1356 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1357 sb
.append("\033[0;7;5;");
1358 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1359 sb
.append("\033[0;1;5;");
1360 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1361 sb
.append("\033[0;1;");
1362 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1363 sb
.append("\033[0;7;");
1364 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1365 sb
.append("\033[0;5;");
1366 } else if ( bold
&& reverse
&& blink
&& underline
) {
1367 sb
.append("\033[0;1;7;5;4;");
1368 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1369 sb
.append("\033[0;1;7;4;");
1370 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1371 sb
.append("\033[0;7;5;4;");
1372 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1373 sb
.append("\033[0;1;5;4;");
1374 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1375 sb
.append("\033[0;1;4;");
1376 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1377 sb
.append("\033[0;7;4;");
1378 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1379 sb
.append("\033[0;5;4;");
1380 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1381 sb
.append("\033[0;4;");
1383 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1384 sb
.append("\033[0;");
1386 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1387 return sb
.toString();
1391 * Create a SGR parameter sequence to reset to defaults. Note package
1394 * @return the string to emit to an ANSI / ECMA-style terminal,
1398 return normal(true);
1402 * Create a SGR parameter sequence to reset to defaults.
1404 * @param header if true, make the full header, otherwise just emit the
1405 * bare parameter e.g. "0;"
1406 * @return the string to emit to an ANSI / ECMA-style terminal,
1409 private String
normal(final boolean header
) {
1411 return "\033[0;37;40m";
1417 * Create a SGR parameter sequence for enabling the visible cursor. Note
1418 * package private access.
1420 * @param on if true, turn on cursor
1421 * @return the string to emit to an ANSI / ECMA-style terminal
1423 String
cursor(final boolean on
) {
1424 if (on
&& !cursorOn
) {
1428 if (!on
&& cursorOn
) {
1436 * Clear the entire screen. Because some terminals use back-color-erase,
1437 * set the color to white-on-black beforehand.
1439 * @return the string to emit to an ANSI / ECMA-style terminal
1441 public String
clearAll() {
1442 return "\033[0;37;40m\033[2J";
1446 * Clear the line from the cursor (inclusive) to the end of the screen.
1447 * Because some terminals use back-color-erase, set the color to
1448 * white-on-black beforehand. Note package private access.
1450 * @return the string to emit to an ANSI / ECMA-style terminal
1452 String
clearRemainingLine() {
1453 return "\033[0;37;40m\033[K";
1457 * Move the cursor to (x, y). Note package private access.
1459 * @param x column coordinate. 0 is the left-most column.
1460 * @param y row coordinate. 0 is the top-most row.
1461 * @return the string to emit to an ANSI / ECMA-style terminal
1463 String
gotoXY(final int x
, final int y
) {
1464 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1468 * Tell (u)xterm that we want to receive mouse events based on "Any event
1469 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1470 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1472 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1474 * Note that this also sets the alternate/primary screen buffer.
1476 * @param on If true, enable mouse report and use the alternate screen
1477 * buffer. If false disable mouse reporting and use the primary screen
1479 * @return the string to emit to xterm
1481 private String
mouse(final boolean on
) {
1483 return "\033[?1002;1003;1005;1006h\033[?1049h";
1485 return "\033[?1002;1003;1006;1005l\033[?1049l";
1489 * Read function runs on a separate thread.
1492 boolean done
= false;
1493 // available() will often return > 1, so we need to read in chunks to
1495 char [] readBuffer
= new char[128];
1496 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1498 while (!done
&& !stopReaderThread
) {
1500 // We assume that if inputStream has bytes available, then
1501 // input won't block on read().
1502 int n
= inputStream
.available();
1504 if (readBuffer
.length
< n
) {
1505 // The buffer wasn't big enough, make it huger
1506 readBuffer
= new char[readBuffer
.length
* 2];
1509 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1510 // System.err.printf("read() %d", rc); System.err.flush();
1515 for (int i
= 0; i
< rc
; i
++) {
1516 int ch
= readBuffer
[i
];
1517 processChar(events
, (char)ch
);
1519 getIdleEvents(events
);
1520 if (events
.size() > 0) {
1521 // Add to the queue for the backend thread to
1522 // be able to obtain.
1523 synchronized (eventQueue
) {
1524 eventQueue
.addAll(events
);
1526 synchronized (listener
) {
1527 listener
.notifyAll();
1533 getIdleEvents(events
);
1534 if (events
.size() > 0) {
1535 synchronized (eventQueue
) {
1536 eventQueue
.addAll(events
);
1539 synchronized (listener
) {
1540 listener
.notifyAll();
1544 // Wait 10 millis for more data
1547 // System.err.println("end while loop"); System.err.flush();
1548 } catch (InterruptedException e
) {
1550 } catch (IOException e
) {
1551 e
.printStackTrace();
1554 } // while ((done == false) && (stopReaderThread == false))
1555 // System.err.println("*** run() exiting..."); System.err.flush();