2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 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
.awt
.image
.BufferedImage
;
32 import java
.io
.BufferedReader
;
33 import java
.io
.FileDescriptor
;
34 import java
.io
.FileInputStream
;
35 import java
.io
.InputStream
;
36 import java
.io
.InputStreamReader
;
37 import java
.io
.IOException
;
38 import java
.io
.OutputStream
;
39 import java
.io
.OutputStreamWriter
;
40 import java
.io
.PrintWriter
;
41 import java
.io
.Reader
;
42 import java
.io
.UnsupportedEncodingException
;
43 import java
.util
.ArrayList
;
44 import java
.util
.Collections
;
45 import java
.util
.HashMap
;
46 import java
.util
.List
;
49 import jexer
.bits
.Cell
;
50 import jexer
.bits
.CellAttributes
;
51 import jexer
.bits
.Color
;
52 import jexer
.event
.TCommandEvent
;
53 import jexer
.event
.TInputEvent
;
54 import jexer
.event
.TKeypressEvent
;
55 import jexer
.event
.TMouseEvent
;
56 import jexer
.event
.TResizeEvent
;
57 import static jexer
.TCommand
.*;
58 import static jexer
.TKeypress
.*;
61 * This class reads keystrokes and mouse events and emits output to ANSI
62 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
64 public class ECMA48Terminal
extends LogicalScreen
65 implements TerminalReader
, Runnable
{
67 // ------------------------------------------------------------------------
68 // Constants --------------------------------------------------------------
69 // ------------------------------------------------------------------------
72 * States in the input parser.
74 private enum ParseState
{
84 // ------------------------------------------------------------------------
85 // Variables --------------------------------------------------------------
86 // ------------------------------------------------------------------------
89 * Emit debugging to stderr.
91 private boolean debugToStderr
= false;
94 * If true, emit T.416-style RGB colors for normal system colors. This
95 * is a) expensive in bandwidth, and b) potentially terrible looking for
98 private static boolean doRgbColor
= false;
101 * The session information.
103 private SessionInfo sessionInfo
;
106 * The event queue, filled up by a thread reading on input.
108 private List
<TInputEvent
> eventQueue
;
111 * If true, we want the reader thread to exit gracefully.
113 private boolean stopReaderThread
;
118 private Thread readerThread
;
121 * Parameters being collected. E.g. if the string is \033[1;3m, then
122 * params[0] will be 1 and params[1] will be 3.
124 private List
<String
> params
;
127 * Current parsing state.
129 private ParseState state
;
132 * The time we entered ESCAPE. If we get a bare escape without a code
133 * following it, this is used to return that bare escape.
135 private long escapeTime
;
138 * The time we last checked the window size. We try not to spawn stty
139 * more than once per second.
141 private long windowSizeTime
;
144 * true if mouse1 was down. Used to report mouse1 on the release event.
146 private boolean mouse1
;
149 * true if mouse2 was down. Used to report mouse2 on the release event.
151 private boolean mouse2
;
154 * true if mouse3 was down. Used to report mouse3 on the release event.
156 private boolean mouse3
;
159 * Cache the cursor visibility value so we only emit the sequence when we
162 private boolean cursorOn
= true;
165 * Cache the last window size to figure out if a TResizeEvent needs to be
168 private TResizeEvent windowResize
= null;
171 * If true, emit wide-char (CJK/Emoji) characters as sixel images.
173 private boolean wideCharImages
= true;
176 * Window width in pixels. Used for sixel support.
178 private int widthPixels
= 640;
181 * Window height in pixels. Used for sixel support.
183 private int heightPixels
= 400;
186 * If true, emit image data via sixel.
188 private boolean sixel
= true;
191 * The sixel palette handler.
193 private SixelPalette palette
= null;
196 * The sixel post-rendered string cache.
198 private SixelCache sixelCache
= null;
201 * Number of colors in the sixel palette. Xterm 335 defines the max as
202 * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
205 private int sixelPaletteSize
= 1024;
208 * If true, then we changed System.in and need to change it back.
210 private boolean setRawMode
;
213 * The terminal's input. If an InputStream is not specified in the
214 * constructor, then this InputStreamReader will be bound to System.in
215 * with UTF-8 encoding.
217 private Reader input
;
220 * The terminal's raw InputStream. If an InputStream is not specified in
221 * the constructor, then this InputReader will be bound to System.in.
222 * This is used by run() to see if bytes are available() before calling
223 * (Reader)input.read().
225 private InputStream inputStream
;
228 * The terminal's output. If an OutputStream is not specified in the
229 * constructor, then this PrintWriter will be bound to System.out with
232 private PrintWriter output
;
235 * The listening object that run() wakes up on new input.
237 private Object listener
;
239 // Colors to map DOS colors to AWT colors.
240 private static java
.awt
.Color MYBLACK
;
241 private static java
.awt
.Color MYRED
;
242 private static java
.awt
.Color MYGREEN
;
243 private static java
.awt
.Color MYYELLOW
;
244 private static java
.awt
.Color MYBLUE
;
245 private static java
.awt
.Color MYMAGENTA
;
246 private static java
.awt
.Color MYCYAN
;
247 private static java
.awt
.Color MYWHITE
;
248 private static java
.awt
.Color MYBOLD_BLACK
;
249 private static java
.awt
.Color MYBOLD_RED
;
250 private static java
.awt
.Color MYBOLD_GREEN
;
251 private static java
.awt
.Color MYBOLD_YELLOW
;
252 private static java
.awt
.Color MYBOLD_BLUE
;
253 private static java
.awt
.Color MYBOLD_MAGENTA
;
254 private static java
.awt
.Color MYBOLD_CYAN
;
255 private static java
.awt
.Color MYBOLD_WHITE
;
258 * SixelPalette is used to manage the conversion of images between 24-bit
259 * RGB color and a palette of sixelPaletteSize colors.
261 private class SixelPalette
{
264 * Color palette for sixel output, sorted low to high.
266 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
269 * Map of color palette index for sixel output, from the order it was
270 * generated by makePalette() to rgbColors.
272 private int [] rgbSortedIndex
= new int[sixelPaletteSize
];
275 * The color palette, organized by hue, saturation, and luminance.
276 * This is used for a fast color match.
278 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
281 * Number of bits for hue.
283 private int hueBits
= -1;
286 * Number of bits for saturation.
288 private int satBits
= -1;
291 * Number of bits for luminance.
293 private int lumBits
= -1;
296 * Step size for hue bins.
298 private int hueStep
= -1;
301 * Step size for saturation bins.
303 private int satStep
= -1;
306 * Cached RGB to HSL result.
308 private int hsl
[] = new int[3];
311 * ColorIdx records a RGB color and its palette index.
313 private class ColorIdx
{
315 * The 24-bit RGB color.
320 * The palette index for this color.
325 * Public constructor.
327 * @param color the 24-bit RGB color
328 * @param index the palette index for this color
330 public ColorIdx(final int color
, final int index
) {
337 * Public constructor.
339 public SixelPalette() {
344 * Find the nearest match for a color in the palette.
346 * @param color the RGB color
347 * @return the index in rgbColors that is closest to color
349 public int matchColor(final int color
) {
354 * matchColor() is a critical performance bottleneck. To make it
355 * decent, we do the following:
357 * 1. Find the nearest two hues that bracket this color.
359 * 2. Find the nearest two saturations that bracket this color.
361 * 3. Iterate within these four bands of luminance values,
362 * returning the closest color by Euclidean distance.
364 * This strategy reduces the search space by about 97%.
366 int red
= (color
>>> 16) & 0xFF;
367 int green
= (color
>>> 8) & 0xFF;
368 int blue
= color
& 0xFF;
370 if (sixelPaletteSize
== 2) {
371 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
380 rgbToHsl(red
, green
, blue
, hsl
);
384 // System.err.printf("%d %d %d\n", hue, sat, lum);
386 double diff
= Double
.MAX_VALUE
;
389 int hue1
= hue
/ (360/hueStep
);
391 if (hue1
>= hslColors
.size() - 1) {
392 // Bracket pure red from above.
393 hue1
= hslColors
.size() - 1;
395 } else if (hue1
== 0) {
396 // Bracket pure red from below.
397 hue2
= hslColors
.size() - 1;
400 for (int hI
= hue1
; hI
!= -1;) {
401 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
404 } else if (hI
== hue2
) {
408 int sMin
= (sat
/ satStep
) - 1;
413 } else if (sMin
== sats
.size() - 1) {
418 assert (sMax
- sMin
== 1);
421 // int sMax = sats.size() - 1;
423 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
424 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
426 // True 3D colorspace match for the remaining values
427 for (ColorIdx c
: lums
) {
428 int rgbColor
= c
.color
;
430 int red2
= (rgbColor
>>> 16) & 0xFF;
431 int green2
= (rgbColor
>>> 8) & 0xFF;
432 int blue2
= rgbColor
& 0xFF;
433 newDiff
+= Math
.pow(red2
- red
, 2);
434 newDiff
+= Math
.pow(green2
- green
, 2);
435 newDiff
+= Math
.pow(blue2
- blue
, 2);
436 if (newDiff
< diff
) {
437 idx
= rgbSortedIndex
[c
.index
];
444 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
445 // Black is a closer match.
447 } else if ((((255 - red
) * (255 - red
)) +
448 ((255 - green
) * (255 - green
)) +
449 ((255 - blue
) * (255 - blue
))) < diff
) {
451 // White is a closer match.
452 idx
= sixelPaletteSize
- 1;
459 * Clamp an int value to [0, 255].
461 * @param x the int value
462 * @return an int between 0 and 255.
464 private int clamp(final int x
) {
475 * Dither an image to a sixelPaletteSize palette. The dithered
476 * image cells will contain indexes into the palette.
478 * @param image the image to dither
479 * @return the dithered image. Every pixel is an index into the
482 public BufferedImage
ditherImage(final BufferedImage image
) {
484 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
485 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
487 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
488 image
.getHeight(), null, 0, image
.getWidth());
489 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
490 rgbArray
, 0, image
.getWidth());
492 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
493 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
494 int oldPixel
= ditheredImage
.getRGB(imageX
,
496 int colorIdx
= matchColor(oldPixel
);
497 assert (colorIdx
>= 0);
498 assert (colorIdx
< sixelPaletteSize
);
499 int newPixel
= rgbColors
.get(colorIdx
);
500 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
502 int oldRed
= (oldPixel
>>> 16) & 0xFF;
503 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
504 int oldBlue
= oldPixel
& 0xFF;
506 int newRed
= (newPixel
>>> 16) & 0xFF;
507 int newGreen
= (newPixel
>>> 8) & 0xFF;
508 int newBlue
= newPixel
& 0xFF;
510 int redError
= (oldRed
- newRed
) / 16;
511 int greenError
= (oldGreen
- newGreen
) / 16;
512 int blueError
= (oldBlue
- newBlue
) / 16;
514 int red
, green
, blue
;
515 if (imageX
< image
.getWidth() - 1) {
516 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
517 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
518 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
519 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
521 green
= clamp(green
);
523 pXpY
= ((red
& 0xFF) << 16);
524 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
525 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
527 if (imageY
< image
.getHeight() - 1) {
528 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
530 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
531 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
532 blue
= ( pXpYp
& 0xFF) + blueError
;
534 green
= clamp(green
);
536 pXpYp
= ((red
& 0xFF) << 16);
537 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
538 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
540 } else if (imageY
< image
.getHeight() - 1) {
541 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
543 int pXYp
= ditheredImage
.getRGB(imageX
,
546 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
547 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
548 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
550 green
= clamp(green
);
552 pXmYp
= ((red
& 0xFF) << 16);
553 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
554 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
556 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
557 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
558 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
560 green
= clamp(green
);
562 pXYp
= ((red
& 0xFF) << 16);
563 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
564 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
566 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
567 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
569 return ditheredImage
;
573 * Convert an RGB color to HSL.
575 * @param red red color, between 0 and 255
576 * @param green green color, between 0 and 255
577 * @param blue blue color, between 0 and 255
578 * @param hsl the hsl color as [hue, saturation, luminance]
580 private void rgbToHsl(final int red
, final int green
,
581 final int blue
, final int [] hsl
) {
583 assert ((red
>= 0) && (red
<= 255));
584 assert ((green
>= 0) && (green
<= 255));
585 assert ((blue
>= 0) && (blue
<= 255));
587 double R
= red
/ 255.0;
588 double G
= green
/ 255.0;
589 double B
= blue
/ 255.0;
590 boolean Rmax
= false;
591 boolean Gmax
= false;
592 boolean Bmax
= false;
593 double min
= (R
< G ? R
: G
);
594 min
= (min
< B ? min
: B
);
596 if ((R
>= G
) && (R
>= B
)) {
599 } else if ((G
>= R
) && (G
>= B
)) {
602 } else if ((B
>= G
) && (B
>= R
)) {
607 double L
= (min
+ max
) / 2.0;
612 S
= (max
- min
) / (max
+ min
);
614 S
= (max
- min
) / (2.0 - max
- min
);
618 assert (Gmax
== false);
619 assert (Bmax
== false);
620 H
= (G
- B
) / (max
- min
);
622 assert (Rmax
== false);
623 assert (Bmax
== false);
624 H
= 2.0 + (B
- R
) / (max
- min
);
626 assert (Rmax
== false);
627 assert (Gmax
== false);
628 H
= 4.0 + (R
- G
) / (max
- min
);
633 hsl
[0] = (int) (H
* 60.0);
634 hsl
[1] = (int) (S
* 100.0);
635 hsl
[2] = (int) (L
* 100.0);
637 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
638 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
639 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
643 * Convert a HSL color to RGB.
645 * @param hue hue, between 0 and 359
646 * @param sat saturation, between 0 and 100
647 * @param lum luminance, between 0 and 100
648 * @return the rgb color as 0x00RRGGBB
650 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
651 assert ((hue
>= 0) && (hue
<= 360));
652 assert ((sat
>= 0) && (sat
<= 100));
653 assert ((lum
>= 0) && (lum
<= 100));
655 double S
= sat
/ 100.0;
656 double L
= lum
/ 100.0;
657 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
658 double Hp
= hue
/ 60.0;
659 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
666 } else if (Hp
<= 2.0) {
669 } else if (Hp
<= 3.0) {
672 } else if (Hp
<= 4.0) {
675 } else if (Hp
<= 5.0) {
678 } else if (Hp
<= 6.0) {
682 double m
= L
- (C
/ 2.0);
683 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
684 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
685 int blue
= (int) ((Bp
+ m
) * 255.0);
687 return (red
| green
| blue
);
691 * Create the sixel palette.
693 private void makePalette() {
694 // Generate the sixel palette. Because we have no idea at this
695 // layer which image(s) will be shown, we have to use a common
696 // palette with sixelPaletteSize colors for everything, and
697 // map the BufferedImage colors to their nearest neighbor in RGB
700 if (sixelPaletteSize
== 2) {
702 rgbColors
.add(0xFFFFFF);
703 rgbSortedIndex
[0] = 0;
704 rgbSortedIndex
[1] = 1;
708 // We build a palette using the Hue-Saturation-Luminence model,
709 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
710 // Luminance. We convert these colors to 24-bit RGB, sort them
711 // ascending, and steal the first index for pure black and the
712 // last for pure white. The 8-bit final palette favors bright
713 // colors, somewhere between pastel and classic television
714 // technicolor. 9- and 10-bit palettes are more uniform.
716 // Default at 256 colors.
721 assert (sixelPaletteSize
>= 256);
722 assert ((sixelPaletteSize
== 256)
723 || (sixelPaletteSize
== 512)
724 || (sixelPaletteSize
== 1024)
725 || (sixelPaletteSize
== 2048));
727 switch (sixelPaletteSize
) {
744 hueStep
= (int) (Math
.pow(2, hueBits
));
745 satStep
= (int) (100 / Math
.pow(2, satBits
));
746 // 1 bit for luminance: 40 and 70.
751 // 2 bits: 20, 40, 60, 80
756 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
762 // System.err.printf("<html><body>\n");
763 // Hue is evenly spaced around the wheel.
764 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
766 final boolean DEBUG
= false;
767 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
769 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
770 hue
+= (360/hueStep
)) {
772 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
773 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
774 hslColors
.add(satList
);
776 // Saturation is linearly spaced between pastel and pure.
777 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
779 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
780 satList
.add(lumList
);
782 // Luminance brackets the pure color, but leaning toward
784 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
786 System.err.printf("<font style = \"color:");
787 System.err.printf("hsl(%d, %d%%, %d%%)",
789 System.err.printf(";\">=</font>\n");
791 int rgbColor
= hslToRgb(hue
, sat
, lum
);
792 rgbColors
.add(rgbColor
);
793 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
794 rgbColors
.size() - 1);
795 lumList
.add(colorIdx
);
797 rawRgbList
.add(rgbColor
);
799 int red
= (rgbColor
>>> 16) & 0xFF;
800 int green
= (rgbColor
>>> 8) & 0xFF;
801 int blue
= rgbColor
& 0xFF;
802 int [] backToHsl
= new int[3];
803 rgbToHsl(red
, green
, blue
, backToHsl
);
804 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
805 hue
, backToHsl
[0], sat
, backToHsl
[1],
811 // System.err.printf("\n</body></html>\n");
813 assert (rgbColors
.size() == sixelPaletteSize
);
816 * We need to sort rgbColors, so that toSixel() can know where
817 * BLACK and WHITE are in it. But we also need to be able to
818 * find the sorted values using the old unsorted indexes. So we
819 * will sort it, put all the indexes into a HashMap, and then
820 * build rgbSortedIndex[].
822 Collections
.sort(rgbColors
);
823 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
824 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
825 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
826 rgbColorIndices
.put(rgbColors
.get(i
), i
);
828 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
829 int rawColor
= rawRgbList
.get(i
);
830 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
833 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
834 assert (rawRgbList
!= null);
835 int idx
= rgbSortedIndex
[i
];
836 int rgbColor
= rgbColors
.get(idx
);
837 if ((idx
!= 0) && (idx
!= sixelPaletteSize
- 1)) {
839 System.err.printf("%d %06x --> %d %06x\n",
840 i, rawRgbList.get(i), idx, rgbColors.get(idx));
842 assert (rgbColor
== rawRgbList
.get(i
));
847 // Set the dimmest color as true black, and the brightest as true
850 rgbColors
.set(sixelPaletteSize
- 1, 0xFFFFFF);
853 System.err.printf("<html><body>\n");
854 for (Integer rgb: rgbColors) {
855 System.err.printf("<font style = \"color:");
856 System.err.printf("#%06x", rgb);
857 System.err.printf(";\">=</font>\n");
859 System.err.printf("\n</body></html>\n");
865 * Emit the sixel palette.
867 * @param sb the StringBuilder to append to
868 * @param used array of booleans set to true for each color actually
869 * used in this cell, or null to emit the entire palette
870 * @return the string to emit to an ANSI / ECMA-style terminal
872 public String
emitPalette(final StringBuilder sb
,
873 final boolean [] used
) {
875 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
876 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
877 int rgbColor
= rgbColors
.get(i
);
878 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
879 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
880 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
881 ( rgbColor
& 0xFF) * 100 / 255));
884 return sb
.toString();
889 * SixelCache is a least-recently-used cache that hangs on to the
890 * post-rendered sixel string for a particular set of cells.
892 private class SixelCache
{
895 * Maximum size of the cache.
897 private int maxSize
= 100;
900 * The entries stored in the cache.
902 private HashMap
<String
, CacheEntry
> cache
= null;
905 * CacheEntry is one entry in the cache.
907 private class CacheEntry
{
919 * The last time this entry was used.
921 public long millis
= 0;
924 * Public constructor.
926 * @param key the cache entry key
927 * @param data the cache entry data
929 public CacheEntry(final String key
, final String data
) {
932 this.millis
= System
.currentTimeMillis();
937 * Public constructor.
939 * @param maxSize the maximum size of the cache
941 public SixelCache(final int maxSize
) {
942 this.maxSize
= maxSize
;
943 cache
= new HashMap
<String
, CacheEntry
>();
947 * Make a unique key for a list of cells.
949 * @param cells the cells
952 private String
makeKey(final ArrayList
<Cell
> cells
) {
953 StringBuilder sb
= new StringBuilder();
954 for (Cell cell
: cells
) {
955 sb
.append(cell
.hashCode());
957 return sb
.toString();
961 * Get an entry from the cache.
963 * @param cells the list of cells that are the cache key
964 * @return the sixel string representing these cells, or null if this
965 * list of cells is not in the cache
967 public String
get(final ArrayList
<Cell
> cells
) {
968 CacheEntry entry
= cache
.get(makeKey(cells
));
972 entry
.millis
= System
.currentTimeMillis();
977 * Put an entry into the cache.
979 * @param cells the list of cells that are the cache key
980 * @param data the sixel string representing these cells
982 public void put(final ArrayList
<Cell
> cells
, final String data
) {
983 String key
= makeKey(cells
);
985 // System.err.println("put() " + key + " size " + cache.size());
987 assert (!cache
.containsKey(key
));
989 assert (cache
.size() <= maxSize
);
990 if (cache
.size() == maxSize
) {
991 // Cache is at limit, evict oldest entry.
992 long oldestTime
= Long
.MAX_VALUE
;
993 String keyToRemove
= null;
994 for (CacheEntry entry
: cache
.values()) {
995 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
996 keyToRemove
= entry
.key
;
997 oldestTime
= entry
.millis
;
1001 System.err.println("put() remove key = " + keyToRemove +
1002 " size " + cache.size());
1004 assert (keyToRemove
!= null);
1005 cache
.remove(keyToRemove
);
1007 System.err.println("put() removed, size " + cache.size());
1010 assert (cache
.size() <= maxSize
);
1011 CacheEntry entry
= new CacheEntry(key
, data
);
1012 assert (key
.equals(entry
.key
));
1013 cache
.put(key
, entry
);
1015 System.err.println("put() added key " + key + " " +
1016 " size " + cache.size());
1022 // ------------------------------------------------------------------------
1023 // Constructors -----------------------------------------------------------
1024 // ------------------------------------------------------------------------
1027 * Constructor sets up state for getEvent(). If either windowWidth or
1028 * windowHeight are less than 1, the terminal is not resized.
1030 * @param listener the object this backend needs to wake up when new
1032 * @param input an InputStream connected to the remote user, or null for
1033 * System.in. If System.in is used, then on non-Windows systems it will
1034 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1035 * cooked mode. input is always converted to a Reader with UTF-8
1037 * @param output an OutputStream connected to the remote user, or null
1038 * for System.out. output is always converted to a Writer with UTF-8
1040 * @param windowWidth the number of text columns to start with
1041 * @param windowHeight the number of text rows to start with
1042 * @throws UnsupportedEncodingException if an exception is thrown when
1043 * creating the InputStreamReader
1045 public ECMA48Terminal(final Object listener
, final InputStream input
,
1046 final OutputStream output
, final int windowWidth
,
1047 final int windowHeight
) throws UnsupportedEncodingException
{
1049 this(listener
, input
, output
);
1051 // Send dtterm/xterm sequences, which will probably not work because
1052 // allowWindowOps is defaulted to false.
1053 if ((windowWidth
> 0) && (windowHeight
> 0)) {
1054 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1056 this.output
.write(resizeString
);
1057 this.output
.flush();
1062 * Constructor sets up state for getEvent().
1064 * @param listener the object this backend needs to wake up when new
1066 * @param input an InputStream connected to the remote user, or null for
1067 * System.in. If System.in is used, then on non-Windows systems it will
1068 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1069 * cooked mode. input is always converted to a Reader with UTF-8
1071 * @param output an OutputStream connected to the remote user, or null
1072 * for System.out. output is always converted to a Writer with UTF-8
1074 * @throws UnsupportedEncodingException if an exception is thrown when
1075 * creating the InputStreamReader
1077 public ECMA48Terminal(final Object listener
, final InputStream input
,
1078 final OutputStream output
) throws UnsupportedEncodingException
{
1084 stopReaderThread
= false;
1085 this.listener
= listener
;
1087 if (input
== null) {
1088 // inputStream = System.in;
1089 inputStream
= new FileInputStream(FileDescriptor
.in
);
1093 inputStream
= input
;
1095 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1097 if (input
instanceof SessionInfo
) {
1098 // This is a TelnetInputStream that exposes window size and
1099 // environment variables from the telnet layer.
1100 sessionInfo
= (SessionInfo
) input
;
1102 if (sessionInfo
== null) {
1103 if (input
== null) {
1104 // Reading right off the tty
1105 sessionInfo
= new TTYSessionInfo();
1107 sessionInfo
= new TSessionInfo();
1111 if (output
== null) {
1112 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1115 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1119 // Request xterm report window dimensions in pixels
1120 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1122 // Enable mouse reporting and metaSendsEscape
1123 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1124 this.output
.flush();
1126 // Query the screen size
1127 sessionInfo
.queryWindowSize();
1128 setDimensions(sessionInfo
.getWindowWidth(),
1129 sessionInfo
.getWindowHeight());
1131 // Hang onto the window size
1132 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1133 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1137 // Spin up the input reader
1138 eventQueue
= new ArrayList
<TInputEvent
>();
1139 readerThread
= new Thread(this);
1140 readerThread
.start();
1143 this.output
.write(clearAll());
1144 this.output
.flush();
1148 * Constructor sets up state for getEvent().
1150 * @param listener the object this backend needs to wake up when new
1152 * @param input the InputStream underlying 'reader'. Its available()
1153 * method is used to determine if reader.read() will block or not.
1154 * @param reader a Reader connected to the remote user.
1155 * @param writer a PrintWriter connected to the remote user.
1156 * @param setRawMode if true, set System.in into raw mode with stty.
1157 * This should in general not be used. It is here solely for Demo3,
1158 * which uses System.in.
1159 * @throws IllegalArgumentException if input, reader, or writer are null.
1161 public ECMA48Terminal(final Object listener
, final InputStream input
,
1162 final Reader reader
, final PrintWriter writer
,
1163 final boolean setRawMode
) {
1165 if (input
== null) {
1166 throw new IllegalArgumentException("InputStream must be specified");
1168 if (reader
== null) {
1169 throw new IllegalArgumentException("Reader must be specified");
1171 if (writer
== null) {
1172 throw new IllegalArgumentException("Writer must be specified");
1178 stopReaderThread
= false;
1179 this.listener
= listener
;
1181 inputStream
= input
;
1182 this.input
= reader
;
1184 if (setRawMode
== true) {
1187 this.setRawMode
= setRawMode
;
1189 if (input
instanceof SessionInfo
) {
1190 // This is a TelnetInputStream that exposes window size and
1191 // environment variables from the telnet layer.
1192 sessionInfo
= (SessionInfo
) input
;
1194 if (sessionInfo
== null) {
1195 if (setRawMode
== true) {
1196 // Reading right off the tty
1197 sessionInfo
= new TTYSessionInfo();
1199 sessionInfo
= new TSessionInfo();
1203 this.output
= writer
;
1205 // Request xterm report window dimensions in pixels
1206 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1208 // Enable mouse reporting and metaSendsEscape
1209 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1210 this.output
.flush();
1212 // Query the screen size
1213 sessionInfo
.queryWindowSize();
1214 setDimensions(sessionInfo
.getWindowWidth(),
1215 sessionInfo
.getWindowHeight());
1217 // Hang onto the window size
1218 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1219 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1223 // Spin up the input reader
1224 eventQueue
= new ArrayList
<TInputEvent
>();
1225 readerThread
= new Thread(this);
1226 readerThread
.start();
1229 this.output
.write(clearAll());
1230 this.output
.flush();
1234 * Constructor sets up state for getEvent().
1236 * @param listener the object this backend needs to wake up when new
1238 * @param input the InputStream underlying 'reader'. Its available()
1239 * method is used to determine if reader.read() will block or not.
1240 * @param reader a Reader connected to the remote user.
1241 * @param writer a PrintWriter connected to the remote user.
1242 * @throws IllegalArgumentException if input, reader, or writer are null.
1244 public ECMA48Terminal(final Object listener
, final InputStream input
,
1245 final Reader reader
, final PrintWriter writer
) {
1247 this(listener
, input
, reader
, writer
, false);
1250 // ------------------------------------------------------------------------
1251 // LogicalScreen ----------------------------------------------------------
1252 // ------------------------------------------------------------------------
1255 * Set the window title.
1257 * @param title the new title
1260 public void setTitle(final String title
) {
1261 output
.write(getSetTitleString(title
));
1266 * Push the logical screen to the physical device.
1269 public void flushPhysical() {
1270 StringBuilder sb
= new StringBuilder();
1274 && (cursorY
<= height
- 1)
1275 && (cursorX
<= width
- 1)
1278 sb
.append(cursor(true));
1279 sb
.append(gotoXY(cursorX
, cursorY
));
1281 sb
.append(cursor(false));
1284 output
.write(sb
.toString());
1289 * Resize the physical screen to match the logical screen dimensions.
1292 public void resizeToScreen() {
1293 // Send dtterm/xterm sequences, which will probably not work because
1294 // allowWindowOps is defaulted to false.
1295 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1297 this.output
.write(resizeString
);
1298 this.output
.flush();
1301 // ------------------------------------------------------------------------
1302 // TerminalReader ---------------------------------------------------------
1303 // ------------------------------------------------------------------------
1306 * Check if there are events in the queue.
1308 * @return if true, getEvents() has something to return to the backend
1310 public boolean hasEvents() {
1311 synchronized (eventQueue
) {
1312 return (eventQueue
.size() > 0);
1317 * Return any events in the IO queue.
1319 * @param queue list to append new events to
1321 public void getEvents(final List
<TInputEvent
> queue
) {
1322 synchronized (eventQueue
) {
1323 if (eventQueue
.size() > 0) {
1324 synchronized (queue
) {
1325 queue
.addAll(eventQueue
);
1333 * Restore terminal to normal state.
1335 public void closeTerminal() {
1337 // System.err.println("=== closeTerminal() ==="); System.err.flush();
1339 // Tell the reader thread to stop looking at input
1340 stopReaderThread
= true;
1342 readerThread
.join();
1343 } catch (InterruptedException e
) {
1344 if (debugToStderr
) {
1345 e
.printStackTrace();
1349 // Disable mouse reporting and show cursor. Defensive null check
1350 // here in case closeTerminal() is called twice.
1351 if (output
!= null) {
1352 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
1359 // We don't close System.in/out
1361 // Shut down the streams, this should wake up the reader thread
1362 // and make it exit.
1363 if (input
!= null) {
1366 } catch (IOException e
) {
1371 if (output
!= null) {
1379 * Set listener to a different Object.
1381 * @param listener the new listening object that run() wakes up on new
1384 public void setListener(final Object listener
) {
1385 this.listener
= listener
;
1389 * Reload options from System properties.
1391 public void reloadOptions() {
1392 // Permit RGB colors only if externally requested.
1393 if (System
.getProperty("jexer.ECMA48.rgbColor",
1394 "false").equals("true")
1401 // Default to using sixel for full-width characters.
1402 if (System
.getProperty("jexer.ECMA48.wideCharImages",
1403 "true").equals("true")) {
1404 wideCharImages
= true;
1406 wideCharImages
= false;
1409 // Pull the system properties for sixel output.
1410 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1417 int paletteSize
= 1024;
1419 paletteSize
= Integer
.parseInt(System
.getProperty(
1420 "jexer.ECMA48.sixelPaletteSize", "1024"));
1421 switch (paletteSize
) {
1427 sixelPaletteSize
= paletteSize
;
1433 } catch (NumberFormatException e
) {
1437 // Set custom colors
1438 setCustomSystemColors();
1441 // ------------------------------------------------------------------------
1442 // Runnable ---------------------------------------------------------------
1443 // ------------------------------------------------------------------------
1446 * Read function runs on a separate thread.
1449 boolean done
= false;
1450 // available() will often return > 1, so we need to read in chunks to
1452 char [] readBuffer
= new char[128];
1453 List
<TInputEvent
> events
= new ArrayList
<TInputEvent
>();
1455 while (!done
&& !stopReaderThread
) {
1457 // We assume that if inputStream has bytes available, then
1458 // input won't block on read().
1459 int n
= inputStream
.available();
1462 System.err.printf("inputStream.available(): %d\n", n);
1467 if (readBuffer
.length
< n
) {
1468 // The buffer wasn't big enough, make it huger
1469 readBuffer
= new char[readBuffer
.length
* 2];
1472 // System.err.printf("BEFORE read()\n"); System.err.flush();
1474 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1477 System.err.printf("AFTER read() %d\n", rc);
1485 for (int i
= 0; i
< rc
; i
++) {
1486 int ch
= readBuffer
[i
];
1487 processChar(events
, (char)ch
);
1489 getIdleEvents(events
);
1490 if (events
.size() > 0) {
1491 // Add to the queue for the backend thread to
1492 // be able to obtain.
1493 synchronized (eventQueue
) {
1494 eventQueue
.addAll(events
);
1496 if (listener
!= null) {
1497 synchronized (listener
) {
1498 listener
.notifyAll();
1505 getIdleEvents(events
);
1506 if (events
.size() > 0) {
1507 synchronized (eventQueue
) {
1508 eventQueue
.addAll(events
);
1510 if (listener
!= null) {
1511 synchronized (listener
) {
1512 listener
.notifyAll();
1518 if (output
.checkError()) {
1523 // Wait 20 millis for more data
1526 // System.err.println("end while loop"); System.err.flush();
1527 } catch (InterruptedException e
) {
1529 } catch (IOException e
) {
1530 e
.printStackTrace();
1533 } // while ((done == false) && (stopReaderThread == false))
1535 // Pass an event up to TApplication to tell it this Backend is done.
1536 synchronized (eventQueue
) {
1537 eventQueue
.add(new TCommandEvent(cmBackendDisconnect
));
1539 if (listener
!= null) {
1540 synchronized (listener
) {
1541 listener
.notifyAll();
1545 // System.err.println("*** run() exiting..."); System.err.flush();
1548 // ------------------------------------------------------------------------
1549 // ECMA48Terminal ---------------------------------------------------------
1550 // ------------------------------------------------------------------------
1553 * Get the width of a character cell in pixels.
1555 * @return the width in pixels of a character cell
1557 public int getTextWidth() {
1558 return (widthPixels
/ sessionInfo
.getWindowWidth());
1562 * Get the height of a character cell in pixels.
1564 * @return the height in pixels of a character cell
1566 public int getTextHeight() {
1567 return (heightPixels
/ sessionInfo
.getWindowHeight());
1571 * Getter for sessionInfo.
1573 * @return the SessionInfo
1575 public SessionInfo
getSessionInfo() {
1580 * Get the output writer.
1582 * @return the Writer
1584 public PrintWriter
getOutput() {
1589 * Call 'stty' to set cooked mode.
1591 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1593 private void sttyCooked() {
1598 * Call 'stty' to set raw mode.
1600 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1601 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1602 * -parenb cs8 min 1 < /dev/tty'
1604 private void sttyRaw() {
1609 * Call 'stty' to set raw or cooked mode.
1611 * @param mode if true, set raw mode, otherwise set cooked mode
1613 private void doStty(final boolean mode
) {
1614 String
[] cmdRaw
= {
1615 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1617 String
[] cmdCooked
= {
1618 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1623 process
= Runtime
.getRuntime().exec(cmdRaw
);
1625 process
= Runtime
.getRuntime().exec(cmdCooked
);
1627 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1628 String line
= in
.readLine();
1629 if ((line
!= null) && (line
.length() > 0)) {
1630 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1633 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1634 line
= err
.readLine();
1635 if ((line
!= null) && (line
.length() > 0)) {
1636 System
.err
.println("Error output from stty: " + line
);
1641 } catch (InterruptedException e
) {
1642 if (debugToStderr
) {
1643 e
.printStackTrace();
1647 int rc
= process
.exitValue();
1649 System
.err
.println("stty returned error code: " + rc
);
1651 } catch (IOException e
) {
1652 e
.printStackTrace();
1659 public void flush() {
1664 * Perform a somewhat-optimal rendering of a line.
1666 * @param y row coordinate. 0 is the top-most row.
1667 * @param sb StringBuilder to write escape sequences to
1668 * @param lastAttr cell attributes from the last call to flushLine
1670 private void flushLine(final int y
, final StringBuilder sb
,
1671 CellAttributes lastAttr
) {
1675 for (int x
= 0; x
< width
; x
++) {
1676 Cell lCell
= logical
[x
][y
];
1677 if (!lCell
.isBlank()) {
1681 // Push textEnd to first column beyond the text area
1685 // reallyCleared = true;
1687 boolean hasImage
= false;
1689 for (int x
= 0; x
< width
; x
++) {
1690 Cell lCell
= logical
[x
][y
];
1691 Cell pCell
= physical
[x
][y
];
1693 if (!lCell
.equals(pCell
) || reallyCleared
) {
1695 if (debugToStderr
) {
1696 System
.err
.printf("\n--\n");
1697 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1698 System
.err
.printf(" lCell: %s\n", lCell
);
1699 System
.err
.printf(" pCell: %s\n", pCell
);
1700 System
.err
.printf(" ==== \n");
1703 if (lastAttr
== null) {
1704 lastAttr
= new CellAttributes();
1705 sb
.append(normal());
1709 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1710 // Advancing at least one cell, or the first gotoXY
1711 sb
.append(gotoXY(x
, y
));
1714 assert (lastAttr
!= null);
1716 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1717 assert (lCell
.isBlank());
1719 for (int i
= x
; i
< width
; i
++) {
1720 assert (logical
[i
][y
].isBlank());
1721 // Physical is always updated
1722 physical
[i
][y
].reset();
1725 // Clear remaining line
1726 sb
.append(clearRemainingLine());
1731 // Image cell: bypass the rest of the loop, it is not
1733 if ((wideCharImages
&& lCell
.isImage())
1736 && (lCell
.getWidth() == Cell
.Width
.SINGLE
))
1740 // Save the last rendered cell
1743 // Physical is always updated
1744 physical
[x
][y
].setTo(lCell
);
1748 assert ((wideCharImages
&& !lCell
.isImage())
1750 && (!lCell
.isImage()
1752 && (lCell
.getWidth() != Cell
.Width
.SINGLE
)))));
1754 if (!wideCharImages
&& (lCell
.getWidth() == Cell
.Width
.RIGHT
)) {
1760 sb
.append(gotoXY(x
, y
));
1763 // Now emit only the modified attributes
1764 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1765 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1767 && (lCell
.isBold() == lastAttr
.isBold())
1768 && (lCell
.isReverse() == lastAttr
.isReverse())
1769 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1770 && (lCell
.isBlink() == lastAttr
.isBlink())
1772 // Both colors changed, attributes the same
1773 sb
.append(color(lCell
.isBold(),
1774 lCell
.getForeColor(), lCell
.getBackColor()));
1776 if (debugToStderr
) {
1777 System
.err
.printf("1 Change only fore/back colors\n");
1780 } else if (lCell
.isRGB()
1781 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1782 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1783 && (lCell
.isBold() == lastAttr
.isBold())
1784 && (lCell
.isReverse() == lastAttr
.isReverse())
1785 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1786 && (lCell
.isBlink() == lastAttr
.isBlink())
1788 // Both colors changed, attributes the same
1789 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1790 lCell
.getBackColorRGB()));
1792 if (debugToStderr
) {
1793 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1795 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1796 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1798 && (lCell
.isBold() != lastAttr
.isBold())
1799 && (lCell
.isReverse() != lastAttr
.isReverse())
1800 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1801 && (lCell
.isBlink() != lastAttr
.isBlink())
1803 // Everything is different
1804 sb
.append(color(lCell
.getForeColor(),
1805 lCell
.getBackColor(),
1806 lCell
.isBold(), lCell
.isReverse(),
1808 lCell
.isUnderline()));
1810 if (debugToStderr
) {
1811 System
.err
.printf("2 Set all attributes\n");
1813 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1814 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1816 && (lCell
.isBold() == lastAttr
.isBold())
1817 && (lCell
.isReverse() == lastAttr
.isReverse())
1818 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1819 && (lCell
.isBlink() == lastAttr
.isBlink())
1822 // Attributes same, foreColor different
1823 sb
.append(color(lCell
.isBold(),
1824 lCell
.getForeColor(), true));
1826 if (debugToStderr
) {
1827 System
.err
.printf("3 Change foreColor\n");
1829 } else if (lCell
.isRGB()
1830 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1831 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1832 && (lCell
.getForeColorRGB() >= 0)
1833 && (lCell
.getBackColorRGB() >= 0)
1834 && (lCell
.isBold() == lastAttr
.isBold())
1835 && (lCell
.isReverse() == lastAttr
.isReverse())
1836 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1837 && (lCell
.isBlink() == lastAttr
.isBlink())
1839 // Attributes same, foreColor different
1840 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1842 if (debugToStderr
) {
1843 System
.err
.printf("3 Change foreColor (RGB)\n");
1845 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1846 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1848 && (lCell
.isBold() == lastAttr
.isBold())
1849 && (lCell
.isReverse() == lastAttr
.isReverse())
1850 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1851 && (lCell
.isBlink() == lastAttr
.isBlink())
1853 // Attributes same, backColor different
1854 sb
.append(color(lCell
.isBold(),
1855 lCell
.getBackColor(), false));
1857 if (debugToStderr
) {
1858 System
.err
.printf("4 Change backColor\n");
1860 } else if (lCell
.isRGB()
1861 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1862 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1863 && (lCell
.isBold() == lastAttr
.isBold())
1864 && (lCell
.isReverse() == lastAttr
.isReverse())
1865 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1866 && (lCell
.isBlink() == lastAttr
.isBlink())
1868 // Attributes same, foreColor different
1869 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1871 if (debugToStderr
) {
1872 System
.err
.printf("4 Change backColor (RGB)\n");
1874 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1875 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1876 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1877 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1878 && (lCell
.isBold() == lastAttr
.isBold())
1879 && (lCell
.isReverse() == lastAttr
.isReverse())
1880 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1881 && (lCell
.isBlink() == lastAttr
.isBlink())
1884 // All attributes the same, just print the char
1887 if (debugToStderr
) {
1888 System
.err
.printf("5 Only emit character\n");
1891 // Just reset everything again
1892 if (!lCell
.isRGB()) {
1893 sb
.append(color(lCell
.getForeColor(),
1894 lCell
.getBackColor(),
1898 lCell
.isUnderline()));
1900 if (debugToStderr
) {
1901 System
.err
.printf("6 Change all attributes\n");
1904 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1905 lCell
.getBackColorRGB(),
1909 lCell
.isUnderline()));
1910 if (debugToStderr
) {
1911 System
.err
.printf("6 Change all attributes (RGB)\n");
1916 // Emit the character
1918 // Don't emit the right-half of full-width chars.
1920 && (lCell
.getWidth() != Cell
.Width
.RIGHT
))
1922 sb
.append(Character
.toChars(lCell
.getChar()));
1925 // Save the last rendered cell
1927 lastAttr
.setTo(lCell
);
1929 // Physical is always updated
1930 physical
[x
][y
].setTo(lCell
);
1932 } // if (!lCell.equals(pCell) || (reallyCleared == true))
1934 } // for (int x = 0; x < width; x++)
1938 * Render the screen to a string that can be emitted to something that
1939 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
1941 * @param sb StringBuilder to write escape sequences to
1942 * @return escape sequences string that provides the updates to the
1945 private String
flushString(final StringBuilder sb
) {
1946 CellAttributes attr
= null;
1948 if (reallyCleared
) {
1949 attr
= new CellAttributes();
1950 sb
.append(clearAll());
1954 * For sixel support, draw all of the sixel output first, and then
1955 * draw everything else afterwards. This works OK, but performance
1956 * is still a drag on larger pictures.
1958 for (int y
= 0; y
< height
; y
++) {
1959 for (int x
= 0; x
< width
; x
++) {
1960 // If physical had non-image data that is now image data, the
1961 // entire row must be redrawn.
1962 Cell lCell
= logical
[x
][y
];
1963 Cell pCell
= physical
[x
][y
];
1964 if (lCell
.isImage() && !pCell
.isImage()) {
1970 for (int y
= 0; y
< height
; y
++) {
1971 for (int x
= 0; x
< width
; x
++) {
1972 Cell lCell
= logical
[x
][y
];
1973 Cell pCell
= physical
[x
][y
];
1975 if (!lCell
.isImage()
1977 && (lCell
.getWidth() != Cell
.Width
.SINGLE
))
1984 while ((right
< width
)
1985 && (logical
[right
][y
].isImage())
1986 && (!logical
[right
][y
].equals(physical
[right
][y
])
1991 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
1992 for (int i
= 0; i
< (right
- x
); i
++) {
1993 assert (logical
[x
+ i
][y
].isImage());
1994 cellsToDraw
.add(logical
[x
+ i
][y
]);
1996 // Physical is always updated.
1997 physical
[x
+ i
][y
].setTo(lCell
);
1999 if (cellsToDraw
.size() > 0) {
2000 sb
.append(toSixel(x
, y
, cellsToDraw
));
2007 // Draw the text part now.
2008 for (int y
= 0; y
< height
; y
++) {
2009 flushLine(y
, sb
, attr
);
2012 reallyCleared
= false;
2014 String result
= sb
.toString();
2015 if (debugToStderr
) {
2016 System
.err
.printf("flushString(): %s\n", result
);
2022 * Reset keyboard/mouse input parser.
2024 private void resetParser() {
2025 state
= ParseState
.GROUND
;
2026 params
= new ArrayList
<String
>();
2032 * Produce a control character or one of the special ones (ENTER, TAB,
2035 * @param ch Unicode code point
2036 * @param alt if true, set alt on the TKeypress
2037 * @return one TKeypress event, either a control character (e.g. isKey ==
2038 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
2041 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
2042 // System.err.printf("controlChar: %02x\n", ch);
2046 // Carriage return --> ENTER
2047 return new TKeypressEvent(kbEnter
, alt
, false, false);
2049 // Linefeed --> ENTER
2050 return new TKeypressEvent(kbEnter
, alt
, false, false);
2053 return new TKeypressEvent(kbEsc
, alt
, false, false);
2056 return new TKeypressEvent(kbTab
, alt
, false, false);
2058 // Make all other control characters come back as the alphabetic
2059 // character with the ctrl field set. So SOH would be 'A' +
2061 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
2067 * Produce special key from CSI Pn ; Pm ; ... ~
2069 * @return one KEYPRESS event representing a special key
2071 private TInputEvent
csiFnKey() {
2073 if (params
.size() > 0) {
2074 key
= Integer
.parseInt(params
.get(0));
2076 boolean alt
= false;
2077 boolean ctrl
= false;
2078 boolean shift
= false;
2079 if (params
.size() > 1) {
2080 shift
= csiIsShift(params
.get(1));
2081 alt
= csiIsAlt(params
.get(1));
2082 ctrl
= csiIsCtrl(params
.get(1));
2087 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2089 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2091 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2093 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2095 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2097 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2099 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2101 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2103 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2105 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2107 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2109 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2111 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2113 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2121 * Produce mouse events based on "Any event tracking" and UTF-8
2123 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2125 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2127 private TInputEvent
parseMouse() {
2128 int buttons
= params
.get(0).charAt(0) - 32;
2129 int x
= params
.get(0).charAt(1) - 32 - 1;
2130 int y
= params
.get(0).charAt(2) - 32 - 1;
2132 // Clamp X and Y to the physical screen coordinates.
2133 if (x
>= windowResize
.getWidth()) {
2134 x
= windowResize
.getWidth() - 1;
2136 if (y
>= windowResize
.getHeight()) {
2137 y
= windowResize
.getHeight() - 1;
2140 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2141 boolean eventMouse1
= false;
2142 boolean eventMouse2
= false;
2143 boolean eventMouse3
= false;
2144 boolean eventMouseWheelUp
= false;
2145 boolean eventMouseWheelDown
= false;
2147 // System.err.printf("buttons: %04x\r\n", buttons);
2164 if (!mouse1
&& !mouse2
&& !mouse3
) {
2165 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2167 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2184 // Dragging with mouse1 down
2187 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2191 // Dragging with mouse2 down
2194 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2198 // Dragging with mouse3 down
2201 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2205 // Dragging with mouse2 down after wheelUp
2208 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2212 // Dragging with mouse2 down after wheelDown
2215 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2219 eventMouseWheelUp
= true;
2223 eventMouseWheelDown
= true;
2227 // Unknown, just make it motion
2228 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2231 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2232 eventMouse1
, eventMouse2
, eventMouse3
,
2233 eventMouseWheelUp
, eventMouseWheelDown
);
2237 * Produce mouse events based on "Any event tracking" and SGR
2239 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2241 * @param release if true, this was a release ('m')
2242 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2244 private TInputEvent
parseMouseSGR(final boolean release
) {
2245 // SGR extended coordinates - mode 1006
2246 if (params
.size() < 3) {
2247 // Invalid position, bail out.
2250 int buttons
= Integer
.parseInt(params
.get(0));
2251 int x
= Integer
.parseInt(params
.get(1)) - 1;
2252 int y
= Integer
.parseInt(params
.get(2)) - 1;
2254 // Clamp X and Y to the physical screen coordinates.
2255 if (x
>= windowResize
.getWidth()) {
2256 x
= windowResize
.getWidth() - 1;
2258 if (y
>= windowResize
.getHeight()) {
2259 y
= windowResize
.getHeight() - 1;
2262 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2263 boolean eventMouse1
= false;
2264 boolean eventMouse2
= false;
2265 boolean eventMouse3
= false;
2266 boolean eventMouseWheelUp
= false;
2267 boolean eventMouseWheelDown
= false;
2270 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2284 // Motion only, no buttons down
2285 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2289 // Dragging with mouse1 down
2291 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2295 // Dragging with mouse2 down
2297 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2301 // Dragging with mouse3 down
2303 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2307 // Dragging with mouse2 down after wheelUp
2309 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2313 // Dragging with mouse2 down after wheelDown
2315 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2319 eventMouseWheelUp
= true;
2323 eventMouseWheelDown
= true;
2327 // Unknown, bail out
2330 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2331 eventMouse1
, eventMouse2
, eventMouse3
,
2332 eventMouseWheelUp
, eventMouseWheelDown
);
2336 * Return any events in the IO queue due to timeout.
2338 * @param queue list to append new events to
2340 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2341 long nowTime
= System
.currentTimeMillis();
2343 // Check for new window size
2344 long windowSizeDelay
= nowTime
- windowSizeTime
;
2345 if (windowSizeDelay
> 1000) {
2346 int oldTextWidth
= getTextWidth();
2347 int oldTextHeight
= getTextHeight();
2349 sessionInfo
.queryWindowSize();
2350 int newWidth
= sessionInfo
.getWindowWidth();
2351 int newHeight
= sessionInfo
.getWindowHeight();
2353 if ((newWidth
!= windowResize
.getWidth())
2354 || (newHeight
!= windowResize
.getHeight())
2357 // Request xterm report window dimensions in pixels again.
2358 // Between now and then, ensure that the reported text cell
2359 // size is the same by setting widthPixels and heightPixels
2360 // to match the new dimensions.
2361 widthPixels
= oldTextWidth
* newWidth
;
2362 heightPixels
= oldTextHeight
* newHeight
;
2364 if (debugToStderr
) {
2365 System
.err
.println("Screen size changed, old size " +
2367 System
.err
.println(" new size " +
2368 newWidth
+ " x " + newHeight
);
2369 System
.err
.println(" old pixels " +
2370 oldTextWidth
+ " x " + oldTextHeight
);
2371 System
.err
.println(" new pixels " +
2372 getTextWidth() + " x " + getTextHeight());
2375 this.output
.printf("%s", xtermReportWindowPixelDimensions());
2376 this.output
.flush();
2378 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2379 newWidth
, newHeight
);
2380 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2381 newWidth
, newHeight
);
2384 windowSizeTime
= nowTime
;
2387 // ESCDELAY type timeout
2388 if (state
== ParseState
.ESCAPE
) {
2389 long escDelay
= nowTime
- escapeTime
;
2390 if (escDelay
> 100) {
2391 // After 0.1 seconds, assume a true escape character
2392 queue
.add(controlChar((char)0x1B, false));
2399 * Returns true if the CSI parameter for a keyboard command means that
2402 private boolean csiIsShift(final String x
) {
2414 * Returns true if the CSI parameter for a keyboard command means that
2417 private boolean csiIsAlt(final String x
) {
2429 * Returns true if the CSI parameter for a keyboard command means that
2432 private boolean csiIsCtrl(final String x
) {
2444 * Parses the next character of input to see if an InputEvent is
2447 * @param events list to append new events to
2448 * @param ch Unicode code point
2450 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2452 // ESCDELAY type timeout
2453 long nowTime
= System
.currentTimeMillis();
2454 if (state
== ParseState
.ESCAPE
) {
2455 long escDelay
= nowTime
- escapeTime
;
2456 if (escDelay
> 250) {
2457 // After 0.25 seconds, assume a true escape character
2458 events
.add(controlChar((char)0x1B, false));
2464 boolean ctrl
= false;
2465 boolean alt
= false;
2466 boolean shift
= false;
2468 // System.err.printf("state: %s ch %c\r\n", state, ch);
2474 state
= ParseState
.ESCAPE
;
2475 escapeTime
= nowTime
;
2480 // Control character
2481 events
.add(controlChar(ch
, false));
2488 events
.add(new TKeypressEvent(false, 0, ch
,
2489 false, false, false));
2498 // ALT-Control character
2499 events
.add(controlChar(ch
, true));
2505 // This will be one of the function keys
2506 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2510 // '[' goes to CSI_ENTRY
2512 state
= ParseState
.CSI_ENTRY
;
2516 // Everything else is assumed to be Alt-keystroke
2517 if ((ch
>= 'A') && (ch
<= 'Z')) {
2521 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2525 case ESCAPE_INTERMEDIATE
:
2526 if ((ch
>= 'P') && (ch
<= 'S')) {
2530 events
.add(new TKeypressEvent(kbF1
));
2533 events
.add(new TKeypressEvent(kbF2
));
2536 events
.add(new TKeypressEvent(kbF3
));
2539 events
.add(new TKeypressEvent(kbF4
));
2548 // Unknown keystroke, ignore
2553 // Numbers - parameter values
2554 if ((ch
>= '0') && (ch
<= '9')) {
2555 params
.set(params
.size() - 1,
2556 params
.get(params
.size() - 1) + ch
);
2557 state
= ParseState
.CSI_PARAM
;
2560 // Parameter separator
2566 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2570 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2575 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2580 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2585 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2590 events
.add(new TKeypressEvent(kbHome
));
2595 events
.add(new TKeypressEvent(kbEnd
));
2599 // CBT - Cursor backward X tab stops (default 1)
2600 events
.add(new TKeypressEvent(kbBackTab
));
2605 state
= ParseState
.MOUSE
;
2608 // Mouse position, SGR (1006) coordinates
2609 state
= ParseState
.MOUSE_SGR
;
2616 // Unknown keystroke, ignore
2621 // Numbers - parameter values
2622 if ((ch
>= '0') && (ch
<= '9')) {
2623 params
.set(params
.size() - 1,
2624 params
.get(params
.size() - 1) + ch
);
2627 // Parameter separator
2635 // Generate a mouse press event
2636 TInputEvent event
= parseMouseSGR(false);
2637 if (event
!= null) {
2643 // Generate a mouse release event
2644 event
= parseMouseSGR(true);
2645 if (event
!= null) {
2654 // Unknown keystroke, ignore
2659 // Numbers - parameter values
2660 if ((ch
>= '0') && (ch
<= '9')) {
2661 params
.set(params
.size() - 1,
2662 params
.get(params
.size() - 1) + ch
);
2663 state
= ParseState
.CSI_PARAM
;
2666 // Parameter separator
2673 events
.add(csiFnKey());
2678 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2682 if (params
.size() > 1) {
2683 shift
= csiIsShift(params
.get(1));
2684 alt
= csiIsAlt(params
.get(1));
2685 ctrl
= csiIsCtrl(params
.get(1));
2687 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2692 if (params
.size() > 1) {
2693 shift
= csiIsShift(params
.get(1));
2694 alt
= csiIsAlt(params
.get(1));
2695 ctrl
= csiIsCtrl(params
.get(1));
2697 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2702 if (params
.size() > 1) {
2703 shift
= csiIsShift(params
.get(1));
2704 alt
= csiIsAlt(params
.get(1));
2705 ctrl
= csiIsCtrl(params
.get(1));
2707 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2712 if (params
.size() > 1) {
2713 shift
= csiIsShift(params
.get(1));
2714 alt
= csiIsAlt(params
.get(1));
2715 ctrl
= csiIsCtrl(params
.get(1));
2717 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2722 if (params
.size() > 1) {
2723 shift
= csiIsShift(params
.get(1));
2724 alt
= csiIsAlt(params
.get(1));
2725 ctrl
= csiIsCtrl(params
.get(1));
2727 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2732 if (params
.size() > 1) {
2733 shift
= csiIsShift(params
.get(1));
2734 alt
= csiIsAlt(params
.get(1));
2735 ctrl
= csiIsCtrl(params
.get(1));
2737 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2742 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2743 if (debugToStderr
) {
2744 System
.err
.printf("windowOp pixels: " +
2745 "height %s width %s\n",
2746 params
.get(1), params
.get(2));
2749 widthPixels
= Integer
.parseInt(params
.get(2));
2750 heightPixels
= Integer
.parseInt(params
.get(1));
2751 } catch (NumberFormatException e
) {
2752 if (debugToStderr
) {
2753 e
.printStackTrace();
2756 if (widthPixels
<= 0) {
2759 if (heightPixels
<= 0) {
2770 // Unknown keystroke, ignore
2775 params
.set(0, params
.get(params
.size() - 1) + ch
);
2776 if (params
.get(0).length() == 3) {
2777 // We have enough to generate a mouse event
2778 events
.add(parseMouse());
2787 // This "should" be impossible to reach
2792 * Request (u)xterm to report the current window size dimensions.
2794 * @return the string to emit to xterm
2796 private String
xtermReportWindowPixelDimensions() {
2801 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2802 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2805 * @param on if true, enable metaSendsEscape
2806 * @return the string to emit to xterm
2808 private String
xtermMetaSendsEscape(final boolean on
) {
2810 return "\033[?1036h\033[?1034l";
2812 return "\033[?1036l";
2816 * Create an xterm OSC sequence to change the window title.
2818 * @param title the new title
2819 * @return the string to emit to xterm
2821 private String
getSetTitleString(final String title
) {
2822 return "\033]2;" + title
+ "\007";
2825 // ------------------------------------------------------------------------
2826 // Sixel output support ---------------------------------------------------
2827 // ------------------------------------------------------------------------
2830 * Get the number of colors in the sixel palette.
2832 * @return the palette size
2834 public int getSixelPaletteSize() {
2835 return sixelPaletteSize
;
2839 * Set the number of colors in the sixel palette.
2841 * @param paletteSize the new palette size
2843 public void setSixelPaletteSize(final int paletteSize
) {
2844 if (paletteSize
== sixelPaletteSize
) {
2848 switch (paletteSize
) {
2856 throw new IllegalArgumentException("Unsupported sixel palette " +
2857 " size: " + paletteSize
);
2860 // Don't step on the screen refresh thread.
2861 synchronized (this) {
2862 sixelPaletteSize
= paletteSize
;
2870 * Start a sixel string for display one row's worth of bitmap data.
2872 * @param x column coordinate. 0 is the left-most column.
2873 * @param y row coordinate. 0 is the top-most row.
2874 * @return the string to emit to an ANSI / ECMA-style terminal
2876 private String
startSixel(final int x
, final int y
) {
2877 StringBuilder sb
= new StringBuilder();
2879 assert (sixel
== true);
2882 sb
.append(gotoXY(x
, y
));
2885 sb
.append("\033Pq");
2887 if (palette
== null) {
2888 palette
= new SixelPalette();
2891 return sb
.toString();
2895 * End a sixel string for display one row's worth of bitmap data.
2897 * @return the string to emit to an ANSI / ECMA-style terminal
2899 private String
endSixel() {
2900 assert (sixel
== true);
2907 * Create a sixel string representing a row of several cells containing
2910 * @param x column coordinate. 0 is the left-most column.
2911 * @param y row coordinate. 0 is the top-most row.
2912 * @param cells the cells containing the bitmap data
2913 * @return the string to emit to an ANSI / ECMA-style terminal
2915 private String
toSixel(final int x
, final int y
,
2916 final ArrayList
<Cell
> cells
) {
2918 StringBuilder sb
= new StringBuilder();
2920 assert (cells
!= null);
2921 assert (cells
.size() > 0);
2922 assert (cells
.get(0).getImage() != null);
2924 if (sixel
== false) {
2925 sb
.append(normal());
2926 sb
.append(gotoXY(x
, y
));
2927 for (int i
= 0; i
< cells
.size(); i
++) {
2930 return sb
.toString();
2933 if (sixelCache
== null) {
2934 sixelCache
= new SixelCache(height
* 10);
2937 // Save and get rows to/from the cache that do NOT have inverted
2939 boolean saveInCache
= true;
2940 for (Cell cell
: cells
) {
2941 if (cell
.isInvertedImage()) {
2942 saveInCache
= false;
2946 String cachedResult
= sixelCache
.get(cells
);
2947 if (cachedResult
!= null) {
2948 // System.err.println("CACHE HIT");
2949 sb
.append(startSixel(x
, y
));
2950 sb
.append(cachedResult
);
2951 sb
.append(endSixel());
2952 return sb
.toString();
2954 // System.err.println("CACHE MISS");
2957 int imageWidth
= cells
.get(0).getImage().getWidth();
2958 int imageHeight
= cells
.get(0).getImage().getHeight();
2960 // cells.get(x).getImage() has a dithered bitmap containing indexes
2961 // into the color palette. Piece these together into one larger
2962 // image for final rendering.
2964 int fullWidth
= cells
.size() * getTextWidth();
2965 int fullHeight
= getTextHeight();
2966 for (int i
= 0; i
< cells
.size(); i
++) {
2967 totalWidth
+= cells
.get(i
).getImage().getWidth();
2970 BufferedImage image
= new BufferedImage(fullWidth
,
2971 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
2974 for (int i
= 0; i
< cells
.size() - 1; i
++) {
2975 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
2977 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
2979 if (false && cells
.get(i
).isInvertedImage()) {
2980 // I used to put an all-white cell over the cursor, don't do
2982 rgbArray
= new int[imageWidth
* imageHeight
];
2983 for (int j
= 0; j
< rgbArray
.length
; j
++) {
2984 rgbArray
[j
] = 0xFFFFFF;
2988 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
2989 tileWidth
, tileHeight
, null, 0, tileWidth
);
2990 } catch (Exception e
) {
2991 throw new RuntimeException("image " + imageWidth
+ "x" +
2993 "tile " + tileWidth
+ "x" +
2995 " cells.get(i).getImage() " +
2996 cells
.get(i
).getImage() +
2998 " fullWidth " + fullWidth
+
2999 " fullHeight " + fullHeight
, e
);
3004 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3005 i * imageWidth, 0, imageWidth, imageHeight,
3007 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3008 fullWidth, fullHeight, cells.size(), getTextWidth());
3011 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3012 rgbArray
, 0, tileWidth
);
3013 if (tileHeight
< fullHeight
) {
3014 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3015 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3016 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3019 image
.setRGB(imageX
, imageY
, backgroundColor
);
3024 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3025 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3026 // I used to put an all-white cell over the cursor, don't do that
3028 rgbArray
= new int[totalWidth
* imageHeight
];
3029 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3030 rgbArray
[j
] = 0xFFFFFF;
3034 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3035 totalWidth
, imageHeight
, null, 0, totalWidth
);
3036 } catch (Exception e
) {
3037 throw new RuntimeException("image " + imageWidth
+ "x" +
3038 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3039 cells
.get(cells
.size() - 1).getImage(), e
);
3042 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3043 imageHeight
, rgbArray
, 0, totalWidth
);
3045 if (totalWidth
< getTextWidth()) {
3046 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3048 for (int imageX
= image
.getWidth() - totalWidth
;
3049 imageX
< image
.getWidth(); imageX
++) {
3051 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3052 image
.setRGB(imageX
, imageY
, backgroundColor
);
3057 // Dither the image. It is ok to lose the original here.
3058 if (palette
== null) {
3059 palette
= new SixelPalette();
3061 image
= palette
.ditherImage(image
);
3063 // Emit the palette, but only for the colors actually used by these
3065 boolean [] usedColors
= new boolean[sixelPaletteSize
];
3066 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3067 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
3068 usedColors
[image
.getRGB(imageX
, imageY
)] = true;
3071 palette
.emitPalette(sb
, usedColors
);
3073 // Render the entire row of cells.
3074 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
3075 int [][] sixels
= new int[image
.getWidth()][6];
3077 // See which colors are actually used in this band of sixels.
3078 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3079 for (int imageY
= 0;
3080 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
3083 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
3084 assert (colorIdx
>= 0);
3085 assert (colorIdx
< sixelPaletteSize
);
3087 sixels
[imageX
][imageY
] = colorIdx
;
3091 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
3092 boolean isUsed
= false;
3093 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3094 for (int j
= 0; j
< 6; j
++) {
3095 if (sixels
[imageX
][j
] == i
) {
3100 if (isUsed
== false) {
3104 // Set to the beginning of scan line for the next set of
3105 // colored pixels, and select the color.
3106 sb
.append(String
.format("$#%d", i
));
3109 int oldDataCount
= 0;
3110 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3112 // Add up all the pixels that match this color.
3115 (j
< 6) && (currentRow
+ j
< fullHeight
);
3118 if (sixels
[imageX
][j
] == i
) {
3145 if (data
== oldData
) {
3148 if (oldDataCount
== 1) {
3149 sb
.append((char) oldData
);
3150 } else if (oldDataCount
> 1) {
3151 sb
.append(String
.format("!%d", oldDataCount
));
3152 sb
.append((char) oldData
);
3158 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
3160 // Emit the last sequence.
3161 if (oldDataCount
== 1) {
3162 sb
.append((char) oldData
);
3163 } else if (oldDataCount
> 1) {
3164 sb
.append(String
.format("!%d", oldDataCount
));
3165 sb
.append((char) oldData
);
3168 } // for (int i = 0; i < sixelPaletteSize; i++)
3170 // Advance to the next scan line.
3173 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3175 // Kill the very last "-", because it is unnecessary.
3176 sb
.deleteCharAt(sb
.length() - 1);
3179 // This row is OK to save into the cache.
3180 sixelCache
.put(cells
, sb
.toString());
3183 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3187 * Get the sixel support flag.
3189 * @return true if this terminal is emitting sixel
3191 public boolean hasSixel() {
3195 // ------------------------------------------------------------------------
3196 // End sixel output support -----------------------------------------------
3197 // ------------------------------------------------------------------------
3200 * Setup system colors to match DOS color palette.
3202 private void setDOSColors() {
3203 MYBLACK
= new java
.awt
.Color(0x00, 0x00, 0x00);
3204 MYRED
= new java
.awt
.Color(0xa8, 0x00, 0x00);
3205 MYGREEN
= new java
.awt
.Color(0x00, 0xa8, 0x00);
3206 MYYELLOW
= new java
.awt
.Color(0xa8, 0x54, 0x00);
3207 MYBLUE
= new java
.awt
.Color(0x00, 0x00, 0xa8);
3208 MYMAGENTA
= new java
.awt
.Color(0xa8, 0x00, 0xa8);
3209 MYCYAN
= new java
.awt
.Color(0x00, 0xa8, 0xa8);
3210 MYWHITE
= new java
.awt
.Color(0xa8, 0xa8, 0xa8);
3211 MYBOLD_BLACK
= new java
.awt
.Color(0x54, 0x54, 0x54);
3212 MYBOLD_RED
= new java
.awt
.Color(0xfc, 0x54, 0x54);
3213 MYBOLD_GREEN
= new java
.awt
.Color(0x54, 0xfc, 0x54);
3214 MYBOLD_YELLOW
= new java
.awt
.Color(0xfc, 0xfc, 0x54);
3215 MYBOLD_BLUE
= new java
.awt
.Color(0x54, 0x54, 0xfc);
3216 MYBOLD_MAGENTA
= new java
.awt
.Color(0xfc, 0x54, 0xfc);
3217 MYBOLD_CYAN
= new java
.awt
.Color(0x54, 0xfc, 0xfc);
3218 MYBOLD_WHITE
= new java
.awt
.Color(0xfc, 0xfc, 0xfc);
3222 * Setup ECMA48 colors to match those provided in system properties.
3224 private void setCustomSystemColors() {
3227 MYBLACK
= getCustomColor("jexer.ECMA48.color0", MYBLACK
);
3228 MYRED
= getCustomColor("jexer.ECMA48.color1", MYRED
);
3229 MYGREEN
= getCustomColor("jexer.ECMA48.color2", MYGREEN
);
3230 MYYELLOW
= getCustomColor("jexer.ECMA48.color3", MYYELLOW
);
3231 MYBLUE
= getCustomColor("jexer.ECMA48.color4", MYBLUE
);
3232 MYMAGENTA
= getCustomColor("jexer.ECMA48.color5", MYMAGENTA
);
3233 MYCYAN
= getCustomColor("jexer.ECMA48.color6", MYCYAN
);
3234 MYWHITE
= getCustomColor("jexer.ECMA48.color7", MYWHITE
);
3235 MYBOLD_BLACK
= getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK
);
3236 MYBOLD_RED
= getCustomColor("jexer.ECMA48.color9", MYBOLD_RED
);
3237 MYBOLD_GREEN
= getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN
);
3238 MYBOLD_YELLOW
= getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW
);
3239 MYBOLD_BLUE
= getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE
);
3240 MYBOLD_MAGENTA
= getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA
);
3241 MYBOLD_CYAN
= getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN
);
3242 MYBOLD_WHITE
= getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE
);
3246 * Setup one system color to match the RGB value provided in system
3249 * @param key the system property key
3250 * @param defaultColor the default color to return if key is not set, or
3252 * @return a color from the RGB string, or defaultColor
3254 private java
.awt
.Color
getCustomColor(final String key
,
3255 final java
.awt
.Color defaultColor
) {
3257 String rgb
= System
.getProperty(key
);
3259 return defaultColor
;
3261 if (rgb
.startsWith("#")) {
3262 rgb
= rgb
.substring(1);
3266 rgbInt
= Integer
.parseInt(rgb
, 16);
3267 } catch (NumberFormatException e
) {
3268 return defaultColor
;
3270 java
.awt
.Color color
= new java
.awt
.Color((rgbInt
& 0xFF0000) >>> 16,
3271 (rgbInt
& 0x00FF00) >>> 8,
3272 (rgbInt
& 0x0000FF));
3278 * Create a T.416 RGB parameter sequence for a custom system color.
3280 * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
3281 * @return the color portion of the string to emit to an ANSI /
3282 * ECMA-style terminal
3284 private String
systemColorRGB(final java
.awt
.Color color
) {
3285 return String
.format("%d;%d;%d", color
.getRed(), color
.getGreen(),
3290 * Create a SGR parameter sequence for a single color change.
3292 * @param bold if true, set bold
3293 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3294 * @param foreground if true, this is a foreground color
3295 * @return the string to emit to an ANSI / ECMA-style terminal,
3298 private String
color(final boolean bold
, final Color color
,
3299 final boolean foreground
) {
3300 return color(color
, foreground
, true) +
3301 rgbColor(bold
, color
, foreground
);
3305 * Create a T.416 RGB parameter sequence for a single color change.
3307 * @param colorRGB a 24-bit RGB value for foreground color
3308 * @param foreground if true, this is a foreground color
3309 * @return the string to emit to an ANSI / ECMA-style terminal,
3312 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
3314 int colorRed
= (colorRGB
>>> 16) & 0xFF;
3315 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
3316 int colorBlue
= colorRGB
& 0xFF;
3318 StringBuilder sb
= new StringBuilder();
3320 sb
.append("\033[38;2;");
3322 sb
.append("\033[48;2;");
3324 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
3325 return sb
.toString();
3329 * Create a T.416 RGB parameter sequence for both foreground and
3330 * background color change.
3332 * @param foreColorRGB a 24-bit RGB value for foreground color
3333 * @param backColorRGB a 24-bit RGB value for foreground color
3334 * @return the string to emit to an ANSI / ECMA-style terminal,
3337 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
3338 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3339 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3340 int foreColorBlue
= foreColorRGB
& 0xFF;
3341 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3342 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3343 int backColorBlue
= backColorRGB
& 0xFF;
3345 StringBuilder sb
= new StringBuilder();
3346 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
3347 foreColorRed
, foreColorGreen
, foreColorBlue
));
3348 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
3349 backColorRed
, backColorGreen
, backColorBlue
));
3350 return sb
.toString();
3354 * Create a T.416 RGB parameter sequence for a single color change.
3356 * @param bold if true, set bold
3357 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3358 * @param foreground if true, this is a foreground color
3359 * @return the string to emit to an xterm terminal with RGB support,
3360 * e.g. "\033[38;2;RR;GG;BBm"
3362 private String
rgbColor(final boolean bold
, final Color color
,
3363 final boolean foreground
) {
3364 if (doRgbColor
== false) {
3367 StringBuilder sb
= new StringBuilder("\033[");
3369 // Bold implies foreground only
3371 if (color
.equals(Color
.BLACK
)) {
3372 sb
.append(systemColorRGB(MYBOLD_BLACK
));
3373 } else if (color
.equals(Color
.RED
)) {
3374 sb
.append(systemColorRGB(MYBOLD_RED
));
3375 } else if (color
.equals(Color
.GREEN
)) {
3376 sb
.append(systemColorRGB(MYBOLD_GREEN
));
3377 } else if (color
.equals(Color
.YELLOW
)) {
3378 sb
.append(systemColorRGB(MYBOLD_YELLOW
));
3379 } else if (color
.equals(Color
.BLUE
)) {
3380 sb
.append(systemColorRGB(MYBOLD_BLUE
));
3381 } else if (color
.equals(Color
.MAGENTA
)) {
3382 sb
.append(systemColorRGB(MYBOLD_MAGENTA
));
3383 } else if (color
.equals(Color
.CYAN
)) {
3384 sb
.append(systemColorRGB(MYBOLD_CYAN
));
3385 } else if (color
.equals(Color
.WHITE
)) {
3386 sb
.append(systemColorRGB(MYBOLD_WHITE
));
3394 if (color
.equals(Color
.BLACK
)) {
3395 sb
.append(systemColorRGB(MYBLACK
));
3396 } else if (color
.equals(Color
.RED
)) {
3397 sb
.append(systemColorRGB(MYRED
));
3398 } else if (color
.equals(Color
.GREEN
)) {
3399 sb
.append(systemColorRGB(MYGREEN
));
3400 } else if (color
.equals(Color
.YELLOW
)) {
3401 sb
.append(systemColorRGB(MYYELLOW
));
3402 } else if (color
.equals(Color
.BLUE
)) {
3403 sb
.append(systemColorRGB(MYBLUE
));
3404 } else if (color
.equals(Color
.MAGENTA
)) {
3405 sb
.append(systemColorRGB(MYMAGENTA
));
3406 } else if (color
.equals(Color
.CYAN
)) {
3407 sb
.append(systemColorRGB(MYCYAN
));
3408 } else if (color
.equals(Color
.WHITE
)) {
3409 sb
.append(systemColorRGB(MYWHITE
));
3413 return sb
.toString();
3417 * Create a T.416 RGB parameter sequence for both foreground and
3418 * background color change.
3420 * @param bold if true, set bold
3421 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3422 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3423 * @return the string to emit to an xterm terminal with RGB support,
3424 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
3426 private String
rgbColor(final boolean bold
, final Color foreColor
,
3427 final Color backColor
) {
3428 if (doRgbColor
== false) {
3432 return rgbColor(bold
, foreColor
, true) +
3433 rgbColor(false, backColor
, false);
3437 * Create a SGR parameter sequence for a single color change.
3439 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3440 * @param foreground if true, this is a foreground color
3441 * @param header if true, make the full header, otherwise just emit the
3442 * color parameter e.g. "42;"
3443 * @return the string to emit to an ANSI / ECMA-style terminal,
3446 private String
color(final Color color
, final boolean foreground
,
3447 final boolean header
) {
3449 int ecmaColor
= color
.getValue();
3451 // Convert Color.* values to SGR numerics
3459 return String
.format("\033[%dm", ecmaColor
);
3461 return String
.format("%d;", ecmaColor
);
3466 * Create a SGR parameter sequence for both foreground and background
3469 * @param bold if true, set bold
3470 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3471 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3472 * @return the string to emit to an ANSI / ECMA-style terminal,
3473 * e.g. "\033[31;42m"
3475 private String
color(final boolean bold
, final Color foreColor
,
3476 final Color backColor
) {
3477 return color(foreColor
, backColor
, true) +
3478 rgbColor(bold
, foreColor
, backColor
);
3482 * Create a SGR parameter sequence for both foreground and
3483 * background color change.
3485 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3486 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3487 * @param header if true, make the full header, otherwise just emit the
3488 * color parameter e.g. "31;42;"
3489 * @return the string to emit to an ANSI / ECMA-style terminal,
3490 * e.g. "\033[31;42m"
3492 private String
color(final Color foreColor
, final Color backColor
,
3493 final boolean header
) {
3495 int ecmaForeColor
= foreColor
.getValue();
3496 int ecmaBackColor
= backColor
.getValue();
3498 // Convert Color.* values to SGR numerics
3499 ecmaBackColor
+= 40;
3500 ecmaForeColor
+= 30;
3503 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
3505 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
3510 * Create a SGR parameter sequence for foreground, background, and
3511 * several attributes. This sequence first resets all attributes to
3512 * default, then sets attributes as per the parameters.
3514 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3515 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3516 * @param bold if true, set bold
3517 * @param reverse if true, set reverse
3518 * @param blink if true, set blink
3519 * @param underline if true, set underline
3520 * @return the string to emit to an ANSI / ECMA-style terminal,
3521 * e.g. "\033[0;1;31;42m"
3523 private String
color(final Color foreColor
, final Color backColor
,
3524 final boolean bold
, final boolean reverse
, final boolean blink
,
3525 final boolean underline
) {
3527 int ecmaForeColor
= foreColor
.getValue();
3528 int ecmaBackColor
= backColor
.getValue();
3530 // Convert Color.* values to SGR numerics
3531 ecmaBackColor
+= 40;
3532 ecmaForeColor
+= 30;
3534 StringBuilder sb
= new StringBuilder();
3535 if ( bold
&& reverse
&& blink
&& !underline
) {
3536 sb
.append("\033[0;1;7;5;");
3537 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3538 sb
.append("\033[0;1;7;");
3539 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3540 sb
.append("\033[0;7;5;");
3541 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3542 sb
.append("\033[0;1;5;");
3543 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3544 sb
.append("\033[0;1;");
3545 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3546 sb
.append("\033[0;7;");
3547 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3548 sb
.append("\033[0;5;");
3549 } else if ( bold
&& reverse
&& blink
&& underline
) {
3550 sb
.append("\033[0;1;7;5;4;");
3551 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3552 sb
.append("\033[0;1;7;4;");
3553 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3554 sb
.append("\033[0;7;5;4;");
3555 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3556 sb
.append("\033[0;1;5;4;");
3557 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3558 sb
.append("\033[0;1;4;");
3559 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3560 sb
.append("\033[0;7;4;");
3561 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3562 sb
.append("\033[0;5;4;");
3563 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3564 sb
.append("\033[0;4;");
3566 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3567 sb
.append("\033[0;");
3569 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
3570 sb
.append(rgbColor(bold
, foreColor
, backColor
));
3571 return sb
.toString();
3575 * Create a SGR parameter sequence for foreground, background, and
3576 * several attributes. This sequence first resets all attributes to
3577 * default, then sets attributes as per the parameters.
3579 * @param foreColorRGB a 24-bit RGB value for foreground color
3580 * @param backColorRGB a 24-bit RGB value for foreground color
3581 * @param bold if true, set bold
3582 * @param reverse if true, set reverse
3583 * @param blink if true, set blink
3584 * @param underline if true, set underline
3585 * @return the string to emit to an ANSI / ECMA-style terminal,
3586 * e.g. "\033[0;1;31;42m"
3588 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
3589 final boolean bold
, final boolean reverse
, final boolean blink
,
3590 final boolean underline
) {
3592 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3593 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3594 int foreColorBlue
= foreColorRGB
& 0xFF;
3595 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3596 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3597 int backColorBlue
= backColorRGB
& 0xFF;
3599 StringBuilder sb
= new StringBuilder();
3600 if ( bold
&& reverse
&& blink
&& !underline
) {
3601 sb
.append("\033[0;1;7;5;");
3602 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3603 sb
.append("\033[0;1;7;");
3604 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3605 sb
.append("\033[0;7;5;");
3606 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3607 sb
.append("\033[0;1;5;");
3608 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3609 sb
.append("\033[0;1;");
3610 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3611 sb
.append("\033[0;7;");
3612 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3613 sb
.append("\033[0;5;");
3614 } else if ( bold
&& reverse
&& blink
&& underline
) {
3615 sb
.append("\033[0;1;7;5;4;");
3616 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3617 sb
.append("\033[0;1;7;4;");
3618 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3619 sb
.append("\033[0;7;5;4;");
3620 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3621 sb
.append("\033[0;1;5;4;");
3622 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3623 sb
.append("\033[0;1;4;");
3624 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3625 sb
.append("\033[0;7;4;");
3626 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3627 sb
.append("\033[0;5;4;");
3628 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3629 sb
.append("\033[0;4;");
3631 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3632 sb
.append("\033[0;");
3635 sb
.append("m\033[38;2;");
3636 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
3638 sb
.append("m\033[48;2;");
3639 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
3642 return sb
.toString();
3646 * Create a SGR parameter sequence to reset to defaults.
3648 * @return the string to emit to an ANSI / ECMA-style terminal,
3651 private String
normal() {
3652 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
3656 * Create a SGR parameter sequence to reset to defaults.
3658 * @param header if true, make the full header, otherwise just emit the
3659 * bare parameter e.g. "0;"
3660 * @return the string to emit to an ANSI / ECMA-style terminal,
3663 private String
normal(final boolean header
) {
3665 return "\033[0;37;40m";
3671 * Create a SGR parameter sequence for enabling the visible cursor.
3673 * @param on if true, turn on cursor
3674 * @return the string to emit to an ANSI / ECMA-style terminal
3676 private String
cursor(final boolean on
) {
3677 if (on
&& !cursorOn
) {
3681 if (!on
&& cursorOn
) {
3689 * Clear the entire screen. Because some terminals use back-color-erase,
3690 * set the color to white-on-black beforehand.
3692 * @return the string to emit to an ANSI / ECMA-style terminal
3694 private String
clearAll() {
3695 return "\033[0;37;40m\033[2J";
3699 * Clear the line from the cursor (inclusive) to the end of the screen.
3700 * Because some terminals use back-color-erase, set the color to
3701 * white-on-black beforehand.
3703 * @return the string to emit to an ANSI / ECMA-style terminal
3705 private String
clearRemainingLine() {
3706 return "\033[0;37;40m\033[K";
3710 * Move the cursor to (x, y).
3712 * @param x column coordinate. 0 is the left-most column.
3713 * @param y row coordinate. 0 is the top-most row.
3714 * @return the string to emit to an ANSI / ECMA-style terminal
3716 private String
gotoXY(final int x
, final int y
) {
3717 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
3721 * Tell (u)xterm that we want to receive mouse events based on "Any event
3722 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
3723 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
3725 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
3727 * Note that this also sets the alternate/primary screen buffer.
3729 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
3730 * mean "hide the mouse pointer." We have to use our own sequence to do
3731 * this because there is no standard in xterm for unilaterally hiding the
3732 * pointer all the time (regardless of typing).
3734 * @param on If true, enable mouse report and use the alternate screen
3735 * buffer. If false disable mouse reporting and use the primary screen
3737 * @return the string to emit to xterm
3739 private String
mouse(final boolean on
) {
3741 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
3743 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";