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 * Set listener to a different Object.
201 * @param listener the new listening object that run() wakes up on new
204 public void setListener(final Object listener
) {
205 this.listener
= listener
;
209 * Get the output writer.
213 public PrintWriter
getOutput() {
218 * Check if there are events in the queue.
220 * @return if true, getEvents() has something to return to the backend
222 public boolean hasEvents() {
223 synchronized (eventQueue
) {
224 return (eventQueue
.size() > 0);
229 * Call 'stty' to set cooked mode.
231 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
233 private void sttyCooked() {
238 * Call 'stty' to set raw mode.
240 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
241 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
242 * -parenb cs8 min 1 < /dev/tty'
244 private void sttyRaw() {
249 * Call 'stty' to set raw or cooked mode.
251 * @param mode if true, set raw mode, otherwise set cooked mode
253 private void doStty(final boolean mode
) {
255 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
257 String
[] cmdCooked
= {
258 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
263 process
= Runtime
.getRuntime().exec(cmdRaw
);
265 process
= Runtime
.getRuntime().exec(cmdCooked
);
267 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
268 String line
= in
.readLine();
269 if ((line
!= null) && (line
.length() > 0)) {
270 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
273 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
274 line
= err
.readLine();
275 if ((line
!= null) && (line
.length() > 0)) {
276 System
.err
.println("Error output from stty: " + line
);
281 } catch (InterruptedException e
) {
285 int rc
= process
.exitValue();
287 System
.err
.println("stty returned error code: " + rc
);
289 } catch (IOException e
) {
295 * Constructor sets up state for getEvent().
297 * @param listener the object this backend needs to wake up when new
299 * @param input an InputStream connected to the remote user, or null for
300 * System.in. If System.in is used, then on non-Windows systems it will
301 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
302 * mode. input is always converted to a Reader with UTF-8 encoding.
303 * @param output an OutputStream connected to the remote user, or null
304 * for System.out. output is always converted to a Writer with UTF-8
306 * @throws UnsupportedEncodingException if an exception is thrown when
307 * creating the InputStreamReader
309 public ECMA48Terminal(final Object listener
, final InputStream input
,
310 final OutputStream output
) throws UnsupportedEncodingException
{
316 stopReaderThread
= false;
317 this.listener
= listener
;
320 // inputStream = System.in;
321 inputStream
= new FileInputStream(FileDescriptor
.in
);
327 this.input
= new InputStreamReader(inputStream
, "UTF-8");
329 if (input
instanceof SessionInfo
) {
330 // This is a TelnetInputStream that exposes window size and
331 // environment variables from the telnet layer.
332 sessionInfo
= (SessionInfo
) input
;
334 if (sessionInfo
== null) {
336 // Reading right off the tty
337 sessionInfo
= new TTYSessionInfo();
339 sessionInfo
= new TSessionInfo();
343 if (output
== null) {
344 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
347 this.output
= new PrintWriter(new OutputStreamWriter(output
,
351 // Enable mouse reporting and metaSendsEscape
352 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
355 // Hang onto the window size
356 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
357 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
359 // Permit RGB colors only if externally requested
360 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
361 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
366 // Spin up the input reader
367 eventQueue
= new LinkedList
<TInputEvent
>();
368 readerThread
= new Thread(this);
369 readerThread
.start();
371 // Query the screen size
372 setDimensions(sessionInfo
.getWindowWidth(),
373 sessionInfo
.getWindowHeight());
376 this.output
.write(clearAll());
381 * Constructor sets up state for getEvent().
383 * @param listener the object this backend needs to wake up when new
385 * @param input the InputStream underlying 'reader'. Its available()
386 * method is used to determine if reader.read() will block or not.
387 * @param reader a Reader connected to the remote user.
388 * @param writer a PrintWriter connected to the remote user.
389 * @param setRawMode if true, set System.in into raw mode with stty.
390 * This should in general not be used. It is here solely for Demo3,
391 * which uses System.in.
392 * @throws IllegalArgumentException if input, reader, or writer are null.
394 public ECMA48Terminal(final Object listener
, final InputStream input
,
395 final Reader reader
, final PrintWriter writer
,
396 final boolean setRawMode
) {
399 throw new IllegalArgumentException("InputStream must be specified");
401 if (reader
== null) {
402 throw new IllegalArgumentException("Reader must be specified");
404 if (writer
== null) {
405 throw new IllegalArgumentException("Writer must be specified");
411 stopReaderThread
= false;
412 this.listener
= listener
;
417 if (setRawMode
== true) {
420 this.setRawMode
= setRawMode
;
422 if (input
instanceof SessionInfo
) {
423 // This is a TelnetInputStream that exposes window size and
424 // environment variables from the telnet layer.
425 sessionInfo
= (SessionInfo
) input
;
427 if (sessionInfo
== null) {
428 if (setRawMode
== true) {
429 // Reading right off the tty
430 sessionInfo
= new TTYSessionInfo();
432 sessionInfo
= new TSessionInfo();
436 this.output
= writer
;
438 // Enable mouse reporting and metaSendsEscape
439 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
442 // Hang onto the window size
443 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
444 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
446 // Permit RGB colors only if externally requested
447 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
448 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
453 // Spin up the input reader
454 eventQueue
= new LinkedList
<TInputEvent
>();
455 readerThread
= new Thread(this);
456 readerThread
.start();
458 // Query the screen size
459 setDimensions(sessionInfo
.getWindowWidth(),
460 sessionInfo
.getWindowHeight());
463 this.output
.write(clearAll());
468 * Constructor sets up state for getEvent().
470 * @param listener the object this backend needs to wake up when new
472 * @param input the InputStream underlying 'reader'. Its available()
473 * method is used to determine if reader.read() will block or not.
474 * @param reader a Reader connected to the remote user.
475 * @param writer a PrintWriter connected to the remote user.
476 * @throws IllegalArgumentException if input, reader, or writer are null.
478 public ECMA48Terminal(final Object listener
, final InputStream input
,
479 final Reader reader
, final PrintWriter writer
) {
481 this(listener
, input
, reader
, writer
, false);
485 * Restore terminal to normal state.
487 public void closeTerminal() {
489 // System.err.println("=== shutdown() ==="); System.err.flush();
491 // Tell the reader thread to stop looking at input
492 stopReaderThread
= true;
495 } catch (InterruptedException e
) {
499 // Disable mouse reporting and show cursor
500 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
506 // We don't close System.in/out
508 // Shut down the streams, this should wake up the reader thread
515 if (output
!= null) {
519 } catch (IOException e
) {
528 public void flush() {
533 * Perform a somewhat-optimal rendering of a line.
535 * @param y row coordinate. 0 is the top-most row.
536 * @param sb StringBuilder to write escape sequences to
537 * @param lastAttr cell attributes from the last call to flushLine
539 private void flushLine(final int y
, final StringBuilder sb
,
540 CellAttributes lastAttr
) {
544 for (int x
= 0; x
< width
; x
++) {
545 Cell lCell
= logical
[x
][y
];
546 if (!lCell
.isBlank()) {
550 // Push textEnd to first column beyond the text area
554 // reallyCleared = true;
556 for (int x
= 0; x
< width
; x
++) {
557 Cell lCell
= logical
[x
][y
];
558 Cell pCell
= physical
[x
][y
];
560 if (!lCell
.equals(pCell
) || reallyCleared
) {
563 System
.err
.printf("\n--\n");
564 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
565 System
.err
.printf(" lCell: %s\n", lCell
);
566 System
.err
.printf(" pCell: %s\n", pCell
);
567 System
.err
.printf(" ==== \n");
570 if (lastAttr
== null) {
571 lastAttr
= new CellAttributes();
576 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
577 // Advancing at least one cell, or the first gotoXY
578 sb
.append(gotoXY(x
, y
));
581 assert (lastAttr
!= null);
583 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
584 assert (lCell
.isBlank());
586 for (int i
= x
; i
< width
; i
++) {
587 assert (logical
[i
][y
].isBlank());
588 // Physical is always updated
589 physical
[i
][y
].reset();
592 // Clear remaining line
593 sb
.append(clearRemainingLine());
598 // Now emit only the modified attributes
599 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
600 && (lCell
.getBackColor() != lastAttr
.getBackColor())
601 && (lCell
.isBold() == lastAttr
.isBold())
602 && (lCell
.isReverse() == lastAttr
.isReverse())
603 && (lCell
.isUnderline() == lastAttr
.isUnderline())
604 && (lCell
.isBlink() == lastAttr
.isBlink())
606 // Both colors changed, attributes the same
607 sb
.append(color(lCell
.isBold(),
608 lCell
.getForeColor(), lCell
.getBackColor()));
611 System
.err
.printf("1 Change only fore/back colors\n");
613 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
614 && (lCell
.getBackColor() != lastAttr
.getBackColor())
615 && (lCell
.isBold() != lastAttr
.isBold())
616 && (lCell
.isReverse() != lastAttr
.isReverse())
617 && (lCell
.isUnderline() != lastAttr
.isUnderline())
618 && (lCell
.isBlink() != lastAttr
.isBlink())
620 // Everything is different
621 sb
.append(color(lCell
.getForeColor(),
622 lCell
.getBackColor(),
623 lCell
.isBold(), lCell
.isReverse(),
625 lCell
.isUnderline()));
628 System
.err
.printf("2 Set all attributes\n");
630 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
631 && (lCell
.getBackColor() == lastAttr
.getBackColor())
632 && (lCell
.isBold() == lastAttr
.isBold())
633 && (lCell
.isReverse() == lastAttr
.isReverse())
634 && (lCell
.isUnderline() == lastAttr
.isUnderline())
635 && (lCell
.isBlink() == lastAttr
.isBlink())
638 // Attributes same, foreColor different
639 sb
.append(color(lCell
.isBold(),
640 lCell
.getForeColor(), true));
643 System
.err
.printf("3 Change foreColor\n");
645 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
646 && (lCell
.getBackColor() != lastAttr
.getBackColor())
647 && (lCell
.isBold() == lastAttr
.isBold())
648 && (lCell
.isReverse() == lastAttr
.isReverse())
649 && (lCell
.isUnderline() == lastAttr
.isUnderline())
650 && (lCell
.isBlink() == lastAttr
.isBlink())
652 // Attributes same, backColor different
653 sb
.append(color(lCell
.isBold(),
654 lCell
.getBackColor(), false));
657 System
.err
.printf("4 Change backColor\n");
659 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
660 && (lCell
.getBackColor() == lastAttr
.getBackColor())
661 && (lCell
.isBold() == lastAttr
.isBold())
662 && (lCell
.isReverse() == lastAttr
.isReverse())
663 && (lCell
.isUnderline() == lastAttr
.isUnderline())
664 && (lCell
.isBlink() == lastAttr
.isBlink())
667 // All attributes the same, just print the char
671 System
.err
.printf("5 Only emit character\n");
674 // Just reset everything again
675 sb
.append(color(lCell
.getForeColor(),
676 lCell
.getBackColor(),
680 lCell
.isUnderline()));
683 System
.err
.printf("6 Change all attributes\n");
686 // Emit the character
687 sb
.append(lCell
.getChar());
689 // Save the last rendered cell
691 lastAttr
.setTo(lCell
);
693 // Physical is always updated
694 physical
[x
][y
].setTo(lCell
);
696 } // if (!lCell.equals(pCell) || (reallyCleared == true))
698 } // for (int x = 0; x < width; x++)
702 * Render the screen to a string that can be emitted to something that
703 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
705 * @return escape sequences string that provides the updates to the
708 private String
flushString() {
710 assert (!reallyCleared
);
714 CellAttributes attr
= null;
716 StringBuilder sb
= new StringBuilder();
718 attr
= new CellAttributes();
719 sb
.append(clearAll());
722 for (int y
= 0; y
< height
; y
++) {
723 flushLine(y
, sb
, attr
);
727 reallyCleared
= false;
729 String result
= sb
.toString();
731 System
.err
.printf("flushString(): %s\n", result
);
737 * Push the logical screen to the physical device.
740 public void flushPhysical() {
741 String result
= flushString();
743 && (cursorY
<= height
- 1)
744 && (cursorX
<= width
- 1)
746 result
+= cursor(true);
747 result
+= gotoXY(cursorX
, cursorY
);
749 result
+= cursor(false);
751 output
.write(result
);
756 * Set the window title.
758 * @param title the new title
760 public void setTitle(final String title
) {
761 output
.write(getSetTitleString(title
));
766 * Reset keyboard/mouse input parser.
768 private void resetParser() {
769 state
= ParseState
.GROUND
;
770 params
= new ArrayList
<String
>();
776 * Produce a control character or one of the special ones (ENTER, TAB,
779 * @param ch Unicode code point
780 * @param alt if true, set alt on the TKeypress
781 * @return one TKeypress event, either a control character (e.g. isKey ==
782 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
785 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
786 // System.err.printf("controlChar: %02x\n", ch);
790 // Carriage return --> ENTER
791 return new TKeypressEvent(kbEnter
, alt
, false, false);
793 // Linefeed --> ENTER
794 return new TKeypressEvent(kbEnter
, alt
, false, false);
797 return new TKeypressEvent(kbEsc
, alt
, false, false);
800 return new TKeypressEvent(kbTab
, alt
, false, false);
802 // Make all other control characters come back as the alphabetic
803 // character with the ctrl field set. So SOH would be 'A' +
805 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
811 * Produce special key from CSI Pn ; Pm ; ... ~
813 * @return one KEYPRESS event representing a special key
815 private TInputEvent
csiFnKey() {
817 if (params
.size() > 0) {
818 key
= Integer
.parseInt(params
.get(0));
821 boolean ctrl
= false;
822 boolean shift
= false;
823 if (params
.size() > 1) {
824 shift
= csiIsShift(params
.get(1));
825 alt
= csiIsAlt(params
.get(1));
826 ctrl
= csiIsCtrl(params
.get(1));
831 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
833 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
835 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
837 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
839 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
841 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
843 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
845 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
847 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
849 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
851 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
853 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
855 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
857 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
865 * Produce mouse events based on "Any event tracking" and UTF-8
867 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
869 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
871 private TInputEvent
parseMouse() {
872 int buttons
= params
.get(0).charAt(0) - 32;
873 int x
= params
.get(0).charAt(1) - 32 - 1;
874 int y
= params
.get(0).charAt(2) - 32 - 1;
876 // Clamp X and Y to the physical screen coordinates.
877 if (x
>= windowResize
.getWidth()) {
878 x
= windowResize
.getWidth() - 1;
880 if (y
>= windowResize
.getHeight()) {
881 y
= windowResize
.getHeight() - 1;
884 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
885 boolean eventMouse1
= false;
886 boolean eventMouse2
= false;
887 boolean eventMouse3
= false;
888 boolean eventMouseWheelUp
= false;
889 boolean eventMouseWheelDown
= false;
891 // System.err.printf("buttons: %04x\r\n", buttons);
908 if (!mouse1
&& !mouse2
&& !mouse3
) {
909 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
911 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
928 // Dragging with mouse1 down
931 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
935 // Dragging with mouse2 down
938 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
942 // Dragging with mouse3 down
945 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
949 // Dragging with mouse2 down after wheelUp
952 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
956 // Dragging with mouse2 down after wheelDown
959 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
963 eventMouseWheelUp
= true;
967 eventMouseWheelDown
= true;
971 // Unknown, just make it motion
972 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
975 return new TMouseEvent(eventType
, x
, y
, x
, y
,
976 eventMouse1
, eventMouse2
, eventMouse3
,
977 eventMouseWheelUp
, eventMouseWheelDown
);
981 * Produce mouse events based on "Any event tracking" and SGR
983 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
985 * @param release if true, this was a release ('m')
986 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
988 private TInputEvent
parseMouseSGR(final boolean release
) {
989 // SGR extended coordinates - mode 1006
990 if (params
.size() < 3) {
991 // Invalid position, bail out.
994 int buttons
= Integer
.parseInt(params
.get(0));
995 int x
= Integer
.parseInt(params
.get(1)) - 1;
996 int y
= Integer
.parseInt(params
.get(2)) - 1;
998 // Clamp X and Y to the physical screen coordinates.
999 if (x
>= windowResize
.getWidth()) {
1000 x
= windowResize
.getWidth() - 1;
1002 if (y
>= windowResize
.getHeight()) {
1003 y
= windowResize
.getHeight() - 1;
1006 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
1007 boolean eventMouse1
= false;
1008 boolean eventMouse2
= false;
1009 boolean eventMouse3
= false;
1010 boolean eventMouseWheelUp
= false;
1011 boolean eventMouseWheelDown
= false;
1014 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
1028 // Motion only, no buttons down
1029 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1033 // Dragging with mouse1 down
1035 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1039 // Dragging with mouse2 down
1041 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1045 // Dragging with mouse3 down
1047 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1051 // Dragging with mouse2 down after wheelUp
1053 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1057 // Dragging with mouse2 down after wheelDown
1059 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1063 eventMouseWheelUp
= true;
1067 eventMouseWheelDown
= true;
1071 // Unknown, bail out
1074 return new TMouseEvent(eventType
, x
, y
, x
, y
,
1075 eventMouse1
, eventMouse2
, eventMouse3
,
1076 eventMouseWheelUp
, eventMouseWheelDown
);
1080 * Return any events in the IO queue.
1082 * @param queue list to append new events to
1084 public void getEvents(final List
<TInputEvent
> queue
) {
1085 synchronized (eventQueue
) {
1086 if (eventQueue
.size() > 0) {
1087 synchronized (queue
) {
1088 queue
.addAll(eventQueue
);
1096 * Return any events in the IO queue due to timeout.
1098 * @param queue list to append new events to
1100 private void getIdleEvents(final List
<TInputEvent
> queue
) {
1101 Date now
= new Date();
1103 // Check for new window size
1104 long windowSizeDelay
= now
.getTime() - windowSizeTime
;
1105 if (windowSizeDelay
> 1000) {
1106 sessionInfo
.queryWindowSize();
1107 int newWidth
= sessionInfo
.getWindowWidth();
1108 int newHeight
= sessionInfo
.getWindowHeight();
1109 if ((newWidth
!= windowResize
.getWidth())
1110 || (newHeight
!= windowResize
.getHeight())
1112 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1113 newWidth
, newHeight
);
1114 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1115 newWidth
, newHeight
);
1118 windowSizeTime
= now
.getTime();
1121 // ESCDELAY type timeout
1122 if (state
== ParseState
.ESCAPE
) {
1123 long escDelay
= now
.getTime() - escapeTime
;
1124 if (escDelay
> 100) {
1125 // After 0.1 seconds, assume a true escape character
1126 queue
.add(controlChar((char)0x1B, false));
1133 * Returns true if the CSI parameter for a keyboard command means that
1136 private boolean csiIsShift(final String x
) {
1148 * Returns true if the CSI parameter for a keyboard command means that
1151 private boolean csiIsAlt(final String x
) {
1163 * Returns true if the CSI parameter for a keyboard command means that
1166 private boolean csiIsCtrl(final String x
) {
1178 * Parses the next character of input to see if an InputEvent is
1181 * @param events list to append new events to
1182 * @param ch Unicode code point
1184 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
1186 // ESCDELAY type timeout
1187 Date now
= new Date();
1188 if (state
== ParseState
.ESCAPE
) {
1189 long escDelay
= now
.getTime() - escapeTime
;
1190 if (escDelay
> 250) {
1191 // After 0.25 seconds, assume a true escape character
1192 events
.add(controlChar((char)0x1B, false));
1198 boolean ctrl
= false;
1199 boolean alt
= false;
1200 boolean shift
= false;
1202 // System.err.printf("state: %s ch %c\r\n", state, ch);
1208 state
= ParseState
.ESCAPE
;
1209 escapeTime
= now
.getTime();
1214 // Control character
1215 events
.add(controlChar(ch
, false));
1222 events
.add(new TKeypressEvent(false, 0, ch
,
1223 false, false, false));
1232 // ALT-Control character
1233 events
.add(controlChar(ch
, true));
1239 // This will be one of the function keys
1240 state
= ParseState
.ESCAPE_INTERMEDIATE
;
1244 // '[' goes to CSI_ENTRY
1246 state
= ParseState
.CSI_ENTRY
;
1250 // Everything else is assumed to be Alt-keystroke
1251 if ((ch
>= 'A') && (ch
<= 'Z')) {
1255 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
1259 case ESCAPE_INTERMEDIATE
:
1260 if ((ch
>= 'P') && (ch
<= 'S')) {
1264 events
.add(new TKeypressEvent(kbF1
));
1267 events
.add(new TKeypressEvent(kbF2
));
1270 events
.add(new TKeypressEvent(kbF3
));
1273 events
.add(new TKeypressEvent(kbF4
));
1282 // Unknown keystroke, ignore
1287 // Numbers - parameter values
1288 if ((ch
>= '0') && (ch
<= '9')) {
1289 params
.set(params
.size() - 1,
1290 params
.get(params
.size() - 1) + ch
);
1291 state
= ParseState
.CSI_PARAM
;
1294 // Parameter separator
1300 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1304 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1309 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1314 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1319 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1324 events
.add(new TKeypressEvent(kbHome
));
1329 events
.add(new TKeypressEvent(kbEnd
));
1333 // CBT - Cursor backward X tab stops (default 1)
1334 events
.add(new TKeypressEvent(kbBackTab
));
1339 state
= ParseState
.MOUSE
;
1342 // Mouse position, SGR (1006) coordinates
1343 state
= ParseState
.MOUSE_SGR
;
1350 // Unknown keystroke, ignore
1355 // Numbers - parameter values
1356 if ((ch
>= '0') && (ch
<= '9')) {
1357 params
.set(params
.size() - 1,
1358 params
.get(params
.size() - 1) + ch
);
1361 // Parameter separator
1369 // Generate a mouse press event
1370 TInputEvent event
= parseMouseSGR(false);
1371 if (event
!= null) {
1377 // Generate a mouse release event
1378 event
= parseMouseSGR(true);
1379 if (event
!= null) {
1388 // Unknown keystroke, ignore
1393 // Numbers - parameter values
1394 if ((ch
>= '0') && (ch
<= '9')) {
1395 params
.set(params
.size() - 1,
1396 params
.get(params
.size() - 1) + ch
);
1397 state
= ParseState
.CSI_PARAM
;
1400 // Parameter separator
1407 events
.add(csiFnKey());
1412 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
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(kbUp
, 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(kbDown
, 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(kbRight
, 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(kbLeft
, 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(kbHome
, alt
, ctrl
, shift
));
1466 if (params
.size() > 1) {
1467 shift
= csiIsShift(params
.get(1));
1468 alt
= csiIsAlt(params
.get(1));
1469 ctrl
= csiIsCtrl(params
.get(1));
1471 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
1479 // Unknown keystroke, ignore
1484 params
.set(0, params
.get(params
.size() - 1) + ch
);
1485 if (params
.get(0).length() == 3) {
1486 // We have enough to generate a mouse event
1487 events
.add(parseMouse());
1496 // This "should" be impossible to reach
1501 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1502 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1505 * @param on if true, enable metaSendsEscape
1506 * @return the string to emit to xterm
1508 private String
xtermMetaSendsEscape(final boolean on
) {
1510 return "\033[?1036h\033[?1034l";
1512 return "\033[?1036l";
1516 * Create an xterm OSC sequence to change the window title.
1518 * @param title the new title
1519 * @return the string to emit to xterm
1521 private String
getSetTitleString(final String title
) {
1522 return "\033]2;" + title
+ "\007";
1526 * Create a SGR parameter sequence for a single color change.
1528 * @param bold if true, set bold
1529 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1530 * @param foreground if true, this is a foreground color
1531 * @return the string to emit to an ANSI / ECMA-style terminal,
1534 private String
color(final boolean bold
, final Color color
,
1535 final boolean foreground
) {
1536 return color(color
, foreground
, true) +
1537 rgbColor(bold
, color
, foreground
);
1541 * Create a T.416 RGB parameter sequence for a single color change.
1543 * @param bold if true, set bold
1544 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1545 * @param foreground if true, this is a foreground color
1546 * @return the string to emit to an xterm terminal with RGB support,
1547 * e.g. "\033[38;2;RR;GG;BBm"
1549 private String
rgbColor(final boolean bold
, final Color color
,
1550 final boolean foreground
) {
1551 if (doRgbColor
== false) {
1554 StringBuilder sb
= new StringBuilder("\033[");
1556 // Bold implies foreground only
1558 if (color
.equals(Color
.BLACK
)) {
1559 sb
.append("84;84;84");
1560 } else if (color
.equals(Color
.RED
)) {
1561 sb
.append("252;84;84");
1562 } else if (color
.equals(Color
.GREEN
)) {
1563 sb
.append("84;252;84");
1564 } else if (color
.equals(Color
.YELLOW
)) {
1565 sb
.append("252;252;84");
1566 } else if (color
.equals(Color
.BLUE
)) {
1567 sb
.append("84;84;252");
1568 } else if (color
.equals(Color
.MAGENTA
)) {
1569 sb
.append("252;84;252");
1570 } else if (color
.equals(Color
.CYAN
)) {
1571 sb
.append("84;252;252");
1572 } else if (color
.equals(Color
.WHITE
)) {
1573 sb
.append("252;252;252");
1581 if (color
.equals(Color
.BLACK
)) {
1583 } else if (color
.equals(Color
.RED
)) {
1584 sb
.append("168;0;0");
1585 } else if (color
.equals(Color
.GREEN
)) {
1586 sb
.append("0;168;0");
1587 } else if (color
.equals(Color
.YELLOW
)) {
1588 sb
.append("168;84;0");
1589 } else if (color
.equals(Color
.BLUE
)) {
1590 sb
.append("0;0;168");
1591 } else if (color
.equals(Color
.MAGENTA
)) {
1592 sb
.append("168;0;168");
1593 } else if (color
.equals(Color
.CYAN
)) {
1594 sb
.append("0;168;168");
1595 } else if (color
.equals(Color
.WHITE
)) {
1596 sb
.append("168;168;168");
1600 return sb
.toString();
1604 * Create a T.416 RGB parameter sequence for both foreground and
1605 * background color change.
1607 * @param bold if true, set bold
1608 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1609 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1610 * @return the string to emit to an xterm terminal with RGB support,
1611 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
1613 private String
rgbColor(final boolean bold
, final Color foreColor
,
1614 final Color backColor
) {
1615 if (doRgbColor
== false) {
1619 return rgbColor(bold
, foreColor
, true) +
1620 rgbColor(false, backColor
, false);
1624 * Create a SGR parameter sequence for a single color change.
1626 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1627 * @param foreground if true, this is a foreground color
1628 * @param header if true, make the full header, otherwise just emit the
1629 * color parameter e.g. "42;"
1630 * @return the string to emit to an ANSI / ECMA-style terminal,
1633 private String
color(final Color color
, final boolean foreground
,
1634 final boolean header
) {
1636 int ecmaColor
= color
.getValue();
1638 // Convert Color.* values to SGR numerics
1646 return String
.format("\033[%dm", ecmaColor
);
1648 return String
.format("%d;", ecmaColor
);
1653 * Create a SGR parameter sequence for both foreground and background
1656 * @param bold if true, set bold
1657 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1658 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1659 * @return the string to emit to an ANSI / ECMA-style terminal,
1660 * e.g. "\033[31;42m"
1662 private String
color(final boolean bold
, final Color foreColor
,
1663 final Color backColor
) {
1664 return color(foreColor
, backColor
, true) +
1665 rgbColor(bold
, foreColor
, backColor
);
1669 * Create a SGR parameter sequence for both foreground and
1670 * background color change.
1672 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1673 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1674 * @param header if true, make the full header, otherwise just emit the
1675 * color parameter e.g. "31;42;"
1676 * @return the string to emit to an ANSI / ECMA-style terminal,
1677 * e.g. "\033[31;42m"
1679 private String
color(final Color foreColor
, final Color backColor
,
1680 final boolean header
) {
1682 int ecmaForeColor
= foreColor
.getValue();
1683 int ecmaBackColor
= backColor
.getValue();
1685 // Convert Color.* values to SGR numerics
1686 ecmaBackColor
+= 40;
1687 ecmaForeColor
+= 30;
1690 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1692 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1697 * Create a SGR parameter sequence for foreground, background, and
1698 * several attributes. This sequence first resets all attributes to
1699 * default, then sets attributes as per the parameters.
1701 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1702 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1703 * @param bold if true, set bold
1704 * @param reverse if true, set reverse
1705 * @param blink if true, set blink
1706 * @param underline if true, set underline
1707 * @return the string to emit to an ANSI / ECMA-style terminal,
1708 * e.g. "\033[0;1;31;42m"
1710 private String
color(final Color foreColor
, final Color backColor
,
1711 final boolean bold
, final boolean reverse
, final boolean blink
,
1712 final boolean underline
) {
1714 int ecmaForeColor
= foreColor
.getValue();
1715 int ecmaBackColor
= backColor
.getValue();
1717 // Convert Color.* values to SGR numerics
1718 ecmaBackColor
+= 40;
1719 ecmaForeColor
+= 30;
1721 StringBuilder sb
= new StringBuilder();
1722 if ( bold
&& reverse
&& blink
&& !underline
) {
1723 sb
.append("\033[0;1;7;5;");
1724 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1725 sb
.append("\033[0;1;7;");
1726 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1727 sb
.append("\033[0;7;5;");
1728 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1729 sb
.append("\033[0;1;5;");
1730 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1731 sb
.append("\033[0;1;");
1732 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1733 sb
.append("\033[0;7;");
1734 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1735 sb
.append("\033[0;5;");
1736 } else if ( bold
&& reverse
&& blink
&& underline
) {
1737 sb
.append("\033[0;1;7;5;4;");
1738 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1739 sb
.append("\033[0;1;7;4;");
1740 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1741 sb
.append("\033[0;7;5;4;");
1742 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1743 sb
.append("\033[0;1;5;4;");
1744 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1745 sb
.append("\033[0;1;4;");
1746 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1747 sb
.append("\033[0;7;4;");
1748 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1749 sb
.append("\033[0;5;4;");
1750 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1751 sb
.append("\033[0;4;");
1753 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1754 sb
.append("\033[0;");
1756 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1757 sb
.append(rgbColor(bold
, foreColor
, backColor
));
1758 return sb
.toString();
1762 * Create a SGR parameter sequence to reset to defaults.
1764 * @return the string to emit to an ANSI / ECMA-style terminal,
1767 private String
normal() {
1768 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
1772 * Create a SGR parameter sequence to reset to defaults.
1774 * @param header if true, make the full header, otherwise just emit the
1775 * bare parameter e.g. "0;"
1776 * @return the string to emit to an ANSI / ECMA-style terminal,
1779 private String
normal(final boolean header
) {
1781 return "\033[0;37;40m";
1787 * Create a SGR parameter sequence for enabling the visible cursor.
1789 * @param on if true, turn on cursor
1790 * @return the string to emit to an ANSI / ECMA-style terminal
1792 private String
cursor(final boolean on
) {
1793 if (on
&& !cursorOn
) {
1797 if (!on
&& cursorOn
) {
1805 * Clear the entire screen. Because some terminals use back-color-erase,
1806 * set the color to white-on-black beforehand.
1808 * @return the string to emit to an ANSI / ECMA-style terminal
1810 private String
clearAll() {
1811 return "\033[0;37;40m\033[2J";
1815 * Clear the line from the cursor (inclusive) to the end of the screen.
1816 * Because some terminals use back-color-erase, set the color to
1817 * white-on-black beforehand.
1819 * @return the string to emit to an ANSI / ECMA-style terminal
1821 private String
clearRemainingLine() {
1822 return "\033[0;37;40m\033[K";
1826 * Move the cursor to (x, y).
1828 * @param x column coordinate. 0 is the left-most column.
1829 * @param y row coordinate. 0 is the top-most row.
1830 * @return the string to emit to an ANSI / ECMA-style terminal
1832 private String
gotoXY(final int x
, final int y
) {
1833 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1837 * Tell (u)xterm that we want to receive mouse events based on "Any event
1838 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1839 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1841 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1843 * Note that this also sets the alternate/primary screen buffer.
1845 * @param on If true, enable mouse report and use the alternate screen
1846 * buffer. If false disable mouse reporting and use the primary screen
1848 * @return the string to emit to xterm
1850 private String
mouse(final boolean on
) {
1852 return "\033[?1002;1003;1005;1006h\033[?1049h";
1854 return "\033[?1002;1003;1006;1005l\033[?1049l";
1858 * Read function runs on a separate thread.
1861 boolean done
= false;
1862 // available() will often return > 1, so we need to read in chunks to
1864 char [] readBuffer
= new char[128];
1865 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1867 while (!done
&& !stopReaderThread
) {
1869 // We assume that if inputStream has bytes available, then
1870 // input won't block on read().
1871 int n
= inputStream
.available();
1873 if (readBuffer
.length
< n
) {
1874 // The buffer wasn't big enough, make it huger
1875 readBuffer
= new char[readBuffer
.length
* 2];
1878 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1879 // System.err.printf("read() %d", rc); System.err.flush();
1884 for (int i
= 0; i
< rc
; i
++) {
1885 int ch
= readBuffer
[i
];
1886 processChar(events
, (char)ch
);
1888 getIdleEvents(events
);
1889 if (events
.size() > 0) {
1890 // Add to the queue for the backend thread to
1891 // be able to obtain.
1892 synchronized (eventQueue
) {
1893 eventQueue
.addAll(events
);
1895 synchronized (listener
) {
1896 listener
.notifyAll();
1902 getIdleEvents(events
);
1903 if (events
.size() > 0) {
1904 synchronized (eventQueue
) {
1905 eventQueue
.addAll(events
);
1908 synchronized (listener
) {
1909 listener
.notifyAll();
1913 // Wait 10 millis for more data
1916 // System.err.println("end while loop"); System.err.flush();
1917 } catch (InterruptedException e
) {
1919 } catch (IOException e
) {
1920 e
.printStackTrace();
1923 } // while ((done == false) && (stopReaderThread == false))
1924 // System.err.println("*** run() exiting..."); System.err.flush();