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
511 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
517 // We don't close System.in/out
519 // Shut down the streams, this should wake up the reader thread
526 if (output
!= null) {
530 } catch (IOException e
) {
537 * Set listener to a different Object.
539 * @param listener the new listening object that run() wakes up on new
542 public void setListener(final Object listener
) {
543 this.listener
= listener
;
546 // ------------------------------------------------------------------------
547 // Runnable ---------------------------------------------------------------
548 // ------------------------------------------------------------------------
551 * Read function runs on a separate thread.
554 boolean done
= false;
555 // available() will often return > 1, so we need to read in chunks to
557 char [] readBuffer
= new char[128];
558 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
560 while (!done
&& !stopReaderThread
) {
562 // We assume that if inputStream has bytes available, then
563 // input won't block on read().
564 int n
= inputStream
.available();
567 System.err.printf("inputStream.available(): %d\n", n);
572 if (readBuffer
.length
< n
) {
573 // The buffer wasn't big enough, make it huger
574 readBuffer
= new char[readBuffer
.length
* 2];
577 // System.err.printf("BEFORE read()\n"); System.err.flush();
579 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
582 System.err.printf("AFTER read() %d\n", rc);
590 for (int i
= 0; i
< rc
; i
++) {
591 int ch
= readBuffer
[i
];
592 processChar(events
, (char)ch
);
594 getIdleEvents(events
);
595 if (events
.size() > 0) {
596 // Add to the queue for the backend thread to
597 // be able to obtain.
598 synchronized (eventQueue
) {
599 eventQueue
.addAll(events
);
601 if (listener
!= null) {
602 synchronized (listener
) {
603 listener
.notifyAll();
610 getIdleEvents(events
);
611 if (events
.size() > 0) {
612 synchronized (eventQueue
) {
613 eventQueue
.addAll(events
);
615 if (listener
!= null) {
616 synchronized (listener
) {
617 listener
.notifyAll();
623 // Wait 20 millis for more data
626 // System.err.println("end while loop"); System.err.flush();
627 } catch (InterruptedException e
) {
629 } catch (IOException e
) {
633 } // while ((done == false) && (stopReaderThread == false))
634 // System.err.println("*** run() exiting..."); System.err.flush();
637 // ------------------------------------------------------------------------
638 // ECMA48Terminal ---------------------------------------------------------
639 // ------------------------------------------------------------------------
642 * Getter for sessionInfo.
644 * @return the SessionInfo
646 public SessionInfo
getSessionInfo() {
651 * Get the output writer.
655 public PrintWriter
getOutput() {
660 * Call 'stty' to set cooked mode.
662 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
664 private void sttyCooked() {
669 * Call 'stty' to set raw mode.
671 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
672 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
673 * -parenb cs8 min 1 < /dev/tty'
675 private void sttyRaw() {
680 * Call 'stty' to set raw or cooked mode.
682 * @param mode if true, set raw mode, otherwise set cooked mode
684 private void doStty(final boolean mode
) {
686 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
688 String
[] cmdCooked
= {
689 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
694 process
= Runtime
.getRuntime().exec(cmdRaw
);
696 process
= Runtime
.getRuntime().exec(cmdCooked
);
698 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
699 String line
= in
.readLine();
700 if ((line
!= null) && (line
.length() > 0)) {
701 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
704 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
705 line
= err
.readLine();
706 if ((line
!= null) && (line
.length() > 0)) {
707 System
.err
.println("Error output from stty: " + line
);
712 } catch (InterruptedException e
) {
716 int rc
= process
.exitValue();
718 System
.err
.println("stty returned error code: " + rc
);
720 } catch (IOException e
) {
728 public void flush() {
733 * Perform a somewhat-optimal rendering of a line.
735 * @param y row coordinate. 0 is the top-most row.
736 * @param sb StringBuilder to write escape sequences to
737 * @param lastAttr cell attributes from the last call to flushLine
739 private void flushLine(final int y
, final StringBuilder sb
,
740 CellAttributes lastAttr
) {
744 for (int x
= 0; x
< width
; x
++) {
745 Cell lCell
= logical
[x
][y
];
746 if (!lCell
.isBlank()) {
750 // Push textEnd to first column beyond the text area
754 // reallyCleared = true;
756 for (int x
= 0; x
< width
; x
++) {
757 Cell lCell
= logical
[x
][y
];
758 Cell pCell
= physical
[x
][y
];
760 if (!lCell
.equals(pCell
) || reallyCleared
) {
763 System
.err
.printf("\n--\n");
764 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
765 System
.err
.printf(" lCell: %s\n", lCell
);
766 System
.err
.printf(" pCell: %s\n", pCell
);
767 System
.err
.printf(" ==== \n");
770 if (lastAttr
== null) {
771 lastAttr
= new CellAttributes();
776 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
777 // Advancing at least one cell, or the first gotoXY
778 sb
.append(gotoXY(x
, y
));
781 assert (lastAttr
!= null);
783 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
784 assert (lCell
.isBlank());
786 for (int i
= x
; i
< width
; i
++) {
787 assert (logical
[i
][y
].isBlank());
788 // Physical is always updated
789 physical
[i
][y
].reset();
792 // Clear remaining line
793 sb
.append(clearRemainingLine());
798 // Now emit only the modified attributes
799 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
800 && (lCell
.getBackColor() != lastAttr
.getBackColor())
802 && (lCell
.isBold() == lastAttr
.isBold())
803 && (lCell
.isReverse() == lastAttr
.isReverse())
804 && (lCell
.isUnderline() == lastAttr
.isUnderline())
805 && (lCell
.isBlink() == lastAttr
.isBlink())
807 // Both colors changed, attributes the same
808 sb
.append(color(lCell
.isBold(),
809 lCell
.getForeColor(), lCell
.getBackColor()));
812 System
.err
.printf("1 Change only fore/back colors\n");
815 } else if (lCell
.isRGB()
816 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
817 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
818 && (lCell
.isBold() == lastAttr
.isBold())
819 && (lCell
.isReverse() == lastAttr
.isReverse())
820 && (lCell
.isUnderline() == lastAttr
.isUnderline())
821 && (lCell
.isBlink() == lastAttr
.isBlink())
823 // Both colors changed, attributes the same
824 sb
.append(colorRGB(lCell
.getForeColorRGB(),
825 lCell
.getBackColorRGB()));
828 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
830 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
831 && (lCell
.getBackColor() != lastAttr
.getBackColor())
833 && (lCell
.isBold() != lastAttr
.isBold())
834 && (lCell
.isReverse() != lastAttr
.isReverse())
835 && (lCell
.isUnderline() != lastAttr
.isUnderline())
836 && (lCell
.isBlink() != lastAttr
.isBlink())
838 // Everything is different
839 sb
.append(color(lCell
.getForeColor(),
840 lCell
.getBackColor(),
841 lCell
.isBold(), lCell
.isReverse(),
843 lCell
.isUnderline()));
846 System
.err
.printf("2 Set all attributes\n");
848 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
849 && (lCell
.getBackColor() == lastAttr
.getBackColor())
851 && (lCell
.isBold() == lastAttr
.isBold())
852 && (lCell
.isReverse() == lastAttr
.isReverse())
853 && (lCell
.isUnderline() == lastAttr
.isUnderline())
854 && (lCell
.isBlink() == lastAttr
.isBlink())
857 // Attributes same, foreColor different
858 sb
.append(color(lCell
.isBold(),
859 lCell
.getForeColor(), true));
862 System
.err
.printf("3 Change foreColor\n");
864 } else if (lCell
.isRGB()
865 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
866 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
867 && (lCell
.getForeColorRGB() >= 0)
868 && (lCell
.getBackColorRGB() >= 0)
869 && (lCell
.isBold() == lastAttr
.isBold())
870 && (lCell
.isReverse() == lastAttr
.isReverse())
871 && (lCell
.isUnderline() == lastAttr
.isUnderline())
872 && (lCell
.isBlink() == lastAttr
.isBlink())
874 // Attributes same, foreColor different
875 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
878 System
.err
.printf("3 Change foreColor (RGB)\n");
880 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
881 && (lCell
.getBackColor() != lastAttr
.getBackColor())
883 && (lCell
.isBold() == lastAttr
.isBold())
884 && (lCell
.isReverse() == lastAttr
.isReverse())
885 && (lCell
.isUnderline() == lastAttr
.isUnderline())
886 && (lCell
.isBlink() == lastAttr
.isBlink())
888 // Attributes same, backColor different
889 sb
.append(color(lCell
.isBold(),
890 lCell
.getBackColor(), false));
893 System
.err
.printf("4 Change backColor\n");
895 } else if (lCell
.isRGB()
896 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
897 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
898 && (lCell
.isBold() == lastAttr
.isBold())
899 && (lCell
.isReverse() == lastAttr
.isReverse())
900 && (lCell
.isUnderline() == lastAttr
.isUnderline())
901 && (lCell
.isBlink() == lastAttr
.isBlink())
903 // Attributes same, foreColor different
904 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
907 System
.err
.printf("4 Change backColor (RGB)\n");
909 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
910 && (lCell
.getBackColor() == lastAttr
.getBackColor())
911 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
912 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
913 && (lCell
.isBold() == lastAttr
.isBold())
914 && (lCell
.isReverse() == lastAttr
.isReverse())
915 && (lCell
.isUnderline() == lastAttr
.isUnderline())
916 && (lCell
.isBlink() == lastAttr
.isBlink())
919 // All attributes the same, just print the char
923 System
.err
.printf("5 Only emit character\n");
926 // Just reset everything again
927 if (!lCell
.isRGB()) {
928 sb
.append(color(lCell
.getForeColor(),
929 lCell
.getBackColor(),
933 lCell
.isUnderline()));
936 System
.err
.printf("6 Change all attributes\n");
939 sb
.append(colorRGB(lCell
.getForeColorRGB(),
940 lCell
.getBackColorRGB(),
944 lCell
.isUnderline()));
946 System
.err
.printf("6 Change all attributes (RGB)\n");
951 // Emit the character
952 sb
.append(lCell
.getChar());
954 // Save the last rendered cell
956 lastAttr
.setTo(lCell
);
958 // Physical is always updated
959 physical
[x
][y
].setTo(lCell
);
961 } // if (!lCell.equals(pCell) || (reallyCleared == true))
963 } // for (int x = 0; x < width; x++)
967 * Render the screen to a string that can be emitted to something that
968 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
970 * @return escape sequences string that provides the updates to the
973 private String
flushString() {
974 CellAttributes attr
= null;
976 StringBuilder sb
= new StringBuilder();
978 attr
= new CellAttributes();
979 sb
.append(clearAll());
982 for (int y
= 0; y
< height
; y
++) {
983 flushLine(y
, sb
, attr
);
986 reallyCleared
= false;
988 String result
= sb
.toString();
990 System
.err
.printf("flushString(): %s\n", result
);
996 * Reset keyboard/mouse input parser.
998 private void resetParser() {
999 state
= ParseState
.GROUND
;
1000 params
= new ArrayList
<String
>();
1006 * Produce a control character or one of the special ones (ENTER, TAB,
1009 * @param ch Unicode code point
1010 * @param alt if true, set alt on the TKeypress
1011 * @return one TKeypress event, either a control character (e.g. isKey ==
1012 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
1015 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
1016 // System.err.printf("controlChar: %02x\n", ch);
1020 // Carriage return --> ENTER
1021 return new TKeypressEvent(kbEnter
, alt
, false, false);
1023 // Linefeed --> ENTER
1024 return new TKeypressEvent(kbEnter
, alt
, false, false);
1027 return new TKeypressEvent(kbEsc
, alt
, false, false);
1030 return new TKeypressEvent(kbTab
, alt
, false, false);
1032 // Make all other control characters come back as the alphabetic
1033 // character with the ctrl field set. So SOH would be 'A' +
1035 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
1041 * Produce special key from CSI Pn ; Pm ; ... ~
1043 * @return one KEYPRESS event representing a special key
1045 private TInputEvent
csiFnKey() {
1047 if (params
.size() > 0) {
1048 key
= Integer
.parseInt(params
.get(0));
1050 boolean alt
= false;
1051 boolean ctrl
= false;
1052 boolean shift
= false;
1053 if (params
.size() > 1) {
1054 shift
= csiIsShift(params
.get(1));
1055 alt
= csiIsAlt(params
.get(1));
1056 ctrl
= csiIsCtrl(params
.get(1));
1061 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
1063 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
1065 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
1067 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
1069 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
1071 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
1073 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
1075 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
1077 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
1079 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
1081 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
1083 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
1085 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
1087 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
1095 * Produce mouse events based on "Any event tracking" and UTF-8
1097 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1099 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
1101 private TInputEvent
parseMouse() {
1102 int buttons
= params
.get(0).charAt(0) - 32;
1103 int x
= params
.get(0).charAt(1) - 32 - 1;
1104 int y
= params
.get(0).charAt(2) - 32 - 1;
1106 // Clamp X and Y to the physical screen coordinates.
1107 if (x
>= windowResize
.getWidth()) {
1108 x
= windowResize
.getWidth() - 1;
1110 if (y
>= windowResize
.getHeight()) {
1111 y
= windowResize
.getHeight() - 1;
1114 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
1115 boolean eventMouse1
= false;
1116 boolean eventMouse2
= false;
1117 boolean eventMouse3
= false;
1118 boolean eventMouseWheelUp
= false;
1119 boolean eventMouseWheelDown
= false;
1121 // System.err.printf("buttons: %04x\r\n", buttons);
1138 if (!mouse1
&& !mouse2
&& !mouse3
) {
1139 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1141 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
1158 // Dragging with mouse1 down
1161 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1165 // Dragging with mouse2 down
1168 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1172 // Dragging with mouse3 down
1175 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1179 // Dragging with mouse2 down after wheelUp
1182 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1186 // Dragging with mouse2 down after wheelDown
1189 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1193 eventMouseWheelUp
= true;
1197 eventMouseWheelDown
= true;
1201 // Unknown, just make it motion
1202 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1205 return new TMouseEvent(eventType
, x
, y
, x
, y
,
1206 eventMouse1
, eventMouse2
, eventMouse3
,
1207 eventMouseWheelUp
, eventMouseWheelDown
);
1211 * Produce mouse events based on "Any event tracking" and SGR
1213 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1215 * @param release if true, this was a release ('m')
1216 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
1218 private TInputEvent
parseMouseSGR(final boolean release
) {
1219 // SGR extended coordinates - mode 1006
1220 if (params
.size() < 3) {
1221 // Invalid position, bail out.
1224 int buttons
= Integer
.parseInt(params
.get(0));
1225 int x
= Integer
.parseInt(params
.get(1)) - 1;
1226 int y
= Integer
.parseInt(params
.get(2)) - 1;
1228 // Clamp X and Y to the physical screen coordinates.
1229 if (x
>= windowResize
.getWidth()) {
1230 x
= windowResize
.getWidth() - 1;
1232 if (y
>= windowResize
.getHeight()) {
1233 y
= windowResize
.getHeight() - 1;
1236 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
1237 boolean eventMouse1
= false;
1238 boolean eventMouse2
= false;
1239 boolean eventMouse3
= false;
1240 boolean eventMouseWheelUp
= false;
1241 boolean eventMouseWheelDown
= false;
1244 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
1258 // Motion only, no buttons down
1259 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1263 // Dragging with mouse1 down
1265 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1269 // Dragging with mouse2 down
1271 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1275 // Dragging with mouse3 down
1277 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1281 // Dragging with mouse2 down after wheelUp
1283 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1287 // Dragging with mouse2 down after wheelDown
1289 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1293 eventMouseWheelUp
= true;
1297 eventMouseWheelDown
= true;
1301 // Unknown, bail out
1304 return new TMouseEvent(eventType
, x
, y
, x
, y
,
1305 eventMouse1
, eventMouse2
, eventMouse3
,
1306 eventMouseWheelUp
, eventMouseWheelDown
);
1310 * Return any events in the IO queue due to timeout.
1312 * @param queue list to append new events to
1314 private void getIdleEvents(final List
<TInputEvent
> queue
) {
1315 long nowTime
= System
.currentTimeMillis();
1317 // Check for new window size
1318 long windowSizeDelay
= nowTime
- windowSizeTime
;
1319 if (windowSizeDelay
> 1000) {
1320 sessionInfo
.queryWindowSize();
1321 int newWidth
= sessionInfo
.getWindowWidth();
1322 int newHeight
= sessionInfo
.getWindowHeight();
1324 if ((newWidth
!= windowResize
.getWidth())
1325 || (newHeight
!= windowResize
.getHeight())
1328 if (debugToStderr
) {
1329 System
.err
.println("Screen size changed, old size " +
1331 System
.err
.println(" new size " +
1332 newWidth
+ " x " + newHeight
);
1335 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1336 newWidth
, newHeight
);
1337 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1338 newWidth
, newHeight
);
1341 windowSizeTime
= nowTime
;
1344 // ESCDELAY type timeout
1345 if (state
== ParseState
.ESCAPE
) {
1346 long escDelay
= nowTime
- escapeTime
;
1347 if (escDelay
> 100) {
1348 // After 0.1 seconds, assume a true escape character
1349 queue
.add(controlChar((char)0x1B, false));
1356 * Returns true if the CSI parameter for a keyboard command means that
1359 private boolean csiIsShift(final String x
) {
1371 * Returns true if the CSI parameter for a keyboard command means that
1374 private boolean csiIsAlt(final String x
) {
1386 * Returns true if the CSI parameter for a keyboard command means that
1389 private boolean csiIsCtrl(final String x
) {
1401 * Parses the next character of input to see if an InputEvent is
1404 * @param events list to append new events to
1405 * @param ch Unicode code point
1407 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
1409 // ESCDELAY type timeout
1410 long nowTime
= System
.currentTimeMillis();
1411 if (state
== ParseState
.ESCAPE
) {
1412 long escDelay
= nowTime
- escapeTime
;
1413 if (escDelay
> 250) {
1414 // After 0.25 seconds, assume a true escape character
1415 events
.add(controlChar((char)0x1B, false));
1421 boolean ctrl
= false;
1422 boolean alt
= false;
1423 boolean shift
= false;
1425 // System.err.printf("state: %s ch %c\r\n", state, ch);
1431 state
= ParseState
.ESCAPE
;
1432 escapeTime
= nowTime
;
1437 // Control character
1438 events
.add(controlChar(ch
, false));
1445 events
.add(new TKeypressEvent(false, 0, ch
,
1446 false, false, false));
1455 // ALT-Control character
1456 events
.add(controlChar(ch
, true));
1462 // This will be one of the function keys
1463 state
= ParseState
.ESCAPE_INTERMEDIATE
;
1467 // '[' goes to CSI_ENTRY
1469 state
= ParseState
.CSI_ENTRY
;
1473 // Everything else is assumed to be Alt-keystroke
1474 if ((ch
>= 'A') && (ch
<= 'Z')) {
1478 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
1482 case ESCAPE_INTERMEDIATE
:
1483 if ((ch
>= 'P') && (ch
<= 'S')) {
1487 events
.add(new TKeypressEvent(kbF1
));
1490 events
.add(new TKeypressEvent(kbF2
));
1493 events
.add(new TKeypressEvent(kbF3
));
1496 events
.add(new TKeypressEvent(kbF4
));
1505 // Unknown keystroke, ignore
1510 // Numbers - parameter values
1511 if ((ch
>= '0') && (ch
<= '9')) {
1512 params
.set(params
.size() - 1,
1513 params
.get(params
.size() - 1) + ch
);
1514 state
= ParseState
.CSI_PARAM
;
1517 // Parameter separator
1523 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1527 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1532 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1537 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1542 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1547 events
.add(new TKeypressEvent(kbHome
));
1552 events
.add(new TKeypressEvent(kbEnd
));
1556 // CBT - Cursor backward X tab stops (default 1)
1557 events
.add(new TKeypressEvent(kbBackTab
));
1562 state
= ParseState
.MOUSE
;
1565 // Mouse position, SGR (1006) coordinates
1566 state
= ParseState
.MOUSE_SGR
;
1573 // Unknown keystroke, ignore
1578 // Numbers - parameter values
1579 if ((ch
>= '0') && (ch
<= '9')) {
1580 params
.set(params
.size() - 1,
1581 params
.get(params
.size() - 1) + ch
);
1584 // Parameter separator
1592 // Generate a mouse press event
1593 TInputEvent event
= parseMouseSGR(false);
1594 if (event
!= null) {
1600 // Generate a mouse release event
1601 event
= parseMouseSGR(true);
1602 if (event
!= null) {
1611 // Unknown keystroke, ignore
1616 // Numbers - parameter values
1617 if ((ch
>= '0') && (ch
<= '9')) {
1618 params
.set(params
.size() - 1,
1619 params
.get(params
.size() - 1) + ch
);
1620 state
= ParseState
.CSI_PARAM
;
1623 // Parameter separator
1630 events
.add(csiFnKey());
1635 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1639 if (params
.size() > 1) {
1640 shift
= csiIsShift(params
.get(1));
1641 alt
= csiIsAlt(params
.get(1));
1642 ctrl
= csiIsCtrl(params
.get(1));
1644 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1649 if (params
.size() > 1) {
1650 shift
= csiIsShift(params
.get(1));
1651 alt
= csiIsAlt(params
.get(1));
1652 ctrl
= csiIsCtrl(params
.get(1));
1654 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1659 if (params
.size() > 1) {
1660 shift
= csiIsShift(params
.get(1));
1661 alt
= csiIsAlt(params
.get(1));
1662 ctrl
= csiIsCtrl(params
.get(1));
1664 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1669 if (params
.size() > 1) {
1670 shift
= csiIsShift(params
.get(1));
1671 alt
= csiIsAlt(params
.get(1));
1672 ctrl
= csiIsCtrl(params
.get(1));
1674 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1679 if (params
.size() > 1) {
1680 shift
= csiIsShift(params
.get(1));
1681 alt
= csiIsAlt(params
.get(1));
1682 ctrl
= csiIsCtrl(params
.get(1));
1684 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
1689 if (params
.size() > 1) {
1690 shift
= csiIsShift(params
.get(1));
1691 alt
= csiIsAlt(params
.get(1));
1692 ctrl
= csiIsCtrl(params
.get(1));
1694 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
1702 // Unknown keystroke, ignore
1707 params
.set(0, params
.get(params
.size() - 1) + ch
);
1708 if (params
.get(0).length() == 3) {
1709 // We have enough to generate a mouse event
1710 events
.add(parseMouse());
1719 // This "should" be impossible to reach
1724 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1725 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1728 * @param on if true, enable metaSendsEscape
1729 * @return the string to emit to xterm
1731 private String
xtermMetaSendsEscape(final boolean on
) {
1733 return "\033[?1036h\033[?1034l";
1735 return "\033[?1036l";
1739 * Create an xterm OSC sequence to change the window title.
1741 * @param title the new title
1742 * @return the string to emit to xterm
1744 private String
getSetTitleString(final String title
) {
1745 return "\033]2;" + title
+ "\007";
1749 * Create a SGR parameter sequence for a single color change.
1751 * @param bold if true, set bold
1752 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1753 * @param foreground if true, this is a foreground color
1754 * @return the string to emit to an ANSI / ECMA-style terminal,
1757 private String
color(final boolean bold
, final Color color
,
1758 final boolean foreground
) {
1759 return color(color
, foreground
, true) +
1760 rgbColor(bold
, color
, foreground
);
1764 * Create a T.416 RGB parameter sequence for a single color change.
1766 * @param colorRGB a 24-bit RGB value for foreground color
1767 * @param foreground if true, this is a foreground color
1768 * @return the string to emit to an ANSI / ECMA-style terminal,
1771 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
1773 int colorRed
= (colorRGB
>> 16) & 0xFF;
1774 int colorGreen
= (colorRGB
>> 8) & 0xFF;
1775 int colorBlue
= colorRGB
& 0xFF;
1777 StringBuilder sb
= new StringBuilder();
1779 sb
.append("\033[38;2;");
1781 sb
.append("\033[48;2;");
1783 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
1784 return sb
.toString();
1788 * Create a T.416 RGB parameter sequence for both foreground and
1789 * background color change.
1791 * @param foreColorRGB a 24-bit RGB value for foreground color
1792 * @param backColorRGB a 24-bit RGB value for foreground color
1793 * @return the string to emit to an ANSI / ECMA-style terminal,
1796 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
1797 int foreColorRed
= (foreColorRGB
>> 16) & 0xFF;
1798 int foreColorGreen
= (foreColorRGB
>> 8) & 0xFF;
1799 int foreColorBlue
= foreColorRGB
& 0xFF;
1800 int backColorRed
= (backColorRGB
>> 16) & 0xFF;
1801 int backColorGreen
= (backColorRGB
>> 8) & 0xFF;
1802 int backColorBlue
= backColorRGB
& 0xFF;
1804 StringBuilder sb
= new StringBuilder();
1805 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
1806 foreColorRed
, foreColorGreen
, foreColorBlue
));
1807 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
1808 backColorRed
, backColorGreen
, backColorBlue
));
1809 return sb
.toString();
1813 * Create a T.416 RGB parameter sequence for a single color change.
1815 * @param bold if true, set bold
1816 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1817 * @param foreground if true, this is a foreground color
1818 * @return the string to emit to an xterm terminal with RGB support,
1819 * e.g. "\033[38;2;RR;GG;BBm"
1821 private String
rgbColor(final boolean bold
, final Color color
,
1822 final boolean foreground
) {
1823 if (doRgbColor
== false) {
1826 StringBuilder sb
= new StringBuilder("\033[");
1828 // Bold implies foreground only
1830 if (color
.equals(Color
.BLACK
)) {
1831 sb
.append("84;84;84");
1832 } else if (color
.equals(Color
.RED
)) {
1833 sb
.append("252;84;84");
1834 } else if (color
.equals(Color
.GREEN
)) {
1835 sb
.append("84;252;84");
1836 } else if (color
.equals(Color
.YELLOW
)) {
1837 sb
.append("252;252;84");
1838 } else if (color
.equals(Color
.BLUE
)) {
1839 sb
.append("84;84;252");
1840 } else if (color
.equals(Color
.MAGENTA
)) {
1841 sb
.append("252;84;252");
1842 } else if (color
.equals(Color
.CYAN
)) {
1843 sb
.append("84;252;252");
1844 } else if (color
.equals(Color
.WHITE
)) {
1845 sb
.append("252;252;252");
1853 if (color
.equals(Color
.BLACK
)) {
1855 } else if (color
.equals(Color
.RED
)) {
1856 sb
.append("168;0;0");
1857 } else if (color
.equals(Color
.GREEN
)) {
1858 sb
.append("0;168;0");
1859 } else if (color
.equals(Color
.YELLOW
)) {
1860 sb
.append("168;84;0");
1861 } else if (color
.equals(Color
.BLUE
)) {
1862 sb
.append("0;0;168");
1863 } else if (color
.equals(Color
.MAGENTA
)) {
1864 sb
.append("168;0;168");
1865 } else if (color
.equals(Color
.CYAN
)) {
1866 sb
.append("0;168;168");
1867 } else if (color
.equals(Color
.WHITE
)) {
1868 sb
.append("168;168;168");
1872 return sb
.toString();
1876 * Create a T.416 RGB parameter sequence for both foreground and
1877 * background color change.
1879 * @param bold if true, set bold
1880 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1881 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1882 * @return the string to emit to an xterm terminal with RGB support,
1883 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
1885 private String
rgbColor(final boolean bold
, final Color foreColor
,
1886 final Color backColor
) {
1887 if (doRgbColor
== false) {
1891 return rgbColor(bold
, foreColor
, true) +
1892 rgbColor(false, backColor
, false);
1896 * Create a SGR parameter sequence for a single color change.
1898 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1899 * @param foreground if true, this is a foreground color
1900 * @param header if true, make the full header, otherwise just emit the
1901 * color parameter e.g. "42;"
1902 * @return the string to emit to an ANSI / ECMA-style terminal,
1905 private String
color(final Color color
, final boolean foreground
,
1906 final boolean header
) {
1908 int ecmaColor
= color
.getValue();
1910 // Convert Color.* values to SGR numerics
1918 return String
.format("\033[%dm", ecmaColor
);
1920 return String
.format("%d;", ecmaColor
);
1925 * Create a SGR parameter sequence for both foreground and background
1928 * @param bold if true, set bold
1929 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1930 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1931 * @return the string to emit to an ANSI / ECMA-style terminal,
1932 * e.g. "\033[31;42m"
1934 private String
color(final boolean bold
, final Color foreColor
,
1935 final Color backColor
) {
1936 return color(foreColor
, backColor
, true) +
1937 rgbColor(bold
, foreColor
, backColor
);
1941 * Create a SGR parameter sequence for both foreground and
1942 * background color change.
1944 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1945 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1946 * @param header if true, make the full header, otherwise just emit the
1947 * color parameter e.g. "31;42;"
1948 * @return the string to emit to an ANSI / ECMA-style terminal,
1949 * e.g. "\033[31;42m"
1951 private String
color(final Color foreColor
, final Color backColor
,
1952 final boolean header
) {
1954 int ecmaForeColor
= foreColor
.getValue();
1955 int ecmaBackColor
= backColor
.getValue();
1957 // Convert Color.* values to SGR numerics
1958 ecmaBackColor
+= 40;
1959 ecmaForeColor
+= 30;
1962 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1964 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1969 * Create a SGR parameter sequence for foreground, background, and
1970 * several attributes. This sequence first resets all attributes to
1971 * default, then sets attributes as per the parameters.
1973 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1974 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1975 * @param bold if true, set bold
1976 * @param reverse if true, set reverse
1977 * @param blink if true, set blink
1978 * @param underline if true, set underline
1979 * @return the string to emit to an ANSI / ECMA-style terminal,
1980 * e.g. "\033[0;1;31;42m"
1982 private String
color(final Color foreColor
, final Color backColor
,
1983 final boolean bold
, final boolean reverse
, final boolean blink
,
1984 final boolean underline
) {
1986 int ecmaForeColor
= foreColor
.getValue();
1987 int ecmaBackColor
= backColor
.getValue();
1989 // Convert Color.* values to SGR numerics
1990 ecmaBackColor
+= 40;
1991 ecmaForeColor
+= 30;
1993 StringBuilder sb
= new StringBuilder();
1994 if ( bold
&& reverse
&& blink
&& !underline
) {
1995 sb
.append("\033[0;1;7;5;");
1996 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1997 sb
.append("\033[0;1;7;");
1998 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1999 sb
.append("\033[0;7;5;");
2000 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
2001 sb
.append("\033[0;1;5;");
2002 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
2003 sb
.append("\033[0;1;");
2004 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
2005 sb
.append("\033[0;7;");
2006 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
2007 sb
.append("\033[0;5;");
2008 } else if ( bold
&& reverse
&& blink
&& underline
) {
2009 sb
.append("\033[0;1;7;5;4;");
2010 } else if ( bold
&& reverse
&& !blink
&& underline
) {
2011 sb
.append("\033[0;1;7;4;");
2012 } else if ( !bold
&& reverse
&& blink
&& underline
) {
2013 sb
.append("\033[0;7;5;4;");
2014 } else if ( bold
&& !reverse
&& blink
&& underline
) {
2015 sb
.append("\033[0;1;5;4;");
2016 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
2017 sb
.append("\033[0;1;4;");
2018 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
2019 sb
.append("\033[0;7;4;");
2020 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
2021 sb
.append("\033[0;5;4;");
2022 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
2023 sb
.append("\033[0;4;");
2025 assert (!bold
&& !reverse
&& !blink
&& !underline
);
2026 sb
.append("\033[0;");
2028 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
2029 sb
.append(rgbColor(bold
, foreColor
, backColor
));
2030 return sb
.toString();
2034 * Create a SGR parameter sequence for foreground, background, and
2035 * several attributes. This sequence first resets all attributes to
2036 * default, then sets attributes as per the parameters.
2038 * @param foreColorRGB a 24-bit RGB value for foreground color
2039 * @param backColorRGB a 24-bit RGB value for foreground color
2040 * @param bold if true, set bold
2041 * @param reverse if true, set reverse
2042 * @param blink if true, set blink
2043 * @param underline if true, set underline
2044 * @return the string to emit to an ANSI / ECMA-style terminal,
2045 * e.g. "\033[0;1;31;42m"
2047 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
2048 final boolean bold
, final boolean reverse
, final boolean blink
,
2049 final boolean underline
) {
2051 int foreColorRed
= (foreColorRGB
>> 16) & 0xFF;
2052 int foreColorGreen
= (foreColorRGB
>> 8) & 0xFF;
2053 int foreColorBlue
= foreColorRGB
& 0xFF;
2054 int backColorRed
= (backColorRGB
>> 16) & 0xFF;
2055 int backColorGreen
= (backColorRGB
>> 8) & 0xFF;
2056 int backColorBlue
= backColorRGB
& 0xFF;
2058 StringBuilder sb
= new StringBuilder();
2059 if ( bold
&& reverse
&& blink
&& !underline
) {
2060 sb
.append("\033[0;1;7;5;");
2061 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
2062 sb
.append("\033[0;1;7;");
2063 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
2064 sb
.append("\033[0;7;5;");
2065 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
2066 sb
.append("\033[0;1;5;");
2067 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
2068 sb
.append("\033[0;1;");
2069 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
2070 sb
.append("\033[0;7;");
2071 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
2072 sb
.append("\033[0;5;");
2073 } else if ( bold
&& reverse
&& blink
&& underline
) {
2074 sb
.append("\033[0;1;7;5;4;");
2075 } else if ( bold
&& reverse
&& !blink
&& underline
) {
2076 sb
.append("\033[0;1;7;4;");
2077 } else if ( !bold
&& reverse
&& blink
&& underline
) {
2078 sb
.append("\033[0;7;5;4;");
2079 } else if ( bold
&& !reverse
&& blink
&& underline
) {
2080 sb
.append("\033[0;1;5;4;");
2081 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
2082 sb
.append("\033[0;1;4;");
2083 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
2084 sb
.append("\033[0;7;4;");
2085 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
2086 sb
.append("\033[0;5;4;");
2087 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
2088 sb
.append("\033[0;4;");
2090 assert (!bold
&& !reverse
&& !blink
&& !underline
);
2091 sb
.append("\033[0;");
2094 sb
.append("m\033[38;2;");
2095 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
2097 sb
.append("m\033[48;2;");
2098 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
2101 return sb
.toString();
2105 * Create a SGR parameter sequence to reset to defaults.
2107 * @return the string to emit to an ANSI / ECMA-style terminal,
2110 private String
normal() {
2111 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
2115 * Create a SGR parameter sequence to reset to defaults.
2117 * @param header if true, make the full header, otherwise just emit the
2118 * bare parameter e.g. "0;"
2119 * @return the string to emit to an ANSI / ECMA-style terminal,
2122 private String
normal(final boolean header
) {
2124 return "\033[0;37;40m";
2130 * Create a SGR parameter sequence for enabling the visible cursor.
2132 * @param on if true, turn on cursor
2133 * @return the string to emit to an ANSI / ECMA-style terminal
2135 private String
cursor(final boolean on
) {
2136 if (on
&& !cursorOn
) {
2140 if (!on
&& cursorOn
) {
2148 * Clear the entire screen. Because some terminals use back-color-erase,
2149 * set the color to white-on-black beforehand.
2151 * @return the string to emit to an ANSI / ECMA-style terminal
2153 private String
clearAll() {
2154 return "\033[0;37;40m\033[2J";
2158 * Clear the line from the cursor (inclusive) to the end of the screen.
2159 * Because some terminals use back-color-erase, set the color to
2160 * white-on-black beforehand.
2162 * @return the string to emit to an ANSI / ECMA-style terminal
2164 private String
clearRemainingLine() {
2165 return "\033[0;37;40m\033[K";
2169 * Move the cursor to (x, y).
2171 * @param x column coordinate. 0 is the left-most column.
2172 * @param y row coordinate. 0 is the top-most row.
2173 * @return the string to emit to an ANSI / ECMA-style terminal
2175 private String
gotoXY(final int x
, final int y
) {
2176 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
2180 * Tell (u)xterm that we want to receive mouse events based on "Any event
2181 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
2182 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
2184 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2186 * Note that this also sets the alternate/primary screen buffer.
2188 * @param on If true, enable mouse report and use the alternate screen
2189 * buffer. If false disable mouse reporting and use the primary screen
2191 * @return the string to emit to xterm
2193 private String
mouse(final boolean on
) {
2195 return "\033[?1002;1003;1005;1006h\033[?1049h";
2197 return "\033[?1002;1003;1006;1005l\033[?1049l";