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
.ByteArrayOutputStream
;
34 import java
.io
.FileDescriptor
;
35 import java
.io
.FileInputStream
;
36 import java
.io
.InputStream
;
37 import java
.io
.InputStreamReader
;
38 import java
.io
.IOException
;
39 import java
.io
.OutputStream
;
40 import java
.io
.OutputStreamWriter
;
41 import java
.io
.PrintWriter
;
42 import java
.io
.Reader
;
43 import java
.io
.UnsupportedEncodingException
;
44 import java
.util
.ArrayList
;
45 import java
.util
.Collections
;
46 import java
.util
.HashMap
;
47 import java
.util
.List
;
48 import javax
.imageio
.ImageIO
;
51 import jexer
.bits
.Cell
;
52 import jexer
.bits
.CellAttributes
;
53 import jexer
.bits
.Color
;
54 import jexer
.event
.TCommandEvent
;
55 import jexer
.event
.TInputEvent
;
56 import jexer
.event
.TKeypressEvent
;
57 import jexer
.event
.TMouseEvent
;
58 import jexer
.event
.TResizeEvent
;
59 import static jexer
.TCommand
.*;
60 import static jexer
.TKeypress
.*;
63 * This class reads keystrokes and mouse events and emits output to ANSI
64 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
66 public class ECMA48Terminal
extends LogicalScreen
67 implements TerminalReader
, Runnable
{
69 // ------------------------------------------------------------------------
70 // Constants --------------------------------------------------------------
71 // ------------------------------------------------------------------------
74 * States in the input parser.
76 private enum ParseState
{
87 * Available Jexer images support.
89 private enum JexerImageOption
{
96 // ------------------------------------------------------------------------
97 // Variables --------------------------------------------------------------
98 // ------------------------------------------------------------------------
101 * Emit debugging to stderr.
103 private boolean debugToStderr
= false;
106 * If true, emit T.416-style RGB colors for normal system colors. This
107 * is a) expensive in bandwidth, and b) potentially terrible looking for
110 private static boolean doRgbColor
= false;
113 * The session information.
115 private SessionInfo sessionInfo
;
118 * The event queue, filled up by a thread reading on input.
120 private List
<TInputEvent
> eventQueue
;
123 * If true, we want the reader thread to exit gracefully.
125 private boolean stopReaderThread
;
130 private Thread readerThread
;
133 * Parameters being collected. E.g. if the string is \033[1;3m, then
134 * params[0] will be 1 and params[1] will be 3.
136 private List
<String
> params
;
139 * Current parsing state.
141 private ParseState state
;
144 * The time we entered ESCAPE. If we get a bare escape without a code
145 * following it, this is used to return that bare escape.
147 private long escapeTime
;
150 * The time we last checked the window size. We try not to spawn stty
151 * more than once per second.
153 private long windowSizeTime
;
156 * true if mouse1 was down. Used to report mouse1 on the release event.
158 private boolean mouse1
;
161 * true if mouse2 was down. Used to report mouse2 on the release event.
163 private boolean mouse2
;
166 * true if mouse3 was down. Used to report mouse3 on the release event.
168 private boolean mouse3
;
171 * Cache the cursor visibility value so we only emit the sequence when we
174 private boolean cursorOn
= true;
177 * Cache the last window size to figure out if a TResizeEvent needs to be
180 private TResizeEvent windowResize
= null;
183 * If true, emit wide-char (CJK/Emoji) characters as sixel images.
185 private boolean wideCharImages
= true;
188 * Window width in pixels. Used for sixel support.
190 private int widthPixels
= 640;
193 * Window height in pixels. Used for sixel support.
195 private int heightPixels
= 400;
198 * If true, emit image data via sixel.
200 private boolean sixel
= true;
203 * The sixel palette handler.
205 private SixelPalette palette
= null;
208 * The sixel post-rendered string cache.
210 private ImageCache sixelCache
= null;
213 * Number of colors in the sixel palette. Xterm 335 defines the max as
214 * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
217 private int sixelPaletteSize
= 1024;
220 * If true, emit image data via iTerm2 image protocol.
222 private boolean iterm2Images
= false;
225 * The iTerm2 post-rendered string cache.
227 private ImageCache iterm2Cache
= null;
230 * If not DISABLED, emit image data via Jexer image protocol if the
231 * terminal supports it.
233 private JexerImageOption jexerImageOption
= JexerImageOption
.JPG
;
236 * The Jexer post-rendered string cache.
238 private ImageCache jexerCache
= null;
241 * Base64 encoder used by iTerm2 and Jexer images.
243 private java
.util
.Base64
.Encoder base64
= null;
246 * If true, then we changed System.in and need to change it back.
248 private boolean setRawMode
= false;
251 * If true, '?' was seen in terminal response.
253 private boolean decPrivateModeFlag
= false;
256 * The terminal's input. If an InputStream is not specified in the
257 * constructor, then this InputStreamReader will be bound to System.in
258 * with UTF-8 encoding.
260 private Reader input
;
263 * The terminal's raw InputStream. If an InputStream is not specified in
264 * the constructor, then this InputReader will be bound to System.in.
265 * This is used by run() to see if bytes are available() before calling
266 * (Reader)input.read().
268 private InputStream inputStream
;
271 * The terminal's output. If an OutputStream is not specified in the
272 * constructor, then this PrintWriter will be bound to System.out with
275 private PrintWriter output
;
278 * The listening object that run() wakes up on new input.
280 private Object listener
;
282 // Colors to map DOS colors to AWT colors.
283 private static java
.awt
.Color MYBLACK
;
284 private static java
.awt
.Color MYRED
;
285 private static java
.awt
.Color MYGREEN
;
286 private static java
.awt
.Color MYYELLOW
;
287 private static java
.awt
.Color MYBLUE
;
288 private static java
.awt
.Color MYMAGENTA
;
289 private static java
.awt
.Color MYCYAN
;
290 private static java
.awt
.Color MYWHITE
;
291 private static java
.awt
.Color MYBOLD_BLACK
;
292 private static java
.awt
.Color MYBOLD_RED
;
293 private static java
.awt
.Color MYBOLD_GREEN
;
294 private static java
.awt
.Color MYBOLD_YELLOW
;
295 private static java
.awt
.Color MYBOLD_BLUE
;
296 private static java
.awt
.Color MYBOLD_MAGENTA
;
297 private static java
.awt
.Color MYBOLD_CYAN
;
298 private static java
.awt
.Color MYBOLD_WHITE
;
301 * SixelPalette is used to manage the conversion of images between 24-bit
302 * RGB color and a palette of sixelPaletteSize colors.
304 private class SixelPalette
{
307 * Color palette for sixel output, sorted low to high.
309 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
312 * Map of color palette index for sixel output, from the order it was
313 * generated by makePalette() to rgbColors.
315 private int [] rgbSortedIndex
= new int[sixelPaletteSize
];
318 * The color palette, organized by hue, saturation, and luminance.
319 * This is used for a fast color match.
321 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
324 * Number of bits for hue.
326 private int hueBits
= -1;
329 * Number of bits for saturation.
331 private int satBits
= -1;
334 * Number of bits for luminance.
336 private int lumBits
= -1;
339 * Step size for hue bins.
341 private int hueStep
= -1;
344 * Step size for saturation bins.
346 private int satStep
= -1;
349 * Cached RGB to HSL result.
351 private int hsl
[] = new int[3];
354 * ColorIdx records a RGB color and its palette index.
356 private class ColorIdx
{
358 * The 24-bit RGB color.
363 * The palette index for this color.
368 * Public constructor.
370 * @param color the 24-bit RGB color
371 * @param index the palette index for this color
373 public ColorIdx(final int color
, final int index
) {
380 * Public constructor.
382 public SixelPalette() {
387 * Find the nearest match for a color in the palette.
389 * @param color the RGB color
390 * @return the index in rgbColors that is closest to color
392 public int matchColor(final int color
) {
397 * matchColor() is a critical performance bottleneck. To make it
398 * decent, we do the following:
400 * 1. Find the nearest two hues that bracket this color.
402 * 2. Find the nearest two saturations that bracket this color.
404 * 3. Iterate within these four bands of luminance values,
405 * returning the closest color by Euclidean distance.
407 * This strategy reduces the search space by about 97%.
409 int red
= (color
>>> 16) & 0xFF;
410 int green
= (color
>>> 8) & 0xFF;
411 int blue
= color
& 0xFF;
413 if (sixelPaletteSize
== 2) {
414 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
423 rgbToHsl(red
, green
, blue
, hsl
);
427 // System.err.printf("%d %d %d\n", hue, sat, lum);
429 double diff
= Double
.MAX_VALUE
;
432 int hue1
= hue
/ (360/hueStep
);
434 if (hue1
>= hslColors
.size() - 1) {
435 // Bracket pure red from above.
436 hue1
= hslColors
.size() - 1;
438 } else if (hue1
== 0) {
439 // Bracket pure red from below.
440 hue2
= hslColors
.size() - 1;
443 for (int hI
= hue1
; hI
!= -1;) {
444 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
447 } else if (hI
== hue2
) {
451 int sMin
= (sat
/ satStep
) - 1;
456 } else if (sMin
== sats
.size() - 1) {
461 assert (sMax
- sMin
== 1);
464 // int sMax = sats.size() - 1;
466 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
467 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
469 // True 3D colorspace match for the remaining values
470 for (ColorIdx c
: lums
) {
471 int rgbColor
= c
.color
;
473 int red2
= (rgbColor
>>> 16) & 0xFF;
474 int green2
= (rgbColor
>>> 8) & 0xFF;
475 int blue2
= rgbColor
& 0xFF;
476 newDiff
+= Math
.pow(red2
- red
, 2);
477 newDiff
+= Math
.pow(green2
- green
, 2);
478 newDiff
+= Math
.pow(blue2
- blue
, 2);
479 if (newDiff
< diff
) {
480 idx
= rgbSortedIndex
[c
.index
];
487 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
488 // Black is a closer match.
490 } else if ((((255 - red
) * (255 - red
)) +
491 ((255 - green
) * (255 - green
)) +
492 ((255 - blue
) * (255 - blue
))) < diff
) {
494 // White is a closer match.
495 idx
= sixelPaletteSize
- 1;
502 * Clamp an int value to [0, 255].
504 * @param x the int value
505 * @return an int between 0 and 255.
507 private int clamp(final int x
) {
518 * Dither an image to a sixelPaletteSize palette. The dithered
519 * image cells will contain indexes into the palette.
521 * @param image the image to dither
522 * @return the dithered image. Every pixel is an index into the
525 public BufferedImage
ditherImage(final BufferedImage image
) {
527 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
528 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
530 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
531 image
.getHeight(), null, 0, image
.getWidth());
532 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
533 rgbArray
, 0, image
.getWidth());
535 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
536 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
537 int oldPixel
= ditheredImage
.getRGB(imageX
,
539 int colorIdx
= matchColor(oldPixel
);
540 assert (colorIdx
>= 0);
541 assert (colorIdx
< sixelPaletteSize
);
542 int newPixel
= rgbColors
.get(colorIdx
);
543 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
545 int oldRed
= (oldPixel
>>> 16) & 0xFF;
546 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
547 int oldBlue
= oldPixel
& 0xFF;
549 int newRed
= (newPixel
>>> 16) & 0xFF;
550 int newGreen
= (newPixel
>>> 8) & 0xFF;
551 int newBlue
= newPixel
& 0xFF;
553 int redError
= (oldRed
- newRed
) / 16;
554 int greenError
= (oldGreen
- newGreen
) / 16;
555 int blueError
= (oldBlue
- newBlue
) / 16;
557 int red
, green
, blue
;
558 if (imageX
< image
.getWidth() - 1) {
559 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
560 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
561 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
562 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
564 green
= clamp(green
);
566 pXpY
= ((red
& 0xFF) << 16);
567 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
568 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
570 if (imageY
< image
.getHeight() - 1) {
571 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
573 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
574 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
575 blue
= ( pXpYp
& 0xFF) + blueError
;
577 green
= clamp(green
);
579 pXpYp
= ((red
& 0xFF) << 16);
580 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
581 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
583 } else if (imageY
< image
.getHeight() - 1) {
584 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
586 int pXYp
= ditheredImage
.getRGB(imageX
,
589 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
590 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
591 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
593 green
= clamp(green
);
595 pXmYp
= ((red
& 0xFF) << 16);
596 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
597 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
599 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
600 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
601 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
603 green
= clamp(green
);
605 pXYp
= ((red
& 0xFF) << 16);
606 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
607 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
609 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
610 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
612 return ditheredImage
;
616 * Convert an RGB color to HSL.
618 * @param red red color, between 0 and 255
619 * @param green green color, between 0 and 255
620 * @param blue blue color, between 0 and 255
621 * @param hsl the hsl color as [hue, saturation, luminance]
623 private void rgbToHsl(final int red
, final int green
,
624 final int blue
, final int [] hsl
) {
626 assert ((red
>= 0) && (red
<= 255));
627 assert ((green
>= 0) && (green
<= 255));
628 assert ((blue
>= 0) && (blue
<= 255));
630 double R
= red
/ 255.0;
631 double G
= green
/ 255.0;
632 double B
= blue
/ 255.0;
633 boolean Rmax
= false;
634 boolean Gmax
= false;
635 boolean Bmax
= false;
636 double min
= (R
< G ? R
: G
);
637 min
= (min
< B ? min
: B
);
639 if ((R
>= G
) && (R
>= B
)) {
642 } else if ((G
>= R
) && (G
>= B
)) {
645 } else if ((B
>= G
) && (B
>= R
)) {
650 double L
= (min
+ max
) / 2.0;
655 S
= (max
- min
) / (max
+ min
);
657 S
= (max
- min
) / (2.0 - max
- min
);
661 assert (Gmax
== false);
662 assert (Bmax
== false);
663 H
= (G
- B
) / (max
- min
);
665 assert (Rmax
== false);
666 assert (Bmax
== false);
667 H
= 2.0 + (B
- R
) / (max
- min
);
669 assert (Rmax
== false);
670 assert (Gmax
== false);
671 H
= 4.0 + (R
- G
) / (max
- min
);
676 hsl
[0] = (int) (H
* 60.0);
677 hsl
[1] = (int) (S
* 100.0);
678 hsl
[2] = (int) (L
* 100.0);
680 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
681 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
682 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
686 * Convert a HSL color to RGB.
688 * @param hue hue, between 0 and 359
689 * @param sat saturation, between 0 and 100
690 * @param lum luminance, between 0 and 100
691 * @return the rgb color as 0x00RRGGBB
693 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
694 assert ((hue
>= 0) && (hue
<= 360));
695 assert ((sat
>= 0) && (sat
<= 100));
696 assert ((lum
>= 0) && (lum
<= 100));
698 double S
= sat
/ 100.0;
699 double L
= lum
/ 100.0;
700 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
701 double Hp
= hue
/ 60.0;
702 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
709 } else if (Hp
<= 2.0) {
712 } else if (Hp
<= 3.0) {
715 } else if (Hp
<= 4.0) {
718 } else if (Hp
<= 5.0) {
721 } else if (Hp
<= 6.0) {
725 double m
= L
- (C
/ 2.0);
726 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
727 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
728 int blue
= (int) ((Bp
+ m
) * 255.0);
730 return (red
| green
| blue
);
734 * Create the sixel palette.
736 private void makePalette() {
737 // Generate the sixel palette. Because we have no idea at this
738 // layer which image(s) will be shown, we have to use a common
739 // palette with sixelPaletteSize colors for everything, and
740 // map the BufferedImage colors to their nearest neighbor in RGB
743 if (sixelPaletteSize
== 2) {
745 rgbColors
.add(0xFFFFFF);
746 rgbSortedIndex
[0] = 0;
747 rgbSortedIndex
[1] = 1;
751 // We build a palette using the Hue-Saturation-Luminence model,
752 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
753 // Luminance. We convert these colors to 24-bit RGB, sort them
754 // ascending, and steal the first index for pure black and the
755 // last for pure white. The 8-bit final palette favors bright
756 // colors, somewhere between pastel and classic television
757 // technicolor. 9- and 10-bit palettes are more uniform.
759 // Default at 256 colors.
764 assert (sixelPaletteSize
>= 256);
765 assert ((sixelPaletteSize
== 256)
766 || (sixelPaletteSize
== 512)
767 || (sixelPaletteSize
== 1024)
768 || (sixelPaletteSize
== 2048));
770 switch (sixelPaletteSize
) {
787 hueStep
= (int) (Math
.pow(2, hueBits
));
788 satStep
= (int) (100 / Math
.pow(2, satBits
));
789 // 1 bit for luminance: 40 and 70.
794 // 2 bits: 20, 40, 60, 80
799 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
805 // System.err.printf("<html><body>\n");
806 // Hue is evenly spaced around the wheel.
807 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
809 final boolean DEBUG
= false;
810 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
812 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
813 hue
+= (360/hueStep
)) {
815 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
816 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
817 hslColors
.add(satList
);
819 // Saturation is linearly spaced between pastel and pure.
820 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
822 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
823 satList
.add(lumList
);
825 // Luminance brackets the pure color, but leaning toward
827 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
829 System.err.printf("<font style = \"color:");
830 System.err.printf("hsl(%d, %d%%, %d%%)",
832 System.err.printf(";\">=</font>\n");
834 int rgbColor
= hslToRgb(hue
, sat
, lum
);
835 rgbColors
.add(rgbColor
);
836 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
837 rgbColors
.size() - 1);
838 lumList
.add(colorIdx
);
840 rawRgbList
.add(rgbColor
);
842 int red
= (rgbColor
>>> 16) & 0xFF;
843 int green
= (rgbColor
>>> 8) & 0xFF;
844 int blue
= rgbColor
& 0xFF;
845 int [] backToHsl
= new int[3];
846 rgbToHsl(red
, green
, blue
, backToHsl
);
847 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
848 hue
, backToHsl
[0], sat
, backToHsl
[1],
854 // System.err.printf("\n</body></html>\n");
856 assert (rgbColors
.size() == sixelPaletteSize
);
859 * We need to sort rgbColors, so that toSixel() can know where
860 * BLACK and WHITE are in it. But we also need to be able to
861 * find the sorted values using the old unsorted indexes. So we
862 * will sort it, put all the indexes into a HashMap, and then
863 * build rgbSortedIndex[].
865 Collections
.sort(rgbColors
);
866 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
867 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
868 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
869 rgbColorIndices
.put(rgbColors
.get(i
), i
);
871 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
872 int rawColor
= rawRgbList
.get(i
);
873 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
876 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
877 assert (rawRgbList
!= null);
878 int idx
= rgbSortedIndex
[i
];
879 int rgbColor
= rgbColors
.get(idx
);
880 if ((idx
!= 0) && (idx
!= sixelPaletteSize
- 1)) {
882 System.err.printf("%d %06x --> %d %06x\n",
883 i, rawRgbList.get(i), idx, rgbColors.get(idx));
885 assert (rgbColor
== rawRgbList
.get(i
));
890 // Set the dimmest color as true black, and the brightest as true
893 rgbColors
.set(sixelPaletteSize
- 1, 0xFFFFFF);
896 System.err.printf("<html><body>\n");
897 for (Integer rgb: rgbColors) {
898 System.err.printf("<font style = \"color:");
899 System.err.printf("#%06x", rgb);
900 System.err.printf(";\">=</font>\n");
902 System.err.printf("\n</body></html>\n");
908 * Emit the sixel palette.
910 * @param sb the StringBuilder to append to
911 * @param used array of booleans set to true for each color actually
912 * used in this cell, or null to emit the entire palette
913 * @return the string to emit to an ANSI / ECMA-style terminal
915 public String
emitPalette(final StringBuilder sb
,
916 final boolean [] used
) {
918 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
919 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
920 int rgbColor
= rgbColors
.get(i
);
921 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
922 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
923 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
924 ( rgbColor
& 0xFF) * 100 / 255));
927 return sb
.toString();
932 * ImageCache is a least-recently-used cache that hangs on to the
933 * post-rendered sixel or iTerm2 string for a particular set of cells.
935 private class ImageCache
{
938 * Maximum size of the cache.
940 private int maxSize
= 100;
943 * The entries stored in the cache.
945 private HashMap
<String
, CacheEntry
> cache
= null;
948 * CacheEntry is one entry in the cache.
950 private class CacheEntry
{
962 * The last time this entry was used.
964 public long millis
= 0;
967 * Public constructor.
969 * @param key the cache entry key
970 * @param data the cache entry data
972 public CacheEntry(final String key
, final String data
) {
975 this.millis
= System
.currentTimeMillis();
980 * Public constructor.
982 * @param maxSize the maximum size of the cache
984 public ImageCache(final int maxSize
) {
985 this.maxSize
= maxSize
;
986 cache
= new HashMap
<String
, CacheEntry
>();
990 * Make a unique key for a list of cells.
992 * @param cells the cells
995 private String
makeKey(final ArrayList
<Cell
> cells
) {
996 StringBuilder sb
= new StringBuilder();
997 for (Cell cell
: cells
) {
998 sb
.append(cell
.hashCode());
1000 return sb
.toString();
1004 * Get an entry from the cache.
1006 * @param cells the list of cells that are the cache key
1007 * @return the sixel string representing these cells, or null if this
1008 * list of cells is not in the cache
1010 public String
get(final ArrayList
<Cell
> cells
) {
1011 CacheEntry entry
= cache
.get(makeKey(cells
));
1012 if (entry
== null) {
1015 entry
.millis
= System
.currentTimeMillis();
1020 * Put an entry into the cache.
1022 * @param cells the list of cells that are the cache key
1023 * @param data the sixel string representing these cells
1025 public void put(final ArrayList
<Cell
> cells
, final String data
) {
1026 String key
= makeKey(cells
);
1028 // System.err.println("put() " + key + " size " + cache.size());
1030 assert (!cache
.containsKey(key
));
1032 assert (cache
.size() <= maxSize
);
1033 if (cache
.size() == maxSize
) {
1034 // Cache is at limit, evict oldest entry.
1035 long oldestTime
= Long
.MAX_VALUE
;
1036 String keyToRemove
= null;
1037 for (CacheEntry entry
: cache
.values()) {
1038 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
1039 keyToRemove
= entry
.key
;
1040 oldestTime
= entry
.millis
;
1044 System.err.println("put() remove key = " + keyToRemove +
1045 " size " + cache.size());
1047 assert (keyToRemove
!= null);
1048 cache
.remove(keyToRemove
);
1050 System.err.println("put() removed, size " + cache.size());
1053 assert (cache
.size() <= maxSize
);
1054 CacheEntry entry
= new CacheEntry(key
, data
);
1055 assert (key
.equals(entry
.key
));
1056 cache
.put(key
, entry
);
1058 System.err.println("put() added key " + key + " " +
1059 " size " + cache.size());
1065 // ------------------------------------------------------------------------
1066 // Constructors -----------------------------------------------------------
1067 // ------------------------------------------------------------------------
1070 * Constructor sets up state for getEvent(). If either windowWidth or
1071 * windowHeight are less than 1, the terminal is not resized.
1073 * @param listener the object this backend needs to wake up when new
1075 * @param input an InputStream connected to the remote user, or null for
1076 * System.in. If System.in is used, then on non-Windows systems it will
1077 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1078 * cooked mode. input is always converted to a Reader with UTF-8
1080 * @param output an OutputStream connected to the remote user, or null
1081 * for System.out. output is always converted to a Writer with UTF-8
1083 * @param windowWidth the number of text columns to start with
1084 * @param windowHeight the number of text rows to start with
1085 * @throws UnsupportedEncodingException if an exception is thrown when
1086 * creating the InputStreamReader
1088 public ECMA48Terminal(final Object listener
, final InputStream input
,
1089 final OutputStream output
, final int windowWidth
,
1090 final int windowHeight
) throws UnsupportedEncodingException
{
1092 this(listener
, input
, output
);
1094 // Send dtterm/xterm sequences, which will probably not work because
1095 // allowWindowOps is defaulted to false.
1096 if ((windowWidth
> 0) && (windowHeight
> 0)) {
1097 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1099 this.output
.write(resizeString
);
1100 this.output
.flush();
1105 * Constructor sets up state for getEvent().
1107 * @param listener the object this backend needs to wake up when new
1109 * @param input an InputStream connected to the remote user, or null for
1110 * System.in. If System.in is used, then on non-Windows systems it will
1111 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1112 * cooked mode. input is always converted to a Reader with UTF-8
1114 * @param output an OutputStream connected to the remote user, or null
1115 * for System.out. output is always converted to a Writer with UTF-8
1117 * @throws UnsupportedEncodingException if an exception is thrown when
1118 * creating the InputStreamReader
1120 public ECMA48Terminal(final Object listener
, final InputStream input
,
1121 final OutputStream output
) throws UnsupportedEncodingException
{
1127 stopReaderThread
= false;
1128 this.listener
= listener
;
1130 if (input
== null) {
1131 // inputStream = System.in;
1132 inputStream
= new FileInputStream(FileDescriptor
.in
);
1136 inputStream
= input
;
1138 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1140 if (input
instanceof SessionInfo
) {
1141 // This is a TelnetInputStream that exposes window size and
1142 // environment variables from the telnet layer.
1143 sessionInfo
= (SessionInfo
) input
;
1145 if (sessionInfo
== null) {
1146 if (input
== null) {
1147 // Reading right off the tty
1148 sessionInfo
= new TTYSessionInfo();
1150 sessionInfo
= new TSessionInfo();
1154 if (output
== null) {
1155 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1158 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1162 // Request Device Attributes
1163 this.output
.printf("\033[c");
1165 // Request xterm report window/cell dimensions in pixels
1166 this.output
.printf("%s", xtermReportPixelDimensions());
1168 // Enable mouse reporting and metaSendsEscape
1169 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1170 this.output
.flush();
1172 // Request xterm use the sixel settings we want
1173 this.output
.printf("%s", xtermSetSixelSettings());
1175 // Query the screen size
1176 sessionInfo
.queryWindowSize();
1177 setDimensions(sessionInfo
.getWindowWidth(),
1178 sessionInfo
.getWindowHeight());
1180 // Hang onto the window size
1181 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1182 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1186 // Spin up the input reader
1187 eventQueue
= new ArrayList
<TInputEvent
>();
1188 readerThread
= new Thread(this);
1189 readerThread
.start();
1192 this.output
.write(clearAll());
1193 this.output
.flush();
1197 * Constructor sets up state for getEvent().
1199 * @param listener the object this backend needs to wake up when new
1201 * @param input the InputStream underlying 'reader'. Its available()
1202 * method is used to determine if reader.read() will block or not.
1203 * @param reader a Reader connected to the remote user.
1204 * @param writer a PrintWriter connected to the remote user.
1205 * @param setRawMode if true, set System.in into raw mode with stty.
1206 * This should in general not be used. It is here solely for Demo3,
1207 * which uses System.in.
1208 * @throws IllegalArgumentException if input, reader, or writer are null.
1210 public ECMA48Terminal(final Object listener
, final InputStream input
,
1211 final Reader reader
, final PrintWriter writer
,
1212 final boolean setRawMode
) {
1214 if (input
== null) {
1215 throw new IllegalArgumentException("InputStream must be specified");
1217 if (reader
== null) {
1218 throw new IllegalArgumentException("Reader must be specified");
1220 if (writer
== null) {
1221 throw new IllegalArgumentException("Writer must be specified");
1227 stopReaderThread
= false;
1228 this.listener
= listener
;
1230 inputStream
= input
;
1231 this.input
= reader
;
1233 if (setRawMode
== true) {
1236 this.setRawMode
= setRawMode
;
1238 if (input
instanceof SessionInfo
) {
1239 // This is a TelnetInputStream that exposes window size and
1240 // environment variables from the telnet layer.
1241 sessionInfo
= (SessionInfo
) input
;
1243 if (sessionInfo
== null) {
1244 if (setRawMode
== true) {
1245 // Reading right off the tty
1246 sessionInfo
= new TTYSessionInfo();
1248 sessionInfo
= new TSessionInfo();
1252 this.output
= writer
;
1254 // Request Device Attributes
1255 this.output
.printf("\033[c");
1257 // Request xterm report window/cell dimensions in pixels
1258 this.output
.printf("%s", xtermReportPixelDimensions());
1260 // Enable mouse reporting and metaSendsEscape
1261 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1262 this.output
.flush();
1264 // Request xterm use the sixel settings we want
1265 this.output
.printf("%s", xtermSetSixelSettings());
1267 // Query the screen size
1268 sessionInfo
.queryWindowSize();
1269 setDimensions(sessionInfo
.getWindowWidth(),
1270 sessionInfo
.getWindowHeight());
1272 // Hang onto the window size
1273 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1274 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1278 // Spin up the input reader
1279 eventQueue
= new ArrayList
<TInputEvent
>();
1280 readerThread
= new Thread(this);
1281 readerThread
.start();
1284 this.output
.write(clearAll());
1285 this.output
.flush();
1289 * Constructor sets up state for getEvent().
1291 * @param listener the object this backend needs to wake up when new
1293 * @param input the InputStream underlying 'reader'. Its available()
1294 * method is used to determine if reader.read() will block or not.
1295 * @param reader a Reader connected to the remote user.
1296 * @param writer a PrintWriter connected to the remote user.
1297 * @throws IllegalArgumentException if input, reader, or writer are null.
1299 public ECMA48Terminal(final Object listener
, final InputStream input
,
1300 final Reader reader
, final PrintWriter writer
) {
1302 this(listener
, input
, reader
, writer
, false);
1305 // ------------------------------------------------------------------------
1306 // LogicalScreen ----------------------------------------------------------
1307 // ------------------------------------------------------------------------
1310 * Set the window title.
1312 * @param title the new title
1315 public void setTitle(final String title
) {
1316 output
.write(getSetTitleString(title
));
1321 * Push the logical screen to the physical device.
1324 public void flushPhysical() {
1325 StringBuilder sb
= new StringBuilder();
1329 && (cursorY
<= height
- 1)
1330 && (cursorX
<= width
- 1)
1333 sb
.append(cursor(true));
1334 sb
.append(gotoXY(cursorX
, cursorY
));
1336 sb
.append(cursor(false));
1339 output
.write(sb
.toString());
1344 * Resize the physical screen to match the logical screen dimensions.
1347 public void resizeToScreen() {
1348 // Send dtterm/xterm sequences, which will probably not work because
1349 // allowWindowOps is defaulted to false.
1350 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1352 this.output
.write(resizeString
);
1353 this.output
.flush();
1356 // ------------------------------------------------------------------------
1357 // TerminalReader ---------------------------------------------------------
1358 // ------------------------------------------------------------------------
1361 * Check if there are events in the queue.
1363 * @return if true, getEvents() has something to return to the backend
1365 public boolean hasEvents() {
1366 synchronized (eventQueue
) {
1367 return (eventQueue
.size() > 0);
1372 * Return any events in the IO queue.
1374 * @param queue list to append new events to
1376 public void getEvents(final List
<TInputEvent
> queue
) {
1377 synchronized (eventQueue
) {
1378 if (eventQueue
.size() > 0) {
1379 synchronized (queue
) {
1380 queue
.addAll(eventQueue
);
1388 * Restore terminal to normal state.
1390 public void closeTerminal() {
1392 // System.err.println("=== closeTerminal() ==="); System.err.flush();
1394 // Tell the reader thread to stop looking at input
1395 stopReaderThread
= true;
1397 readerThread
.join();
1398 } catch (InterruptedException e
) {
1399 if (debugToStderr
) {
1400 e
.printStackTrace();
1404 // Disable mouse reporting and show cursor. Defensive null check
1405 // here in case closeTerminal() is called twice.
1406 if (output
!= null) {
1407 output
.printf("%s%s%s%s", mouse(false), cursor(true),
1408 defaultColor(), xtermResetSixelSettings());
1415 // We don't close System.in/out
1417 // Shut down the streams, this should wake up the reader thread
1418 // and make it exit.
1419 if (input
!= null) {
1422 } catch (IOException e
) {
1427 if (output
!= null) {
1435 * Set listener to a different Object.
1437 * @param listener the new listening object that run() wakes up on new
1440 public void setListener(final Object listener
) {
1441 this.listener
= listener
;
1445 * Reload options from System properties.
1447 public void reloadOptions() {
1448 // Permit RGB colors only if externally requested.
1449 if (System
.getProperty("jexer.ECMA48.rgbColor",
1450 "false").equals("true")
1457 // Default to using images for full-width characters.
1458 if (System
.getProperty("jexer.ECMA48.wideCharImages",
1459 "true").equals("true")) {
1460 wideCharImages
= true;
1462 wideCharImages
= false;
1465 // Pull the system properties for sixel output.
1466 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1473 int paletteSize
= 1024;
1475 paletteSize
= Integer
.parseInt(System
.getProperty(
1476 "jexer.ECMA48.sixelPaletteSize", "1024"));
1477 switch (paletteSize
) {
1483 sixelPaletteSize
= paletteSize
;
1489 } catch (NumberFormatException e
) {
1493 // Default to not supporting iTerm2 images.
1494 if (System
.getProperty("jexer.ECMA48.iTerm2Images",
1495 "false").equals("true")) {
1496 iterm2Images
= true;
1498 iterm2Images
= false;
1501 // Default to using JPG Jexer images if terminal supports it.
1502 String jexerImageStr
= System
.getProperty("jexer.ECMA48.jexerImages",
1503 "jpg").toLowerCase();
1504 if (jexerImageStr
.equals("false")) {
1505 jexerImageOption
= JexerImageOption
.DISABLED
;
1506 } else if (jexerImageStr
.equals("jpg")) {
1507 jexerImageOption
= JexerImageOption
.JPG
;
1508 } else if (jexerImageStr
.equals("png")) {
1509 jexerImageOption
= JexerImageOption
.PNG
;
1510 } else if (jexerImageStr
.equals("rgb")) {
1511 jexerImageOption
= JexerImageOption
.RGB
;
1514 // Set custom colors
1515 setCustomSystemColors();
1518 // ------------------------------------------------------------------------
1519 // Runnable ---------------------------------------------------------------
1520 // ------------------------------------------------------------------------
1523 * Read function runs on a separate thread.
1526 boolean done
= false;
1527 // available() will often return > 1, so we need to read in chunks to
1529 char [] readBuffer
= new char[128];
1530 List
<TInputEvent
> events
= new ArrayList
<TInputEvent
>();
1532 while (!done
&& !stopReaderThread
) {
1534 // We assume that if inputStream has bytes available, then
1535 // input won't block on read().
1536 int n
= inputStream
.available();
1539 System.err.printf("inputStream.available(): %d\n", n);
1544 if (readBuffer
.length
< n
) {
1545 // The buffer wasn't big enough, make it huger
1546 readBuffer
= new char[readBuffer
.length
* 2];
1549 // System.err.printf("BEFORE read()\n"); System.err.flush();
1551 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1554 System.err.printf("AFTER read() %d\n", rc);
1562 for (int i
= 0; i
< rc
; i
++) {
1563 int ch
= readBuffer
[i
];
1564 processChar(events
, (char)ch
);
1566 getIdleEvents(events
);
1567 if (events
.size() > 0) {
1568 // Add to the queue for the backend thread to
1569 // be able to obtain.
1570 synchronized (eventQueue
) {
1571 eventQueue
.addAll(events
);
1573 if (listener
!= null) {
1574 synchronized (listener
) {
1575 listener
.notifyAll();
1582 getIdleEvents(events
);
1583 if (events
.size() > 0) {
1584 synchronized (eventQueue
) {
1585 eventQueue
.addAll(events
);
1587 if (listener
!= null) {
1588 synchronized (listener
) {
1589 listener
.notifyAll();
1595 if (output
.checkError()) {
1600 // Wait 20 millis for more data
1603 // System.err.println("end while loop"); System.err.flush();
1604 } catch (InterruptedException e
) {
1606 } catch (IOException e
) {
1607 e
.printStackTrace();
1610 } // while ((done == false) && (stopReaderThread == false))
1612 // Pass an event up to TApplication to tell it this Backend is done.
1613 synchronized (eventQueue
) {
1614 eventQueue
.add(new TCommandEvent(cmBackendDisconnect
));
1616 if (listener
!= null) {
1617 synchronized (listener
) {
1618 listener
.notifyAll();
1622 // System.err.println("*** run() exiting..."); System.err.flush();
1625 // ------------------------------------------------------------------------
1626 // ECMA48Terminal ---------------------------------------------------------
1627 // ------------------------------------------------------------------------
1630 * Get the width of a character cell in pixels.
1632 * @return the width in pixels of a character cell
1634 public int getTextWidth() {
1635 return (widthPixels
/ sessionInfo
.getWindowWidth());
1639 * Get the height of a character cell in pixels.
1641 * @return the height in pixels of a character cell
1643 public int getTextHeight() {
1644 return (heightPixels
/ sessionInfo
.getWindowHeight());
1648 * Getter for sessionInfo.
1650 * @return the SessionInfo
1652 public SessionInfo
getSessionInfo() {
1657 * Get the output writer.
1659 * @return the Writer
1661 public PrintWriter
getOutput() {
1666 * Call 'stty' to set cooked mode.
1668 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1670 private void sttyCooked() {
1675 * Call 'stty' to set raw mode.
1677 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1678 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1679 * -parenb cs8 min 1 < /dev/tty'
1681 private void sttyRaw() {
1686 * Call 'stty' to set raw or cooked mode.
1688 * @param mode if true, set raw mode, otherwise set cooked mode
1690 private void doStty(final boolean mode
) {
1691 String
[] cmdRaw
= {
1692 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1694 String
[] cmdCooked
= {
1695 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1700 process
= Runtime
.getRuntime().exec(cmdRaw
);
1702 process
= Runtime
.getRuntime().exec(cmdCooked
);
1704 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1705 String line
= in
.readLine();
1706 if ((line
!= null) && (line
.length() > 0)) {
1707 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1710 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1711 line
= err
.readLine();
1712 if ((line
!= null) && (line
.length() > 0)) {
1713 System
.err
.println("Error output from stty: " + line
);
1718 } catch (InterruptedException e
) {
1719 if (debugToStderr
) {
1720 e
.printStackTrace();
1724 int rc
= process
.exitValue();
1726 System
.err
.println("stty returned error code: " + rc
);
1728 } catch (IOException e
) {
1729 e
.printStackTrace();
1736 public void flush() {
1741 * Perform a somewhat-optimal rendering of a line.
1743 * @param y row coordinate. 0 is the top-most row.
1744 * @param sb StringBuilder to write escape sequences to
1745 * @param lastAttr cell attributes from the last call to flushLine
1747 private void flushLine(final int y
, final StringBuilder sb
,
1748 CellAttributes lastAttr
) {
1752 for (int x
= 0; x
< width
; x
++) {
1753 Cell lCell
= logical
[x
][y
];
1754 if (!lCell
.isBlank()) {
1758 // Push textEnd to first column beyond the text area
1762 // reallyCleared = true;
1764 boolean hasImage
= false;
1766 for (int x
= 0; x
< width
; x
++) {
1767 Cell lCell
= logical
[x
][y
];
1768 Cell pCell
= physical
[x
][y
];
1770 if (!lCell
.equals(pCell
) || reallyCleared
) {
1772 if (debugToStderr
) {
1773 System
.err
.printf("\n--\n");
1774 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1775 System
.err
.printf(" lCell: %s\n", lCell
);
1776 System
.err
.printf(" pCell: %s\n", pCell
);
1777 System
.err
.printf(" ==== \n");
1780 if (lastAttr
== null) {
1781 lastAttr
= new CellAttributes();
1782 sb
.append(normal());
1786 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1787 // Advancing at least one cell, or the first gotoXY
1788 sb
.append(gotoXY(x
, y
));
1791 assert (lastAttr
!= null);
1793 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1794 assert (lCell
.isBlank());
1796 for (int i
= x
; i
< width
; i
++) {
1797 assert (logical
[i
][y
].isBlank());
1798 // Physical is always updated
1799 physical
[i
][y
].reset();
1802 // Clear remaining line
1803 sb
.append(clearRemainingLine());
1808 // Image cell: bypass the rest of the loop, it is not
1810 if ((wideCharImages
&& lCell
.isImage())
1813 && (lCell
.getWidth() == Cell
.Width
.SINGLE
))
1817 // Save the last rendered cell
1820 // Physical is always updated
1821 physical
[x
][y
].setTo(lCell
);
1825 assert ((wideCharImages
&& !lCell
.isImage())
1827 && (!lCell
.isImage()
1829 && (lCell
.getWidth() != Cell
.Width
.SINGLE
)))));
1831 if (!wideCharImages
&& (lCell
.getWidth() == Cell
.Width
.RIGHT
)) {
1837 sb
.append(gotoXY(x
, y
));
1840 // Now emit only the modified attributes
1841 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1842 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1844 && (lCell
.isBold() == lastAttr
.isBold())
1845 && (lCell
.isReverse() == lastAttr
.isReverse())
1846 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1847 && (lCell
.isBlink() == lastAttr
.isBlink())
1849 // Both colors changed, attributes the same
1850 sb
.append(color(lCell
.isBold(),
1851 lCell
.getForeColor(), lCell
.getBackColor()));
1853 if (debugToStderr
) {
1854 System
.err
.printf("1 Change only fore/back colors\n");
1857 } else if (lCell
.isRGB()
1858 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1859 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1860 && (lCell
.isBold() == lastAttr
.isBold())
1861 && (lCell
.isReverse() == lastAttr
.isReverse())
1862 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1863 && (lCell
.isBlink() == lastAttr
.isBlink())
1865 // Both colors changed, attributes the same
1866 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1867 lCell
.getBackColorRGB()));
1869 if (debugToStderr
) {
1870 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1872 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1873 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1875 && (lCell
.isBold() != lastAttr
.isBold())
1876 && (lCell
.isReverse() != lastAttr
.isReverse())
1877 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1878 && (lCell
.isBlink() != lastAttr
.isBlink())
1880 // Everything is different
1881 sb
.append(color(lCell
.getForeColor(),
1882 lCell
.getBackColor(),
1883 lCell
.isBold(), lCell
.isReverse(),
1885 lCell
.isUnderline()));
1887 if (debugToStderr
) {
1888 System
.err
.printf("2 Set all attributes\n");
1890 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1891 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1893 && (lCell
.isBold() == lastAttr
.isBold())
1894 && (lCell
.isReverse() == lastAttr
.isReverse())
1895 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1896 && (lCell
.isBlink() == lastAttr
.isBlink())
1899 // Attributes same, foreColor different
1900 sb
.append(color(lCell
.isBold(),
1901 lCell
.getForeColor(), true));
1903 if (debugToStderr
) {
1904 System
.err
.printf("3 Change foreColor\n");
1906 } else if (lCell
.isRGB()
1907 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1908 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1909 && (lCell
.getForeColorRGB() >= 0)
1910 && (lCell
.getBackColorRGB() >= 0)
1911 && (lCell
.isBold() == lastAttr
.isBold())
1912 && (lCell
.isReverse() == lastAttr
.isReverse())
1913 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1914 && (lCell
.isBlink() == lastAttr
.isBlink())
1916 // Attributes same, foreColor different
1917 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1919 if (debugToStderr
) {
1920 System
.err
.printf("3 Change foreColor (RGB)\n");
1922 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1923 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1925 && (lCell
.isBold() == lastAttr
.isBold())
1926 && (lCell
.isReverse() == lastAttr
.isReverse())
1927 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1928 && (lCell
.isBlink() == lastAttr
.isBlink())
1930 // Attributes same, backColor different
1931 sb
.append(color(lCell
.isBold(),
1932 lCell
.getBackColor(), false));
1934 if (debugToStderr
) {
1935 System
.err
.printf("4 Change backColor\n");
1937 } else if (lCell
.isRGB()
1938 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1939 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1940 && (lCell
.isBold() == lastAttr
.isBold())
1941 && (lCell
.isReverse() == lastAttr
.isReverse())
1942 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1943 && (lCell
.isBlink() == lastAttr
.isBlink())
1945 // Attributes same, foreColor different
1946 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1948 if (debugToStderr
) {
1949 System
.err
.printf("4 Change backColor (RGB)\n");
1951 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1952 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1953 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1954 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1955 && (lCell
.isBold() == lastAttr
.isBold())
1956 && (lCell
.isReverse() == lastAttr
.isReverse())
1957 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1958 && (lCell
.isBlink() == lastAttr
.isBlink())
1961 // All attributes the same, just print the char
1964 if (debugToStderr
) {
1965 System
.err
.printf("5 Only emit character\n");
1968 // Just reset everything again
1969 if (!lCell
.isRGB()) {
1970 sb
.append(color(lCell
.getForeColor(),
1971 lCell
.getBackColor(),
1975 lCell
.isUnderline()));
1977 if (debugToStderr
) {
1978 System
.err
.printf("6 Change all attributes\n");
1981 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1982 lCell
.getBackColorRGB(),
1986 lCell
.isUnderline()));
1987 if (debugToStderr
) {
1988 System
.err
.printf("6 Change all attributes (RGB)\n");
1993 // Emit the character
1995 // Don't emit the right-half of full-width chars.
1997 && (lCell
.getWidth() != Cell
.Width
.RIGHT
))
1999 sb
.append(Character
.toChars(lCell
.getChar()));
2002 // Save the last rendered cell
2004 lastAttr
.setTo(lCell
);
2006 // Physical is always updated
2007 physical
[x
][y
].setTo(lCell
);
2009 } // if (!lCell.equals(pCell) || (reallyCleared == true))
2011 } // for (int x = 0; x < width; x++)
2015 * Render the screen to a string that can be emitted to something that
2016 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
2018 * @param sb StringBuilder to write escape sequences to
2019 * @return escape sequences string that provides the updates to the
2022 private String
flushString(final StringBuilder sb
) {
2023 CellAttributes attr
= null;
2025 if (reallyCleared
) {
2026 attr
= new CellAttributes();
2027 sb
.append(clearAll());
2031 * For images support, draw all of the image output first, and then
2032 * draw everything else afterwards. This works OK, but performance
2033 * is still a drag on larger pictures.
2035 for (int y
= 0; y
< height
; y
++) {
2036 for (int x
= 0; x
< width
; x
++) {
2037 // If physical had non-image data that is now image data, the
2038 // entire row must be redrawn.
2039 Cell lCell
= logical
[x
][y
];
2040 Cell pCell
= physical
[x
][y
];
2041 if (lCell
.isImage() && !pCell
.isImage()) {
2047 for (int y
= 0; y
< height
; y
++) {
2048 for (int x
= 0; x
< width
; x
++) {
2049 Cell lCell
= logical
[x
][y
];
2050 Cell pCell
= physical
[x
][y
];
2052 if (!lCell
.isImage()
2054 && (lCell
.getWidth() != Cell
.Width
.SINGLE
))
2061 while ((right
< width
)
2062 && (logical
[right
][y
].isImage())
2063 && (!logical
[right
][y
].equals(physical
[right
][y
])
2068 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
2069 for (int i
= 0; i
< (right
- x
); i
++) {
2070 assert (logical
[x
+ i
][y
].isImage());
2071 cellsToDraw
.add(logical
[x
+ i
][y
]);
2073 // Physical is always updated.
2074 physical
[x
+ i
][y
].setTo(lCell
);
2076 if (cellsToDraw
.size() > 0) {
2078 sb
.append(toIterm2Image(x
, y
, cellsToDraw
));
2079 } else if (jexerImageOption
!= JexerImageOption
.DISABLED
) {
2080 sb
.append(toJexerImage(x
, y
, cellsToDraw
));
2082 sb
.append(toSixel(x
, y
, cellsToDraw
));
2090 // Draw the text part now.
2091 for (int y
= 0; y
< height
; y
++) {
2092 flushLine(y
, sb
, attr
);
2095 reallyCleared
= false;
2097 String result
= sb
.toString();
2098 if (debugToStderr
) {
2099 System
.err
.printf("flushString(): %s\n", result
);
2105 * Reset keyboard/mouse input parser.
2107 private void resetParser() {
2108 state
= ParseState
.GROUND
;
2109 params
= new ArrayList
<String
>();
2112 decPrivateModeFlag
= false;
2116 * Produce a control character or one of the special ones (ENTER, TAB,
2119 * @param ch Unicode code point
2120 * @param alt if true, set alt on the TKeypress
2121 * @return one TKeypress event, either a control character (e.g. isKey ==
2122 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
2125 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
2126 // System.err.printf("controlChar: %02x\n", ch);
2130 // Carriage return --> ENTER
2131 return new TKeypressEvent(kbEnter
, alt
, false, false);
2133 // Linefeed --> ENTER
2134 return new TKeypressEvent(kbEnter
, alt
, false, false);
2137 return new TKeypressEvent(kbEsc
, alt
, false, false);
2140 return new TKeypressEvent(kbTab
, alt
, false, false);
2142 // Make all other control characters come back as the alphabetic
2143 // character with the ctrl field set. So SOH would be 'A' +
2145 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
2151 * Produce special key from CSI Pn ; Pm ; ... ~
2153 * @return one KEYPRESS event representing a special key
2155 private TInputEvent
csiFnKey() {
2157 if (params
.size() > 0) {
2158 key
= Integer
.parseInt(params
.get(0));
2160 boolean alt
= false;
2161 boolean ctrl
= false;
2162 boolean shift
= false;
2163 if (params
.size() > 1) {
2164 shift
= csiIsShift(params
.get(1));
2165 alt
= csiIsAlt(params
.get(1));
2166 ctrl
= csiIsCtrl(params
.get(1));
2171 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2173 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2175 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2177 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2179 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2181 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2183 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2185 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2187 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2189 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2191 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2193 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2195 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2197 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2205 * Produce mouse events based on "Any event tracking" and UTF-8
2207 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2209 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2211 private TInputEvent
parseMouse() {
2212 int buttons
= params
.get(0).charAt(0) - 32;
2213 int x
= params
.get(0).charAt(1) - 32 - 1;
2214 int y
= params
.get(0).charAt(2) - 32 - 1;
2216 // Clamp X and Y to the physical screen coordinates.
2217 if (x
>= windowResize
.getWidth()) {
2218 x
= windowResize
.getWidth() - 1;
2220 if (y
>= windowResize
.getHeight()) {
2221 y
= windowResize
.getHeight() - 1;
2224 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2225 boolean eventMouse1
= false;
2226 boolean eventMouse2
= false;
2227 boolean eventMouse3
= false;
2228 boolean eventMouseWheelUp
= false;
2229 boolean eventMouseWheelDown
= false;
2231 // System.err.printf("buttons: %04x\r\n", buttons);
2248 if (!mouse1
&& !mouse2
&& !mouse3
) {
2249 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2251 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2268 // Dragging with mouse1 down
2271 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2275 // Dragging with mouse2 down
2278 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2282 // Dragging with mouse3 down
2285 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2289 // Dragging with mouse2 down after wheelUp
2292 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2296 // Dragging with mouse2 down after wheelDown
2299 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2303 eventMouseWheelUp
= true;
2307 eventMouseWheelDown
= true;
2311 // Unknown, just make it motion
2312 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2315 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2316 eventMouse1
, eventMouse2
, eventMouse3
,
2317 eventMouseWheelUp
, eventMouseWheelDown
);
2321 * Produce mouse events based on "Any event tracking" and SGR
2323 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2325 * @param release if true, this was a release ('m')
2326 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2328 private TInputEvent
parseMouseSGR(final boolean release
) {
2329 // SGR extended coordinates - mode 1006
2330 if (params
.size() < 3) {
2331 // Invalid position, bail out.
2334 int buttons
= Integer
.parseInt(params
.get(0));
2335 int x
= Integer
.parseInt(params
.get(1)) - 1;
2336 int y
= Integer
.parseInt(params
.get(2)) - 1;
2338 // Clamp X and Y to the physical screen coordinates.
2339 if (x
>= windowResize
.getWidth()) {
2340 x
= windowResize
.getWidth() - 1;
2342 if (y
>= windowResize
.getHeight()) {
2343 y
= windowResize
.getHeight() - 1;
2346 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2347 boolean eventMouse1
= false;
2348 boolean eventMouse2
= false;
2349 boolean eventMouse3
= false;
2350 boolean eventMouseWheelUp
= false;
2351 boolean eventMouseWheelDown
= false;
2354 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2368 // Motion only, no buttons down
2369 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2373 // Dragging with mouse1 down
2375 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2379 // Dragging with mouse2 down
2381 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2385 // Dragging with mouse3 down
2387 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2391 // Dragging with mouse2 down after wheelUp
2393 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2397 // Dragging with mouse2 down after wheelDown
2399 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2403 eventMouseWheelUp
= true;
2407 eventMouseWheelDown
= true;
2411 // Unknown, bail out
2414 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2415 eventMouse1
, eventMouse2
, eventMouse3
,
2416 eventMouseWheelUp
, eventMouseWheelDown
);
2420 * Return any events in the IO queue due to timeout.
2422 * @param queue list to append new events to
2424 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2425 long nowTime
= System
.currentTimeMillis();
2427 // Check for new window size
2428 long windowSizeDelay
= nowTime
- windowSizeTime
;
2429 if (windowSizeDelay
> 1000) {
2430 int oldTextWidth
= getTextWidth();
2431 int oldTextHeight
= getTextHeight();
2433 sessionInfo
.queryWindowSize();
2434 int newWidth
= sessionInfo
.getWindowWidth();
2435 int newHeight
= sessionInfo
.getWindowHeight();
2437 if ((newWidth
!= windowResize
.getWidth())
2438 || (newHeight
!= windowResize
.getHeight())
2441 // Request xterm report window dimensions in pixels again.
2442 // Between now and then, ensure that the reported text cell
2443 // size is the same by setting widthPixels and heightPixels
2444 // to match the new dimensions.
2445 widthPixels
= oldTextWidth
* newWidth
;
2446 heightPixels
= oldTextHeight
* newHeight
;
2448 if (debugToStderr
) {
2449 System
.err
.println("Screen size changed, old size " +
2451 System
.err
.println(" new size " +
2452 newWidth
+ " x " + newHeight
);
2453 System
.err
.println(" old pixels " +
2454 oldTextWidth
+ " x " + oldTextHeight
);
2455 System
.err
.println(" new pixels " +
2456 getTextWidth() + " x " + getTextHeight());
2459 this.output
.printf("%s", xtermReportPixelDimensions());
2460 this.output
.flush();
2462 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2463 newWidth
, newHeight
);
2464 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2465 newWidth
, newHeight
);
2468 windowSizeTime
= nowTime
;
2471 // ESCDELAY type timeout
2472 if (state
== ParseState
.ESCAPE
) {
2473 long escDelay
= nowTime
- escapeTime
;
2474 if (escDelay
> 100) {
2475 // After 0.1 seconds, assume a true escape character
2476 queue
.add(controlChar((char)0x1B, false));
2483 * Returns true if the CSI parameter for a keyboard command means that
2486 private boolean csiIsShift(final String x
) {
2498 * Returns true if the CSI parameter for a keyboard command means that
2501 private boolean csiIsAlt(final String x
) {
2513 * Returns true if the CSI parameter for a keyboard command means that
2516 private boolean csiIsCtrl(final String x
) {
2528 * Parses the next character of input to see if an InputEvent is
2531 * @param events list to append new events to
2532 * @param ch Unicode code point
2534 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2536 // ESCDELAY type timeout
2537 long nowTime
= System
.currentTimeMillis();
2538 if (state
== ParseState
.ESCAPE
) {
2539 long escDelay
= nowTime
- escapeTime
;
2540 if (escDelay
> 250) {
2541 // After 0.25 seconds, assume a true escape character
2542 events
.add(controlChar((char)0x1B, false));
2548 boolean ctrl
= false;
2549 boolean alt
= false;
2550 boolean shift
= false;
2552 // System.err.printf("state: %s ch %c\r\n", state, ch);
2558 state
= ParseState
.ESCAPE
;
2559 escapeTime
= nowTime
;
2564 // Control character
2565 events
.add(controlChar(ch
, false));
2572 events
.add(new TKeypressEvent(false, 0, ch
,
2573 false, false, false));
2582 // ALT-Control character
2583 events
.add(controlChar(ch
, true));
2589 // This will be one of the function keys
2590 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2594 // '[' goes to CSI_ENTRY
2596 state
= ParseState
.CSI_ENTRY
;
2600 // Everything else is assumed to be Alt-keystroke
2601 if ((ch
>= 'A') && (ch
<= 'Z')) {
2605 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2609 case ESCAPE_INTERMEDIATE
:
2610 if ((ch
>= 'P') && (ch
<= 'S')) {
2614 events
.add(new TKeypressEvent(kbF1
));
2617 events
.add(new TKeypressEvent(kbF2
));
2620 events
.add(new TKeypressEvent(kbF3
));
2623 events
.add(new TKeypressEvent(kbF4
));
2632 // Unknown keystroke, ignore
2637 // Numbers - parameter values
2638 if ((ch
>= '0') && (ch
<= '9')) {
2639 params
.set(params
.size() - 1,
2640 params
.get(params
.size() - 1) + ch
);
2641 state
= ParseState
.CSI_PARAM
;
2644 // Parameter separator
2650 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2654 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2659 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2664 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2669 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2674 events
.add(new TKeypressEvent(kbHome
));
2679 events
.add(new TKeypressEvent(kbEnd
));
2683 // CBT - Cursor backward X tab stops (default 1)
2684 events
.add(new TKeypressEvent(kbBackTab
));
2689 state
= ParseState
.MOUSE
;
2692 // Mouse position, SGR (1006) coordinates
2693 state
= ParseState
.MOUSE_SGR
;
2696 // DEC private mode flag
2697 decPrivateModeFlag
= true;
2704 // Unknown keystroke, ignore
2709 // Numbers - parameter values
2710 if ((ch
>= '0') && (ch
<= '9')) {
2711 params
.set(params
.size() - 1,
2712 params
.get(params
.size() - 1) + ch
);
2715 // Parameter separator
2723 // Generate a mouse press event
2724 TInputEvent event
= parseMouseSGR(false);
2725 if (event
!= null) {
2731 // Generate a mouse release event
2732 event
= parseMouseSGR(true);
2733 if (event
!= null) {
2742 // Unknown keystroke, ignore
2747 // Numbers - parameter values
2748 if ((ch
>= '0') && (ch
<= '9')) {
2749 params
.set(params
.size() - 1,
2750 params
.get(params
.size() - 1) + ch
);
2751 state
= ParseState
.CSI_PARAM
;
2754 // Parameter separator
2761 events
.add(csiFnKey());
2766 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2770 if (params
.size() > 1) {
2771 shift
= csiIsShift(params
.get(1));
2772 alt
= csiIsAlt(params
.get(1));
2773 ctrl
= csiIsCtrl(params
.get(1));
2775 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2780 if (params
.size() > 1) {
2781 shift
= csiIsShift(params
.get(1));
2782 alt
= csiIsAlt(params
.get(1));
2783 ctrl
= csiIsCtrl(params
.get(1));
2785 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2790 if (params
.size() > 1) {
2791 shift
= csiIsShift(params
.get(1));
2792 alt
= csiIsAlt(params
.get(1));
2793 ctrl
= csiIsCtrl(params
.get(1));
2795 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2800 if (params
.size() > 1) {
2801 shift
= csiIsShift(params
.get(1));
2802 alt
= csiIsAlt(params
.get(1));
2803 ctrl
= csiIsCtrl(params
.get(1));
2805 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2810 if (params
.size() > 1) {
2811 shift
= csiIsShift(params
.get(1));
2812 alt
= csiIsAlt(params
.get(1));
2813 ctrl
= csiIsCtrl(params
.get(1));
2815 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2820 if (params
.size() > 1) {
2821 shift
= csiIsShift(params
.get(1));
2822 alt
= csiIsAlt(params
.get(1));
2823 ctrl
= csiIsCtrl(params
.get(1));
2825 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2829 // Device Attributes
2830 if (decPrivateModeFlag
== false) {
2833 boolean jexerImages
= false;
2834 for (String x
: params
) {
2835 if (x
.equals("4")) {
2836 // Terminal reports sixel support
2837 if (debugToStderr
) {
2838 System
.err
.println("Device Attributes: sixel");
2841 if (x
.equals("444")) {
2842 // Terminal reports Jexer images support
2843 if (debugToStderr
) {
2844 System
.err
.println("Device Attributes: Jexer images");
2849 if (jexerImages
== false) {
2850 // Terminal does not support Jexer images, disable
2852 jexerImageOption
= JexerImageOption
.DISABLED
;
2857 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2858 if (debugToStderr
) {
2859 System
.err
.printf("windowOp pixels: " +
2860 "height %s width %s\n",
2861 params
.get(1), params
.get(2));
2864 widthPixels
= Integer
.parseInt(params
.get(2));
2865 heightPixels
= Integer
.parseInt(params
.get(1));
2866 } catch (NumberFormatException e
) {
2867 if (debugToStderr
) {
2868 e
.printStackTrace();
2871 if (widthPixels
<= 0) {
2874 if (heightPixels
<= 0) {
2878 if ((params
.size() > 2) && (params
.get(0).equals("6"))) {
2879 if (debugToStderr
) {
2880 System
.err
.printf("windowOp text cell pixels: " +
2881 "height %s width %s\n",
2882 params
.get(1), params
.get(2));
2885 widthPixels
= width
* Integer
.parseInt(params
.get(2));
2886 heightPixels
= height
* Integer
.parseInt(params
.get(1));
2887 } catch (NumberFormatException e
) {
2888 if (debugToStderr
) {
2889 e
.printStackTrace();
2892 if (widthPixels
<= 0) {
2895 if (heightPixels
<= 0) {
2906 // Unknown keystroke, ignore
2911 params
.set(0, params
.get(params
.size() - 1) + ch
);
2912 if (params
.get(0).length() == 3) {
2913 // We have enough to generate a mouse event
2914 events
.add(parseMouse());
2923 // This "should" be impossible to reach
2928 * Request (u)xterm to use the sixel settings we need:
2930 * - enable sixel scrolling
2932 * - disable private color registers (so that we can use one common
2935 * @return the string to emit to xterm
2937 private String
xtermSetSixelSettings() {
2938 return "\033[?80h\033[?1070l";
2942 * Restore (u)xterm its default sixel settings:
2944 * - enable sixel scrolling
2946 * - enable private color registers
2948 * @return the string to emit to xterm
2950 private String
xtermResetSixelSettings() {
2951 return "\033[?80h\033[?1070h";
2955 * Request (u)xterm to report the current window and cell size dimensions
2958 * @return the string to emit to xterm
2960 private String
xtermReportPixelDimensions() {
2961 // We will ask for both window and text cell dimensions, and
2962 // hopefully one of them will work.
2963 return "\033[14t\033[16t";
2967 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2968 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2971 * @param on if true, enable metaSendsEscape
2972 * @return the string to emit to xterm
2974 private String
xtermMetaSendsEscape(final boolean on
) {
2976 return "\033[?1036h\033[?1034l";
2978 return "\033[?1036l";
2982 * Create an xterm OSC sequence to change the window title.
2984 * @param title the new title
2985 * @return the string to emit to xterm
2987 private String
getSetTitleString(final String title
) {
2988 return "\033]2;" + title
+ "\007";
2991 // ------------------------------------------------------------------------
2992 // Sixel output support ---------------------------------------------------
2993 // ------------------------------------------------------------------------
2996 * Get the number of colors in the sixel palette.
2998 * @return the palette size
3000 public int getSixelPaletteSize() {
3001 return sixelPaletteSize
;
3005 * Set the number of colors in the sixel palette.
3007 * @param paletteSize the new palette size
3009 public void setSixelPaletteSize(final int paletteSize
) {
3010 if (paletteSize
== sixelPaletteSize
) {
3014 switch (paletteSize
) {
3022 throw new IllegalArgumentException("Unsupported sixel palette " +
3023 " size: " + paletteSize
);
3026 // Don't step on the screen refresh thread.
3027 synchronized (this) {
3028 sixelPaletteSize
= paletteSize
;
3036 * Start a sixel string for display one row's worth of bitmap data.
3038 * @param x column coordinate. 0 is the left-most column.
3039 * @param y row coordinate. 0 is the top-most row.
3040 * @return the string to emit to an ANSI / ECMA-style terminal
3042 private String
startSixel(final int x
, final int y
) {
3043 StringBuilder sb
= new StringBuilder();
3045 assert (sixel
== true);
3048 sb
.append(gotoXY(x
, y
));
3051 sb
.append("\033Pq");
3053 if (palette
== null) {
3054 palette
= new SixelPalette();
3055 // TODO: make this an option (shared palette or not)
3056 palette
.emitPalette(sb
, null);
3059 return sb
.toString();
3063 * End a sixel string for display one row's worth of bitmap data.
3065 * @return the string to emit to an ANSI / ECMA-style terminal
3067 private String
endSixel() {
3068 assert (sixel
== true);
3075 * Create a sixel string representing a row of several cells containing
3078 * @param x column coordinate. 0 is the left-most column.
3079 * @param y row coordinate. 0 is the top-most row.
3080 * @param cells the cells containing the bitmap data
3081 * @return the string to emit to an ANSI / ECMA-style terminal
3083 private String
toSixel(final int x
, final int y
,
3084 final ArrayList
<Cell
> cells
) {
3086 StringBuilder sb
= new StringBuilder();
3088 assert (cells
!= null);
3089 assert (cells
.size() > 0);
3090 assert (cells
.get(0).getImage() != null);
3092 if (sixel
== false) {
3093 sb
.append(normal());
3094 sb
.append(gotoXY(x
, y
));
3095 for (int i
= 0; i
< cells
.size(); i
++) {
3098 return sb
.toString();
3101 if (y
== height
- 1) {
3102 // We are on the bottom row. If scrolling mode is enabled
3103 // (default), then VT320/xterm will scroll the entire screen if
3104 // we draw any pixels here.
3106 // TODO: support sixel scrolling mode disabled as an option.
3107 sb
.append(normal());
3108 sb
.append(gotoXY(x
, y
));
3109 for (int j
= 0; j
< cells
.size(); j
++) {
3112 return sb
.toString();
3115 if (sixelCache
== null) {
3116 sixelCache
= new ImageCache(height
* 10);
3119 // Save and get rows to/from the cache that do NOT have inverted
3121 boolean saveInCache
= true;
3122 for (Cell cell
: cells
) {
3123 if (cell
.isInvertedImage()) {
3124 saveInCache
= false;
3128 String cachedResult
= sixelCache
.get(cells
);
3129 if (cachedResult
!= null) {
3130 // System.err.println("CACHE HIT");
3131 sb
.append(startSixel(x
, y
));
3132 sb
.append(cachedResult
);
3133 sb
.append(endSixel());
3134 return sb
.toString();
3136 // System.err.println("CACHE MISS");
3139 int imageWidth
= cells
.get(0).getImage().getWidth();
3140 int imageHeight
= cells
.get(0).getImage().getHeight();
3142 // cells.get(x).getImage() has a dithered bitmap containing indexes
3143 // into the color palette. Piece these together into one larger
3144 // image for final rendering.
3146 int fullWidth
= cells
.size() * getTextWidth();
3147 int fullHeight
= getTextHeight();
3148 for (int i
= 0; i
< cells
.size(); i
++) {
3149 totalWidth
+= cells
.get(i
).getImage().getWidth();
3152 BufferedImage image
= new BufferedImage(fullWidth
,
3153 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3156 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3157 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
3159 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
3162 if (false && cells
.get(i
).isInvertedImage()) {
3163 // I used to put an all-white cell over the cursor, don't do
3165 rgbArray
= new int[imageWidth
* imageHeight
];
3166 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3167 rgbArray
[j
] = 0xFFFFFF;
3171 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3172 tileWidth
, tileHeight
, null, 0, tileWidth
);
3173 } catch (Exception e
) {
3174 throw new RuntimeException("image " + imageWidth
+ "x" +
3176 "tile " + tileWidth
+ "x" +
3178 " cells.get(i).getImage() " +
3179 cells
.get(i
).getImage() +
3181 " fullWidth " + fullWidth
+
3182 " fullHeight " + fullHeight
, e
);
3187 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3188 i * imageWidth, 0, imageWidth, imageHeight,
3190 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3191 fullWidth, fullHeight, cells.size(), getTextWidth());
3194 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3195 rgbArray
, 0, tileWidth
);
3196 if (tileHeight
< fullHeight
) {
3197 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3198 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3199 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3202 image
.setRGB(imageX
, imageY
, backgroundColor
);
3207 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3208 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3209 // I used to put an all-white cell over the cursor, don't do that
3211 rgbArray
= new int[totalWidth
* imageHeight
];
3212 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3213 rgbArray
[j
] = 0xFFFFFF;
3217 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3218 totalWidth
, imageHeight
, null, 0, totalWidth
);
3219 } catch (Exception e
) {
3220 throw new RuntimeException("image " + imageWidth
+ "x" +
3221 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3222 cells
.get(cells
.size() - 1).getImage(), e
);
3225 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3226 imageHeight
, rgbArray
, 0, totalWidth
);
3228 if (totalWidth
< getTextWidth()) {
3229 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3231 for (int imageX
= image
.getWidth() - totalWidth
;
3232 imageX
< image
.getWidth(); imageX
++) {
3234 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3235 image
.setRGB(imageX
, imageY
, backgroundColor
);
3240 // Dither the image. It is ok to lose the original here.
3241 if (palette
== null) {
3242 palette
= new SixelPalette();
3243 // TODO: make this an option (shared palette or not)
3244 palette
.emitPalette(sb
, null);
3246 image
= palette
.ditherImage(image
);
3248 // Collect the raster information
3249 int rasterHeight
= 0;
3250 int rasterWidth
= image
.getWidth();
3254 // TODO: make this an option (shared palette or not)
3256 // Emit the palette, but only for the colors actually used by these
3258 boolean [] usedColors = new boolean[sixelPaletteSize];
3259 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3260 for (int imageY = 0; imageY < image.getHeight(); imageY++) {
3261 usedColors[image.getRGB(imageX, imageY)] = true;
3264 palette.emitPalette(sb, usedColors);
3267 // Render the entire row of cells.
3268 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
3269 int [][] sixels
= new int[image
.getWidth()][6];
3271 // See which colors are actually used in this band of sixels.
3272 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3273 for (int imageY
= 0;
3274 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
3277 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
3278 assert (colorIdx
>= 0);
3279 assert (colorIdx
< sixelPaletteSize
);
3281 sixels
[imageX
][imageY
] = colorIdx
;
3285 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
3286 boolean isUsed
= false;
3287 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3288 for (int j
= 0; j
< 6; j
++) {
3289 if (sixels
[imageX
][j
] == i
) {
3294 if (isUsed
== false) {
3298 // Set to the beginning of scan line for the next set of
3299 // colored pixels, and select the color.
3300 sb
.append(String
.format("$#%d", i
));
3303 int oldDataCount
= 0;
3304 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3306 // Add up all the pixels that match this color.
3309 (j
< 6) && (currentRow
+ j
< fullHeight
);
3312 if (sixels
[imageX
][j
] == i
) {
3333 if ((currentRow
+ j
+ 1) > rasterHeight
) {
3334 rasterHeight
= currentRow
+ j
+ 1;
3342 if (data
== oldData
) {
3345 if (oldDataCount
== 1) {
3346 sb
.append((char) oldData
);
3347 } else if (oldDataCount
> 1) {
3348 sb
.append(String
.format("!%d", oldDataCount
));
3349 sb
.append((char) oldData
);
3355 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
3357 // Emit the last sequence.
3358 if (oldDataCount
== 1) {
3359 sb
.append((char) oldData
);
3360 } else if (oldDataCount
> 1) {
3361 sb
.append(String
.format("!%d", oldDataCount
));
3362 sb
.append((char) oldData
);
3365 } // for (int i = 0; i < sixelPaletteSize; i++)
3367 // Advance to the next scan line.
3370 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3372 // Kill the very last "-", because it is unnecessary.
3373 sb
.deleteCharAt(sb
.length() - 1);
3375 // Add the raster information
3376 sb
.insert(0, String
.format("\"1;1;%d;%d", rasterWidth
, rasterHeight
));
3379 // This row is OK to save into the cache.
3380 sixelCache
.put(cells
, sb
.toString());
3383 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3387 * Get the sixel support flag.
3389 * @return true if this terminal is emitting sixel
3391 public boolean hasSixel() {
3395 // ------------------------------------------------------------------------
3396 // End sixel output support -----------------------------------------------
3397 // ------------------------------------------------------------------------
3399 // ------------------------------------------------------------------------
3400 // iTerm2 image output support --------------------------------------------
3401 // ------------------------------------------------------------------------
3404 * Create an iTerm2 images string representing a row of several cells
3405 * containing bitmap data.
3407 * @param x column coordinate. 0 is the left-most column.
3408 * @param y row coordinate. 0 is the top-most row.
3409 * @param cells the cells containing the bitmap data
3410 * @return the string to emit to an ANSI / ECMA-style terminal
3412 private String
toIterm2Image(final int x
, final int y
,
3413 final ArrayList
<Cell
> cells
) {
3415 StringBuilder sb
= new StringBuilder();
3417 assert (cells
!= null);
3418 assert (cells
.size() > 0);
3419 assert (cells
.get(0).getImage() != null);
3421 if (iterm2Images
== false) {
3422 sb
.append(normal());
3423 sb
.append(gotoXY(x
, y
));
3424 for (int i
= 0; i
< cells
.size(); i
++) {
3427 return sb
.toString();
3430 if (iterm2Cache
== null) {
3431 iterm2Cache
= new ImageCache(height
* 10);
3432 base64
= java
.util
.Base64
.getEncoder();
3435 // Save and get rows to/from the cache that do NOT have inverted
3437 boolean saveInCache
= true;
3438 for (Cell cell
: cells
) {
3439 if (cell
.isInvertedImage()) {
3440 saveInCache
= false;
3444 String cachedResult
= iterm2Cache
.get(cells
);
3445 if (cachedResult
!= null) {
3446 // System.err.println("CACHE HIT");
3447 sb
.append(gotoXY(x
, y
));
3448 sb
.append(cachedResult
);
3449 return sb
.toString();
3451 // System.err.println("CACHE MISS");
3454 int imageWidth
= cells
.get(0).getImage().getWidth();
3455 int imageHeight
= cells
.get(0).getImage().getHeight();
3457 // Piece cells.get(x).getImage() pieces together into one larger
3458 // image for final rendering.
3460 int fullWidth
= cells
.size() * getTextWidth();
3461 int fullHeight
= getTextHeight();
3462 for (int i
= 0; i
< cells
.size(); i
++) {
3463 totalWidth
+= cells
.get(i
).getImage().getWidth();
3466 BufferedImage image
= new BufferedImage(fullWidth
,
3467 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3470 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3471 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
3473 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
3475 if (false && cells
.get(i
).isInvertedImage()) {
3476 // I used to put an all-white cell over the cursor, don't do
3478 rgbArray
= new int[imageWidth
* imageHeight
];
3479 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3480 rgbArray
[j
] = 0xFFFFFF;
3484 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3485 tileWidth
, tileHeight
, null, 0, tileWidth
);
3486 } catch (Exception e
) {
3487 throw new RuntimeException("image " + imageWidth
+ "x" +
3489 "tile " + tileWidth
+ "x" +
3491 " cells.get(i).getImage() " +
3492 cells
.get(i
).getImage() +
3494 " fullWidth " + fullWidth
+
3495 " fullHeight " + fullHeight
, e
);
3500 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3501 i * imageWidth, 0, imageWidth, imageHeight,
3503 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3504 fullWidth, fullHeight, cells.size(), getTextWidth());
3507 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3508 rgbArray
, 0, tileWidth
);
3509 if (tileHeight
< fullHeight
) {
3510 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3511 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3512 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3515 image
.setRGB(imageX
, imageY
, backgroundColor
);
3520 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3521 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3522 // I used to put an all-white cell over the cursor, don't do that
3524 rgbArray
= new int[totalWidth
* imageHeight
];
3525 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3526 rgbArray
[j
] = 0xFFFFFF;
3530 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3531 totalWidth
, imageHeight
, null, 0, totalWidth
);
3532 } catch (Exception e
) {
3533 throw new RuntimeException("image " + imageWidth
+ "x" +
3534 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3535 cells
.get(cells
.size() - 1).getImage(), e
);
3538 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3539 imageHeight
, rgbArray
, 0, totalWidth
);
3541 if (totalWidth
< getTextWidth()) {
3542 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3544 for (int imageX
= image
.getWidth() - totalWidth
;
3545 imageX
< image
.getWidth(); imageX
++) {
3547 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3548 image
.setRGB(imageX
, imageY
, backgroundColor
);
3554 * From https://iterm2.com/documentation-images.html:
3558 * iTerm2 extends the xterm protocol with a set of proprietary escape
3559 * sequences. In general, the pattern is:
3561 * ESC ] 1337 ; key = value ^G
3563 * Whitespace is shown here for ease of reading: in practice, no
3564 * spaces should be used.
3566 * For file transfer and inline images, the code is:
3568 * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
3570 * The optional arguments are formatted as key=value with a semicolon
3571 * between each key-value pair. They are described below:
3573 * Key Description of value
3574 * name base-64 encoded filename. Defaults to "Unnamed file".
3575 * size File size in bytes. Optional; this is only used by the
3576 * progress indicator.
3577 * width Width to render. See notes below.
3578 * height Height to render. See notes below.
3579 * preserveAspectRatio If set to 0, then the image's inherent aspect
3580 * ratio will not be respected; otherwise, it
3581 * will fill the specified width and height as
3582 * much as possible without stretching. Defaults
3584 * inline If set to 1, the file will be displayed inline. Otherwise,
3585 * it will be downloaded with no visual representation in the
3586 * terminal session. Defaults to 0.
3588 * The width and height are given as a number followed by a unit, or
3591 * N: N character cells.
3593 * N%: N percent of the session's width or height.
3594 * auto: The image's inherent size will be used to determine an
3595 * appropriate dimension.
3599 // File contents can be several image formats. We will use PNG.
3600 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3602 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3603 Math
.min(image
.getHeight(), fullHeight
)),
3604 "PNG", pngOutputStream
)
3606 // We failed to render image, bail out.
3609 } catch (IOException e
) {
3610 // We failed to render image, bail out.
3614 // iTerm2 does not advance the cursor automatically, so place it
3616 sb
.append("\033]1337;File=");
3618 sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
3622 sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
3623 image.getWidth(), Math.min(image.getHeight(),
3626 sb
.append("inline=1:");
3627 sb
.append(base64
.encodeToString(pngOutputStream
.toByteArray()));
3631 // This row is OK to save into the cache.
3632 iterm2Cache
.put(cells
, sb
.toString());
3635 return (gotoXY(x
, y
) + sb
.toString());
3639 * Get the iTerm2 images support flag.
3641 * @return true if this terminal is emitting iTerm2 images
3643 public boolean hasIterm2Images() {
3644 return iterm2Images
;
3647 // ------------------------------------------------------------------------
3648 // End iTerm2 image output support ----------------------------------------
3649 // ------------------------------------------------------------------------
3651 // ------------------------------------------------------------------------
3652 // Jexer image output support ---------------------------------------------
3653 // ------------------------------------------------------------------------
3656 * Create a Jexer images string representing a row of several cells
3657 * containing bitmap data.
3659 * @param x column coordinate. 0 is the left-most column.
3660 * @param y row coordinate. 0 is the top-most row.
3661 * @param cells the cells containing the bitmap data
3662 * @return the string to emit to an ANSI / ECMA-style terminal
3664 private String
toJexerImage(final int x
, final int y
,
3665 final ArrayList
<Cell
> cells
) {
3667 StringBuilder sb
= new StringBuilder();
3669 assert (cells
!= null);
3670 assert (cells
.size() > 0);
3671 assert (cells
.get(0).getImage() != null);
3673 if (jexerImageOption
== JexerImageOption
.DISABLED
) {
3674 sb
.append(normal());
3675 sb
.append(gotoXY(x
, y
));
3676 for (int i
= 0; i
< cells
.size(); i
++) {
3679 return sb
.toString();
3682 if (jexerCache
== null) {
3683 jexerCache
= new ImageCache(height
* 10);
3684 base64
= java
.util
.Base64
.getEncoder();
3687 // Save and get rows to/from the cache that do NOT have inverted
3689 boolean saveInCache
= true;
3690 for (Cell cell
: cells
) {
3691 if (cell
.isInvertedImage()) {
3692 saveInCache
= false;
3696 String cachedResult
= jexerCache
.get(cells
);
3697 if (cachedResult
!= null) {
3698 // System.err.println("CACHE HIT");
3699 sb
.append(gotoXY(x
, y
));
3700 sb
.append(cachedResult
);
3701 return sb
.toString();
3703 // System.err.println("CACHE MISS");
3706 int imageWidth
= cells
.get(0).getImage().getWidth();
3707 int imageHeight
= cells
.get(0).getImage().getHeight();
3709 // Piece cells.get(x).getImage() pieces together into one larger
3710 // image for final rendering.
3712 int fullWidth
= cells
.size() * getTextWidth();
3713 int fullHeight
= getTextHeight();
3714 for (int i
= 0; i
< cells
.size(); i
++) {
3715 totalWidth
+= cells
.get(i
).getImage().getWidth();
3718 BufferedImage image
= new BufferedImage(fullWidth
,
3719 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3722 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3723 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
3725 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
3727 if (false && cells
.get(i
).isInvertedImage()) {
3728 // I used to put an all-white cell over the cursor, don't do
3730 rgbArray
= new int[imageWidth
* imageHeight
];
3731 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3732 rgbArray
[j
] = 0xFFFFFF;
3736 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3737 tileWidth
, tileHeight
, null, 0, tileWidth
);
3738 } catch (Exception e
) {
3739 throw new RuntimeException("image " + imageWidth
+ "x" +
3741 "tile " + tileWidth
+ "x" +
3743 " cells.get(i).getImage() " +
3744 cells
.get(i
).getImage() +
3746 " fullWidth " + fullWidth
+
3747 " fullHeight " + fullHeight
, e
);
3752 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3753 i * imageWidth, 0, imageWidth, imageHeight,
3755 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3756 fullWidth, fullHeight, cells.size(), getTextWidth());
3759 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3760 rgbArray
, 0, tileWidth
);
3761 if (tileHeight
< fullHeight
) {
3762 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3763 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3764 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3767 image
.setRGB(imageX
, imageY
, backgroundColor
);
3772 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3773 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3774 // I used to put an all-white cell over the cursor, don't do that
3776 rgbArray
= new int[totalWidth
* imageHeight
];
3777 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3778 rgbArray
[j
] = 0xFFFFFF;
3782 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3783 totalWidth
, imageHeight
, null, 0, totalWidth
);
3784 } catch (Exception e
) {
3785 throw new RuntimeException("image " + imageWidth
+ "x" +
3786 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3787 cells
.get(cells
.size() - 1).getImage(), e
);
3790 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3791 imageHeight
, rgbArray
, 0, totalWidth
);
3793 if (totalWidth
< getTextWidth()) {
3794 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3796 for (int imageX
= image
.getWidth() - totalWidth
;
3797 imageX
< image
.getWidth(); imageX
++) {
3799 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3800 image
.setRGB(imageX
, imageY
, backgroundColor
);
3805 if (jexerImageOption
== JexerImageOption
.PNG
) {
3807 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3809 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3810 Math
.min(image
.getHeight(), fullHeight
)),
3811 "PNG", pngOutputStream
)
3813 // We failed to render image, bail out.
3816 } catch (IOException e
) {
3817 // We failed to render image, bail out.
3821 sb
.append("\033]444;1;0;");
3822 sb
.append(base64
.encodeToString(pngOutputStream
.toByteArray()));
3825 } else if (jexerImageOption
== JexerImageOption
.JPG
) {
3828 ByteArrayOutputStream jpgOutputStream
= new ByteArrayOutputStream(1024);
3830 // Convert from ARGB to RGB, otherwise the JPG encode will fail.
3831 BufferedImage jpgImage
= new BufferedImage(image
.getWidth(),
3832 image
.getHeight(), BufferedImage
.TYPE_INT_RGB
);
3833 int [] pixels
= new int[image
.getWidth() * image
.getHeight()];
3834 image
.getRGB(0, 0, image
.getWidth(), image
.getHeight(), pixels
,
3835 0, image
.getWidth());
3836 jpgImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(), pixels
,
3837 0, image
.getWidth());
3840 if (!ImageIO
.write(jpgImage
.getSubimage(0, 0,
3841 jpgImage
.getWidth(),
3842 Math
.min(jpgImage
.getHeight(), fullHeight
)),
3843 "JPG", jpgOutputStream
)
3845 // We failed to render image, bail out.
3848 } catch (IOException e
) {
3849 // We failed to render image, bail out.
3853 sb
.append("\033]444;2;0;");
3854 sb
.append(base64
.encodeToString(jpgOutputStream
.toByteArray()));
3857 } else if (jexerImageOption
== JexerImageOption
.RGB
) {
3860 sb
.append(String
.format("\033]444;0;%d;%d;0;", image
.getWidth(),
3861 Math
.min(image
.getHeight(), fullHeight
)));
3863 byte [] bytes
= new byte[image
.getWidth() * image
.getHeight() * 3];
3864 int stride
= image
.getWidth();
3865 for (int px
= 0; px
< stride
; px
++) {
3866 for (int py
= 0; py
< image
.getHeight(); py
++) {
3867 int rgb
= image
.getRGB(px
, py
);
3868 bytes
[(py
* stride
* 3) + (px
* 3)] = (byte) ((rgb
>>> 16) & 0xFF);
3869 bytes
[(py
* stride
* 3) + (px
* 3) + 1] = (byte) ((rgb
>>> 8) & 0xFF);
3870 bytes
[(py
* stride
* 3) + (px
* 3) + 2] = (byte) ( rgb
& 0xFF);
3873 sb
.append(base64
.encodeToString(bytes
));
3878 // This row is OK to save into the cache.
3879 jexerCache
.put(cells
, sb
.toString());
3882 return (gotoXY(x
, y
) + sb
.toString());
3886 * Get the Jexer images support flag.
3888 * @return true if this terminal is emitting Jexer images
3890 public boolean hasJexerImages() {
3891 return (jexerImageOption
!= JexerImageOption
.DISABLED
);
3894 // ------------------------------------------------------------------------
3895 // End Jexer image output support -----------------------------------------
3896 // ------------------------------------------------------------------------
3899 * Setup system colors to match DOS color palette.
3901 private void setDOSColors() {
3902 MYBLACK
= new java
.awt
.Color(0x00, 0x00, 0x00);
3903 MYRED
= new java
.awt
.Color(0xa8, 0x00, 0x00);
3904 MYGREEN
= new java
.awt
.Color(0x00, 0xa8, 0x00);
3905 MYYELLOW
= new java
.awt
.Color(0xa8, 0x54, 0x00);
3906 MYBLUE
= new java
.awt
.Color(0x00, 0x00, 0xa8);
3907 MYMAGENTA
= new java
.awt
.Color(0xa8, 0x00, 0xa8);
3908 MYCYAN
= new java
.awt
.Color(0x00, 0xa8, 0xa8);
3909 MYWHITE
= new java
.awt
.Color(0xa8, 0xa8, 0xa8);
3910 MYBOLD_BLACK
= new java
.awt
.Color(0x54, 0x54, 0x54);
3911 MYBOLD_RED
= new java
.awt
.Color(0xfc, 0x54, 0x54);
3912 MYBOLD_GREEN
= new java
.awt
.Color(0x54, 0xfc, 0x54);
3913 MYBOLD_YELLOW
= new java
.awt
.Color(0xfc, 0xfc, 0x54);
3914 MYBOLD_BLUE
= new java
.awt
.Color(0x54, 0x54, 0xfc);
3915 MYBOLD_MAGENTA
= new java
.awt
.Color(0xfc, 0x54, 0xfc);
3916 MYBOLD_CYAN
= new java
.awt
.Color(0x54, 0xfc, 0xfc);
3917 MYBOLD_WHITE
= new java
.awt
.Color(0xfc, 0xfc, 0xfc);
3921 * Setup ECMA48 colors to match those provided in system properties.
3923 private void setCustomSystemColors() {
3926 MYBLACK
= getCustomColor("jexer.ECMA48.color0", MYBLACK
);
3927 MYRED
= getCustomColor("jexer.ECMA48.color1", MYRED
);
3928 MYGREEN
= getCustomColor("jexer.ECMA48.color2", MYGREEN
);
3929 MYYELLOW
= getCustomColor("jexer.ECMA48.color3", MYYELLOW
);
3930 MYBLUE
= getCustomColor("jexer.ECMA48.color4", MYBLUE
);
3931 MYMAGENTA
= getCustomColor("jexer.ECMA48.color5", MYMAGENTA
);
3932 MYCYAN
= getCustomColor("jexer.ECMA48.color6", MYCYAN
);
3933 MYWHITE
= getCustomColor("jexer.ECMA48.color7", MYWHITE
);
3934 MYBOLD_BLACK
= getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK
);
3935 MYBOLD_RED
= getCustomColor("jexer.ECMA48.color9", MYBOLD_RED
);
3936 MYBOLD_GREEN
= getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN
);
3937 MYBOLD_YELLOW
= getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW
);
3938 MYBOLD_BLUE
= getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE
);
3939 MYBOLD_MAGENTA
= getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA
);
3940 MYBOLD_CYAN
= getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN
);
3941 MYBOLD_WHITE
= getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE
);
3945 * Setup one system color to match the RGB value provided in system
3948 * @param key the system property key
3949 * @param defaultColor the default color to return if key is not set, or
3951 * @return a color from the RGB string, or defaultColor
3953 private java
.awt
.Color
getCustomColor(final String key
,
3954 final java
.awt
.Color defaultColor
) {
3956 String rgb
= System
.getProperty(key
);
3958 return defaultColor
;
3960 if (rgb
.startsWith("#")) {
3961 rgb
= rgb
.substring(1);
3965 rgbInt
= Integer
.parseInt(rgb
, 16);
3966 } catch (NumberFormatException e
) {
3967 return defaultColor
;
3969 java
.awt
.Color color
= new java
.awt
.Color((rgbInt
& 0xFF0000) >>> 16,
3970 (rgbInt
& 0x00FF00) >>> 8,
3971 (rgbInt
& 0x0000FF));
3977 * Create a T.416 RGB parameter sequence for a custom system color.
3979 * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
3980 * @return the color portion of the string to emit to an ANSI /
3981 * ECMA-style terminal
3983 private String
systemColorRGB(final java
.awt
.Color color
) {
3984 return String
.format("%d;%d;%d", color
.getRed(), color
.getGreen(),
3989 * Create a SGR parameter sequence for a single color change.
3991 * @param bold if true, set bold
3992 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3993 * @param foreground if true, this is a foreground color
3994 * @return the string to emit to an ANSI / ECMA-style terminal,
3997 private String
color(final boolean bold
, final Color color
,
3998 final boolean foreground
) {
3999 return color(color
, foreground
, true) +
4000 rgbColor(bold
, color
, foreground
);
4004 * Create a T.416 RGB parameter sequence for a single color change.
4006 * @param colorRGB a 24-bit RGB value for foreground color
4007 * @param foreground if true, this is a foreground color
4008 * @return the string to emit to an ANSI / ECMA-style terminal,
4011 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
4013 int colorRed
= (colorRGB
>>> 16) & 0xFF;
4014 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
4015 int colorBlue
= colorRGB
& 0xFF;
4017 StringBuilder sb
= new StringBuilder();
4019 sb
.append("\033[38;2;");
4021 sb
.append("\033[48;2;");
4023 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
4024 return sb
.toString();
4028 * Create a T.416 RGB parameter sequence for both foreground and
4029 * background color change.
4031 * @param foreColorRGB a 24-bit RGB value for foreground color
4032 * @param backColorRGB a 24-bit RGB value for foreground color
4033 * @return the string to emit to an ANSI / ECMA-style terminal,
4036 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
4037 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
4038 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
4039 int foreColorBlue
= foreColorRGB
& 0xFF;
4040 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
4041 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
4042 int backColorBlue
= backColorRGB
& 0xFF;
4044 StringBuilder sb
= new StringBuilder();
4045 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
4046 foreColorRed
, foreColorGreen
, foreColorBlue
));
4047 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
4048 backColorRed
, backColorGreen
, backColorBlue
));
4049 return sb
.toString();
4053 * Create a T.416 RGB parameter sequence for a single color change.
4055 * @param bold if true, set bold
4056 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4057 * @param foreground if true, this is a foreground color
4058 * @return the string to emit to an xterm terminal with RGB support,
4059 * e.g. "\033[38;2;RR;GG;BBm"
4061 private String
rgbColor(final boolean bold
, final Color color
,
4062 final boolean foreground
) {
4063 if (doRgbColor
== false) {
4066 StringBuilder sb
= new StringBuilder("\033[");
4068 // Bold implies foreground only
4070 if (color
.equals(Color
.BLACK
)) {
4071 sb
.append(systemColorRGB(MYBOLD_BLACK
));
4072 } else if (color
.equals(Color
.RED
)) {
4073 sb
.append(systemColorRGB(MYBOLD_RED
));
4074 } else if (color
.equals(Color
.GREEN
)) {
4075 sb
.append(systemColorRGB(MYBOLD_GREEN
));
4076 } else if (color
.equals(Color
.YELLOW
)) {
4077 sb
.append(systemColorRGB(MYBOLD_YELLOW
));
4078 } else if (color
.equals(Color
.BLUE
)) {
4079 sb
.append(systemColorRGB(MYBOLD_BLUE
));
4080 } else if (color
.equals(Color
.MAGENTA
)) {
4081 sb
.append(systemColorRGB(MYBOLD_MAGENTA
));
4082 } else if (color
.equals(Color
.CYAN
)) {
4083 sb
.append(systemColorRGB(MYBOLD_CYAN
));
4084 } else if (color
.equals(Color
.WHITE
)) {
4085 sb
.append(systemColorRGB(MYBOLD_WHITE
));
4093 if (color
.equals(Color
.BLACK
)) {
4094 sb
.append(systemColorRGB(MYBLACK
));
4095 } else if (color
.equals(Color
.RED
)) {
4096 sb
.append(systemColorRGB(MYRED
));
4097 } else if (color
.equals(Color
.GREEN
)) {
4098 sb
.append(systemColorRGB(MYGREEN
));
4099 } else if (color
.equals(Color
.YELLOW
)) {
4100 sb
.append(systemColorRGB(MYYELLOW
));
4101 } else if (color
.equals(Color
.BLUE
)) {
4102 sb
.append(systemColorRGB(MYBLUE
));
4103 } else if (color
.equals(Color
.MAGENTA
)) {
4104 sb
.append(systemColorRGB(MYMAGENTA
));
4105 } else if (color
.equals(Color
.CYAN
)) {
4106 sb
.append(systemColorRGB(MYCYAN
));
4107 } else if (color
.equals(Color
.WHITE
)) {
4108 sb
.append(systemColorRGB(MYWHITE
));
4112 return sb
.toString();
4116 * Create a T.416 RGB parameter sequence for both foreground and
4117 * background color change.
4119 * @param bold if true, set bold
4120 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4121 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4122 * @return the string to emit to an xterm terminal with RGB support,
4123 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
4125 private String
rgbColor(final boolean bold
, final Color foreColor
,
4126 final Color backColor
) {
4127 if (doRgbColor
== false) {
4131 return rgbColor(bold
, foreColor
, true) +
4132 rgbColor(false, backColor
, false);
4136 * Create a SGR parameter sequence for a single color change.
4138 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4139 * @param foreground if true, this is a foreground color
4140 * @param header if true, make the full header, otherwise just emit the
4141 * color parameter e.g. "42;"
4142 * @return the string to emit to an ANSI / ECMA-style terminal,
4145 private String
color(final Color color
, final boolean foreground
,
4146 final boolean header
) {
4148 int ecmaColor
= color
.getValue();
4150 // Convert Color.* values to SGR numerics
4158 return String
.format("\033[%dm", ecmaColor
);
4160 return String
.format("%d;", ecmaColor
);
4165 * Create a SGR parameter sequence for both foreground and background
4168 * @param bold if true, set bold
4169 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4170 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4171 * @return the string to emit to an ANSI / ECMA-style terminal,
4172 * e.g. "\033[31;42m"
4174 private String
color(final boolean bold
, final Color foreColor
,
4175 final Color backColor
) {
4176 return color(foreColor
, backColor
, true) +
4177 rgbColor(bold
, foreColor
, backColor
);
4181 * Create a SGR parameter sequence for both foreground and
4182 * background color change.
4184 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4185 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4186 * @param header if true, make the full header, otherwise just emit the
4187 * color parameter e.g. "31;42;"
4188 * @return the string to emit to an ANSI / ECMA-style terminal,
4189 * e.g. "\033[31;42m"
4191 private String
color(final Color foreColor
, final Color backColor
,
4192 final boolean header
) {
4194 int ecmaForeColor
= foreColor
.getValue();
4195 int ecmaBackColor
= backColor
.getValue();
4197 // Convert Color.* values to SGR numerics
4198 ecmaBackColor
+= 40;
4199 ecmaForeColor
+= 30;
4202 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
4204 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
4209 * Create a SGR parameter sequence for foreground, background, and
4210 * several attributes. This sequence first resets all attributes to
4211 * default, then sets attributes as per the parameters.
4213 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4214 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4215 * @param bold if true, set bold
4216 * @param reverse if true, set reverse
4217 * @param blink if true, set blink
4218 * @param underline if true, set underline
4219 * @return the string to emit to an ANSI / ECMA-style terminal,
4220 * e.g. "\033[0;1;31;42m"
4222 private String
color(final Color foreColor
, final Color backColor
,
4223 final boolean bold
, final boolean reverse
, final boolean blink
,
4224 final boolean underline
) {
4226 int ecmaForeColor
= foreColor
.getValue();
4227 int ecmaBackColor
= backColor
.getValue();
4229 // Convert Color.* values to SGR numerics
4230 ecmaBackColor
+= 40;
4231 ecmaForeColor
+= 30;
4233 StringBuilder sb
= new StringBuilder();
4234 if ( bold
&& reverse
&& blink
&& !underline
) {
4235 sb
.append("\033[0;1;7;5;");
4236 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4237 sb
.append("\033[0;1;7;");
4238 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4239 sb
.append("\033[0;7;5;");
4240 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4241 sb
.append("\033[0;1;5;");
4242 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4243 sb
.append("\033[0;1;");
4244 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4245 sb
.append("\033[0;7;");
4246 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4247 sb
.append("\033[0;5;");
4248 } else if ( bold
&& reverse
&& blink
&& underline
) {
4249 sb
.append("\033[0;1;7;5;4;");
4250 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4251 sb
.append("\033[0;1;7;4;");
4252 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4253 sb
.append("\033[0;7;5;4;");
4254 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4255 sb
.append("\033[0;1;5;4;");
4256 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4257 sb
.append("\033[0;1;4;");
4258 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4259 sb
.append("\033[0;7;4;");
4260 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4261 sb
.append("\033[0;5;4;");
4262 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4263 sb
.append("\033[0;4;");
4265 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4266 sb
.append("\033[0;");
4268 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
4269 sb
.append(rgbColor(bold
, foreColor
, backColor
));
4270 return sb
.toString();
4274 * Create a SGR parameter sequence for foreground, background, and
4275 * several attributes. This sequence first resets all attributes to
4276 * default, then sets attributes as per the parameters.
4278 * @param foreColorRGB a 24-bit RGB value for foreground color
4279 * @param backColorRGB a 24-bit RGB value for foreground color
4280 * @param bold if true, set bold
4281 * @param reverse if true, set reverse
4282 * @param blink if true, set blink
4283 * @param underline if true, set underline
4284 * @return the string to emit to an ANSI / ECMA-style terminal,
4285 * e.g. "\033[0;1;31;42m"
4287 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
4288 final boolean bold
, final boolean reverse
, final boolean blink
,
4289 final boolean underline
) {
4291 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
4292 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
4293 int foreColorBlue
= foreColorRGB
& 0xFF;
4294 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
4295 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
4296 int backColorBlue
= backColorRGB
& 0xFF;
4298 StringBuilder sb
= new StringBuilder();
4299 if ( bold
&& reverse
&& blink
&& !underline
) {
4300 sb
.append("\033[0;1;7;5;");
4301 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4302 sb
.append("\033[0;1;7;");
4303 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4304 sb
.append("\033[0;7;5;");
4305 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4306 sb
.append("\033[0;1;5;");
4307 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4308 sb
.append("\033[0;1;");
4309 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4310 sb
.append("\033[0;7;");
4311 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4312 sb
.append("\033[0;5;");
4313 } else if ( bold
&& reverse
&& blink
&& underline
) {
4314 sb
.append("\033[0;1;7;5;4;");
4315 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4316 sb
.append("\033[0;1;7;4;");
4317 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4318 sb
.append("\033[0;7;5;4;");
4319 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4320 sb
.append("\033[0;1;5;4;");
4321 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4322 sb
.append("\033[0;1;4;");
4323 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4324 sb
.append("\033[0;7;4;");
4325 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4326 sb
.append("\033[0;5;4;");
4327 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4328 sb
.append("\033[0;4;");
4330 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4331 sb
.append("\033[0;");
4334 sb
.append("m\033[38;2;");
4335 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
4337 sb
.append("m\033[48;2;");
4338 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
4341 return sb
.toString();
4345 * Create a SGR parameter sequence to reset to VT100 defaults.
4347 * @return the string to emit to an ANSI / ECMA-style terminal,
4350 private String
normal() {
4351 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
4355 * Create a SGR parameter sequence to reset to ECMA-48 default
4356 * foreground/background.
4358 * @return the string to emit to an ANSI / ECMA-style terminal,
4361 private String
defaultColor() {
4364 * Normal (neither bold nor faint).
4367 * Steady (not blinking).
4368 * Positive (not inverse).
4369 * Visible (not hidden).
4371 * Default foreground color.
4372 * Default background color.
4374 return "\033[0;22;23;24;25;27;28;29;39;49m";
4378 * Create a SGR parameter sequence to reset to defaults.
4380 * @param header if true, make the full header, otherwise just emit the
4381 * bare parameter e.g. "0;"
4382 * @return the string to emit to an ANSI / ECMA-style terminal,
4385 private String
normal(final boolean header
) {
4387 return "\033[0;37;40m";
4393 * Create a SGR parameter sequence for enabling the visible cursor.
4395 * @param on if true, turn on cursor
4396 * @return the string to emit to an ANSI / ECMA-style terminal
4398 private String
cursor(final boolean on
) {
4399 if (on
&& !cursorOn
) {
4403 if (!on
&& cursorOn
) {
4411 * Clear the entire screen. Because some terminals use back-color-erase,
4412 * set the color to white-on-black beforehand.
4414 * @return the string to emit to an ANSI / ECMA-style terminal
4416 private String
clearAll() {
4417 return "\033[0;37;40m\033[2J";
4421 * Clear the line from the cursor (inclusive) to the end of the screen.
4422 * Because some terminals use back-color-erase, set the color to
4423 * white-on-black beforehand.
4425 * @return the string to emit to an ANSI / ECMA-style terminal
4427 private String
clearRemainingLine() {
4428 return "\033[0;37;40m\033[K";
4432 * Move the cursor to (x, y).
4434 * @param x column coordinate. 0 is the left-most column.
4435 * @param y row coordinate. 0 is the top-most row.
4436 * @return the string to emit to an ANSI / ECMA-style terminal
4438 private String
gotoXY(final int x
, final int y
) {
4439 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
4443 * Tell (u)xterm that we want to receive mouse events based on "Any event
4444 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
4445 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
4447 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
4449 * Note that this also sets the alternate/primary screen buffer.
4451 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
4452 * mean "hide the mouse pointer." We have to use our own sequence to do
4453 * this because there is no standard in xterm for unilaterally hiding the
4454 * pointer all the time (regardless of typing).
4456 * @param on If true, enable mouse report and use the alternate screen
4457 * buffer. If false disable mouse reporting and use the primary screen
4459 * @return the string to emit to xterm
4461 private String
mouse(final boolean on
) {
4463 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
4465 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";