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
.List
;
44 import java
.util
.LinkedList
;
46 import jexer
.bits
.Cell
;
47 import jexer
.bits
.CellAttributes
;
48 import jexer
.bits
.Color
;
49 import jexer
.event
.TInputEvent
;
50 import jexer
.event
.TKeypressEvent
;
51 import jexer
.event
.TMouseEvent
;
52 import jexer
.event
.TResizeEvent
;
53 import static jexer
.TKeypress
.*;
56 * This class reads keystrokes and mouse events and emits output to ANSI
57 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
59 public class ECMA48Terminal
extends LogicalScreen
60 implements TerminalReader
, Runnable
{
62 // ------------------------------------------------------------------------
63 // Constants --------------------------------------------------------------
64 // ------------------------------------------------------------------------
67 * States in the input parser.
69 private enum ParseState
{
79 // ------------------------------------------------------------------------
80 // Variables --------------------------------------------------------------
81 // ------------------------------------------------------------------------
84 * Emit debugging to stderr.
86 private boolean debugToStderr
= false;
89 * If true, emit T.416-style RGB colors for normal system colors. This
90 * is a) expensive in bandwidth, and b) potentially terrible looking for
93 private static boolean doRgbColor
= false;
96 * The session information.
98 private SessionInfo sessionInfo
;
101 * The event queue, filled up by a thread reading on input.
103 private List
<TInputEvent
> eventQueue
;
106 * If true, we want the reader thread to exit gracefully.
108 private boolean stopReaderThread
;
113 private Thread readerThread
;
116 * Parameters being collected. E.g. if the string is \033[1;3m, then
117 * params[0] will be 1 and params[1] will be 3.
119 private List
<String
> params
;
122 * Current parsing state.
124 private ParseState state
;
127 * The time we entered ESCAPE. If we get a bare escape without a code
128 * following it, this is used to return that bare escape.
130 private long escapeTime
;
133 * The time we last checked the window size. We try not to spawn stty
134 * more than once per second.
136 private long windowSizeTime
;
139 * true if mouse1 was down. Used to report mouse1 on the release event.
141 private boolean mouse1
;
144 * true if mouse2 was down. Used to report mouse2 on the release event.
146 private boolean mouse2
;
149 * true if mouse3 was down. Used to report mouse3 on the release event.
151 private boolean mouse3
;
154 * Cache the cursor visibility value so we only emit the sequence when we
157 private boolean cursorOn
= true;
160 * Cache the last window size to figure out if a TResizeEvent needs to be
163 private TResizeEvent windowResize
= null;
166 * If true, then we changed System.in and need to change it back.
168 private boolean setRawMode
;
171 * The terminal's input. If an InputStream is not specified in the
172 * constructor, then this InputStreamReader will be bound to System.in
173 * with UTF-8 encoding.
175 private Reader input
;
178 * The terminal's raw InputStream. If an InputStream is not specified in
179 * the constructor, then this InputReader will be bound to System.in.
180 * This is used by run() to see if bytes are available() before calling
181 * (Reader)input.read().
183 private InputStream inputStream
;
186 * The terminal's output. If an OutputStream is not specified in the
187 * constructor, then this PrintWriter will be bound to System.out with
190 private PrintWriter output
;
193 * The listening object that run() wakes up on new input.
195 private Object listener
;
197 // ------------------------------------------------------------------------
198 // Constructors -----------------------------------------------------------
199 // ------------------------------------------------------------------------
202 * Constructor sets up state for getEvent().
204 * @param listener the object this backend needs to wake up when new
206 * @param input an InputStream connected to the remote user, or null for
207 * System.in. If System.in is used, then on non-Windows systems it will
208 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
209 * mode. input is always converted to a Reader with UTF-8 encoding.
210 * @param output an OutputStream connected to the remote user, or null
211 * for System.out. output is always converted to a Writer with UTF-8
213 * @param windowWidth the number of text columns to start with
214 * @param windowHeight the number of text rows to start with
215 * @throws UnsupportedEncodingException if an exception is thrown when
216 * creating the InputStreamReader
218 public ECMA48Terminal(final Object listener
, final InputStream input
,
219 final OutputStream output
, final int windowWidth
,
220 final int windowHeight
) throws UnsupportedEncodingException
{
222 this(listener
, input
, output
);
224 // Send dtterm/xterm sequences, which will probably not work because
225 // allowWindowOps is defaulted to false.
226 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
228 this.output
.write(resizeString
);
233 * Constructor sets up state for getEvent().
235 * @param listener the object this backend needs to wake up when new
237 * @param input an InputStream connected to the remote user, or null for
238 * System.in. If System.in is used, then on non-Windows systems it will
239 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
240 * mode. input is always converted to a Reader with UTF-8 encoding.
241 * @param output an OutputStream connected to the remote user, or null
242 * for System.out. output is always converted to a Writer with UTF-8
244 * @throws UnsupportedEncodingException if an exception is thrown when
245 * creating the InputStreamReader
247 public ECMA48Terminal(final Object listener
, final InputStream input
,
248 final OutputStream output
) throws UnsupportedEncodingException
{
254 stopReaderThread
= false;
255 this.listener
= listener
;
258 // inputStream = System.in;
259 inputStream
= new FileInputStream(FileDescriptor
.in
);
265 this.input
= new InputStreamReader(inputStream
, "UTF-8");
267 if (input
instanceof SessionInfo
) {
268 // This is a TelnetInputStream that exposes window size and
269 // environment variables from the telnet layer.
270 sessionInfo
= (SessionInfo
) input
;
272 if (sessionInfo
== null) {
274 // Reading right off the tty
275 sessionInfo
= new TTYSessionInfo();
277 sessionInfo
= new TSessionInfo();
281 if (output
== null) {
282 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
285 this.output
= new PrintWriter(new OutputStreamWriter(output
,
289 // Enable mouse reporting and metaSendsEscape
290 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
293 // Query the screen size
294 sessionInfo
.queryWindowSize();
295 setDimensions(sessionInfo
.getWindowWidth(),
296 sessionInfo
.getWindowHeight());
298 // Hang onto the window size
299 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
300 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
302 // Permit RGB colors only if externally requested
303 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
304 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
311 // Spin up the input reader
312 eventQueue
= new LinkedList
<TInputEvent
>();
313 readerThread
= new Thread(this);
314 readerThread
.start();
317 this.output
.write(clearAll());
322 * Constructor sets up state for getEvent().
324 * @param listener the object this backend needs to wake up when new
326 * @param input the InputStream underlying 'reader'. Its available()
327 * method is used to determine if reader.read() will block or not.
328 * @param reader a Reader connected to the remote user.
329 * @param writer a PrintWriter connected to the remote user.
330 * @param setRawMode if true, set System.in into raw mode with stty.
331 * This should in general not be used. It is here solely for Demo3,
332 * which uses System.in.
333 * @throws IllegalArgumentException if input, reader, or writer are null.
335 public ECMA48Terminal(final Object listener
, final InputStream input
,
336 final Reader reader
, final PrintWriter writer
,
337 final boolean setRawMode
) {
340 throw new IllegalArgumentException("InputStream must be specified");
342 if (reader
== null) {
343 throw new IllegalArgumentException("Reader must be specified");
345 if (writer
== null) {
346 throw new IllegalArgumentException("Writer must be specified");
352 stopReaderThread
= false;
353 this.listener
= listener
;
358 if (setRawMode
== true) {
361 this.setRawMode
= setRawMode
;
363 if (input
instanceof SessionInfo
) {
364 // This is a TelnetInputStream that exposes window size and
365 // environment variables from the telnet layer.
366 sessionInfo
= (SessionInfo
) input
;
368 if (sessionInfo
== null) {
369 if (setRawMode
== true) {
370 // Reading right off the tty
371 sessionInfo
= new TTYSessionInfo();
373 sessionInfo
= new TSessionInfo();
377 this.output
= writer
;
379 // Enable mouse reporting and metaSendsEscape
380 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
383 // Query the screen size
384 sessionInfo
.queryWindowSize();
385 setDimensions(sessionInfo
.getWindowWidth(),
386 sessionInfo
.getWindowHeight());
388 // Hang onto the window size
389 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
390 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
392 // Permit RGB colors only if externally requested
393 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
394 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
401 // Spin up the input reader
402 eventQueue
= new LinkedList
<TInputEvent
>();
403 readerThread
= new Thread(this);
404 readerThread
.start();
407 this.output
.write(clearAll());
412 * Constructor sets up state for getEvent().
414 * @param listener the object this backend needs to wake up when new
416 * @param input the InputStream underlying 'reader'. Its available()
417 * method is used to determine if reader.read() will block or not.
418 * @param reader a Reader connected to the remote user.
419 * @param writer a PrintWriter connected to the remote user.
420 * @throws IllegalArgumentException if input, reader, or writer are null.
422 public ECMA48Terminal(final Object listener
, final InputStream input
,
423 final Reader reader
, final PrintWriter writer
) {
425 this(listener
, input
, reader
, writer
, false);
428 // ------------------------------------------------------------------------
429 // LogicalScreen ----------------------------------------------------------
430 // ------------------------------------------------------------------------
433 * Set the window title.
435 * @param title the new title
438 public void setTitle(final String title
) {
439 output
.write(getSetTitleString(title
));
444 * Push the logical screen to the physical device.
447 public void flushPhysical() {
448 String result
= flushString();
452 && (cursorY
<= height
- 1)
453 && (cursorX
<= width
- 1)
455 result
+= cursor(true);
456 result
+= gotoXY(cursorX
, cursorY
);
458 result
+= cursor(false);
460 output
.write(result
);
464 // ------------------------------------------------------------------------
465 // TerminalReader ---------------------------------------------------------
466 // ------------------------------------------------------------------------
469 * Check if there are events in the queue.
471 * @return if true, getEvents() has something to return to the backend
473 public boolean hasEvents() {
474 synchronized (eventQueue
) {
475 return (eventQueue
.size() > 0);
480 * Return any events in the IO queue.
482 * @param queue list to append new events to
484 public void getEvents(final List
<TInputEvent
> queue
) {
485 synchronized (eventQueue
) {
486 if (eventQueue
.size() > 0) {
487 synchronized (queue
) {
488 queue
.addAll(eventQueue
);
496 * Restore terminal to normal state.
498 public void closeTerminal() {
500 // System.err.println("=== shutdown() ==="); System.err.flush();
502 // Tell the reader thread to stop looking at input
503 stopReaderThread
= true;
506 } catch (InterruptedException e
) {
510 // Disable mouse reporting and show cursor. Defensive null check
511 // here in case closeTerminal() is called twice.
512 if (output
!= null) {
513 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
520 // We don't close System.in/out
522 // Shut down the streams, this should wake up the reader thread
529 if (output
!= null) {
533 } catch (IOException e
) {
540 * Set listener to a different Object.
542 * @param listener the new listening object that run() wakes up on new
545 public void setListener(final Object listener
) {
546 this.listener
= listener
;
549 // ------------------------------------------------------------------------
550 // Runnable ---------------------------------------------------------------
551 // ------------------------------------------------------------------------
554 * Read function runs on a separate thread.
557 boolean done
= false;
558 // available() will often return > 1, so we need to read in chunks to
560 char [] readBuffer
= new char[128];
561 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
563 while (!done
&& !stopReaderThread
) {
565 // We assume that if inputStream has bytes available, then
566 // input won't block on read().
567 int n
= inputStream
.available();
570 System.err.printf("inputStream.available(): %d\n", n);
575 if (readBuffer
.length
< n
) {
576 // The buffer wasn't big enough, make it huger
577 readBuffer
= new char[readBuffer
.length
* 2];
580 // System.err.printf("BEFORE read()\n"); System.err.flush();
582 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
585 System.err.printf("AFTER read() %d\n", rc);
593 for (int i
= 0; i
< rc
; i
++) {
594 int ch
= readBuffer
[i
];
595 processChar(events
, (char)ch
);
597 getIdleEvents(events
);
598 if (events
.size() > 0) {
599 // Add to the queue for the backend thread to
600 // be able to obtain.
601 synchronized (eventQueue
) {
602 eventQueue
.addAll(events
);
604 if (listener
!= null) {
605 synchronized (listener
) {
606 listener
.notifyAll();
613 getIdleEvents(events
);
614 if (events
.size() > 0) {
615 synchronized (eventQueue
) {
616 eventQueue
.addAll(events
);
618 if (listener
!= null) {
619 synchronized (listener
) {
620 listener
.notifyAll();
626 // Wait 20 millis for more data
629 // System.err.println("end while loop"); System.err.flush();
630 } catch (InterruptedException e
) {
632 } catch (IOException e
) {
636 } // while ((done == false) && (stopReaderThread == false))
637 // System.err.println("*** run() exiting..."); System.err.flush();
640 // ------------------------------------------------------------------------
641 // ECMA48Terminal ---------------------------------------------------------
642 // ------------------------------------------------------------------------
645 * Getter for sessionInfo.
647 * @return the SessionInfo
649 public SessionInfo
getSessionInfo() {
654 * Get the output writer.
658 public PrintWriter
getOutput() {
663 * Call 'stty' to set cooked mode.
665 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
667 private void sttyCooked() {
672 * Call 'stty' to set raw mode.
674 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
675 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
676 * -parenb cs8 min 1 < /dev/tty'
678 private void sttyRaw() {
683 * Call 'stty' to set raw or cooked mode.
685 * @param mode if true, set raw mode, otherwise set cooked mode
687 private void doStty(final boolean mode
) {
689 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
691 String
[] cmdCooked
= {
692 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
697 process
= Runtime
.getRuntime().exec(cmdRaw
);
699 process
= Runtime
.getRuntime().exec(cmdCooked
);
701 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
702 String line
= in
.readLine();
703 if ((line
!= null) && (line
.length() > 0)) {
704 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
707 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
708 line
= err
.readLine();
709 if ((line
!= null) && (line
.length() > 0)) {
710 System
.err
.println("Error output from stty: " + line
);
715 } catch (InterruptedException e
) {
719 int rc
= process
.exitValue();
721 System
.err
.println("stty returned error code: " + rc
);
723 } catch (IOException e
) {
731 public void flush() {
736 * Perform a somewhat-optimal rendering of a line.
738 * @param y row coordinate. 0 is the top-most row.
739 * @param sb StringBuilder to write escape sequences to
740 * @param lastAttr cell attributes from the last call to flushLine
742 private void flushLine(final int y
, final StringBuilder sb
,
743 CellAttributes lastAttr
) {
747 for (int x
= 0; x
< width
; x
++) {
748 Cell lCell
= logical
[x
][y
];
749 if (!lCell
.isBlank()) {
753 // Push textEnd to first column beyond the text area
757 // reallyCleared = true;
759 for (int x
= 0; x
< width
; x
++) {
760 Cell lCell
= logical
[x
][y
];
761 Cell pCell
= physical
[x
][y
];
763 if (!lCell
.equals(pCell
) || reallyCleared
) {
766 System
.err
.printf("\n--\n");
767 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
768 System
.err
.printf(" lCell: %s\n", lCell
);
769 System
.err
.printf(" pCell: %s\n", pCell
);
770 System
.err
.printf(" ==== \n");
773 if (lastAttr
== null) {
774 lastAttr
= new CellAttributes();
779 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
780 // Advancing at least one cell, or the first gotoXY
781 sb
.append(gotoXY(x
, y
));
784 assert (lastAttr
!= null);
786 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
787 assert (lCell
.isBlank());
789 for (int i
= x
; i
< width
; i
++) {
790 assert (logical
[i
][y
].isBlank());
791 // Physical is always updated
792 physical
[i
][y
].reset();
795 // Clear remaining line
796 sb
.append(clearRemainingLine());
801 // Now emit only the modified attributes
802 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
803 && (lCell
.getBackColor() != lastAttr
.getBackColor())
805 && (lCell
.isBold() == lastAttr
.isBold())
806 && (lCell
.isReverse() == lastAttr
.isReverse())
807 && (lCell
.isUnderline() == lastAttr
.isUnderline())
808 && (lCell
.isBlink() == lastAttr
.isBlink())
810 // Both colors changed, attributes the same
811 sb
.append(color(lCell
.isBold(),
812 lCell
.getForeColor(), lCell
.getBackColor()));
815 System
.err
.printf("1 Change only fore/back colors\n");
818 } else if (lCell
.isRGB()
819 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
820 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
821 && (lCell
.isBold() == lastAttr
.isBold())
822 && (lCell
.isReverse() == lastAttr
.isReverse())
823 && (lCell
.isUnderline() == lastAttr
.isUnderline())
824 && (lCell
.isBlink() == lastAttr
.isBlink())
826 // Both colors changed, attributes the same
827 sb
.append(colorRGB(lCell
.getForeColorRGB(),
828 lCell
.getBackColorRGB()));
831 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
833 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
834 && (lCell
.getBackColor() != lastAttr
.getBackColor())
836 && (lCell
.isBold() != lastAttr
.isBold())
837 && (lCell
.isReverse() != lastAttr
.isReverse())
838 && (lCell
.isUnderline() != lastAttr
.isUnderline())
839 && (lCell
.isBlink() != lastAttr
.isBlink())
841 // Everything is different
842 sb
.append(color(lCell
.getForeColor(),
843 lCell
.getBackColor(),
844 lCell
.isBold(), lCell
.isReverse(),
846 lCell
.isUnderline()));
849 System
.err
.printf("2 Set all attributes\n");
851 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
852 && (lCell
.getBackColor() == lastAttr
.getBackColor())
854 && (lCell
.isBold() == lastAttr
.isBold())
855 && (lCell
.isReverse() == lastAttr
.isReverse())
856 && (lCell
.isUnderline() == lastAttr
.isUnderline())
857 && (lCell
.isBlink() == lastAttr
.isBlink())
860 // Attributes same, foreColor different
861 sb
.append(color(lCell
.isBold(),
862 lCell
.getForeColor(), true));
865 System
.err
.printf("3 Change foreColor\n");
867 } else if (lCell
.isRGB()
868 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
869 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
870 && (lCell
.getForeColorRGB() >= 0)
871 && (lCell
.getBackColorRGB() >= 0)
872 && (lCell
.isBold() == lastAttr
.isBold())
873 && (lCell
.isReverse() == lastAttr
.isReverse())
874 && (lCell
.isUnderline() == lastAttr
.isUnderline())
875 && (lCell
.isBlink() == lastAttr
.isBlink())
877 // Attributes same, foreColor different
878 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
881 System
.err
.printf("3 Change foreColor (RGB)\n");
883 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
884 && (lCell
.getBackColor() != lastAttr
.getBackColor())
886 && (lCell
.isBold() == lastAttr
.isBold())
887 && (lCell
.isReverse() == lastAttr
.isReverse())
888 && (lCell
.isUnderline() == lastAttr
.isUnderline())
889 && (lCell
.isBlink() == lastAttr
.isBlink())
891 // Attributes same, backColor different
892 sb
.append(color(lCell
.isBold(),
893 lCell
.getBackColor(), false));
896 System
.err
.printf("4 Change backColor\n");
898 } else if (lCell
.isRGB()
899 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
900 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
901 && (lCell
.isBold() == lastAttr
.isBold())
902 && (lCell
.isReverse() == lastAttr
.isReverse())
903 && (lCell
.isUnderline() == lastAttr
.isUnderline())
904 && (lCell
.isBlink() == lastAttr
.isBlink())
906 // Attributes same, foreColor different
907 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
910 System
.err
.printf("4 Change backColor (RGB)\n");
912 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
913 && (lCell
.getBackColor() == lastAttr
.getBackColor())
914 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
915 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
916 && (lCell
.isBold() == lastAttr
.isBold())
917 && (lCell
.isReverse() == lastAttr
.isReverse())
918 && (lCell
.isUnderline() == lastAttr
.isUnderline())
919 && (lCell
.isBlink() == lastAttr
.isBlink())
922 // All attributes the same, just print the char
926 System
.err
.printf("5 Only emit character\n");
929 // Just reset everything again
930 if (!lCell
.isRGB()) {
931 sb
.append(color(lCell
.getForeColor(),
932 lCell
.getBackColor(),
936 lCell
.isUnderline()));
939 System
.err
.printf("6 Change all attributes\n");
942 sb
.append(colorRGB(lCell
.getForeColorRGB(),
943 lCell
.getBackColorRGB(),
947 lCell
.isUnderline()));
949 System
.err
.printf("6 Change all attributes (RGB)\n");
954 // Emit the character
955 sb
.append(lCell
.getChar());
957 // Save the last rendered cell
959 lastAttr
.setTo(lCell
);
961 // Physical is always updated
962 physical
[x
][y
].setTo(lCell
);
964 } // if (!lCell.equals(pCell) || (reallyCleared == true))
966 } // for (int x = 0; x < width; x++)
970 * Render the screen to a string that can be emitted to something that
971 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
973 * @return escape sequences string that provides the updates to the
976 private String
flushString() {
977 CellAttributes attr
= null;
979 StringBuilder sb
= new StringBuilder();
981 attr
= new CellAttributes();
982 sb
.append(clearAll());
985 for (int y
= 0; y
< height
; y
++) {
986 flushLine(y
, sb
, attr
);
989 reallyCleared
= false;
991 String result
= sb
.toString();
993 System
.err
.printf("flushString(): %s\n", result
);
999 * Reset keyboard/mouse input parser.
1001 private void resetParser() {
1002 state
= ParseState
.GROUND
;
1003 params
= new ArrayList
<String
>();
1009 * Produce a control character or one of the special ones (ENTER, TAB,
1012 * @param ch Unicode code point
1013 * @param alt if true, set alt on the TKeypress
1014 * @return one TKeypress event, either a control character (e.g. isKey ==
1015 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
1018 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
1019 // System.err.printf("controlChar: %02x\n", ch);
1023 // Carriage return --> ENTER
1024 return new TKeypressEvent(kbEnter
, alt
, false, false);
1026 // Linefeed --> ENTER
1027 return new TKeypressEvent(kbEnter
, alt
, false, false);
1030 return new TKeypressEvent(kbEsc
, alt
, false, false);
1033 return new TKeypressEvent(kbTab
, alt
, false, false);
1035 // Make all other control characters come back as the alphabetic
1036 // character with the ctrl field set. So SOH would be 'A' +
1038 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
1044 * Produce special key from CSI Pn ; Pm ; ... ~
1046 * @return one KEYPRESS event representing a special key
1048 private TInputEvent
csiFnKey() {
1050 if (params
.size() > 0) {
1051 key
= Integer
.parseInt(params
.get(0));
1053 boolean alt
= false;
1054 boolean ctrl
= false;
1055 boolean shift
= false;
1056 if (params
.size() > 1) {
1057 shift
= csiIsShift(params
.get(1));
1058 alt
= csiIsAlt(params
.get(1));
1059 ctrl
= csiIsCtrl(params
.get(1));
1064 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
1066 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
1068 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
1070 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
1072 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
1074 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
1076 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
1078 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
1080 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
1082 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
1084 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
1086 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
1088 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
1090 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
1098 * Produce mouse events based on "Any event tracking" and UTF-8
1100 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1102 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
1104 private TInputEvent
parseMouse() {
1105 int buttons
= params
.get(0).charAt(0) - 32;
1106 int x
= params
.get(0).charAt(1) - 32 - 1;
1107 int y
= params
.get(0).charAt(2) - 32 - 1;
1109 // Clamp X and Y to the physical screen coordinates.
1110 if (x
>= windowResize
.getWidth()) {
1111 x
= windowResize
.getWidth() - 1;
1113 if (y
>= windowResize
.getHeight()) {
1114 y
= windowResize
.getHeight() - 1;
1117 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
1118 boolean eventMouse1
= false;
1119 boolean eventMouse2
= false;
1120 boolean eventMouse3
= false;
1121 boolean eventMouseWheelUp
= false;
1122 boolean eventMouseWheelDown
= false;
1124 // System.err.printf("buttons: %04x\r\n", buttons);
1141 if (!mouse1
&& !mouse2
&& !mouse3
) {
1142 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1144 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
1161 // Dragging with mouse1 down
1164 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1168 // Dragging with mouse2 down
1171 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1175 // Dragging with mouse3 down
1178 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1182 // Dragging with mouse2 down after wheelUp
1185 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1189 // Dragging with mouse2 down after wheelDown
1192 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1196 eventMouseWheelUp
= true;
1200 eventMouseWheelDown
= true;
1204 // Unknown, just make it motion
1205 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1208 return new TMouseEvent(eventType
, x
, y
, x
, y
,
1209 eventMouse1
, eventMouse2
, eventMouse3
,
1210 eventMouseWheelUp
, eventMouseWheelDown
);
1214 * Produce mouse events based on "Any event tracking" and SGR
1216 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1218 * @param release if true, this was a release ('m')
1219 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
1221 private TInputEvent
parseMouseSGR(final boolean release
) {
1222 // SGR extended coordinates - mode 1006
1223 if (params
.size() < 3) {
1224 // Invalid position, bail out.
1227 int buttons
= Integer
.parseInt(params
.get(0));
1228 int x
= Integer
.parseInt(params
.get(1)) - 1;
1229 int y
= Integer
.parseInt(params
.get(2)) - 1;
1231 // Clamp X and Y to the physical screen coordinates.
1232 if (x
>= windowResize
.getWidth()) {
1233 x
= windowResize
.getWidth() - 1;
1235 if (y
>= windowResize
.getHeight()) {
1236 y
= windowResize
.getHeight() - 1;
1239 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
1240 boolean eventMouse1
= false;
1241 boolean eventMouse2
= false;
1242 boolean eventMouse3
= false;
1243 boolean eventMouseWheelUp
= false;
1244 boolean eventMouseWheelDown
= false;
1247 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
1261 // Motion only, no buttons down
1262 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1266 // Dragging with mouse1 down
1268 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1272 // Dragging with mouse2 down
1274 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1278 // Dragging with mouse3 down
1280 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1284 // Dragging with mouse2 down after wheelUp
1286 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1290 // Dragging with mouse2 down after wheelDown
1292 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1296 eventMouseWheelUp
= true;
1300 eventMouseWheelDown
= true;
1304 // Unknown, bail out
1307 return new TMouseEvent(eventType
, x
, y
, x
, y
,
1308 eventMouse1
, eventMouse2
, eventMouse3
,
1309 eventMouseWheelUp
, eventMouseWheelDown
);
1313 * Return any events in the IO queue due to timeout.
1315 * @param queue list to append new events to
1317 private void getIdleEvents(final List
<TInputEvent
> queue
) {
1318 long nowTime
= System
.currentTimeMillis();
1320 // Check for new window size
1321 long windowSizeDelay
= nowTime
- windowSizeTime
;
1322 if (windowSizeDelay
> 1000) {
1323 sessionInfo
.queryWindowSize();
1324 int newWidth
= sessionInfo
.getWindowWidth();
1325 int newHeight
= sessionInfo
.getWindowHeight();
1327 if ((newWidth
!= windowResize
.getWidth())
1328 || (newHeight
!= windowResize
.getHeight())
1331 if (debugToStderr
) {
1332 System
.err
.println("Screen size changed, old size " +
1334 System
.err
.println(" new size " +
1335 newWidth
+ " x " + newHeight
);
1338 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1339 newWidth
, newHeight
);
1340 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1341 newWidth
, newHeight
);
1344 windowSizeTime
= nowTime
;
1347 // ESCDELAY type timeout
1348 if (state
== ParseState
.ESCAPE
) {
1349 long escDelay
= nowTime
- escapeTime
;
1350 if (escDelay
> 100) {
1351 // After 0.1 seconds, assume a true escape character
1352 queue
.add(controlChar((char)0x1B, false));
1359 * Returns true if the CSI parameter for a keyboard command means that
1362 private boolean csiIsShift(final String x
) {
1374 * Returns true if the CSI parameter for a keyboard command means that
1377 private boolean csiIsAlt(final String x
) {
1389 * Returns true if the CSI parameter for a keyboard command means that
1392 private boolean csiIsCtrl(final String x
) {
1404 * Parses the next character of input to see if an InputEvent is
1407 * @param events list to append new events to
1408 * @param ch Unicode code point
1410 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
1412 // ESCDELAY type timeout
1413 long nowTime
= System
.currentTimeMillis();
1414 if (state
== ParseState
.ESCAPE
) {
1415 long escDelay
= nowTime
- escapeTime
;
1416 if (escDelay
> 250) {
1417 // After 0.25 seconds, assume a true escape character
1418 events
.add(controlChar((char)0x1B, false));
1424 boolean ctrl
= false;
1425 boolean alt
= false;
1426 boolean shift
= false;
1428 // System.err.printf("state: %s ch %c\r\n", state, ch);
1434 state
= ParseState
.ESCAPE
;
1435 escapeTime
= nowTime
;
1440 // Control character
1441 events
.add(controlChar(ch
, false));
1448 events
.add(new TKeypressEvent(false, 0, ch
,
1449 false, false, false));
1458 // ALT-Control character
1459 events
.add(controlChar(ch
, true));
1465 // This will be one of the function keys
1466 state
= ParseState
.ESCAPE_INTERMEDIATE
;
1470 // '[' goes to CSI_ENTRY
1472 state
= ParseState
.CSI_ENTRY
;
1476 // Everything else is assumed to be Alt-keystroke
1477 if ((ch
>= 'A') && (ch
<= 'Z')) {
1481 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
1485 case ESCAPE_INTERMEDIATE
:
1486 if ((ch
>= 'P') && (ch
<= 'S')) {
1490 events
.add(new TKeypressEvent(kbF1
));
1493 events
.add(new TKeypressEvent(kbF2
));
1496 events
.add(new TKeypressEvent(kbF3
));
1499 events
.add(new TKeypressEvent(kbF4
));
1508 // Unknown keystroke, ignore
1513 // Numbers - parameter values
1514 if ((ch
>= '0') && (ch
<= '9')) {
1515 params
.set(params
.size() - 1,
1516 params
.get(params
.size() - 1) + ch
);
1517 state
= ParseState
.CSI_PARAM
;
1520 // Parameter separator
1526 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1530 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1535 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1540 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1545 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1550 events
.add(new TKeypressEvent(kbHome
));
1555 events
.add(new TKeypressEvent(kbEnd
));
1559 // CBT - Cursor backward X tab stops (default 1)
1560 events
.add(new TKeypressEvent(kbBackTab
));
1565 state
= ParseState
.MOUSE
;
1568 // Mouse position, SGR (1006) coordinates
1569 state
= ParseState
.MOUSE_SGR
;
1576 // Unknown keystroke, ignore
1581 // Numbers - parameter values
1582 if ((ch
>= '0') && (ch
<= '9')) {
1583 params
.set(params
.size() - 1,
1584 params
.get(params
.size() - 1) + ch
);
1587 // Parameter separator
1595 // Generate a mouse press event
1596 TInputEvent event
= parseMouseSGR(false);
1597 if (event
!= null) {
1603 // Generate a mouse release event
1604 event
= parseMouseSGR(true);
1605 if (event
!= null) {
1614 // Unknown keystroke, ignore
1619 // Numbers - parameter values
1620 if ((ch
>= '0') && (ch
<= '9')) {
1621 params
.set(params
.size() - 1,
1622 params
.get(params
.size() - 1) + ch
);
1623 state
= ParseState
.CSI_PARAM
;
1626 // Parameter separator
1633 events
.add(csiFnKey());
1638 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1642 if (params
.size() > 1) {
1643 shift
= csiIsShift(params
.get(1));
1644 alt
= csiIsAlt(params
.get(1));
1645 ctrl
= csiIsCtrl(params
.get(1));
1647 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1652 if (params
.size() > 1) {
1653 shift
= csiIsShift(params
.get(1));
1654 alt
= csiIsAlt(params
.get(1));
1655 ctrl
= csiIsCtrl(params
.get(1));
1657 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1662 if (params
.size() > 1) {
1663 shift
= csiIsShift(params
.get(1));
1664 alt
= csiIsAlt(params
.get(1));
1665 ctrl
= csiIsCtrl(params
.get(1));
1667 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1672 if (params
.size() > 1) {
1673 shift
= csiIsShift(params
.get(1));
1674 alt
= csiIsAlt(params
.get(1));
1675 ctrl
= csiIsCtrl(params
.get(1));
1677 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1682 if (params
.size() > 1) {
1683 shift
= csiIsShift(params
.get(1));
1684 alt
= csiIsAlt(params
.get(1));
1685 ctrl
= csiIsCtrl(params
.get(1));
1687 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
1692 if (params
.size() > 1) {
1693 shift
= csiIsShift(params
.get(1));
1694 alt
= csiIsAlt(params
.get(1));
1695 ctrl
= csiIsCtrl(params
.get(1));
1697 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
1705 // Unknown keystroke, ignore
1710 params
.set(0, params
.get(params
.size() - 1) + ch
);
1711 if (params
.get(0).length() == 3) {
1712 // We have enough to generate a mouse event
1713 events
.add(parseMouse());
1722 // This "should" be impossible to reach
1727 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1728 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1731 * @param on if true, enable metaSendsEscape
1732 * @return the string to emit to xterm
1734 private String
xtermMetaSendsEscape(final boolean on
) {
1736 return "\033[?1036h\033[?1034l";
1738 return "\033[?1036l";
1742 * Create an xterm OSC sequence to change the window title.
1744 * @param title the new title
1745 * @return the string to emit to xterm
1747 private String
getSetTitleString(final String title
) {
1748 return "\033]2;" + title
+ "\007";
1752 * Create a SGR parameter sequence for a single color change.
1754 * @param bold if true, set bold
1755 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1756 * @param foreground if true, this is a foreground color
1757 * @return the string to emit to an ANSI / ECMA-style terminal,
1760 private String
color(final boolean bold
, final Color color
,
1761 final boolean foreground
) {
1762 return color(color
, foreground
, true) +
1763 rgbColor(bold
, color
, foreground
);
1767 * Create a T.416 RGB parameter sequence for a single color change.
1769 * @param colorRGB a 24-bit RGB value for foreground color
1770 * @param foreground if true, this is a foreground color
1771 * @return the string to emit to an ANSI / ECMA-style terminal,
1774 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
1776 int colorRed
= (colorRGB
>> 16) & 0xFF;
1777 int colorGreen
= (colorRGB
>> 8) & 0xFF;
1778 int colorBlue
= colorRGB
& 0xFF;
1780 StringBuilder sb
= new StringBuilder();
1782 sb
.append("\033[38;2;");
1784 sb
.append("\033[48;2;");
1786 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
1787 return sb
.toString();
1791 * Create a T.416 RGB parameter sequence for both foreground and
1792 * background color change.
1794 * @param foreColorRGB a 24-bit RGB value for foreground color
1795 * @param backColorRGB a 24-bit RGB value for foreground color
1796 * @return the string to emit to an ANSI / ECMA-style terminal,
1799 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
1800 int foreColorRed
= (foreColorRGB
>> 16) & 0xFF;
1801 int foreColorGreen
= (foreColorRGB
>> 8) & 0xFF;
1802 int foreColorBlue
= foreColorRGB
& 0xFF;
1803 int backColorRed
= (backColorRGB
>> 16) & 0xFF;
1804 int backColorGreen
= (backColorRGB
>> 8) & 0xFF;
1805 int backColorBlue
= backColorRGB
& 0xFF;
1807 StringBuilder sb
= new StringBuilder();
1808 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
1809 foreColorRed
, foreColorGreen
, foreColorBlue
));
1810 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
1811 backColorRed
, backColorGreen
, backColorBlue
));
1812 return sb
.toString();
1816 * Create a T.416 RGB parameter sequence for a single color change.
1818 * @param bold if true, set bold
1819 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1820 * @param foreground if true, this is a foreground color
1821 * @return the string to emit to an xterm terminal with RGB support,
1822 * e.g. "\033[38;2;RR;GG;BBm"
1824 private String
rgbColor(final boolean bold
, final Color color
,
1825 final boolean foreground
) {
1826 if (doRgbColor
== false) {
1829 StringBuilder sb
= new StringBuilder("\033[");
1831 // Bold implies foreground only
1833 if (color
.equals(Color
.BLACK
)) {
1834 sb
.append("84;84;84");
1835 } else if (color
.equals(Color
.RED
)) {
1836 sb
.append("252;84;84");
1837 } else if (color
.equals(Color
.GREEN
)) {
1838 sb
.append("84;252;84");
1839 } else if (color
.equals(Color
.YELLOW
)) {
1840 sb
.append("252;252;84");
1841 } else if (color
.equals(Color
.BLUE
)) {
1842 sb
.append("84;84;252");
1843 } else if (color
.equals(Color
.MAGENTA
)) {
1844 sb
.append("252;84;252");
1845 } else if (color
.equals(Color
.CYAN
)) {
1846 sb
.append("84;252;252");
1847 } else if (color
.equals(Color
.WHITE
)) {
1848 sb
.append("252;252;252");
1856 if (color
.equals(Color
.BLACK
)) {
1858 } else if (color
.equals(Color
.RED
)) {
1859 sb
.append("168;0;0");
1860 } else if (color
.equals(Color
.GREEN
)) {
1861 sb
.append("0;168;0");
1862 } else if (color
.equals(Color
.YELLOW
)) {
1863 sb
.append("168;84;0");
1864 } else if (color
.equals(Color
.BLUE
)) {
1865 sb
.append("0;0;168");
1866 } else if (color
.equals(Color
.MAGENTA
)) {
1867 sb
.append("168;0;168");
1868 } else if (color
.equals(Color
.CYAN
)) {
1869 sb
.append("0;168;168");
1870 } else if (color
.equals(Color
.WHITE
)) {
1871 sb
.append("168;168;168");
1875 return sb
.toString();
1879 * Create a T.416 RGB parameter sequence for both foreground and
1880 * background color change.
1882 * @param bold if true, set bold
1883 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1884 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1885 * @return the string to emit to an xterm terminal with RGB support,
1886 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
1888 private String
rgbColor(final boolean bold
, final Color foreColor
,
1889 final Color backColor
) {
1890 if (doRgbColor
== false) {
1894 return rgbColor(bold
, foreColor
, true) +
1895 rgbColor(false, backColor
, false);
1899 * Create a SGR parameter sequence for a single color change.
1901 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1902 * @param foreground if true, this is a foreground color
1903 * @param header if true, make the full header, otherwise just emit the
1904 * color parameter e.g. "42;"
1905 * @return the string to emit to an ANSI / ECMA-style terminal,
1908 private String
color(final Color color
, final boolean foreground
,
1909 final boolean header
) {
1911 int ecmaColor
= color
.getValue();
1913 // Convert Color.* values to SGR numerics
1921 return String
.format("\033[%dm", ecmaColor
);
1923 return String
.format("%d;", ecmaColor
);
1928 * Create a SGR parameter sequence for both foreground and background
1931 * @param bold if true, set bold
1932 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1933 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1934 * @return the string to emit to an ANSI / ECMA-style terminal,
1935 * e.g. "\033[31;42m"
1937 private String
color(final boolean bold
, final Color foreColor
,
1938 final Color backColor
) {
1939 return color(foreColor
, backColor
, true) +
1940 rgbColor(bold
, foreColor
, backColor
);
1944 * Create a SGR parameter sequence for both foreground and
1945 * background color change.
1947 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1948 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1949 * @param header if true, make the full header, otherwise just emit the
1950 * color parameter e.g. "31;42;"
1951 * @return the string to emit to an ANSI / ECMA-style terminal,
1952 * e.g. "\033[31;42m"
1954 private String
color(final Color foreColor
, final Color backColor
,
1955 final boolean header
) {
1957 int ecmaForeColor
= foreColor
.getValue();
1958 int ecmaBackColor
= backColor
.getValue();
1960 // Convert Color.* values to SGR numerics
1961 ecmaBackColor
+= 40;
1962 ecmaForeColor
+= 30;
1965 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1967 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1972 * Create a SGR parameter sequence for foreground, background, and
1973 * several attributes. This sequence first resets all attributes to
1974 * default, then sets attributes as per the parameters.
1976 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1977 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1978 * @param bold if true, set bold
1979 * @param reverse if true, set reverse
1980 * @param blink if true, set blink
1981 * @param underline if true, set underline
1982 * @return the string to emit to an ANSI / ECMA-style terminal,
1983 * e.g. "\033[0;1;31;42m"
1985 private String
color(final Color foreColor
, final Color backColor
,
1986 final boolean bold
, final boolean reverse
, final boolean blink
,
1987 final boolean underline
) {
1989 int ecmaForeColor
= foreColor
.getValue();
1990 int ecmaBackColor
= backColor
.getValue();
1992 // Convert Color.* values to SGR numerics
1993 ecmaBackColor
+= 40;
1994 ecmaForeColor
+= 30;
1996 StringBuilder sb
= new StringBuilder();
1997 if ( bold
&& reverse
&& blink
&& !underline
) {
1998 sb
.append("\033[0;1;7;5;");
1999 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
2000 sb
.append("\033[0;1;7;");
2001 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
2002 sb
.append("\033[0;7;5;");
2003 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
2004 sb
.append("\033[0;1;5;");
2005 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
2006 sb
.append("\033[0;1;");
2007 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
2008 sb
.append("\033[0;7;");
2009 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
2010 sb
.append("\033[0;5;");
2011 } else if ( bold
&& reverse
&& blink
&& underline
) {
2012 sb
.append("\033[0;1;7;5;4;");
2013 } else if ( bold
&& reverse
&& !blink
&& underline
) {
2014 sb
.append("\033[0;1;7;4;");
2015 } else if ( !bold
&& reverse
&& blink
&& underline
) {
2016 sb
.append("\033[0;7;5;4;");
2017 } else if ( bold
&& !reverse
&& blink
&& underline
) {
2018 sb
.append("\033[0;1;5;4;");
2019 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
2020 sb
.append("\033[0;1;4;");
2021 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
2022 sb
.append("\033[0;7;4;");
2023 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
2024 sb
.append("\033[0;5;4;");
2025 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
2026 sb
.append("\033[0;4;");
2028 assert (!bold
&& !reverse
&& !blink
&& !underline
);
2029 sb
.append("\033[0;");
2031 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
2032 sb
.append(rgbColor(bold
, foreColor
, backColor
));
2033 return sb
.toString();
2037 * Create a SGR parameter sequence for foreground, background, and
2038 * several attributes. This sequence first resets all attributes to
2039 * default, then sets attributes as per the parameters.
2041 * @param foreColorRGB a 24-bit RGB value for foreground color
2042 * @param backColorRGB a 24-bit RGB value for foreground color
2043 * @param bold if true, set bold
2044 * @param reverse if true, set reverse
2045 * @param blink if true, set blink
2046 * @param underline if true, set underline
2047 * @return the string to emit to an ANSI / ECMA-style terminal,
2048 * e.g. "\033[0;1;31;42m"
2050 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
2051 final boolean bold
, final boolean reverse
, final boolean blink
,
2052 final boolean underline
) {
2054 int foreColorRed
= (foreColorRGB
>> 16) & 0xFF;
2055 int foreColorGreen
= (foreColorRGB
>> 8) & 0xFF;
2056 int foreColorBlue
= foreColorRGB
& 0xFF;
2057 int backColorRed
= (backColorRGB
>> 16) & 0xFF;
2058 int backColorGreen
= (backColorRGB
>> 8) & 0xFF;
2059 int backColorBlue
= backColorRGB
& 0xFF;
2061 StringBuilder sb
= new StringBuilder();
2062 if ( bold
&& reverse
&& blink
&& !underline
) {
2063 sb
.append("\033[0;1;7;5;");
2064 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
2065 sb
.append("\033[0;1;7;");
2066 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
2067 sb
.append("\033[0;7;5;");
2068 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
2069 sb
.append("\033[0;1;5;");
2070 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
2071 sb
.append("\033[0;1;");
2072 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
2073 sb
.append("\033[0;7;");
2074 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
2075 sb
.append("\033[0;5;");
2076 } else if ( bold
&& reverse
&& blink
&& underline
) {
2077 sb
.append("\033[0;1;7;5;4;");
2078 } else if ( bold
&& reverse
&& !blink
&& underline
) {
2079 sb
.append("\033[0;1;7;4;");
2080 } else if ( !bold
&& reverse
&& blink
&& underline
) {
2081 sb
.append("\033[0;7;5;4;");
2082 } else if ( bold
&& !reverse
&& blink
&& underline
) {
2083 sb
.append("\033[0;1;5;4;");
2084 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
2085 sb
.append("\033[0;1;4;");
2086 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
2087 sb
.append("\033[0;7;4;");
2088 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
2089 sb
.append("\033[0;5;4;");
2090 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
2091 sb
.append("\033[0;4;");
2093 assert (!bold
&& !reverse
&& !blink
&& !underline
);
2094 sb
.append("\033[0;");
2097 sb
.append("m\033[38;2;");
2098 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
2100 sb
.append("m\033[48;2;");
2101 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
2104 return sb
.toString();
2108 * Create a SGR parameter sequence to reset to defaults.
2110 * @return the string to emit to an ANSI / ECMA-style terminal,
2113 private String
normal() {
2114 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
2118 * Create a SGR parameter sequence to reset to defaults.
2120 * @param header if true, make the full header, otherwise just emit the
2121 * bare parameter e.g. "0;"
2122 * @return the string to emit to an ANSI / ECMA-style terminal,
2125 private String
normal(final boolean header
) {
2127 return "\033[0;37;40m";
2133 * Create a SGR parameter sequence for enabling the visible cursor.
2135 * @param on if true, turn on cursor
2136 * @return the string to emit to an ANSI / ECMA-style terminal
2138 private String
cursor(final boolean on
) {
2139 if (on
&& !cursorOn
) {
2143 if (!on
&& cursorOn
) {
2151 * Clear the entire screen. Because some terminals use back-color-erase,
2152 * set the color to white-on-black beforehand.
2154 * @return the string to emit to an ANSI / ECMA-style terminal
2156 private String
clearAll() {
2157 return "\033[0;37;40m\033[2J";
2161 * Clear the line from the cursor (inclusive) to the end of the screen.
2162 * Because some terminals use back-color-erase, set the color to
2163 * white-on-black beforehand.
2165 * @return the string to emit to an ANSI / ECMA-style terminal
2167 private String
clearRemainingLine() {
2168 return "\033[0;37;40m\033[K";
2172 * Move the cursor to (x, y).
2174 * @param x column coordinate. 0 is the left-most column.
2175 * @param y row coordinate. 0 is the top-most row.
2176 * @return the string to emit to an ANSI / ECMA-style terminal
2178 private String
gotoXY(final int x
, final int y
) {
2179 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
2183 * Tell (u)xterm that we want to receive mouse events based on "Any event
2184 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
2185 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
2187 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2189 * Note that this also sets the alternate/primary screen buffer.
2191 * @param on If true, enable mouse report and use the alternate screen
2192 * buffer. If false disable mouse reporting and use the primary screen
2194 * @return the string to emit to xterm
2196 private String
mouse(final boolean on
) {
2198 return "\033[?1002;1003;1005;1006h\033[?1049h";
2200 return "\033[?1002;1003;1006;1005l\033[?1049l";