2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2017 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 * If true, emit T.416-style RGB colors. This is a) expensive in
65 * bandwidth, and b) potentially terrible looking for non-xterms.
67 private static boolean doRgbColor
= false;
70 * The session information.
72 private SessionInfo sessionInfo
;
75 * Getter for sessionInfo.
77 * @return the SessionInfo
79 public SessionInfo
getSessionInfo() {
84 * The event queue, filled up by a thread reading on input.
86 private List
<TInputEvent
> eventQueue
;
89 * If true, we want the reader thread to exit gracefully.
91 private boolean stopReaderThread
;
96 private Thread readerThread
;
99 * Parameters being collected. E.g. if the string is \033[1;3m, then
100 * params[0] will be 1 and params[1] will be 3.
102 private ArrayList
<String
> params
;
105 * States in the input parser.
107 private enum ParseState
{
118 * Current parsing state.
120 private ParseState state
;
123 * The time we entered ESCAPE. If we get a bare escape without a code
124 * following it, this is used to return that bare escape.
126 private long escapeTime
;
129 * The time we last checked the window size. We try not to spawn stty
130 * more than once per second.
132 private long windowSizeTime
;
135 * true if mouse1 was down. Used to report mouse1 on the release event.
137 private boolean mouse1
;
140 * true if mouse2 was down. Used to report mouse2 on the release event.
142 private boolean mouse2
;
145 * true if mouse3 was down. Used to report mouse3 on the release event.
147 private boolean mouse3
;
150 * Cache the cursor visibility value so we only emit the sequence when we
153 private boolean cursorOn
= true;
156 * Cache the last window size to figure out if a TResizeEvent needs to be
159 private TResizeEvent windowResize
= null;
162 * If true, then we changed System.in and need to change it back.
164 private boolean setRawMode
;
167 * The terminal's input. If an InputStream is not specified in the
168 * constructor, then this InputStreamReader will be bound to System.in
169 * with UTF-8 encoding.
171 private Reader input
;
174 * The terminal's raw InputStream. If an InputStream is not specified in
175 * the constructor, then this InputReader will be bound to System.in.
176 * This is used by run() to see if bytes are available() before calling
177 * (Reader)input.read().
179 private InputStream inputStream
;
182 * The terminal's output. If an OutputStream is not specified in the
183 * constructor, then this PrintWriter will be bound to System.out with
186 private PrintWriter output
;
189 * The listening object that run() wakes up on new input.
191 private Object listener
;
194 * Get the output writer.
198 public PrintWriter
getOutput() {
203 * Check if there are events in the queue.
205 * @return if true, getEvents() has something to return to the backend
207 public boolean hasEvents() {
208 synchronized (eventQueue
) {
209 return (eventQueue
.size() > 0);
214 * Call 'stty' to set cooked mode.
216 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
218 private void sttyCooked() {
223 * Call 'stty' to set raw mode.
225 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
226 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
227 * -parenb cs8 min 1 < /dev/tty'
229 private void sttyRaw() {
234 * Call 'stty' to set raw or cooked mode.
236 * @param mode if true, set raw mode, otherwise set cooked mode
238 private void doStty(final boolean mode
) {
240 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
242 String
[] cmdCooked
= {
243 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
248 process
= Runtime
.getRuntime().exec(cmdRaw
);
250 process
= Runtime
.getRuntime().exec(cmdCooked
);
252 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
253 String line
= in
.readLine();
254 if ((line
!= null) && (line
.length() > 0)) {
255 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
258 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
259 line
= err
.readLine();
260 if ((line
!= null) && (line
.length() > 0)) {
261 System
.err
.println("Error output from stty: " + line
);
266 } catch (InterruptedException e
) {
270 int rc
= process
.exitValue();
272 System
.err
.println("stty returned error code: " + rc
);
274 } catch (IOException e
) {
280 * Constructor sets up state for getEvent().
282 * @param listener the object this backend needs to wake up when new
284 * @param input an InputStream connected to the remote user, or null for
285 * System.in. If System.in is used, then on non-Windows systems it will
286 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
287 * mode. input is always converted to a Reader with UTF-8 encoding.
288 * @param output an OutputStream connected to the remote user, or null
289 * for System.out. output is always converted to a Writer with UTF-8
291 * @throws UnsupportedEncodingException if an exception is thrown when
292 * creating the InputStreamReader
294 public ECMA48Terminal(final Object listener
, final InputStream input
,
295 final OutputStream output
) throws UnsupportedEncodingException
{
301 stopReaderThread
= false;
302 this.listener
= listener
;
305 // inputStream = System.in;
306 inputStream
= new FileInputStream(FileDescriptor
.in
);
312 this.input
= new InputStreamReader(inputStream
, "UTF-8");
314 if (input
instanceof SessionInfo
) {
315 // This is a TelnetInputStream that exposes window size and
316 // environment variables from the telnet layer.
317 sessionInfo
= (SessionInfo
) input
;
319 if (sessionInfo
== null) {
321 // Reading right off the tty
322 sessionInfo
= new TTYSessionInfo();
324 sessionInfo
= new TSessionInfo();
328 if (output
== null) {
329 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
332 this.output
= new PrintWriter(new OutputStreamWriter(output
,
336 // Enable mouse reporting and metaSendsEscape
337 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
340 // Hang onto the window size
341 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
342 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
344 // Permit RGB colors only if externally requested
345 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
346 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
351 // Spin up the input reader
352 eventQueue
= new LinkedList
<TInputEvent
>();
353 readerThread
= new Thread(this);
354 readerThread
.start();
358 * Constructor sets up state for getEvent().
360 * @param listener the object this backend needs to wake up when new
362 * @param input the InputStream underlying 'reader'. Its available()
363 * method is used to determine if reader.read() will block or not.
364 * @param reader a Reader connected to the remote user.
365 * @param writer a PrintWriter connected to the remote user.
366 * @param setRawMode if true, set System.in into raw mode with stty.
367 * This should in general not be used. It is here solely for Demo3,
368 * which uses System.in.
369 * @throws IllegalArgumentException if input, reader, or writer are null.
371 public ECMA48Terminal(final Object listener
, final InputStream input
,
372 final Reader reader
, final PrintWriter writer
,
373 final boolean setRawMode
) {
376 throw new IllegalArgumentException("InputStream must be specified");
378 if (reader
== null) {
379 throw new IllegalArgumentException("Reader must be specified");
381 if (writer
== null) {
382 throw new IllegalArgumentException("Writer must be specified");
388 stopReaderThread
= false;
389 this.listener
= listener
;
394 if (setRawMode
== true) {
397 this.setRawMode
= setRawMode
;
399 if (input
instanceof SessionInfo
) {
400 // This is a TelnetInputStream that exposes window size and
401 // environment variables from the telnet layer.
402 sessionInfo
= (SessionInfo
) input
;
404 if (sessionInfo
== null) {
405 if (setRawMode
== true) {
406 // Reading right off the tty
407 sessionInfo
= new TTYSessionInfo();
409 sessionInfo
= new TSessionInfo();
413 this.output
= writer
;
415 // Enable mouse reporting and metaSendsEscape
416 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
419 // Hang onto the window size
420 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
421 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
423 // Permit RGB colors only if externally requested
424 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
425 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
430 // Spin up the input reader
431 eventQueue
= new LinkedList
<TInputEvent
>();
432 readerThread
= new Thread(this);
433 readerThread
.start();
437 * Constructor sets up state for getEvent().
439 * @param listener the object this backend needs to wake up when new
441 * @param input the InputStream underlying 'reader'. Its available()
442 * method is used to determine if reader.read() will block or not.
443 * @param reader a Reader connected to the remote user.
444 * @param writer a PrintWriter connected to the remote user.
445 * @throws IllegalArgumentException if input, reader, or writer are null.
447 public ECMA48Terminal(final Object listener
, final InputStream input
,
448 final Reader reader
, final PrintWriter writer
) {
450 this(listener
, input
, reader
, writer
, false);
454 * Restore terminal to normal state.
456 public void shutdown() {
458 // System.err.println("=== shutdown() ==="); System.err.flush();
460 // Tell the reader thread to stop looking at input
461 stopReaderThread
= true;
464 } catch (InterruptedException e
) {
468 // Disable mouse reporting and show cursor
469 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
475 // We don't close System.in/out
477 // Shut down the streams, this should wake up the reader thread
484 if (output
!= null) {
488 } catch (IOException e
) {
497 public void flush() {
502 * Reset keyboard/mouse input parser.
504 private void reset() {
505 state
= ParseState
.GROUND
;
506 params
= new ArrayList
<String
>();
512 * Produce a control character or one of the special ones (ENTER, TAB,
515 * @param ch Unicode code point
516 * @param alt if true, set alt on the TKeypress
517 * @return one TKeypress event, either a control character (e.g. isKey ==
518 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
521 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
522 // System.err.printf("controlChar: %02x\n", ch);
526 // Carriage return --> ENTER
527 return new TKeypressEvent(kbEnter
, alt
, false, false);
529 // Linefeed --> ENTER
530 return new TKeypressEvent(kbEnter
, alt
, false, false);
533 return new TKeypressEvent(kbEsc
, alt
, false, false);
536 return new TKeypressEvent(kbTab
, alt
, false, false);
538 // Make all other control characters come back as the alphabetic
539 // character with the ctrl field set. So SOH would be 'A' +
541 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
547 * Produce special key from CSI Pn ; Pm ; ... ~
549 * @return one KEYPRESS event representing a special key
551 private TInputEvent
csiFnKey() {
553 if (params
.size() > 0) {
554 key
= Integer
.parseInt(params
.get(0));
557 boolean ctrl
= false;
558 boolean shift
= false;
559 if (params
.size() > 1) {
560 shift
= csiIsShift(params
.get(1));
561 alt
= csiIsAlt(params
.get(1));
562 ctrl
= csiIsCtrl(params
.get(1));
567 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
569 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
571 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
573 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
575 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
577 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
579 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
581 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
583 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
585 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
587 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
589 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
591 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
593 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
601 * Produce mouse events based on "Any event tracking" and UTF-8
603 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
605 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
607 private TInputEvent
parseMouse() {
608 int buttons
= params
.get(0).charAt(0) - 32;
609 int x
= params
.get(0).charAt(1) - 32 - 1;
610 int y
= params
.get(0).charAt(2) - 32 - 1;
612 // Clamp X and Y to the physical screen coordinates.
613 if (x
>= windowResize
.getWidth()) {
614 x
= windowResize
.getWidth() - 1;
616 if (y
>= windowResize
.getHeight()) {
617 y
= windowResize
.getHeight() - 1;
620 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
621 boolean eventMouse1
= false;
622 boolean eventMouse2
= false;
623 boolean eventMouse3
= false;
624 boolean eventMouseWheelUp
= false;
625 boolean eventMouseWheelDown
= false;
627 // System.err.printf("buttons: %04x\r\n", buttons);
644 if (!mouse1
&& !mouse2
&& !mouse3
) {
645 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
647 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
664 // Dragging with mouse1 down
667 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
671 // Dragging with mouse2 down
674 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
678 // Dragging with mouse3 down
681 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
685 // Dragging with mouse2 down after wheelUp
688 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
692 // Dragging with mouse2 down after wheelDown
695 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
699 eventMouseWheelUp
= true;
703 eventMouseWheelDown
= true;
707 // Unknown, just make it motion
708 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
711 return new TMouseEvent(eventType
, x
, y
, x
, y
,
712 eventMouse1
, eventMouse2
, eventMouse3
,
713 eventMouseWheelUp
, eventMouseWheelDown
);
717 * Produce mouse events based on "Any event tracking" and SGR
719 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
721 * @param release if true, this was a release ('m')
722 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
724 private TInputEvent
parseMouseSGR(final boolean release
) {
725 // SGR extended coordinates - mode 1006
726 if (params
.size() < 3) {
727 // Invalid position, bail out.
730 int buttons
= Integer
.parseInt(params
.get(0));
731 int x
= Integer
.parseInt(params
.get(1)) - 1;
732 int y
= Integer
.parseInt(params
.get(2)) - 1;
734 // Clamp X and Y to the physical screen coordinates.
735 if (x
>= windowResize
.getWidth()) {
736 x
= windowResize
.getWidth() - 1;
738 if (y
>= windowResize
.getHeight()) {
739 y
= windowResize
.getHeight() - 1;
742 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
743 boolean eventMouse1
= false;
744 boolean eventMouse2
= false;
745 boolean eventMouse3
= false;
746 boolean eventMouseWheelUp
= false;
747 boolean eventMouseWheelDown
= false;
750 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
764 // Motion only, no buttons down
765 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
769 // Dragging with mouse1 down
771 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
775 // Dragging with mouse2 down
777 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
781 // Dragging with mouse3 down
783 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
787 // Dragging with mouse2 down after wheelUp
789 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
793 // Dragging with mouse2 down after wheelDown
795 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
799 eventMouseWheelUp
= true;
803 eventMouseWheelDown
= true;
810 return new TMouseEvent(eventType
, x
, y
, x
, y
,
811 eventMouse1
, eventMouse2
, eventMouse3
,
812 eventMouseWheelUp
, eventMouseWheelDown
);
816 * Return any events in the IO queue.
818 * @param queue list to append new events to
820 public void getEvents(final List
<TInputEvent
> queue
) {
821 synchronized (eventQueue
) {
822 if (eventQueue
.size() > 0) {
823 synchronized (queue
) {
824 queue
.addAll(eventQueue
);
832 * Return any events in the IO queue due to timeout.
834 * @param queue list to append new events to
836 private void getIdleEvents(final List
<TInputEvent
> queue
) {
837 Date now
= new Date();
839 // Check for new window size
840 long windowSizeDelay
= now
.getTime() - windowSizeTime
;
841 if (windowSizeDelay
> 1000) {
842 sessionInfo
.queryWindowSize();
843 int newWidth
= sessionInfo
.getWindowWidth();
844 int newHeight
= sessionInfo
.getWindowHeight();
845 if ((newWidth
!= windowResize
.getWidth())
846 || (newHeight
!= windowResize
.getHeight())
848 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
849 newWidth
, newHeight
);
850 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
851 newWidth
, newHeight
);
854 windowSizeTime
= now
.getTime();
857 // ESCDELAY type timeout
858 if (state
== ParseState
.ESCAPE
) {
859 long escDelay
= now
.getTime() - escapeTime
;
860 if (escDelay
> 100) {
861 // After 0.1 seconds, assume a true escape character
862 queue
.add(controlChar((char)0x1B, false));
869 * Returns true if the CSI parameter for a keyboard command means that
872 private boolean csiIsShift(final String x
) {
884 * Returns true if the CSI parameter for a keyboard command means that
887 private boolean csiIsAlt(final String x
) {
899 * Returns true if the CSI parameter for a keyboard command means that
902 private boolean csiIsCtrl(final String x
) {
914 * Parses the next character of input to see if an InputEvent is
917 * @param events list to append new events to
918 * @param ch Unicode code point
920 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
922 // ESCDELAY type timeout
923 Date now
= new Date();
924 if (state
== ParseState
.ESCAPE
) {
925 long escDelay
= now
.getTime() - escapeTime
;
926 if (escDelay
> 250) {
927 // After 0.25 seconds, assume a true escape character
928 events
.add(controlChar((char)0x1B, false));
934 boolean ctrl
= false;
936 boolean shift
= false;
938 // System.err.printf("state: %s ch %c\r\n", state, ch);
944 state
= ParseState
.ESCAPE
;
945 escapeTime
= now
.getTime();
951 events
.add(controlChar(ch
, false));
958 events
.add(new TKeypressEvent(false, 0, ch
,
959 false, false, false));
968 // ALT-Control character
969 events
.add(controlChar(ch
, true));
975 // This will be one of the function keys
976 state
= ParseState
.ESCAPE_INTERMEDIATE
;
980 // '[' goes to CSI_ENTRY
982 state
= ParseState
.CSI_ENTRY
;
986 // Everything else is assumed to be Alt-keystroke
987 if ((ch
>= 'A') && (ch
<= 'Z')) {
991 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
995 case ESCAPE_INTERMEDIATE
:
996 if ((ch
>= 'P') && (ch
<= 'S')) {
1000 events
.add(new TKeypressEvent(kbF1
));
1003 events
.add(new TKeypressEvent(kbF2
));
1006 events
.add(new TKeypressEvent(kbF3
));
1009 events
.add(new TKeypressEvent(kbF4
));
1018 // Unknown keystroke, ignore
1023 // Numbers - parameter values
1024 if ((ch
>= '0') && (ch
<= '9')) {
1025 params
.set(params
.size() - 1,
1026 params
.get(params
.size() - 1) + ch
);
1027 state
= ParseState
.CSI_PARAM
;
1030 // Parameter separator
1036 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1040 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1045 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1050 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1055 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1060 events
.add(new TKeypressEvent(kbHome
));
1065 events
.add(new TKeypressEvent(kbEnd
));
1069 // CBT - Cursor backward X tab stops (default 1)
1070 events
.add(new TKeypressEvent(kbBackTab
));
1075 state
= ParseState
.MOUSE
;
1078 // Mouse position, SGR (1006) coordinates
1079 state
= ParseState
.MOUSE_SGR
;
1086 // Unknown keystroke, ignore
1091 // Numbers - parameter values
1092 if ((ch
>= '0') && (ch
<= '9')) {
1093 params
.set(params
.size() - 1,
1094 params
.get(params
.size() - 1) + ch
);
1097 // Parameter separator
1105 // Generate a mouse press event
1106 TInputEvent event
= parseMouseSGR(false);
1107 if (event
!= null) {
1113 // Generate a mouse release event
1114 event
= parseMouseSGR(true);
1115 if (event
!= null) {
1124 // Unknown keystroke, ignore
1129 // Numbers - parameter values
1130 if ((ch
>= '0') && (ch
<= '9')) {
1131 params
.set(params
.size() - 1,
1132 params
.get(params
.size() - 1) + ch
);
1133 state
= ParseState
.CSI_PARAM
;
1136 // Parameter separator
1143 events
.add(csiFnKey());
1148 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
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(kbUp
, 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(kbDown
, 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(kbRight
, 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(kbLeft
, alt
, ctrl
, shift
));
1192 if (params
.size() > 1) {
1193 shift
= csiIsShift(params
.get(1));
1194 alt
= csiIsAlt(params
.get(1));
1195 ctrl
= csiIsCtrl(params
.get(1));
1197 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
1202 if (params
.size() > 1) {
1203 shift
= csiIsShift(params
.get(1));
1204 alt
= csiIsAlt(params
.get(1));
1205 ctrl
= csiIsCtrl(params
.get(1));
1207 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
1215 // Unknown keystroke, ignore
1220 params
.set(0, params
.get(params
.size() - 1) + ch
);
1221 if (params
.get(0).length() == 3) {
1222 // We have enough to generate a mouse event
1223 events
.add(parseMouse());
1232 // This "should" be impossible to reach
1237 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1238 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1241 * @param on if true, enable metaSendsEscape
1242 * @return the string to emit to xterm
1244 private String
xtermMetaSendsEscape(final boolean on
) {
1246 return "\033[?1036h\033[?1034l";
1248 return "\033[?1036l";
1252 * Create an xterm OSC sequence to change the window title. Note package
1255 * @param title the new title
1256 * @return the string to emit to xterm
1258 String
setTitle(final String title
) {
1259 return "\033]2;" + title
+ "\007";
1263 * Create a SGR parameter sequence for a single color change. Note
1264 * package private access.
1266 * @param bold if true, set bold
1267 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1268 * @param foreground if true, this is a foreground color
1269 * @return the string to emit to an ANSI / ECMA-style terminal,
1272 String
color(final boolean bold
, final Color color
,
1273 final boolean foreground
) {
1274 return color(color
, foreground
, true) +
1275 rgbColor(bold
, color
, foreground
);
1279 * Create a T.416 RGB parameter sequence for a single color change.
1281 * @param bold if true, set bold
1282 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1283 * @param foreground if true, this is a foreground color
1284 * @return the string to emit to an xterm terminal with RGB support,
1285 * e.g. "\033[38;2;RR;GG;BBm"
1287 private String
rgbColor(final boolean bold
, final Color color
,
1288 final boolean foreground
) {
1289 if (doRgbColor
== false) {
1292 StringBuilder sb
= new StringBuilder("\033[");
1294 // Bold implies foreground only
1296 if (color
.equals(Color
.BLACK
)) {
1297 sb
.append("84;84;84");
1298 } else if (color
.equals(Color
.RED
)) {
1299 sb
.append("252;84;84");
1300 } else if (color
.equals(Color
.GREEN
)) {
1301 sb
.append("84;252;84");
1302 } else if (color
.equals(Color
.YELLOW
)) {
1303 sb
.append("252;252;84");
1304 } else if (color
.equals(Color
.BLUE
)) {
1305 sb
.append("84;84;252");
1306 } else if (color
.equals(Color
.MAGENTA
)) {
1307 sb
.append("252;84;252");
1308 } else if (color
.equals(Color
.CYAN
)) {
1309 sb
.append("84;252;252");
1310 } else if (color
.equals(Color
.WHITE
)) {
1311 sb
.append("252;252;252");
1319 if (color
.equals(Color
.BLACK
)) {
1321 } else if (color
.equals(Color
.RED
)) {
1322 sb
.append("168;0;0");
1323 } else if (color
.equals(Color
.GREEN
)) {
1324 sb
.append("0;168;0");
1325 } else if (color
.equals(Color
.YELLOW
)) {
1326 sb
.append("168;84;0");
1327 } else if (color
.equals(Color
.BLUE
)) {
1328 sb
.append("0;0;168");
1329 } else if (color
.equals(Color
.MAGENTA
)) {
1330 sb
.append("168;0;168");
1331 } else if (color
.equals(Color
.CYAN
)) {
1332 sb
.append("0;168;168");
1333 } else if (color
.equals(Color
.WHITE
)) {
1334 sb
.append("168;168;168");
1338 return sb
.toString();
1342 * Create a T.416 RGB parameter sequence for both foreground and
1343 * background color change.
1345 * @param bold if true, set bold
1346 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1347 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1348 * @return the string to emit to an xterm terminal with RGB support,
1349 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
1351 private String
rgbColor(final boolean bold
, final Color foreColor
,
1352 final Color backColor
) {
1353 if (doRgbColor
== false) {
1357 return rgbColor(bold
, foreColor
, true) +
1358 rgbColor(false, backColor
, false);
1362 * Create a SGR parameter sequence for a single color change.
1364 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1365 * @param foreground if true, this is a foreground color
1366 * @param header if true, make the full header, otherwise just emit the
1367 * color parameter e.g. "42;"
1368 * @return the string to emit to an ANSI / ECMA-style terminal,
1371 private String
color(final Color color
, final boolean foreground
,
1372 final boolean header
) {
1374 int ecmaColor
= color
.getValue();
1376 // Convert Color.* values to SGR numerics
1384 return String
.format("\033[%dm", ecmaColor
);
1386 return String
.format("%d;", ecmaColor
);
1391 * Create a SGR parameter sequence for both foreground and background
1392 * color change. Note package private access.
1394 * @param bold if true, set bold
1395 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1396 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1397 * @return the string to emit to an ANSI / ECMA-style terminal,
1398 * e.g. "\033[31;42m"
1400 String
color(final boolean bold
, final Color foreColor
,
1401 final Color backColor
) {
1402 return color(foreColor
, backColor
, true) +
1403 rgbColor(bold
, foreColor
, backColor
);
1407 * Create a SGR parameter sequence for both foreground and
1408 * background color change.
1410 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1411 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1412 * @param header if true, make the full header, otherwise just emit the
1413 * color parameter e.g. "31;42;"
1414 * @return the string to emit to an ANSI / ECMA-style terminal,
1415 * e.g. "\033[31;42m"
1417 private String
color(final Color foreColor
, final Color backColor
,
1418 final boolean header
) {
1420 int ecmaForeColor
= foreColor
.getValue();
1421 int ecmaBackColor
= backColor
.getValue();
1423 // Convert Color.* values to SGR numerics
1424 ecmaBackColor
+= 40;
1425 ecmaForeColor
+= 30;
1428 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1430 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1435 * Create a SGR parameter sequence for foreground, background, and
1436 * several attributes. This sequence first resets all attributes to
1437 * default, then sets attributes as per the parameters. Note package
1440 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1441 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1442 * @param bold if true, set bold
1443 * @param reverse if true, set reverse
1444 * @param blink if true, set blink
1445 * @param underline if true, set underline
1446 * @return the string to emit to an ANSI / ECMA-style terminal,
1447 * e.g. "\033[0;1;31;42m"
1449 String
color(final Color foreColor
, final Color backColor
,
1450 final boolean bold
, final boolean reverse
, final boolean blink
,
1451 final boolean underline
) {
1453 int ecmaForeColor
= foreColor
.getValue();
1454 int ecmaBackColor
= backColor
.getValue();
1456 // Convert Color.* values to SGR numerics
1457 ecmaBackColor
+= 40;
1458 ecmaForeColor
+= 30;
1460 StringBuilder sb
= new StringBuilder();
1461 if ( bold
&& reverse
&& blink
&& !underline
) {
1462 sb
.append("\033[0;1;7;5;");
1463 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1464 sb
.append("\033[0;1;7;");
1465 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1466 sb
.append("\033[0;7;5;");
1467 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1468 sb
.append("\033[0;1;5;");
1469 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1470 sb
.append("\033[0;1;");
1471 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1472 sb
.append("\033[0;7;");
1473 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1474 sb
.append("\033[0;5;");
1475 } else if ( bold
&& reverse
&& blink
&& underline
) {
1476 sb
.append("\033[0;1;7;5;4;");
1477 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1478 sb
.append("\033[0;1;7;4;");
1479 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1480 sb
.append("\033[0;7;5;4;");
1481 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1482 sb
.append("\033[0;1;5;4;");
1483 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1484 sb
.append("\033[0;1;4;");
1485 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1486 sb
.append("\033[0;7;4;");
1487 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1488 sb
.append("\033[0;5;4;");
1489 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1490 sb
.append("\033[0;4;");
1492 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1493 sb
.append("\033[0;");
1495 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1496 sb
.append(rgbColor(bold
, foreColor
, backColor
));
1497 return sb
.toString();
1501 * Create a SGR parameter sequence to reset to defaults. Note package
1504 * @return the string to emit to an ANSI / ECMA-style terminal,
1508 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
1512 * Create a SGR parameter sequence to reset to defaults.
1514 * @param header if true, make the full header, otherwise just emit the
1515 * bare parameter e.g. "0;"
1516 * @return the string to emit to an ANSI / ECMA-style terminal,
1519 private String
normal(final boolean header
) {
1521 return "\033[0;37;40m";
1527 * Create a SGR parameter sequence for enabling the visible cursor. Note
1528 * package private access.
1530 * @param on if true, turn on cursor
1531 * @return the string to emit to an ANSI / ECMA-style terminal
1533 String
cursor(final boolean on
) {
1534 if (on
&& !cursorOn
) {
1538 if (!on
&& cursorOn
) {
1546 * Clear the entire screen. Because some terminals use back-color-erase,
1547 * set the color to white-on-black beforehand.
1549 * @return the string to emit to an ANSI / ECMA-style terminal
1551 public String
clearAll() {
1552 return "\033[0;37;40m\033[2J";
1556 * Clear the line from the cursor (inclusive) to the end of the screen.
1557 * Because some terminals use back-color-erase, set the color to
1558 * white-on-black beforehand. Note package private access.
1560 * @return the string to emit to an ANSI / ECMA-style terminal
1562 String
clearRemainingLine() {
1563 return "\033[0;37;40m\033[K";
1567 * Move the cursor to (x, y). Note package private access.
1569 * @param x column coordinate. 0 is the left-most column.
1570 * @param y row coordinate. 0 is the top-most row.
1571 * @return the string to emit to an ANSI / ECMA-style terminal
1573 String
gotoXY(final int x
, final int y
) {
1574 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1578 * Tell (u)xterm that we want to receive mouse events based on "Any event
1579 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1580 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1582 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1584 * Note that this also sets the alternate/primary screen buffer.
1586 * @param on If true, enable mouse report and use the alternate screen
1587 * buffer. If false disable mouse reporting and use the primary screen
1589 * @return the string to emit to xterm
1591 private String
mouse(final boolean on
) {
1593 return "\033[?1002;1003;1005;1006h\033[?1049h";
1595 return "\033[?1002;1003;1006;1005l\033[?1049l";
1599 * Read function runs on a separate thread.
1602 boolean done
= false;
1603 // available() will often return > 1, so we need to read in chunks to
1605 char [] readBuffer
= new char[128];
1606 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1608 while (!done
&& !stopReaderThread
) {
1610 // We assume that if inputStream has bytes available, then
1611 // input won't block on read().
1612 int n
= inputStream
.available();
1614 if (readBuffer
.length
< n
) {
1615 // The buffer wasn't big enough, make it huger
1616 readBuffer
= new char[readBuffer
.length
* 2];
1619 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1620 // System.err.printf("read() %d", rc); System.err.flush();
1625 for (int i
= 0; i
< rc
; i
++) {
1626 int ch
= readBuffer
[i
];
1627 processChar(events
, (char)ch
);
1629 getIdleEvents(events
);
1630 if (events
.size() > 0) {
1631 // Add to the queue for the backend thread to
1632 // be able to obtain.
1633 synchronized (eventQueue
) {
1634 eventQueue
.addAll(events
);
1636 synchronized (listener
) {
1637 listener
.notifyAll();
1643 getIdleEvents(events
);
1644 if (events
.size() > 0) {
1645 synchronized (eventQueue
) {
1646 eventQueue
.addAll(events
);
1649 synchronized (listener
) {
1650 listener
.notifyAll();
1654 // Wait 10 millis for more data
1657 // System.err.println("end while loop"); System.err.flush();
1658 } catch (InterruptedException e
) {
1660 } catch (IOException e
) {
1661 e
.printStackTrace();
1664 } // while ((done == false) && (stopReaderThread == false))
1665 // System.err.println("*** run() exiting..."); System.err.flush();