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]
29 package jexer
.backend
;
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
.Cell
;
48 import jexer
.bits
.CellAttributes
;
49 import jexer
.bits
.Color
;
50 import jexer
.event
.TInputEvent
;
51 import jexer
.event
.TKeypressEvent
;
52 import jexer
.event
.TMouseEvent
;
53 import jexer
.event
.TResizeEvent
;
54 import static jexer
.TKeypress
.*;
57 * This class reads keystrokes and mouse events and emits output to ANSI
58 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
60 public final class ECMA48Terminal
extends LogicalScreen
61 implements TerminalReader
, Runnable
{
64 * Emit debugging to stderr.
66 private boolean debugToStderr
= false;
69 * If true, emit T.416-style RGB colors. This is a) expensive in
70 * bandwidth, and b) potentially terrible looking for non-xterms.
72 private static boolean doRgbColor
= false;
75 * The session information.
77 private SessionInfo sessionInfo
;
80 * Getter for sessionInfo.
82 * @return the SessionInfo
84 public SessionInfo
getSessionInfo() {
89 * The event queue, filled up by a thread reading on input.
91 private List
<TInputEvent
> eventQueue
;
94 * If true, we want the reader thread to exit gracefully.
96 private boolean stopReaderThread
;
101 private Thread readerThread
;
104 * Parameters being collected. E.g. if the string is \033[1;3m, then
105 * params[0] will be 1 and params[1] will be 3.
107 private ArrayList
<String
> params
;
110 * States in the input parser.
112 private enum ParseState
{
123 * Current parsing state.
125 private ParseState state
;
128 * The time we entered ESCAPE. If we get a bare escape without a code
129 * following it, this is used to return that bare escape.
131 private long escapeTime
;
134 * The time we last checked the window size. We try not to spawn stty
135 * more than once per second.
137 private long windowSizeTime
;
140 * true if mouse1 was down. Used to report mouse1 on the release event.
142 private boolean mouse1
;
145 * true if mouse2 was down. Used to report mouse2 on the release event.
147 private boolean mouse2
;
150 * true if mouse3 was down. Used to report mouse3 on the release event.
152 private boolean mouse3
;
155 * Cache the cursor visibility value so we only emit the sequence when we
158 private boolean cursorOn
= true;
161 * Cache the last window size to figure out if a TResizeEvent needs to be
164 private TResizeEvent windowResize
= null;
167 * If true, then we changed System.in and need to change it back.
169 private boolean setRawMode
;
172 * The terminal's input. If an InputStream is not specified in the
173 * constructor, then this InputStreamReader will be bound to System.in
174 * with UTF-8 encoding.
176 private Reader input
;
179 * The terminal's raw InputStream. If an InputStream is not specified in
180 * the constructor, then this InputReader will be bound to System.in.
181 * This is used by run() to see if bytes are available() before calling
182 * (Reader)input.read().
184 private InputStream inputStream
;
187 * The terminal's output. If an OutputStream is not specified in the
188 * constructor, then this PrintWriter will be bound to System.out with
191 private PrintWriter output
;
194 * The listening object that run() wakes up on new input.
196 private Object listener
;
199 * Get the output writer.
203 public PrintWriter
getOutput() {
208 * Check if there are events in the queue.
210 * @return if true, getEvents() has something to return to the backend
212 public boolean hasEvents() {
213 synchronized (eventQueue
) {
214 return (eventQueue
.size() > 0);
219 * Call 'stty' to set cooked mode.
221 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
223 private void sttyCooked() {
228 * Call 'stty' to set raw mode.
230 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
231 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
232 * -parenb cs8 min 1 < /dev/tty'
234 private void sttyRaw() {
239 * Call 'stty' to set raw or cooked mode.
241 * @param mode if true, set raw mode, otherwise set cooked mode
243 private void doStty(final boolean mode
) {
245 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
247 String
[] cmdCooked
= {
248 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
253 process
= Runtime
.getRuntime().exec(cmdRaw
);
255 process
= Runtime
.getRuntime().exec(cmdCooked
);
257 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
258 String line
= in
.readLine();
259 if ((line
!= null) && (line
.length() > 0)) {
260 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
263 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
264 line
= err
.readLine();
265 if ((line
!= null) && (line
.length() > 0)) {
266 System
.err
.println("Error output from stty: " + line
);
271 } catch (InterruptedException e
) {
275 int rc
= process
.exitValue();
277 System
.err
.println("stty returned error code: " + rc
);
279 } catch (IOException e
) {
285 * Constructor sets up state for getEvent().
287 * @param listener the object this backend needs to wake up when new
289 * @param input an InputStream connected to the remote user, or null for
290 * System.in. If System.in is used, then on non-Windows systems it will
291 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
292 * mode. input is always converted to a Reader with UTF-8 encoding.
293 * @param output an OutputStream connected to the remote user, or null
294 * for System.out. output is always converted to a Writer with UTF-8
296 * @throws UnsupportedEncodingException if an exception is thrown when
297 * creating the InputStreamReader
299 public ECMA48Terminal(final Object listener
, final InputStream input
,
300 final OutputStream output
) throws UnsupportedEncodingException
{
306 stopReaderThread
= false;
307 this.listener
= listener
;
310 // inputStream = System.in;
311 inputStream
= new FileInputStream(FileDescriptor
.in
);
317 this.input
= new InputStreamReader(inputStream
, "UTF-8");
319 if (input
instanceof SessionInfo
) {
320 // This is a TelnetInputStream that exposes window size and
321 // environment variables from the telnet layer.
322 sessionInfo
= (SessionInfo
) input
;
324 if (sessionInfo
== null) {
326 // Reading right off the tty
327 sessionInfo
= new TTYSessionInfo();
329 sessionInfo
= new TSessionInfo();
333 if (output
== null) {
334 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
337 this.output
= new PrintWriter(new OutputStreamWriter(output
,
341 // Enable mouse reporting and metaSendsEscape
342 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
345 // Hang onto the window size
346 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
347 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
349 // Permit RGB colors only if externally requested
350 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
351 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
356 // Spin up the input reader
357 eventQueue
= new LinkedList
<TInputEvent
>();
358 readerThread
= new Thread(this);
359 readerThread
.start();
361 // Query the screen size
362 setDimensions(sessionInfo
.getWindowWidth(),
363 sessionInfo
.getWindowHeight());
366 this.output
.write(clearAll());
371 * Constructor sets up state for getEvent().
373 * @param listener the object this backend needs to wake up when new
375 * @param input the InputStream underlying 'reader'. Its available()
376 * method is used to determine if reader.read() will block or not.
377 * @param reader a Reader connected to the remote user.
378 * @param writer a PrintWriter connected to the remote user.
379 * @param setRawMode if true, set System.in into raw mode with stty.
380 * This should in general not be used. It is here solely for Demo3,
381 * which uses System.in.
382 * @throws IllegalArgumentException if input, reader, or writer are null.
384 public ECMA48Terminal(final Object listener
, final InputStream input
,
385 final Reader reader
, final PrintWriter writer
,
386 final boolean setRawMode
) {
389 throw new IllegalArgumentException("InputStream must be specified");
391 if (reader
== null) {
392 throw new IllegalArgumentException("Reader must be specified");
394 if (writer
== null) {
395 throw new IllegalArgumentException("Writer must be specified");
401 stopReaderThread
= false;
402 this.listener
= listener
;
407 if (setRawMode
== true) {
410 this.setRawMode
= setRawMode
;
412 if (input
instanceof SessionInfo
) {
413 // This is a TelnetInputStream that exposes window size and
414 // environment variables from the telnet layer.
415 sessionInfo
= (SessionInfo
) input
;
417 if (sessionInfo
== null) {
418 if (setRawMode
== true) {
419 // Reading right off the tty
420 sessionInfo
= new TTYSessionInfo();
422 sessionInfo
= new TSessionInfo();
426 this.output
= writer
;
428 // Enable mouse reporting and metaSendsEscape
429 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
432 // Hang onto the window size
433 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
434 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
436 // Permit RGB colors only if externally requested
437 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
438 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
443 // Spin up the input reader
444 eventQueue
= new LinkedList
<TInputEvent
>();
445 readerThread
= new Thread(this);
446 readerThread
.start();
448 // Query the screen size
449 setDimensions(sessionInfo
.getWindowWidth(),
450 sessionInfo
.getWindowHeight());
453 this.output
.write(clearAll());
458 * Constructor sets up state for getEvent().
460 * @param listener the object this backend needs to wake up when new
462 * @param input the InputStream underlying 'reader'. Its available()
463 * method is used to determine if reader.read() will block or not.
464 * @param reader a Reader connected to the remote user.
465 * @param writer a PrintWriter connected to the remote user.
466 * @throws IllegalArgumentException if input, reader, or writer are null.
468 public ECMA48Terminal(final Object listener
, final InputStream input
,
469 final Reader reader
, final PrintWriter writer
) {
471 this(listener
, input
, reader
, writer
, false);
475 * Restore terminal to normal state.
477 public void closeTerminal() {
479 // System.err.println("=== shutdown() ==="); System.err.flush();
481 // Tell the reader thread to stop looking at input
482 stopReaderThread
= true;
485 } catch (InterruptedException e
) {
489 // Disable mouse reporting and show cursor
490 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
496 // We don't close System.in/out
498 // Shut down the streams, this should wake up the reader thread
505 if (output
!= null) {
509 } catch (IOException e
) {
518 public void flush() {
523 * Perform a somewhat-optimal rendering of a line.
525 * @param y row coordinate. 0 is the top-most row.
526 * @param sb StringBuilder to write escape sequences to
527 * @param lastAttr cell attributes from the last call to flushLine
529 private void flushLine(final int y
, final StringBuilder sb
,
530 CellAttributes lastAttr
) {
534 for (int x
= 0; x
< width
; x
++) {
535 Cell lCell
= logical
[x
][y
];
536 if (!lCell
.isBlank()) {
540 // Push textEnd to first column beyond the text area
544 // reallyCleared = true;
546 for (int x
= 0; x
< width
; x
++) {
547 Cell lCell
= logical
[x
][y
];
548 Cell pCell
= physical
[x
][y
];
550 if (!lCell
.equals(pCell
) || reallyCleared
) {
553 System
.err
.printf("\n--\n");
554 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
555 System
.err
.printf(" lCell: %s\n", lCell
);
556 System
.err
.printf(" pCell: %s\n", pCell
);
557 System
.err
.printf(" ==== \n");
560 if (lastAttr
== null) {
561 lastAttr
= new CellAttributes();
566 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
567 // Advancing at least one cell, or the first gotoXY
568 sb
.append(gotoXY(x
, y
));
571 assert (lastAttr
!= null);
573 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
574 assert (lCell
.isBlank());
576 for (int i
= x
; i
< width
; i
++) {
577 assert (logical
[i
][y
].isBlank());
578 // Physical is always updated
579 physical
[i
][y
].reset();
582 // Clear remaining line
583 sb
.append(clearRemainingLine());
588 // Now emit only the modified attributes
589 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
590 && (lCell
.getBackColor() != lastAttr
.getBackColor())
591 && (lCell
.isBold() == lastAttr
.isBold())
592 && (lCell
.isReverse() == lastAttr
.isReverse())
593 && (lCell
.isUnderline() == lastAttr
.isUnderline())
594 && (lCell
.isBlink() == lastAttr
.isBlink())
596 // Both colors changed, attributes the same
597 sb
.append(color(lCell
.isBold(),
598 lCell
.getForeColor(), lCell
.getBackColor()));
601 System
.err
.printf("1 Change only fore/back colors\n");
603 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
604 && (lCell
.getBackColor() != lastAttr
.getBackColor())
605 && (lCell
.isBold() != lastAttr
.isBold())
606 && (lCell
.isReverse() != lastAttr
.isReverse())
607 && (lCell
.isUnderline() != lastAttr
.isUnderline())
608 && (lCell
.isBlink() != lastAttr
.isBlink())
610 // Everything is different
611 sb
.append(color(lCell
.getForeColor(),
612 lCell
.getBackColor(),
613 lCell
.isBold(), lCell
.isReverse(),
615 lCell
.isUnderline()));
618 System
.err
.printf("2 Set all attributes\n");
620 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
621 && (lCell
.getBackColor() == lastAttr
.getBackColor())
622 && (lCell
.isBold() == lastAttr
.isBold())
623 && (lCell
.isReverse() == lastAttr
.isReverse())
624 && (lCell
.isUnderline() == lastAttr
.isUnderline())
625 && (lCell
.isBlink() == lastAttr
.isBlink())
628 // Attributes same, foreColor different
629 sb
.append(color(lCell
.isBold(),
630 lCell
.getForeColor(), true));
633 System
.err
.printf("3 Change foreColor\n");
635 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
636 && (lCell
.getBackColor() != lastAttr
.getBackColor())
637 && (lCell
.isBold() == lastAttr
.isBold())
638 && (lCell
.isReverse() == lastAttr
.isReverse())
639 && (lCell
.isUnderline() == lastAttr
.isUnderline())
640 && (lCell
.isBlink() == lastAttr
.isBlink())
642 // Attributes same, backColor different
643 sb
.append(color(lCell
.isBold(),
644 lCell
.getBackColor(), false));
647 System
.err
.printf("4 Change backColor\n");
649 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
650 && (lCell
.getBackColor() == lastAttr
.getBackColor())
651 && (lCell
.isBold() == lastAttr
.isBold())
652 && (lCell
.isReverse() == lastAttr
.isReverse())
653 && (lCell
.isUnderline() == lastAttr
.isUnderline())
654 && (lCell
.isBlink() == lastAttr
.isBlink())
657 // All attributes the same, just print the char
661 System
.err
.printf("5 Only emit character\n");
664 // Just reset everything again
665 sb
.append(color(lCell
.getForeColor(),
666 lCell
.getBackColor(),
670 lCell
.isUnderline()));
673 System
.err
.printf("6 Change all attributes\n");
676 // Emit the character
677 sb
.append(lCell
.getChar());
679 // Save the last rendered cell
681 lastAttr
.setTo(lCell
);
683 // Physical is always updated
684 physical
[x
][y
].setTo(lCell
);
686 } // if (!lCell.equals(pCell) || (reallyCleared == true))
688 } // for (int x = 0; x < width; x++)
692 * Render the screen to a string that can be emitted to something that
693 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
695 * @return escape sequences string that provides the updates to the
698 private String
flushString() {
700 assert (!reallyCleared
);
704 CellAttributes attr
= null;
706 StringBuilder sb
= new StringBuilder();
708 attr
= new CellAttributes();
709 sb
.append(clearAll());
712 for (int y
= 0; y
< height
; y
++) {
713 flushLine(y
, sb
, attr
);
717 reallyCleared
= false;
719 String result
= sb
.toString();
721 System
.err
.printf("flushString(): %s\n", result
);
727 * Push the logical screen to the physical device.
730 public void flushPhysical() {
731 String result
= flushString();
733 && (cursorY
<= height
- 1)
734 && (cursorX
<= width
- 1)
736 result
+= cursor(true);
737 result
+= gotoXY(cursorX
, cursorY
);
739 result
+= cursor(false);
741 output
.write(result
);
746 * Set the window title.
748 * @param title the new title
750 public void setTitle(final String title
) {
751 output
.write(getSetTitleString(title
));
756 * Reset keyboard/mouse input parser.
758 private void resetParser() {
759 state
= ParseState
.GROUND
;
760 params
= new ArrayList
<String
>();
766 * Produce a control character or one of the special ones (ENTER, TAB,
769 * @param ch Unicode code point
770 * @param alt if true, set alt on the TKeypress
771 * @return one TKeypress event, either a control character (e.g. isKey ==
772 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
775 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
776 // System.err.printf("controlChar: %02x\n", ch);
780 // Carriage return --> ENTER
781 return new TKeypressEvent(kbEnter
, alt
, false, false);
783 // Linefeed --> ENTER
784 return new TKeypressEvent(kbEnter
, alt
, false, false);
787 return new TKeypressEvent(kbEsc
, alt
, false, false);
790 return new TKeypressEvent(kbTab
, alt
, false, false);
792 // Make all other control characters come back as the alphabetic
793 // character with the ctrl field set. So SOH would be 'A' +
795 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
801 * Produce special key from CSI Pn ; Pm ; ... ~
803 * @return one KEYPRESS event representing a special key
805 private TInputEvent
csiFnKey() {
807 if (params
.size() > 0) {
808 key
= Integer
.parseInt(params
.get(0));
811 boolean ctrl
= false;
812 boolean shift
= false;
813 if (params
.size() > 1) {
814 shift
= csiIsShift(params
.get(1));
815 alt
= csiIsAlt(params
.get(1));
816 ctrl
= csiIsCtrl(params
.get(1));
821 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
823 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
825 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
827 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
829 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
831 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
833 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
835 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
837 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
839 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
841 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
843 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
845 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
847 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
855 * Produce mouse events based on "Any event tracking" and UTF-8
857 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
859 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
861 private TInputEvent
parseMouse() {
862 int buttons
= params
.get(0).charAt(0) - 32;
863 int x
= params
.get(0).charAt(1) - 32 - 1;
864 int y
= params
.get(0).charAt(2) - 32 - 1;
866 // Clamp X and Y to the physical screen coordinates.
867 if (x
>= windowResize
.getWidth()) {
868 x
= windowResize
.getWidth() - 1;
870 if (y
>= windowResize
.getHeight()) {
871 y
= windowResize
.getHeight() - 1;
874 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
875 boolean eventMouse1
= false;
876 boolean eventMouse2
= false;
877 boolean eventMouse3
= false;
878 boolean eventMouseWheelUp
= false;
879 boolean eventMouseWheelDown
= false;
881 // System.err.printf("buttons: %04x\r\n", buttons);
898 if (!mouse1
&& !mouse2
&& !mouse3
) {
899 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
901 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
918 // Dragging with mouse1 down
921 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
925 // Dragging with mouse2 down
928 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
932 // Dragging with mouse3 down
935 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
939 // Dragging with mouse2 down after wheelUp
942 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
946 // Dragging with mouse2 down after wheelDown
949 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
953 eventMouseWheelUp
= true;
957 eventMouseWheelDown
= true;
961 // Unknown, just make it motion
962 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
965 return new TMouseEvent(eventType
, x
, y
, x
, y
,
966 eventMouse1
, eventMouse2
, eventMouse3
,
967 eventMouseWheelUp
, eventMouseWheelDown
);
971 * Produce mouse events based on "Any event tracking" and SGR
973 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
975 * @param release if true, this was a release ('m')
976 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
978 private TInputEvent
parseMouseSGR(final boolean release
) {
979 // SGR extended coordinates - mode 1006
980 if (params
.size() < 3) {
981 // Invalid position, bail out.
984 int buttons
= Integer
.parseInt(params
.get(0));
985 int x
= Integer
.parseInt(params
.get(1)) - 1;
986 int y
= Integer
.parseInt(params
.get(2)) - 1;
988 // Clamp X and Y to the physical screen coordinates.
989 if (x
>= windowResize
.getWidth()) {
990 x
= windowResize
.getWidth() - 1;
992 if (y
>= windowResize
.getHeight()) {
993 y
= windowResize
.getHeight() - 1;
996 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
997 boolean eventMouse1
= false;
998 boolean eventMouse2
= false;
999 boolean eventMouse3
= false;
1000 boolean eventMouseWheelUp
= false;
1001 boolean eventMouseWheelDown
= false;
1004 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
1018 // Motion only, no buttons down
1019 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1023 // Dragging with mouse1 down
1025 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1029 // Dragging with mouse2 down
1031 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1035 // Dragging with mouse3 down
1037 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1041 // Dragging with mouse2 down after wheelUp
1043 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1047 // Dragging with mouse2 down after wheelDown
1049 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1053 eventMouseWheelUp
= true;
1057 eventMouseWheelDown
= true;
1061 // Unknown, bail out
1064 return new TMouseEvent(eventType
, x
, y
, x
, y
,
1065 eventMouse1
, eventMouse2
, eventMouse3
,
1066 eventMouseWheelUp
, eventMouseWheelDown
);
1070 * Return any events in the IO queue.
1072 * @param queue list to append new events to
1074 public void getEvents(final List
<TInputEvent
> queue
) {
1075 synchronized (eventQueue
) {
1076 if (eventQueue
.size() > 0) {
1077 synchronized (queue
) {
1078 queue
.addAll(eventQueue
);
1086 * Return any events in the IO queue due to timeout.
1088 * @param queue list to append new events to
1090 private void getIdleEvents(final List
<TInputEvent
> queue
) {
1091 Date now
= new Date();
1093 // Check for new window size
1094 long windowSizeDelay
= now
.getTime() - windowSizeTime
;
1095 if (windowSizeDelay
> 1000) {
1096 sessionInfo
.queryWindowSize();
1097 int newWidth
= sessionInfo
.getWindowWidth();
1098 int newHeight
= sessionInfo
.getWindowHeight();
1099 if ((newWidth
!= windowResize
.getWidth())
1100 || (newHeight
!= windowResize
.getHeight())
1102 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1103 newWidth
, newHeight
);
1104 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1105 newWidth
, newHeight
);
1108 windowSizeTime
= now
.getTime();
1111 // ESCDELAY type timeout
1112 if (state
== ParseState
.ESCAPE
) {
1113 long escDelay
= now
.getTime() - escapeTime
;
1114 if (escDelay
> 100) {
1115 // After 0.1 seconds, assume a true escape character
1116 queue
.add(controlChar((char)0x1B, false));
1123 * Returns true if the CSI parameter for a keyboard command means that
1126 private boolean csiIsShift(final String x
) {
1138 * Returns true if the CSI parameter for a keyboard command means that
1141 private boolean csiIsAlt(final String x
) {
1153 * Returns true if the CSI parameter for a keyboard command means that
1156 private boolean csiIsCtrl(final String x
) {
1168 * Parses the next character of input to see if an InputEvent is
1171 * @param events list to append new events to
1172 * @param ch Unicode code point
1174 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
1176 // ESCDELAY type timeout
1177 Date now
= new Date();
1178 if (state
== ParseState
.ESCAPE
) {
1179 long escDelay
= now
.getTime() - escapeTime
;
1180 if (escDelay
> 250) {
1181 // After 0.25 seconds, assume a true escape character
1182 events
.add(controlChar((char)0x1B, false));
1188 boolean ctrl
= false;
1189 boolean alt
= false;
1190 boolean shift
= false;
1192 // System.err.printf("state: %s ch %c\r\n", state, ch);
1198 state
= ParseState
.ESCAPE
;
1199 escapeTime
= now
.getTime();
1204 // Control character
1205 events
.add(controlChar(ch
, false));
1212 events
.add(new TKeypressEvent(false, 0, ch
,
1213 false, false, false));
1222 // ALT-Control character
1223 events
.add(controlChar(ch
, true));
1229 // This will be one of the function keys
1230 state
= ParseState
.ESCAPE_INTERMEDIATE
;
1234 // '[' goes to CSI_ENTRY
1236 state
= ParseState
.CSI_ENTRY
;
1240 // Everything else is assumed to be Alt-keystroke
1241 if ((ch
>= 'A') && (ch
<= 'Z')) {
1245 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
1249 case ESCAPE_INTERMEDIATE
:
1250 if ((ch
>= 'P') && (ch
<= 'S')) {
1254 events
.add(new TKeypressEvent(kbF1
));
1257 events
.add(new TKeypressEvent(kbF2
));
1260 events
.add(new TKeypressEvent(kbF3
));
1263 events
.add(new TKeypressEvent(kbF4
));
1272 // Unknown keystroke, ignore
1277 // Numbers - parameter values
1278 if ((ch
>= '0') && (ch
<= '9')) {
1279 params
.set(params
.size() - 1,
1280 params
.get(params
.size() - 1) + ch
);
1281 state
= ParseState
.CSI_PARAM
;
1284 // Parameter separator
1290 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1294 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1299 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1304 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1309 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1314 events
.add(new TKeypressEvent(kbHome
));
1319 events
.add(new TKeypressEvent(kbEnd
));
1323 // CBT - Cursor backward X tab stops (default 1)
1324 events
.add(new TKeypressEvent(kbBackTab
));
1329 state
= ParseState
.MOUSE
;
1332 // Mouse position, SGR (1006) coordinates
1333 state
= ParseState
.MOUSE_SGR
;
1340 // Unknown keystroke, ignore
1345 // Numbers - parameter values
1346 if ((ch
>= '0') && (ch
<= '9')) {
1347 params
.set(params
.size() - 1,
1348 params
.get(params
.size() - 1) + ch
);
1351 // Parameter separator
1359 // Generate a mouse press event
1360 TInputEvent event
= parseMouseSGR(false);
1361 if (event
!= null) {
1367 // Generate a mouse release event
1368 event
= parseMouseSGR(true);
1369 if (event
!= null) {
1378 // Unknown keystroke, ignore
1383 // Numbers - parameter values
1384 if ((ch
>= '0') && (ch
<= '9')) {
1385 params
.set(params
.size() - 1,
1386 params
.get(params
.size() - 1) + ch
);
1387 state
= ParseState
.CSI_PARAM
;
1390 // Parameter separator
1397 events
.add(csiFnKey());
1402 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1406 if (params
.size() > 1) {
1407 shift
= csiIsShift(params
.get(1));
1408 alt
= csiIsAlt(params
.get(1));
1409 ctrl
= csiIsCtrl(params
.get(1));
1411 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1416 if (params
.size() > 1) {
1417 shift
= csiIsShift(params
.get(1));
1418 alt
= csiIsAlt(params
.get(1));
1419 ctrl
= csiIsCtrl(params
.get(1));
1421 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1426 if (params
.size() > 1) {
1427 shift
= csiIsShift(params
.get(1));
1428 alt
= csiIsAlt(params
.get(1));
1429 ctrl
= csiIsCtrl(params
.get(1));
1431 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1436 if (params
.size() > 1) {
1437 shift
= csiIsShift(params
.get(1));
1438 alt
= csiIsAlt(params
.get(1));
1439 ctrl
= csiIsCtrl(params
.get(1));
1441 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1446 if (params
.size() > 1) {
1447 shift
= csiIsShift(params
.get(1));
1448 alt
= csiIsAlt(params
.get(1));
1449 ctrl
= csiIsCtrl(params
.get(1));
1451 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
1456 if (params
.size() > 1) {
1457 shift
= csiIsShift(params
.get(1));
1458 alt
= csiIsAlt(params
.get(1));
1459 ctrl
= csiIsCtrl(params
.get(1));
1461 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
1469 // Unknown keystroke, ignore
1474 params
.set(0, params
.get(params
.size() - 1) + ch
);
1475 if (params
.get(0).length() == 3) {
1476 // We have enough to generate a mouse event
1477 events
.add(parseMouse());
1486 // This "should" be impossible to reach
1491 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1492 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1495 * @param on if true, enable metaSendsEscape
1496 * @return the string to emit to xterm
1498 private String
xtermMetaSendsEscape(final boolean on
) {
1500 return "\033[?1036h\033[?1034l";
1502 return "\033[?1036l";
1506 * Create an xterm OSC sequence to change the window title.
1508 * @param title the new title
1509 * @return the string to emit to xterm
1511 private String
getSetTitleString(final String title
) {
1512 return "\033]2;" + title
+ "\007";
1516 * Create a SGR parameter sequence for a single color change.
1518 * @param bold if true, set bold
1519 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1520 * @param foreground if true, this is a foreground color
1521 * @return the string to emit to an ANSI / ECMA-style terminal,
1524 private String
color(final boolean bold
, final Color color
,
1525 final boolean foreground
) {
1526 return color(color
, foreground
, true) +
1527 rgbColor(bold
, color
, foreground
);
1531 * Create a T.416 RGB parameter sequence for a single color change.
1533 * @param bold if true, set bold
1534 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1535 * @param foreground if true, this is a foreground color
1536 * @return the string to emit to an xterm terminal with RGB support,
1537 * e.g. "\033[38;2;RR;GG;BBm"
1539 private String
rgbColor(final boolean bold
, final Color color
,
1540 final boolean foreground
) {
1541 if (doRgbColor
== false) {
1544 StringBuilder sb
= new StringBuilder("\033[");
1546 // Bold implies foreground only
1548 if (color
.equals(Color
.BLACK
)) {
1549 sb
.append("84;84;84");
1550 } else if (color
.equals(Color
.RED
)) {
1551 sb
.append("252;84;84");
1552 } else if (color
.equals(Color
.GREEN
)) {
1553 sb
.append("84;252;84");
1554 } else if (color
.equals(Color
.YELLOW
)) {
1555 sb
.append("252;252;84");
1556 } else if (color
.equals(Color
.BLUE
)) {
1557 sb
.append("84;84;252");
1558 } else if (color
.equals(Color
.MAGENTA
)) {
1559 sb
.append("252;84;252");
1560 } else if (color
.equals(Color
.CYAN
)) {
1561 sb
.append("84;252;252");
1562 } else if (color
.equals(Color
.WHITE
)) {
1563 sb
.append("252;252;252");
1571 if (color
.equals(Color
.BLACK
)) {
1573 } else if (color
.equals(Color
.RED
)) {
1574 sb
.append("168;0;0");
1575 } else if (color
.equals(Color
.GREEN
)) {
1576 sb
.append("0;168;0");
1577 } else if (color
.equals(Color
.YELLOW
)) {
1578 sb
.append("168;84;0");
1579 } else if (color
.equals(Color
.BLUE
)) {
1580 sb
.append("0;0;168");
1581 } else if (color
.equals(Color
.MAGENTA
)) {
1582 sb
.append("168;0;168");
1583 } else if (color
.equals(Color
.CYAN
)) {
1584 sb
.append("0;168;168");
1585 } else if (color
.equals(Color
.WHITE
)) {
1586 sb
.append("168;168;168");
1590 return sb
.toString();
1594 * Create a T.416 RGB parameter sequence for both foreground and
1595 * background color change.
1597 * @param bold if true, set bold
1598 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1599 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1600 * @return the string to emit to an xterm terminal with RGB support,
1601 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
1603 private String
rgbColor(final boolean bold
, final Color foreColor
,
1604 final Color backColor
) {
1605 if (doRgbColor
== false) {
1609 return rgbColor(bold
, foreColor
, true) +
1610 rgbColor(false, backColor
, false);
1614 * Create a SGR parameter sequence for a single color change.
1616 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1617 * @param foreground if true, this is a foreground color
1618 * @param header if true, make the full header, otherwise just emit the
1619 * color parameter e.g. "42;"
1620 * @return the string to emit to an ANSI / ECMA-style terminal,
1623 private String
color(final Color color
, final boolean foreground
,
1624 final boolean header
) {
1626 int ecmaColor
= color
.getValue();
1628 // Convert Color.* values to SGR numerics
1636 return String
.format("\033[%dm", ecmaColor
);
1638 return String
.format("%d;", ecmaColor
);
1643 * Create a SGR parameter sequence for both foreground and background
1646 * @param bold if true, set bold
1647 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1648 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1649 * @return the string to emit to an ANSI / ECMA-style terminal,
1650 * e.g. "\033[31;42m"
1652 private String
color(final boolean bold
, final Color foreColor
,
1653 final Color backColor
) {
1654 return color(foreColor
, backColor
, true) +
1655 rgbColor(bold
, foreColor
, backColor
);
1659 * Create a SGR parameter sequence for both foreground and
1660 * background color change.
1662 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1663 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1664 * @param header if true, make the full header, otherwise just emit the
1665 * color parameter e.g. "31;42;"
1666 * @return the string to emit to an ANSI / ECMA-style terminal,
1667 * e.g. "\033[31;42m"
1669 private String
color(final Color foreColor
, final Color backColor
,
1670 final boolean header
) {
1672 int ecmaForeColor
= foreColor
.getValue();
1673 int ecmaBackColor
= backColor
.getValue();
1675 // Convert Color.* values to SGR numerics
1676 ecmaBackColor
+= 40;
1677 ecmaForeColor
+= 30;
1680 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1682 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1687 * Create a SGR parameter sequence for foreground, background, and
1688 * several attributes. This sequence first resets all attributes to
1689 * default, then sets attributes as per the parameters.
1691 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1692 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1693 * @param bold if true, set bold
1694 * @param reverse if true, set reverse
1695 * @param blink if true, set blink
1696 * @param underline if true, set underline
1697 * @return the string to emit to an ANSI / ECMA-style terminal,
1698 * e.g. "\033[0;1;31;42m"
1700 private String
color(final Color foreColor
, final Color backColor
,
1701 final boolean bold
, final boolean reverse
, final boolean blink
,
1702 final boolean underline
) {
1704 int ecmaForeColor
= foreColor
.getValue();
1705 int ecmaBackColor
= backColor
.getValue();
1707 // Convert Color.* values to SGR numerics
1708 ecmaBackColor
+= 40;
1709 ecmaForeColor
+= 30;
1711 StringBuilder sb
= new StringBuilder();
1712 if ( bold
&& reverse
&& blink
&& !underline
) {
1713 sb
.append("\033[0;1;7;5;");
1714 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1715 sb
.append("\033[0;1;7;");
1716 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1717 sb
.append("\033[0;7;5;");
1718 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1719 sb
.append("\033[0;1;5;");
1720 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1721 sb
.append("\033[0;1;");
1722 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1723 sb
.append("\033[0;7;");
1724 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1725 sb
.append("\033[0;5;");
1726 } else if ( bold
&& reverse
&& blink
&& underline
) {
1727 sb
.append("\033[0;1;7;5;4;");
1728 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1729 sb
.append("\033[0;1;7;4;");
1730 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1731 sb
.append("\033[0;7;5;4;");
1732 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1733 sb
.append("\033[0;1;5;4;");
1734 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1735 sb
.append("\033[0;1;4;");
1736 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1737 sb
.append("\033[0;7;4;");
1738 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1739 sb
.append("\033[0;5;4;");
1740 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1741 sb
.append("\033[0;4;");
1743 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1744 sb
.append("\033[0;");
1746 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1747 sb
.append(rgbColor(bold
, foreColor
, backColor
));
1748 return sb
.toString();
1752 * Create a SGR parameter sequence to reset to defaults.
1754 * @return the string to emit to an ANSI / ECMA-style terminal,
1757 private String
normal() {
1758 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
1762 * Create a SGR parameter sequence to reset to defaults.
1764 * @param header if true, make the full header, otherwise just emit the
1765 * bare parameter e.g. "0;"
1766 * @return the string to emit to an ANSI / ECMA-style terminal,
1769 private String
normal(final boolean header
) {
1771 return "\033[0;37;40m";
1777 * Create a SGR parameter sequence for enabling the visible cursor.
1779 * @param on if true, turn on cursor
1780 * @return the string to emit to an ANSI / ECMA-style terminal
1782 private String
cursor(final boolean on
) {
1783 if (on
&& !cursorOn
) {
1787 if (!on
&& cursorOn
) {
1795 * Clear the entire screen. Because some terminals use back-color-erase,
1796 * set the color to white-on-black beforehand.
1798 * @return the string to emit to an ANSI / ECMA-style terminal
1800 private String
clearAll() {
1801 return "\033[0;37;40m\033[2J";
1805 * Clear the line from the cursor (inclusive) to the end of the screen.
1806 * Because some terminals use back-color-erase, set the color to
1807 * white-on-black beforehand.
1809 * @return the string to emit to an ANSI / ECMA-style terminal
1811 private String
clearRemainingLine() {
1812 return "\033[0;37;40m\033[K";
1816 * Move the cursor to (x, y).
1818 * @param x column coordinate. 0 is the left-most column.
1819 * @param y row coordinate. 0 is the top-most row.
1820 * @return the string to emit to an ANSI / ECMA-style terminal
1822 private String
gotoXY(final int x
, final int y
) {
1823 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1827 * Tell (u)xterm that we want to receive mouse events based on "Any event
1828 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1829 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1831 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1833 * Note that this also sets the alternate/primary screen buffer.
1835 * @param on If true, enable mouse report and use the alternate screen
1836 * buffer. If false disable mouse reporting and use the primary screen
1838 * @return the string to emit to xterm
1840 private String
mouse(final boolean on
) {
1842 return "\033[?1002;1003;1005;1006h\033[?1049h";
1844 return "\033[?1002;1003;1006;1005l\033[?1049l";
1848 * Read function runs on a separate thread.
1851 boolean done
= false;
1852 // available() will often return > 1, so we need to read in chunks to
1854 char [] readBuffer
= new char[128];
1855 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1857 while (!done
&& !stopReaderThread
) {
1859 // We assume that if inputStream has bytes available, then
1860 // input won't block on read().
1861 int n
= inputStream
.available();
1863 if (readBuffer
.length
< n
) {
1864 // The buffer wasn't big enough, make it huger
1865 readBuffer
= new char[readBuffer
.length
* 2];
1868 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1869 // System.err.printf("read() %d", rc); System.err.flush();
1874 for (int i
= 0; i
< rc
; i
++) {
1875 int ch
= readBuffer
[i
];
1876 processChar(events
, (char)ch
);
1878 getIdleEvents(events
);
1879 if (events
.size() > 0) {
1880 // Add to the queue for the backend thread to
1881 // be able to obtain.
1882 synchronized (eventQueue
) {
1883 eventQueue
.addAll(events
);
1885 synchronized (listener
) {
1886 listener
.notifyAll();
1892 getIdleEvents(events
);
1893 if (events
.size() > 0) {
1894 synchronized (eventQueue
) {
1895 eventQueue
.addAll(events
);
1898 synchronized (listener
) {
1899 listener
.notifyAll();
1903 // Wait 10 millis for more data
1906 // System.err.println("end while loop"); System.err.flush();
1907 } catch (InterruptedException e
) {
1909 } catch (IOException e
) {
1910 e
.printStackTrace();
1913 } // while ((done == false) && (stopReaderThread == false))
1914 // System.err.println("*** run() exiting..."); System.err.flush();