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
{
86 // ------------------------------------------------------------------------
87 // Variables --------------------------------------------------------------
88 // ------------------------------------------------------------------------
91 * Emit debugging to stderr.
93 private boolean debugToStderr
= false;
96 * If true, emit T.416-style RGB colors for normal system colors. This
97 * is a) expensive in bandwidth, and b) potentially terrible looking for
100 private static boolean doRgbColor
= false;
103 * The session information.
105 private SessionInfo sessionInfo
;
108 * The event queue, filled up by a thread reading on input.
110 private List
<TInputEvent
> eventQueue
;
113 * If true, we want the reader thread to exit gracefully.
115 private boolean stopReaderThread
;
120 private Thread readerThread
;
123 * Parameters being collected. E.g. if the string is \033[1;3m, then
124 * params[0] will be 1 and params[1] will be 3.
126 private List
<String
> params
;
129 * Current parsing state.
131 private ParseState state
;
134 * The time we entered ESCAPE. If we get a bare escape without a code
135 * following it, this is used to return that bare escape.
137 private long escapeTime
;
140 * The time we last checked the window size. We try not to spawn stty
141 * more than once per second.
143 private long windowSizeTime
;
146 * true if mouse1 was down. Used to report mouse1 on the release event.
148 private boolean mouse1
;
151 * true if mouse2 was down. Used to report mouse2 on the release event.
153 private boolean mouse2
;
156 * true if mouse3 was down. Used to report mouse3 on the release event.
158 private boolean mouse3
;
161 * Cache the cursor visibility value so we only emit the sequence when we
164 private boolean cursorOn
= true;
167 * Cache the last window size to figure out if a TResizeEvent needs to be
170 private TResizeEvent windowResize
= null;
173 * If true, emit wide-char (CJK/Emoji) characters as sixel images.
175 private boolean wideCharImages
= true;
178 * Window width in pixels. Used for sixel support.
180 private int widthPixels
= 640;
183 * Window height in pixels. Used for sixel support.
185 private int heightPixels
= 400;
188 * If true, emit image data via sixel.
190 private boolean sixel
= true;
193 * The sixel palette handler.
195 private SixelPalette palette
= null;
198 * The sixel post-rendered string cache.
200 private ImageCache sixelCache
= null;
203 * Number of colors in the sixel palette. Xterm 335 defines the max as
204 * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
207 private int sixelPaletteSize
= 1024;
210 * If true, emit image data via iTerm2 image protocol.
212 private boolean iterm2Images
= false;
215 * The iTerm2 post-rendered string cache.
217 private ImageCache iterm2Cache
= null;
220 * If true, emit image data via Jexer image protocol.
222 private boolean jexerImages
= false;
225 * The Jexer post-rendered string cache.
227 private ImageCache jexerCache
= null;
230 * Base64 encoder used by iTerm2 and Jexer images.
232 private java
.util
.Base64
.Encoder base64
= null;
235 * If true, then we changed System.in and need to change it back.
237 private boolean setRawMode
= false;
240 * If true, '?' was seen in terminal response.
242 private boolean decPrivateModeFlag
= false;
245 * The terminal's input. If an InputStream is not specified in the
246 * constructor, then this InputStreamReader will be bound to System.in
247 * with UTF-8 encoding.
249 private Reader input
;
252 * The terminal's raw InputStream. If an InputStream is not specified in
253 * the constructor, then this InputReader will be bound to System.in.
254 * This is used by run() to see if bytes are available() before calling
255 * (Reader)input.read().
257 private InputStream inputStream
;
260 * The terminal's output. If an OutputStream is not specified in the
261 * constructor, then this PrintWriter will be bound to System.out with
264 private PrintWriter output
;
267 * The listening object that run() wakes up on new input.
269 private Object listener
;
271 // Colors to map DOS colors to AWT colors.
272 private static java
.awt
.Color MYBLACK
;
273 private static java
.awt
.Color MYRED
;
274 private static java
.awt
.Color MYGREEN
;
275 private static java
.awt
.Color MYYELLOW
;
276 private static java
.awt
.Color MYBLUE
;
277 private static java
.awt
.Color MYMAGENTA
;
278 private static java
.awt
.Color MYCYAN
;
279 private static java
.awt
.Color MYWHITE
;
280 private static java
.awt
.Color MYBOLD_BLACK
;
281 private static java
.awt
.Color MYBOLD_RED
;
282 private static java
.awt
.Color MYBOLD_GREEN
;
283 private static java
.awt
.Color MYBOLD_YELLOW
;
284 private static java
.awt
.Color MYBOLD_BLUE
;
285 private static java
.awt
.Color MYBOLD_MAGENTA
;
286 private static java
.awt
.Color MYBOLD_CYAN
;
287 private static java
.awt
.Color MYBOLD_WHITE
;
290 * SixelPalette is used to manage the conversion of images between 24-bit
291 * RGB color and a palette of sixelPaletteSize colors.
293 private class SixelPalette
{
296 * Color palette for sixel output, sorted low to high.
298 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
301 * Map of color palette index for sixel output, from the order it was
302 * generated by makePalette() to rgbColors.
304 private int [] rgbSortedIndex
= new int[sixelPaletteSize
];
307 * The color palette, organized by hue, saturation, and luminance.
308 * This is used for a fast color match.
310 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
313 * Number of bits for hue.
315 private int hueBits
= -1;
318 * Number of bits for saturation.
320 private int satBits
= -1;
323 * Number of bits for luminance.
325 private int lumBits
= -1;
328 * Step size for hue bins.
330 private int hueStep
= -1;
333 * Step size for saturation bins.
335 private int satStep
= -1;
338 * Cached RGB to HSL result.
340 private int hsl
[] = new int[3];
343 * ColorIdx records a RGB color and its palette index.
345 private class ColorIdx
{
347 * The 24-bit RGB color.
352 * The palette index for this color.
357 * Public constructor.
359 * @param color the 24-bit RGB color
360 * @param index the palette index for this color
362 public ColorIdx(final int color
, final int index
) {
369 * Public constructor.
371 public SixelPalette() {
376 * Find the nearest match for a color in the palette.
378 * @param color the RGB color
379 * @return the index in rgbColors that is closest to color
381 public int matchColor(final int color
) {
386 * matchColor() is a critical performance bottleneck. To make it
387 * decent, we do the following:
389 * 1. Find the nearest two hues that bracket this color.
391 * 2. Find the nearest two saturations that bracket this color.
393 * 3. Iterate within these four bands of luminance values,
394 * returning the closest color by Euclidean distance.
396 * This strategy reduces the search space by about 97%.
398 int red
= (color
>>> 16) & 0xFF;
399 int green
= (color
>>> 8) & 0xFF;
400 int blue
= color
& 0xFF;
402 if (sixelPaletteSize
== 2) {
403 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
412 rgbToHsl(red
, green
, blue
, hsl
);
416 // System.err.printf("%d %d %d\n", hue, sat, lum);
418 double diff
= Double
.MAX_VALUE
;
421 int hue1
= hue
/ (360/hueStep
);
423 if (hue1
>= hslColors
.size() - 1) {
424 // Bracket pure red from above.
425 hue1
= hslColors
.size() - 1;
427 } else if (hue1
== 0) {
428 // Bracket pure red from below.
429 hue2
= hslColors
.size() - 1;
432 for (int hI
= hue1
; hI
!= -1;) {
433 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
436 } else if (hI
== hue2
) {
440 int sMin
= (sat
/ satStep
) - 1;
445 } else if (sMin
== sats
.size() - 1) {
450 assert (sMax
- sMin
== 1);
453 // int sMax = sats.size() - 1;
455 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
456 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
458 // True 3D colorspace match for the remaining values
459 for (ColorIdx c
: lums
) {
460 int rgbColor
= c
.color
;
462 int red2
= (rgbColor
>>> 16) & 0xFF;
463 int green2
= (rgbColor
>>> 8) & 0xFF;
464 int blue2
= rgbColor
& 0xFF;
465 newDiff
+= Math
.pow(red2
- red
, 2);
466 newDiff
+= Math
.pow(green2
- green
, 2);
467 newDiff
+= Math
.pow(blue2
- blue
, 2);
468 if (newDiff
< diff
) {
469 idx
= rgbSortedIndex
[c
.index
];
476 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
477 // Black is a closer match.
479 } else if ((((255 - red
) * (255 - red
)) +
480 ((255 - green
) * (255 - green
)) +
481 ((255 - blue
) * (255 - blue
))) < diff
) {
483 // White is a closer match.
484 idx
= sixelPaletteSize
- 1;
491 * Clamp an int value to [0, 255].
493 * @param x the int value
494 * @return an int between 0 and 255.
496 private int clamp(final int x
) {
507 * Dither an image to a sixelPaletteSize palette. The dithered
508 * image cells will contain indexes into the palette.
510 * @param image the image to dither
511 * @return the dithered image. Every pixel is an index into the
514 public BufferedImage
ditherImage(final BufferedImage image
) {
516 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
517 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
519 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
520 image
.getHeight(), null, 0, image
.getWidth());
521 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
522 rgbArray
, 0, image
.getWidth());
524 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
525 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
526 int oldPixel
= ditheredImage
.getRGB(imageX
,
528 int colorIdx
= matchColor(oldPixel
);
529 assert (colorIdx
>= 0);
530 assert (colorIdx
< sixelPaletteSize
);
531 int newPixel
= rgbColors
.get(colorIdx
);
532 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
534 int oldRed
= (oldPixel
>>> 16) & 0xFF;
535 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
536 int oldBlue
= oldPixel
& 0xFF;
538 int newRed
= (newPixel
>>> 16) & 0xFF;
539 int newGreen
= (newPixel
>>> 8) & 0xFF;
540 int newBlue
= newPixel
& 0xFF;
542 int redError
= (oldRed
- newRed
) / 16;
543 int greenError
= (oldGreen
- newGreen
) / 16;
544 int blueError
= (oldBlue
- newBlue
) / 16;
546 int red
, green
, blue
;
547 if (imageX
< image
.getWidth() - 1) {
548 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
549 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
550 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
551 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
553 green
= clamp(green
);
555 pXpY
= ((red
& 0xFF) << 16);
556 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
557 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
559 if (imageY
< image
.getHeight() - 1) {
560 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
562 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
563 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
564 blue
= ( pXpYp
& 0xFF) + blueError
;
566 green
= clamp(green
);
568 pXpYp
= ((red
& 0xFF) << 16);
569 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
570 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
572 } else if (imageY
< image
.getHeight() - 1) {
573 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
575 int pXYp
= ditheredImage
.getRGB(imageX
,
578 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
579 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
580 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
582 green
= clamp(green
);
584 pXmYp
= ((red
& 0xFF) << 16);
585 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
586 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
588 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
589 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
590 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
592 green
= clamp(green
);
594 pXYp
= ((red
& 0xFF) << 16);
595 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
596 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
598 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
599 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
601 return ditheredImage
;
605 * Convert an RGB color to HSL.
607 * @param red red color, between 0 and 255
608 * @param green green color, between 0 and 255
609 * @param blue blue color, between 0 and 255
610 * @param hsl the hsl color as [hue, saturation, luminance]
612 private void rgbToHsl(final int red
, final int green
,
613 final int blue
, final int [] hsl
) {
615 assert ((red
>= 0) && (red
<= 255));
616 assert ((green
>= 0) && (green
<= 255));
617 assert ((blue
>= 0) && (blue
<= 255));
619 double R
= red
/ 255.0;
620 double G
= green
/ 255.0;
621 double B
= blue
/ 255.0;
622 boolean Rmax
= false;
623 boolean Gmax
= false;
624 boolean Bmax
= false;
625 double min
= (R
< G ? R
: G
);
626 min
= (min
< B ? min
: B
);
628 if ((R
>= G
) && (R
>= B
)) {
631 } else if ((G
>= R
) && (G
>= B
)) {
634 } else if ((B
>= G
) && (B
>= R
)) {
639 double L
= (min
+ max
) / 2.0;
644 S
= (max
- min
) / (max
+ min
);
646 S
= (max
- min
) / (2.0 - max
- min
);
650 assert (Gmax
== false);
651 assert (Bmax
== false);
652 H
= (G
- B
) / (max
- min
);
654 assert (Rmax
== false);
655 assert (Bmax
== false);
656 H
= 2.0 + (B
- R
) / (max
- min
);
658 assert (Rmax
== false);
659 assert (Gmax
== false);
660 H
= 4.0 + (R
- G
) / (max
- min
);
665 hsl
[0] = (int) (H
* 60.0);
666 hsl
[1] = (int) (S
* 100.0);
667 hsl
[2] = (int) (L
* 100.0);
669 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
670 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
671 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
675 * Convert a HSL color to RGB.
677 * @param hue hue, between 0 and 359
678 * @param sat saturation, between 0 and 100
679 * @param lum luminance, between 0 and 100
680 * @return the rgb color as 0x00RRGGBB
682 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
683 assert ((hue
>= 0) && (hue
<= 360));
684 assert ((sat
>= 0) && (sat
<= 100));
685 assert ((lum
>= 0) && (lum
<= 100));
687 double S
= sat
/ 100.0;
688 double L
= lum
/ 100.0;
689 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
690 double Hp
= hue
/ 60.0;
691 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
698 } else if (Hp
<= 2.0) {
701 } else if (Hp
<= 3.0) {
704 } else if (Hp
<= 4.0) {
707 } else if (Hp
<= 5.0) {
710 } else if (Hp
<= 6.0) {
714 double m
= L
- (C
/ 2.0);
715 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
716 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
717 int blue
= (int) ((Bp
+ m
) * 255.0);
719 return (red
| green
| blue
);
723 * Create the sixel palette.
725 private void makePalette() {
726 // Generate the sixel palette. Because we have no idea at this
727 // layer which image(s) will be shown, we have to use a common
728 // palette with sixelPaletteSize colors for everything, and
729 // map the BufferedImage colors to their nearest neighbor in RGB
732 if (sixelPaletteSize
== 2) {
734 rgbColors
.add(0xFFFFFF);
735 rgbSortedIndex
[0] = 0;
736 rgbSortedIndex
[1] = 1;
740 // We build a palette using the Hue-Saturation-Luminence model,
741 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
742 // Luminance. We convert these colors to 24-bit RGB, sort them
743 // ascending, and steal the first index for pure black and the
744 // last for pure white. The 8-bit final palette favors bright
745 // colors, somewhere between pastel and classic television
746 // technicolor. 9- and 10-bit palettes are more uniform.
748 // Default at 256 colors.
753 assert (sixelPaletteSize
>= 256);
754 assert ((sixelPaletteSize
== 256)
755 || (sixelPaletteSize
== 512)
756 || (sixelPaletteSize
== 1024)
757 || (sixelPaletteSize
== 2048));
759 switch (sixelPaletteSize
) {
776 hueStep
= (int) (Math
.pow(2, hueBits
));
777 satStep
= (int) (100 / Math
.pow(2, satBits
));
778 // 1 bit for luminance: 40 and 70.
783 // 2 bits: 20, 40, 60, 80
788 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
794 // System.err.printf("<html><body>\n");
795 // Hue is evenly spaced around the wheel.
796 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
798 final boolean DEBUG
= false;
799 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
801 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
802 hue
+= (360/hueStep
)) {
804 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
805 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
806 hslColors
.add(satList
);
808 // Saturation is linearly spaced between pastel and pure.
809 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
811 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
812 satList
.add(lumList
);
814 // Luminance brackets the pure color, but leaning toward
816 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
818 System.err.printf("<font style = \"color:");
819 System.err.printf("hsl(%d, %d%%, %d%%)",
821 System.err.printf(";\">=</font>\n");
823 int rgbColor
= hslToRgb(hue
, sat
, lum
);
824 rgbColors
.add(rgbColor
);
825 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
826 rgbColors
.size() - 1);
827 lumList
.add(colorIdx
);
829 rawRgbList
.add(rgbColor
);
831 int red
= (rgbColor
>>> 16) & 0xFF;
832 int green
= (rgbColor
>>> 8) & 0xFF;
833 int blue
= rgbColor
& 0xFF;
834 int [] backToHsl
= new int[3];
835 rgbToHsl(red
, green
, blue
, backToHsl
);
836 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
837 hue
, backToHsl
[0], sat
, backToHsl
[1],
843 // System.err.printf("\n</body></html>\n");
845 assert (rgbColors
.size() == sixelPaletteSize
);
848 * We need to sort rgbColors, so that toSixel() can know where
849 * BLACK and WHITE are in it. But we also need to be able to
850 * find the sorted values using the old unsorted indexes. So we
851 * will sort it, put all the indexes into a HashMap, and then
852 * build rgbSortedIndex[].
854 Collections
.sort(rgbColors
);
855 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
856 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
857 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
858 rgbColorIndices
.put(rgbColors
.get(i
), i
);
860 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
861 int rawColor
= rawRgbList
.get(i
);
862 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
865 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
866 assert (rawRgbList
!= null);
867 int idx
= rgbSortedIndex
[i
];
868 int rgbColor
= rgbColors
.get(idx
);
869 if ((idx
!= 0) && (idx
!= sixelPaletteSize
- 1)) {
871 System.err.printf("%d %06x --> %d %06x\n",
872 i, rawRgbList.get(i), idx, rgbColors.get(idx));
874 assert (rgbColor
== rawRgbList
.get(i
));
879 // Set the dimmest color as true black, and the brightest as true
882 rgbColors
.set(sixelPaletteSize
- 1, 0xFFFFFF);
885 System.err.printf("<html><body>\n");
886 for (Integer rgb: rgbColors) {
887 System.err.printf("<font style = \"color:");
888 System.err.printf("#%06x", rgb);
889 System.err.printf(";\">=</font>\n");
891 System.err.printf("\n</body></html>\n");
897 * Emit the sixel palette.
899 * @param sb the StringBuilder to append to
900 * @param used array of booleans set to true for each color actually
901 * used in this cell, or null to emit the entire palette
902 * @return the string to emit to an ANSI / ECMA-style terminal
904 public String
emitPalette(final StringBuilder sb
,
905 final boolean [] used
) {
907 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
908 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
909 int rgbColor
= rgbColors
.get(i
);
910 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
911 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
912 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
913 ( rgbColor
& 0xFF) * 100 / 255));
916 return sb
.toString();
921 * ImageCache is a least-recently-used cache that hangs on to the
922 * post-rendered sixel or iTerm2 string for a particular set of cells.
924 private class ImageCache
{
927 * Maximum size of the cache.
929 private int maxSize
= 100;
932 * The entries stored in the cache.
934 private HashMap
<String
, CacheEntry
> cache
= null;
937 * CacheEntry is one entry in the cache.
939 private class CacheEntry
{
951 * The last time this entry was used.
953 public long millis
= 0;
956 * Public constructor.
958 * @param key the cache entry key
959 * @param data the cache entry data
961 public CacheEntry(final String key
, final String data
) {
964 this.millis
= System
.currentTimeMillis();
969 * Public constructor.
971 * @param maxSize the maximum size of the cache
973 public ImageCache(final int maxSize
) {
974 this.maxSize
= maxSize
;
975 cache
= new HashMap
<String
, CacheEntry
>();
979 * Make a unique key for a list of cells.
981 * @param cells the cells
984 private String
makeKey(final ArrayList
<Cell
> cells
) {
985 StringBuilder sb
= new StringBuilder();
986 for (Cell cell
: cells
) {
987 sb
.append(cell
.hashCode());
989 return sb
.toString();
993 * Get an entry from the cache.
995 * @param cells the list of cells that are the cache key
996 * @return the sixel string representing these cells, or null if this
997 * list of cells is not in the cache
999 public String
get(final ArrayList
<Cell
> cells
) {
1000 CacheEntry entry
= cache
.get(makeKey(cells
));
1001 if (entry
== null) {
1004 entry
.millis
= System
.currentTimeMillis();
1009 * Put an entry into the cache.
1011 * @param cells the list of cells that are the cache key
1012 * @param data the sixel string representing these cells
1014 public void put(final ArrayList
<Cell
> cells
, final String data
) {
1015 String key
= makeKey(cells
);
1017 // System.err.println("put() " + key + " size " + cache.size());
1019 assert (!cache
.containsKey(key
));
1021 assert (cache
.size() <= maxSize
);
1022 if (cache
.size() == maxSize
) {
1023 // Cache is at limit, evict oldest entry.
1024 long oldestTime
= Long
.MAX_VALUE
;
1025 String keyToRemove
= null;
1026 for (CacheEntry entry
: cache
.values()) {
1027 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
1028 keyToRemove
= entry
.key
;
1029 oldestTime
= entry
.millis
;
1033 System.err.println("put() remove key = " + keyToRemove +
1034 " size " + cache.size());
1036 assert (keyToRemove
!= null);
1037 cache
.remove(keyToRemove
);
1039 System.err.println("put() removed, size " + cache.size());
1042 assert (cache
.size() <= maxSize
);
1043 CacheEntry entry
= new CacheEntry(key
, data
);
1044 assert (key
.equals(entry
.key
));
1045 cache
.put(key
, entry
);
1047 System.err.println("put() added key " + key + " " +
1048 " size " + cache.size());
1054 // ------------------------------------------------------------------------
1055 // Constructors -----------------------------------------------------------
1056 // ------------------------------------------------------------------------
1059 * Constructor sets up state for getEvent(). If either windowWidth or
1060 * windowHeight are less than 1, the terminal is not resized.
1062 * @param listener the object this backend needs to wake up when new
1064 * @param input an InputStream connected to the remote user, or null for
1065 * System.in. If System.in is used, then on non-Windows systems it will
1066 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1067 * cooked mode. input is always converted to a Reader with UTF-8
1069 * @param output an OutputStream connected to the remote user, or null
1070 * for System.out. output is always converted to a Writer with UTF-8
1072 * @param windowWidth the number of text columns to start with
1073 * @param windowHeight the number of text rows to start with
1074 * @throws UnsupportedEncodingException if an exception is thrown when
1075 * creating the InputStreamReader
1077 public ECMA48Terminal(final Object listener
, final InputStream input
,
1078 final OutputStream output
, final int windowWidth
,
1079 final int windowHeight
) throws UnsupportedEncodingException
{
1081 this(listener
, input
, output
);
1083 // Send dtterm/xterm sequences, which will probably not work because
1084 // allowWindowOps is defaulted to false.
1085 if ((windowWidth
> 0) && (windowHeight
> 0)) {
1086 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1088 this.output
.write(resizeString
);
1089 this.output
.flush();
1094 * Constructor sets up state for getEvent().
1096 * @param listener the object this backend needs to wake up when new
1098 * @param input an InputStream connected to the remote user, or null for
1099 * System.in. If System.in is used, then on non-Windows systems it will
1100 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1101 * cooked mode. input is always converted to a Reader with UTF-8
1103 * @param output an OutputStream connected to the remote user, or null
1104 * for System.out. output is always converted to a Writer with UTF-8
1106 * @throws UnsupportedEncodingException if an exception is thrown when
1107 * creating the InputStreamReader
1109 public ECMA48Terminal(final Object listener
, final InputStream input
,
1110 final OutputStream output
) throws UnsupportedEncodingException
{
1116 stopReaderThread
= false;
1117 this.listener
= listener
;
1119 if (input
== null) {
1120 // inputStream = System.in;
1121 inputStream
= new FileInputStream(FileDescriptor
.in
);
1125 inputStream
= input
;
1127 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1129 if (input
instanceof SessionInfo
) {
1130 // This is a TelnetInputStream that exposes window size and
1131 // environment variables from the telnet layer.
1132 sessionInfo
= (SessionInfo
) input
;
1134 if (sessionInfo
== null) {
1135 if (input
== null) {
1136 // Reading right off the tty
1137 sessionInfo
= new TTYSessionInfo();
1139 sessionInfo
= new TSessionInfo();
1143 if (output
== null) {
1144 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1147 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1151 // Request Device Attributes
1152 this.output
.printf("\033[c");
1154 // Request xterm report window/cell dimensions in pixels
1155 this.output
.printf("%s", xtermReportPixelDimensions());
1157 // Enable mouse reporting and metaSendsEscape
1158 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1159 this.output
.flush();
1161 // Request xterm use the sixel settings we want
1162 this.output
.printf("%s", xtermSetSixelSettings());
1164 // Query the screen size
1165 sessionInfo
.queryWindowSize();
1166 setDimensions(sessionInfo
.getWindowWidth(),
1167 sessionInfo
.getWindowHeight());
1169 // Hang onto the window size
1170 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1171 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1175 // Spin up the input reader
1176 eventQueue
= new ArrayList
<TInputEvent
>();
1177 readerThread
= new Thread(this);
1178 readerThread
.start();
1181 this.output
.write(clearAll());
1182 this.output
.flush();
1186 * Constructor sets up state for getEvent().
1188 * @param listener the object this backend needs to wake up when new
1190 * @param input the InputStream underlying 'reader'. Its available()
1191 * method is used to determine if reader.read() will block or not.
1192 * @param reader a Reader connected to the remote user.
1193 * @param writer a PrintWriter connected to the remote user.
1194 * @param setRawMode if true, set System.in into raw mode with stty.
1195 * This should in general not be used. It is here solely for Demo3,
1196 * which uses System.in.
1197 * @throws IllegalArgumentException if input, reader, or writer are null.
1199 public ECMA48Terminal(final Object listener
, final InputStream input
,
1200 final Reader reader
, final PrintWriter writer
,
1201 final boolean setRawMode
) {
1203 if (input
== null) {
1204 throw new IllegalArgumentException("InputStream must be specified");
1206 if (reader
== null) {
1207 throw new IllegalArgumentException("Reader must be specified");
1209 if (writer
== null) {
1210 throw new IllegalArgumentException("Writer must be specified");
1216 stopReaderThread
= false;
1217 this.listener
= listener
;
1219 inputStream
= input
;
1220 this.input
= reader
;
1222 if (setRawMode
== true) {
1225 this.setRawMode
= setRawMode
;
1227 if (input
instanceof SessionInfo
) {
1228 // This is a TelnetInputStream that exposes window size and
1229 // environment variables from the telnet layer.
1230 sessionInfo
= (SessionInfo
) input
;
1232 if (sessionInfo
== null) {
1233 if (setRawMode
== true) {
1234 // Reading right off the tty
1235 sessionInfo
= new TTYSessionInfo();
1237 sessionInfo
= new TSessionInfo();
1241 this.output
= writer
;
1243 // Request Device Attributes
1244 this.output
.printf("\033[c");
1246 // Request xterm report window/cell dimensions in pixels
1247 this.output
.printf("%s", xtermReportPixelDimensions());
1249 // Enable mouse reporting and metaSendsEscape
1250 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1251 this.output
.flush();
1253 // Request xterm use the sixel settings we want
1254 this.output
.printf("%s", xtermSetSixelSettings());
1256 // Query the screen size
1257 sessionInfo
.queryWindowSize();
1258 setDimensions(sessionInfo
.getWindowWidth(),
1259 sessionInfo
.getWindowHeight());
1261 // Hang onto the window size
1262 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1263 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1267 // Spin up the input reader
1268 eventQueue
= new ArrayList
<TInputEvent
>();
1269 readerThread
= new Thread(this);
1270 readerThread
.start();
1273 this.output
.write(clearAll());
1274 this.output
.flush();
1278 * Constructor sets up state for getEvent().
1280 * @param listener the object this backend needs to wake up when new
1282 * @param input the InputStream underlying 'reader'. Its available()
1283 * method is used to determine if reader.read() will block or not.
1284 * @param reader a Reader connected to the remote user.
1285 * @param writer a PrintWriter connected to the remote user.
1286 * @throws IllegalArgumentException if input, reader, or writer are null.
1288 public ECMA48Terminal(final Object listener
, final InputStream input
,
1289 final Reader reader
, final PrintWriter writer
) {
1291 this(listener
, input
, reader
, writer
, false);
1294 // ------------------------------------------------------------------------
1295 // LogicalScreen ----------------------------------------------------------
1296 // ------------------------------------------------------------------------
1299 * Set the window title.
1301 * @param title the new title
1304 public void setTitle(final String title
) {
1305 output
.write(getSetTitleString(title
));
1310 * Push the logical screen to the physical device.
1313 public void flushPhysical() {
1314 StringBuilder sb
= new StringBuilder();
1318 && (cursorY
<= height
- 1)
1319 && (cursorX
<= width
- 1)
1322 sb
.append(cursor(true));
1323 sb
.append(gotoXY(cursorX
, cursorY
));
1325 sb
.append(cursor(false));
1328 output
.write(sb
.toString());
1333 * Resize the physical screen to match the logical screen dimensions.
1336 public void resizeToScreen() {
1337 // Send dtterm/xterm sequences, which will probably not work because
1338 // allowWindowOps is defaulted to false.
1339 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1341 this.output
.write(resizeString
);
1342 this.output
.flush();
1345 // ------------------------------------------------------------------------
1346 // TerminalReader ---------------------------------------------------------
1347 // ------------------------------------------------------------------------
1350 * Check if there are events in the queue.
1352 * @return if true, getEvents() has something to return to the backend
1354 public boolean hasEvents() {
1355 synchronized (eventQueue
) {
1356 return (eventQueue
.size() > 0);
1361 * Return any events in the IO queue.
1363 * @param queue list to append new events to
1365 public void getEvents(final List
<TInputEvent
> queue
) {
1366 synchronized (eventQueue
) {
1367 if (eventQueue
.size() > 0) {
1368 synchronized (queue
) {
1369 queue
.addAll(eventQueue
);
1377 * Restore terminal to normal state.
1379 public void closeTerminal() {
1381 // System.err.println("=== closeTerminal() ==="); System.err.flush();
1383 // Tell the reader thread to stop looking at input
1384 stopReaderThread
= true;
1386 readerThread
.join();
1387 } catch (InterruptedException e
) {
1388 if (debugToStderr
) {
1389 e
.printStackTrace();
1393 // Disable mouse reporting and show cursor. Defensive null check
1394 // here in case closeTerminal() is called twice.
1395 if (output
!= null) {
1396 output
.printf("%s%s%s%s", mouse(false), cursor(true),
1397 defaultColor(), xtermResetSixelSettings());
1404 // We don't close System.in/out
1406 // Shut down the streams, this should wake up the reader thread
1407 // and make it exit.
1408 if (input
!= null) {
1411 } catch (IOException e
) {
1416 if (output
!= null) {
1424 * Set listener to a different Object.
1426 * @param listener the new listening object that run() wakes up on new
1429 public void setListener(final Object listener
) {
1430 this.listener
= listener
;
1434 * Reload options from System properties.
1436 public void reloadOptions() {
1437 // Permit RGB colors only if externally requested.
1438 if (System
.getProperty("jexer.ECMA48.rgbColor",
1439 "false").equals("true")
1446 // Default to using images for full-width characters.
1447 if (System
.getProperty("jexer.ECMA48.wideCharImages",
1448 "true").equals("true")) {
1449 wideCharImages
= true;
1451 wideCharImages
= false;
1454 // Pull the system properties for sixel output.
1455 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1462 int paletteSize
= 1024;
1464 paletteSize
= Integer
.parseInt(System
.getProperty(
1465 "jexer.ECMA48.sixelPaletteSize", "1024"));
1466 switch (paletteSize
) {
1472 sixelPaletteSize
= paletteSize
;
1478 } catch (NumberFormatException e
) {
1482 // Default to using images for full-width characters.
1483 if (System
.getProperty("jexer.ECMA48.iTerm2Images",
1484 "false").equals("true")) {
1485 iterm2Images
= true;
1487 iterm2Images
= false;
1490 // Set custom colors
1491 setCustomSystemColors();
1494 // ------------------------------------------------------------------------
1495 // Runnable ---------------------------------------------------------------
1496 // ------------------------------------------------------------------------
1499 * Read function runs on a separate thread.
1502 boolean done
= false;
1503 // available() will often return > 1, so we need to read in chunks to
1505 char [] readBuffer
= new char[128];
1506 List
<TInputEvent
> events
= new ArrayList
<TInputEvent
>();
1508 while (!done
&& !stopReaderThread
) {
1510 // We assume that if inputStream has bytes available, then
1511 // input won't block on read().
1512 int n
= inputStream
.available();
1515 System.err.printf("inputStream.available(): %d\n", n);
1520 if (readBuffer
.length
< n
) {
1521 // The buffer wasn't big enough, make it huger
1522 readBuffer
= new char[readBuffer
.length
* 2];
1525 // System.err.printf("BEFORE read()\n"); System.err.flush();
1527 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1530 System.err.printf("AFTER read() %d\n", rc);
1538 for (int i
= 0; i
< rc
; i
++) {
1539 int ch
= readBuffer
[i
];
1540 processChar(events
, (char)ch
);
1542 getIdleEvents(events
);
1543 if (events
.size() > 0) {
1544 // Add to the queue for the backend thread to
1545 // be able to obtain.
1546 synchronized (eventQueue
) {
1547 eventQueue
.addAll(events
);
1549 if (listener
!= null) {
1550 synchronized (listener
) {
1551 listener
.notifyAll();
1558 getIdleEvents(events
);
1559 if (events
.size() > 0) {
1560 synchronized (eventQueue
) {
1561 eventQueue
.addAll(events
);
1563 if (listener
!= null) {
1564 synchronized (listener
) {
1565 listener
.notifyAll();
1571 if (output
.checkError()) {
1576 // Wait 20 millis for more data
1579 // System.err.println("end while loop"); System.err.flush();
1580 } catch (InterruptedException e
) {
1582 } catch (IOException e
) {
1583 e
.printStackTrace();
1586 } // while ((done == false) && (stopReaderThread == false))
1588 // Pass an event up to TApplication to tell it this Backend is done.
1589 synchronized (eventQueue
) {
1590 eventQueue
.add(new TCommandEvent(cmBackendDisconnect
));
1592 if (listener
!= null) {
1593 synchronized (listener
) {
1594 listener
.notifyAll();
1598 // System.err.println("*** run() exiting..."); System.err.flush();
1601 // ------------------------------------------------------------------------
1602 // ECMA48Terminal ---------------------------------------------------------
1603 // ------------------------------------------------------------------------
1606 * Get the width of a character cell in pixels.
1608 * @return the width in pixels of a character cell
1610 public int getTextWidth() {
1611 return (widthPixels
/ sessionInfo
.getWindowWidth());
1615 * Get the height of a character cell in pixels.
1617 * @return the height in pixels of a character cell
1619 public int getTextHeight() {
1620 return (heightPixels
/ sessionInfo
.getWindowHeight());
1624 * Getter for sessionInfo.
1626 * @return the SessionInfo
1628 public SessionInfo
getSessionInfo() {
1633 * Get the output writer.
1635 * @return the Writer
1637 public PrintWriter
getOutput() {
1642 * Call 'stty' to set cooked mode.
1644 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1646 private void sttyCooked() {
1651 * Call 'stty' to set raw mode.
1653 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1654 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1655 * -parenb cs8 min 1 < /dev/tty'
1657 private void sttyRaw() {
1662 * Call 'stty' to set raw or cooked mode.
1664 * @param mode if true, set raw mode, otherwise set cooked mode
1666 private void doStty(final boolean mode
) {
1667 String
[] cmdRaw
= {
1668 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1670 String
[] cmdCooked
= {
1671 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1676 process
= Runtime
.getRuntime().exec(cmdRaw
);
1678 process
= Runtime
.getRuntime().exec(cmdCooked
);
1680 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1681 String line
= in
.readLine();
1682 if ((line
!= null) && (line
.length() > 0)) {
1683 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1686 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1687 line
= err
.readLine();
1688 if ((line
!= null) && (line
.length() > 0)) {
1689 System
.err
.println("Error output from stty: " + line
);
1694 } catch (InterruptedException e
) {
1695 if (debugToStderr
) {
1696 e
.printStackTrace();
1700 int rc
= process
.exitValue();
1702 System
.err
.println("stty returned error code: " + rc
);
1704 } catch (IOException e
) {
1705 e
.printStackTrace();
1712 public void flush() {
1717 * Perform a somewhat-optimal rendering of a line.
1719 * @param y row coordinate. 0 is the top-most row.
1720 * @param sb StringBuilder to write escape sequences to
1721 * @param lastAttr cell attributes from the last call to flushLine
1723 private void flushLine(final int y
, final StringBuilder sb
,
1724 CellAttributes lastAttr
) {
1728 for (int x
= 0; x
< width
; x
++) {
1729 Cell lCell
= logical
[x
][y
];
1730 if (!lCell
.isBlank()) {
1734 // Push textEnd to first column beyond the text area
1738 // reallyCleared = true;
1740 boolean hasImage
= false;
1742 for (int x
= 0; x
< width
; x
++) {
1743 Cell lCell
= logical
[x
][y
];
1744 Cell pCell
= physical
[x
][y
];
1746 if (!lCell
.equals(pCell
) || reallyCleared
) {
1748 if (debugToStderr
) {
1749 System
.err
.printf("\n--\n");
1750 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1751 System
.err
.printf(" lCell: %s\n", lCell
);
1752 System
.err
.printf(" pCell: %s\n", pCell
);
1753 System
.err
.printf(" ==== \n");
1756 if (lastAttr
== null) {
1757 lastAttr
= new CellAttributes();
1758 sb
.append(normal());
1762 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1763 // Advancing at least one cell, or the first gotoXY
1764 sb
.append(gotoXY(x
, y
));
1767 assert (lastAttr
!= null);
1769 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1770 assert (lCell
.isBlank());
1772 for (int i
= x
; i
< width
; i
++) {
1773 assert (logical
[i
][y
].isBlank());
1774 // Physical is always updated
1775 physical
[i
][y
].reset();
1778 // Clear remaining line
1779 sb
.append(clearRemainingLine());
1784 // Image cell: bypass the rest of the loop, it is not
1786 if ((wideCharImages
&& lCell
.isImage())
1789 && (lCell
.getWidth() == Cell
.Width
.SINGLE
))
1793 // Save the last rendered cell
1796 // Physical is always updated
1797 physical
[x
][y
].setTo(lCell
);
1801 assert ((wideCharImages
&& !lCell
.isImage())
1803 && (!lCell
.isImage()
1805 && (lCell
.getWidth() != Cell
.Width
.SINGLE
)))));
1807 if (!wideCharImages
&& (lCell
.getWidth() == Cell
.Width
.RIGHT
)) {
1813 sb
.append(gotoXY(x
, y
));
1816 // Now emit only the modified attributes
1817 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1818 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1820 && (lCell
.isBold() == lastAttr
.isBold())
1821 && (lCell
.isReverse() == lastAttr
.isReverse())
1822 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1823 && (lCell
.isBlink() == lastAttr
.isBlink())
1825 // Both colors changed, attributes the same
1826 sb
.append(color(lCell
.isBold(),
1827 lCell
.getForeColor(), lCell
.getBackColor()));
1829 if (debugToStderr
) {
1830 System
.err
.printf("1 Change only fore/back colors\n");
1833 } else if (lCell
.isRGB()
1834 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1835 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1836 && (lCell
.isBold() == lastAttr
.isBold())
1837 && (lCell
.isReverse() == lastAttr
.isReverse())
1838 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1839 && (lCell
.isBlink() == lastAttr
.isBlink())
1841 // Both colors changed, attributes the same
1842 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1843 lCell
.getBackColorRGB()));
1845 if (debugToStderr
) {
1846 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1848 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1849 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1851 && (lCell
.isBold() != lastAttr
.isBold())
1852 && (lCell
.isReverse() != lastAttr
.isReverse())
1853 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1854 && (lCell
.isBlink() != lastAttr
.isBlink())
1856 // Everything is different
1857 sb
.append(color(lCell
.getForeColor(),
1858 lCell
.getBackColor(),
1859 lCell
.isBold(), lCell
.isReverse(),
1861 lCell
.isUnderline()));
1863 if (debugToStderr
) {
1864 System
.err
.printf("2 Set all attributes\n");
1866 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1867 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1869 && (lCell
.isBold() == lastAttr
.isBold())
1870 && (lCell
.isReverse() == lastAttr
.isReverse())
1871 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1872 && (lCell
.isBlink() == lastAttr
.isBlink())
1875 // Attributes same, foreColor different
1876 sb
.append(color(lCell
.isBold(),
1877 lCell
.getForeColor(), true));
1879 if (debugToStderr
) {
1880 System
.err
.printf("3 Change foreColor\n");
1882 } else if (lCell
.isRGB()
1883 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1884 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1885 && (lCell
.getForeColorRGB() >= 0)
1886 && (lCell
.getBackColorRGB() >= 0)
1887 && (lCell
.isBold() == lastAttr
.isBold())
1888 && (lCell
.isReverse() == lastAttr
.isReverse())
1889 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1890 && (lCell
.isBlink() == lastAttr
.isBlink())
1892 // Attributes same, foreColor different
1893 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1895 if (debugToStderr
) {
1896 System
.err
.printf("3 Change foreColor (RGB)\n");
1898 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1899 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1901 && (lCell
.isBold() == lastAttr
.isBold())
1902 && (lCell
.isReverse() == lastAttr
.isReverse())
1903 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1904 && (lCell
.isBlink() == lastAttr
.isBlink())
1906 // Attributes same, backColor different
1907 sb
.append(color(lCell
.isBold(),
1908 lCell
.getBackColor(), false));
1910 if (debugToStderr
) {
1911 System
.err
.printf("4 Change backColor\n");
1913 } else if (lCell
.isRGB()
1914 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1915 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1916 && (lCell
.isBold() == lastAttr
.isBold())
1917 && (lCell
.isReverse() == lastAttr
.isReverse())
1918 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1919 && (lCell
.isBlink() == lastAttr
.isBlink())
1921 // Attributes same, foreColor different
1922 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1924 if (debugToStderr
) {
1925 System
.err
.printf("4 Change backColor (RGB)\n");
1927 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1928 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1929 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1930 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1931 && (lCell
.isBold() == lastAttr
.isBold())
1932 && (lCell
.isReverse() == lastAttr
.isReverse())
1933 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1934 && (lCell
.isBlink() == lastAttr
.isBlink())
1937 // All attributes the same, just print the char
1940 if (debugToStderr
) {
1941 System
.err
.printf("5 Only emit character\n");
1944 // Just reset everything again
1945 if (!lCell
.isRGB()) {
1946 sb
.append(color(lCell
.getForeColor(),
1947 lCell
.getBackColor(),
1951 lCell
.isUnderline()));
1953 if (debugToStderr
) {
1954 System
.err
.printf("6 Change all attributes\n");
1957 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1958 lCell
.getBackColorRGB(),
1962 lCell
.isUnderline()));
1963 if (debugToStderr
) {
1964 System
.err
.printf("6 Change all attributes (RGB)\n");
1969 // Emit the character
1971 // Don't emit the right-half of full-width chars.
1973 && (lCell
.getWidth() != Cell
.Width
.RIGHT
))
1975 sb
.append(Character
.toChars(lCell
.getChar()));
1978 // Save the last rendered cell
1980 lastAttr
.setTo(lCell
);
1982 // Physical is always updated
1983 physical
[x
][y
].setTo(lCell
);
1985 } // if (!lCell.equals(pCell) || (reallyCleared == true))
1987 } // for (int x = 0; x < width; x++)
1991 * Render the screen to a string that can be emitted to something that
1992 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
1994 * @param sb StringBuilder to write escape sequences to
1995 * @return escape sequences string that provides the updates to the
1998 private String
flushString(final StringBuilder sb
) {
1999 CellAttributes attr
= null;
2001 if (reallyCleared
) {
2002 attr
= new CellAttributes();
2003 sb
.append(clearAll());
2007 * For images support, draw all of the image output first, and then
2008 * draw everything else afterwards. This works OK, but performance
2009 * is still a drag on larger pictures.
2011 for (int y
= 0; y
< height
; y
++) {
2012 for (int x
= 0; x
< width
; x
++) {
2013 // If physical had non-image data that is now image data, the
2014 // entire row must be redrawn.
2015 Cell lCell
= logical
[x
][y
];
2016 Cell pCell
= physical
[x
][y
];
2017 if (lCell
.isImage() && !pCell
.isImage()) {
2023 for (int y
= 0; y
< height
; y
++) {
2024 for (int x
= 0; x
< width
; x
++) {
2025 Cell lCell
= logical
[x
][y
];
2026 Cell pCell
= physical
[x
][y
];
2028 if (!lCell
.isImage()
2030 && (lCell
.getWidth() != Cell
.Width
.SINGLE
))
2037 while ((right
< width
)
2038 && (logical
[right
][y
].isImage())
2039 && (!logical
[right
][y
].equals(physical
[right
][y
])
2044 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
2045 for (int i
= 0; i
< (right
- x
); i
++) {
2046 assert (logical
[x
+ i
][y
].isImage());
2047 cellsToDraw
.add(logical
[x
+ i
][y
]);
2049 // Physical is always updated.
2050 physical
[x
+ i
][y
].setTo(lCell
);
2052 if (cellsToDraw
.size() > 0) {
2054 sb
.append(toIterm2Image(x
, y
, cellsToDraw
));
2055 } else if (jexerImages
) {
2056 sb
.append(toJexerImage(x
, y
, cellsToDraw
));
2058 sb
.append(toSixel(x
, y
, cellsToDraw
));
2066 // Draw the text part now.
2067 for (int y
= 0; y
< height
; y
++) {
2068 flushLine(y
, sb
, attr
);
2071 reallyCleared
= false;
2073 String result
= sb
.toString();
2074 if (debugToStderr
) {
2075 System
.err
.printf("flushString(): %s\n", result
);
2081 * Reset keyboard/mouse input parser.
2083 private void resetParser() {
2084 state
= ParseState
.GROUND
;
2085 params
= new ArrayList
<String
>();
2088 decPrivateModeFlag
= false;
2092 * Produce a control character or one of the special ones (ENTER, TAB,
2095 * @param ch Unicode code point
2096 * @param alt if true, set alt on the TKeypress
2097 * @return one TKeypress event, either a control character (e.g. isKey ==
2098 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
2101 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
2102 // System.err.printf("controlChar: %02x\n", ch);
2106 // Carriage return --> ENTER
2107 return new TKeypressEvent(kbEnter
, alt
, false, false);
2109 // Linefeed --> ENTER
2110 return new TKeypressEvent(kbEnter
, alt
, false, false);
2113 return new TKeypressEvent(kbEsc
, alt
, false, false);
2116 return new TKeypressEvent(kbTab
, alt
, false, false);
2118 // Make all other control characters come back as the alphabetic
2119 // character with the ctrl field set. So SOH would be 'A' +
2121 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
2127 * Produce special key from CSI Pn ; Pm ; ... ~
2129 * @return one KEYPRESS event representing a special key
2131 private TInputEvent
csiFnKey() {
2133 if (params
.size() > 0) {
2134 key
= Integer
.parseInt(params
.get(0));
2136 boolean alt
= false;
2137 boolean ctrl
= false;
2138 boolean shift
= false;
2139 if (params
.size() > 1) {
2140 shift
= csiIsShift(params
.get(1));
2141 alt
= csiIsAlt(params
.get(1));
2142 ctrl
= csiIsCtrl(params
.get(1));
2147 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2149 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2151 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2153 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2155 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2157 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2159 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2161 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2163 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2165 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2167 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2169 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2171 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2173 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2181 * Produce mouse events based on "Any event tracking" and UTF-8
2183 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2185 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2187 private TInputEvent
parseMouse() {
2188 int buttons
= params
.get(0).charAt(0) - 32;
2189 int x
= params
.get(0).charAt(1) - 32 - 1;
2190 int y
= params
.get(0).charAt(2) - 32 - 1;
2192 // Clamp X and Y to the physical screen coordinates.
2193 if (x
>= windowResize
.getWidth()) {
2194 x
= windowResize
.getWidth() - 1;
2196 if (y
>= windowResize
.getHeight()) {
2197 y
= windowResize
.getHeight() - 1;
2200 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2201 boolean eventMouse1
= false;
2202 boolean eventMouse2
= false;
2203 boolean eventMouse3
= false;
2204 boolean eventMouseWheelUp
= false;
2205 boolean eventMouseWheelDown
= false;
2207 // System.err.printf("buttons: %04x\r\n", buttons);
2224 if (!mouse1
&& !mouse2
&& !mouse3
) {
2225 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2227 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2244 // Dragging with mouse1 down
2247 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2251 // Dragging with mouse2 down
2254 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2258 // Dragging with mouse3 down
2261 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2265 // Dragging with mouse2 down after wheelUp
2268 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2272 // Dragging with mouse2 down after wheelDown
2275 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2279 eventMouseWheelUp
= true;
2283 eventMouseWheelDown
= true;
2287 // Unknown, just make it motion
2288 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2291 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2292 eventMouse1
, eventMouse2
, eventMouse3
,
2293 eventMouseWheelUp
, eventMouseWheelDown
);
2297 * Produce mouse events based on "Any event tracking" and SGR
2299 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2301 * @param release if true, this was a release ('m')
2302 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2304 private TInputEvent
parseMouseSGR(final boolean release
) {
2305 // SGR extended coordinates - mode 1006
2306 if (params
.size() < 3) {
2307 // Invalid position, bail out.
2310 int buttons
= Integer
.parseInt(params
.get(0));
2311 int x
= Integer
.parseInt(params
.get(1)) - 1;
2312 int y
= Integer
.parseInt(params
.get(2)) - 1;
2314 // Clamp X and Y to the physical screen coordinates.
2315 if (x
>= windowResize
.getWidth()) {
2316 x
= windowResize
.getWidth() - 1;
2318 if (y
>= windowResize
.getHeight()) {
2319 y
= windowResize
.getHeight() - 1;
2322 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2323 boolean eventMouse1
= false;
2324 boolean eventMouse2
= false;
2325 boolean eventMouse3
= false;
2326 boolean eventMouseWheelUp
= false;
2327 boolean eventMouseWheelDown
= false;
2330 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2344 // Motion only, no buttons down
2345 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2349 // Dragging with mouse1 down
2351 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2355 // Dragging with mouse2 down
2357 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2361 // Dragging with mouse3 down
2363 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2367 // Dragging with mouse2 down after wheelUp
2369 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2373 // Dragging with mouse2 down after wheelDown
2375 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2379 eventMouseWheelUp
= true;
2383 eventMouseWheelDown
= true;
2387 // Unknown, bail out
2390 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2391 eventMouse1
, eventMouse2
, eventMouse3
,
2392 eventMouseWheelUp
, eventMouseWheelDown
);
2396 * Return any events in the IO queue due to timeout.
2398 * @param queue list to append new events to
2400 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2401 long nowTime
= System
.currentTimeMillis();
2403 // Check for new window size
2404 long windowSizeDelay
= nowTime
- windowSizeTime
;
2405 if (windowSizeDelay
> 1000) {
2406 int oldTextWidth
= getTextWidth();
2407 int oldTextHeight
= getTextHeight();
2409 sessionInfo
.queryWindowSize();
2410 int newWidth
= sessionInfo
.getWindowWidth();
2411 int newHeight
= sessionInfo
.getWindowHeight();
2413 if ((newWidth
!= windowResize
.getWidth())
2414 || (newHeight
!= windowResize
.getHeight())
2417 // Request xterm report window dimensions in pixels again.
2418 // Between now and then, ensure that the reported text cell
2419 // size is the same by setting widthPixels and heightPixels
2420 // to match the new dimensions.
2421 widthPixels
= oldTextWidth
* newWidth
;
2422 heightPixels
= oldTextHeight
* newHeight
;
2424 if (debugToStderr
) {
2425 System
.err
.println("Screen size changed, old size " +
2427 System
.err
.println(" new size " +
2428 newWidth
+ " x " + newHeight
);
2429 System
.err
.println(" old pixels " +
2430 oldTextWidth
+ " x " + oldTextHeight
);
2431 System
.err
.println(" new pixels " +
2432 getTextWidth() + " x " + getTextHeight());
2435 this.output
.printf("%s", xtermReportPixelDimensions());
2436 this.output
.flush();
2438 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2439 newWidth
, newHeight
);
2440 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2441 newWidth
, newHeight
);
2444 windowSizeTime
= nowTime
;
2447 // ESCDELAY type timeout
2448 if (state
== ParseState
.ESCAPE
) {
2449 long escDelay
= nowTime
- escapeTime
;
2450 if (escDelay
> 100) {
2451 // After 0.1 seconds, assume a true escape character
2452 queue
.add(controlChar((char)0x1B, false));
2459 * Returns true if the CSI parameter for a keyboard command means that
2462 private boolean csiIsShift(final String x
) {
2474 * Returns true if the CSI parameter for a keyboard command means that
2477 private boolean csiIsAlt(final String x
) {
2489 * Returns true if the CSI parameter for a keyboard command means that
2492 private boolean csiIsCtrl(final String x
) {
2504 * Parses the next character of input to see if an InputEvent is
2507 * @param events list to append new events to
2508 * @param ch Unicode code point
2510 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2512 // ESCDELAY type timeout
2513 long nowTime
= System
.currentTimeMillis();
2514 if (state
== ParseState
.ESCAPE
) {
2515 long escDelay
= nowTime
- escapeTime
;
2516 if (escDelay
> 250) {
2517 // After 0.25 seconds, assume a true escape character
2518 events
.add(controlChar((char)0x1B, false));
2524 boolean ctrl
= false;
2525 boolean alt
= false;
2526 boolean shift
= false;
2528 // System.err.printf("state: %s ch %c\r\n", state, ch);
2534 state
= ParseState
.ESCAPE
;
2535 escapeTime
= nowTime
;
2540 // Control character
2541 events
.add(controlChar(ch
, false));
2548 events
.add(new TKeypressEvent(false, 0, ch
,
2549 false, false, false));
2558 // ALT-Control character
2559 events
.add(controlChar(ch
, true));
2565 // This will be one of the function keys
2566 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2570 // '[' goes to CSI_ENTRY
2572 state
= ParseState
.CSI_ENTRY
;
2576 // Everything else is assumed to be Alt-keystroke
2577 if ((ch
>= 'A') && (ch
<= 'Z')) {
2581 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2585 case ESCAPE_INTERMEDIATE
:
2586 if ((ch
>= 'P') && (ch
<= 'S')) {
2590 events
.add(new TKeypressEvent(kbF1
));
2593 events
.add(new TKeypressEvent(kbF2
));
2596 events
.add(new TKeypressEvent(kbF3
));
2599 events
.add(new TKeypressEvent(kbF4
));
2608 // Unknown keystroke, ignore
2613 // Numbers - parameter values
2614 if ((ch
>= '0') && (ch
<= '9')) {
2615 params
.set(params
.size() - 1,
2616 params
.get(params
.size() - 1) + ch
);
2617 state
= ParseState
.CSI_PARAM
;
2620 // Parameter separator
2626 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2630 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2635 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2640 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2645 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2650 events
.add(new TKeypressEvent(kbHome
));
2655 events
.add(new TKeypressEvent(kbEnd
));
2659 // CBT - Cursor backward X tab stops (default 1)
2660 events
.add(new TKeypressEvent(kbBackTab
));
2665 state
= ParseState
.MOUSE
;
2668 // Mouse position, SGR (1006) coordinates
2669 state
= ParseState
.MOUSE_SGR
;
2672 // DEC private mode flag
2673 decPrivateModeFlag
= true;
2680 // Unknown keystroke, ignore
2685 // Numbers - parameter values
2686 if ((ch
>= '0') && (ch
<= '9')) {
2687 params
.set(params
.size() - 1,
2688 params
.get(params
.size() - 1) + ch
);
2691 // Parameter separator
2699 // Generate a mouse press event
2700 TInputEvent event
= parseMouseSGR(false);
2701 if (event
!= null) {
2707 // Generate a mouse release event
2708 event
= parseMouseSGR(true);
2709 if (event
!= null) {
2718 // Unknown keystroke, ignore
2723 // Numbers - parameter values
2724 if ((ch
>= '0') && (ch
<= '9')) {
2725 params
.set(params
.size() - 1,
2726 params
.get(params
.size() - 1) + ch
);
2727 state
= ParseState
.CSI_PARAM
;
2730 // Parameter separator
2737 events
.add(csiFnKey());
2742 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2746 if (params
.size() > 1) {
2747 shift
= csiIsShift(params
.get(1));
2748 alt
= csiIsAlt(params
.get(1));
2749 ctrl
= csiIsCtrl(params
.get(1));
2751 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2756 if (params
.size() > 1) {
2757 shift
= csiIsShift(params
.get(1));
2758 alt
= csiIsAlt(params
.get(1));
2759 ctrl
= csiIsCtrl(params
.get(1));
2761 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2766 if (params
.size() > 1) {
2767 shift
= csiIsShift(params
.get(1));
2768 alt
= csiIsAlt(params
.get(1));
2769 ctrl
= csiIsCtrl(params
.get(1));
2771 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2776 if (params
.size() > 1) {
2777 shift
= csiIsShift(params
.get(1));
2778 alt
= csiIsAlt(params
.get(1));
2779 ctrl
= csiIsCtrl(params
.get(1));
2781 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2786 if (params
.size() > 1) {
2787 shift
= csiIsShift(params
.get(1));
2788 alt
= csiIsAlt(params
.get(1));
2789 ctrl
= csiIsCtrl(params
.get(1));
2791 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2796 if (params
.size() > 1) {
2797 shift
= csiIsShift(params
.get(1));
2798 alt
= csiIsAlt(params
.get(1));
2799 ctrl
= csiIsCtrl(params
.get(1));
2801 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2805 // Device Attributes
2806 if (decPrivateModeFlag
== false) {
2809 for (String x
: params
) {
2810 if (x
.equals("4")) {
2811 // Terminal reports sixel support
2812 if (debugToStderr
) {
2813 System
.err
.println("Device Attributes: sixel");
2816 if (x
.equals("444")) {
2817 // Terminal reports Jexer images support
2818 if (debugToStderr
) {
2819 System
.err
.println("Device Attributes: Jexer images");
2827 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2828 if (debugToStderr
) {
2829 System
.err
.printf("windowOp pixels: " +
2830 "height %s width %s\n",
2831 params
.get(1), params
.get(2));
2834 widthPixels
= Integer
.parseInt(params
.get(2));
2835 heightPixels
= Integer
.parseInt(params
.get(1));
2836 } catch (NumberFormatException e
) {
2837 if (debugToStderr
) {
2838 e
.printStackTrace();
2841 if (widthPixels
<= 0) {
2844 if (heightPixels
<= 0) {
2848 if ((params
.size() > 2) && (params
.get(0).equals("6"))) {
2849 if (debugToStderr
) {
2850 System
.err
.printf("windowOp text cell pixels: " +
2851 "height %s width %s\n",
2852 params
.get(1), params
.get(2));
2855 widthPixels
= width
* Integer
.parseInt(params
.get(2));
2856 heightPixels
= height
* Integer
.parseInt(params
.get(1));
2857 } catch (NumberFormatException e
) {
2858 if (debugToStderr
) {
2859 e
.printStackTrace();
2862 if (widthPixels
<= 0) {
2865 if (heightPixels
<= 0) {
2876 // Unknown keystroke, ignore
2881 params
.set(0, params
.get(params
.size() - 1) + ch
);
2882 if (params
.get(0).length() == 3) {
2883 // We have enough to generate a mouse event
2884 events
.add(parseMouse());
2893 // This "should" be impossible to reach
2898 * Request (u)xterm to use the sixel settings we need:
2900 * - enable sixel scrolling
2902 * - disable private color registers (so that we can use one common
2905 * @return the string to emit to xterm
2907 private String
xtermSetSixelSettings() {
2908 return "\033[?80h\033[?1070l";
2912 * Restore (u)xterm its default sixel settings:
2914 * - enable sixel scrolling
2916 * - enable private color registers
2918 * @return the string to emit to xterm
2920 private String
xtermResetSixelSettings() {
2921 return "\033[?80h\033[?1070h";
2925 * Request (u)xterm to report the current window and cell size dimensions
2928 * @return the string to emit to xterm
2930 private String
xtermReportPixelDimensions() {
2931 // We will ask for both window and text cell dimensions, and
2932 // hopefully one of them will work.
2933 return "\033[14t\033[16t";
2937 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2938 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2941 * @param on if true, enable metaSendsEscape
2942 * @return the string to emit to xterm
2944 private String
xtermMetaSendsEscape(final boolean on
) {
2946 return "\033[?1036h\033[?1034l";
2948 return "\033[?1036l";
2952 * Create an xterm OSC sequence to change the window title.
2954 * @param title the new title
2955 * @return the string to emit to xterm
2957 private String
getSetTitleString(final String title
) {
2958 return "\033]2;" + title
+ "\007";
2961 // ------------------------------------------------------------------------
2962 // Sixel output support ---------------------------------------------------
2963 // ------------------------------------------------------------------------
2966 * Get the number of colors in the sixel palette.
2968 * @return the palette size
2970 public int getSixelPaletteSize() {
2971 return sixelPaletteSize
;
2975 * Set the number of colors in the sixel palette.
2977 * @param paletteSize the new palette size
2979 public void setSixelPaletteSize(final int paletteSize
) {
2980 if (paletteSize
== sixelPaletteSize
) {
2984 switch (paletteSize
) {
2992 throw new IllegalArgumentException("Unsupported sixel palette " +
2993 " size: " + paletteSize
);
2996 // Don't step on the screen refresh thread.
2997 synchronized (this) {
2998 sixelPaletteSize
= paletteSize
;
3006 * Start a sixel string for display one row's worth of bitmap data.
3008 * @param x column coordinate. 0 is the left-most column.
3009 * @param y row coordinate. 0 is the top-most row.
3010 * @return the string to emit to an ANSI / ECMA-style terminal
3012 private String
startSixel(final int x
, final int y
) {
3013 StringBuilder sb
= new StringBuilder();
3015 assert (sixel
== true);
3018 sb
.append(gotoXY(x
, y
));
3021 sb
.append("\033Pq");
3023 if (palette
== null) {
3024 palette
= new SixelPalette();
3025 // TODO: make this an option (shared palette or not)
3026 palette
.emitPalette(sb
, null);
3029 return sb
.toString();
3033 * End a sixel string for display one row's worth of bitmap data.
3035 * @return the string to emit to an ANSI / ECMA-style terminal
3037 private String
endSixel() {
3038 assert (sixel
== true);
3045 * Create a sixel string representing a row of several cells containing
3048 * @param x column coordinate. 0 is the left-most column.
3049 * @param y row coordinate. 0 is the top-most row.
3050 * @param cells the cells containing the bitmap data
3051 * @return the string to emit to an ANSI / ECMA-style terminal
3053 private String
toSixel(final int x
, final int y
,
3054 final ArrayList
<Cell
> cells
) {
3056 StringBuilder sb
= new StringBuilder();
3058 assert (cells
!= null);
3059 assert (cells
.size() > 0);
3060 assert (cells
.get(0).getImage() != null);
3062 if (sixel
== false) {
3063 sb
.append(normal());
3064 sb
.append(gotoXY(x
, y
));
3065 for (int i
= 0; i
< cells
.size(); i
++) {
3068 return sb
.toString();
3071 if (y
== height
- 1) {
3072 // We are on the bottom row. If scrolling mode is enabled
3073 // (default), then VT320/xterm will scroll the entire screen if
3074 // we draw any pixels here.
3076 // TODO: support sixel scrolling mode disabled as an option.
3077 sb
.append(normal());
3078 sb
.append(gotoXY(x
, y
));
3079 for (int j
= 0; j
< cells
.size(); j
++) {
3082 return sb
.toString();
3085 if (sixelCache
== null) {
3086 sixelCache
= new ImageCache(height
* 10);
3089 // Save and get rows to/from the cache that do NOT have inverted
3091 boolean saveInCache
= true;
3092 for (Cell cell
: cells
) {
3093 if (cell
.isInvertedImage()) {
3094 saveInCache
= false;
3098 String cachedResult
= sixelCache
.get(cells
);
3099 if (cachedResult
!= null) {
3100 // System.err.println("CACHE HIT");
3101 sb
.append(startSixel(x
, y
));
3102 sb
.append(cachedResult
);
3103 sb
.append(endSixel());
3104 return sb
.toString();
3106 // System.err.println("CACHE MISS");
3109 int imageWidth
= cells
.get(0).getImage().getWidth();
3110 int imageHeight
= cells
.get(0).getImage().getHeight();
3112 // cells.get(x).getImage() has a dithered bitmap containing indexes
3113 // into the color palette. Piece these together into one larger
3114 // image for final rendering.
3116 int fullWidth
= cells
.size() * getTextWidth();
3117 int fullHeight
= getTextHeight();
3118 for (int i
= 0; i
< cells
.size(); i
++) {
3119 totalWidth
+= cells
.get(i
).getImage().getWidth();
3122 BufferedImage image
= new BufferedImage(fullWidth
,
3123 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3126 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3127 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
3129 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
3132 if (false && cells
.get(i
).isInvertedImage()) {
3133 // I used to put an all-white cell over the cursor, don't do
3135 rgbArray
= new int[imageWidth
* imageHeight
];
3136 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3137 rgbArray
[j
] = 0xFFFFFF;
3141 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3142 tileWidth
, tileHeight
, null, 0, tileWidth
);
3143 } catch (Exception e
) {
3144 throw new RuntimeException("image " + imageWidth
+ "x" +
3146 "tile " + tileWidth
+ "x" +
3148 " cells.get(i).getImage() " +
3149 cells
.get(i
).getImage() +
3151 " fullWidth " + fullWidth
+
3152 " fullHeight " + fullHeight
, e
);
3157 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3158 i * imageWidth, 0, imageWidth, imageHeight,
3160 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3161 fullWidth, fullHeight, cells.size(), getTextWidth());
3164 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3165 rgbArray
, 0, tileWidth
);
3166 if (tileHeight
< fullHeight
) {
3167 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3168 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3169 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3172 image
.setRGB(imageX
, imageY
, backgroundColor
);
3177 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3178 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3179 // I used to put an all-white cell over the cursor, don't do that
3181 rgbArray
= new int[totalWidth
* imageHeight
];
3182 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3183 rgbArray
[j
] = 0xFFFFFF;
3187 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3188 totalWidth
, imageHeight
, null, 0, totalWidth
);
3189 } catch (Exception e
) {
3190 throw new RuntimeException("image " + imageWidth
+ "x" +
3191 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3192 cells
.get(cells
.size() - 1).getImage(), e
);
3195 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3196 imageHeight
, rgbArray
, 0, totalWidth
);
3198 if (totalWidth
< getTextWidth()) {
3199 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3201 for (int imageX
= image
.getWidth() - totalWidth
;
3202 imageX
< image
.getWidth(); imageX
++) {
3204 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3205 image
.setRGB(imageX
, imageY
, backgroundColor
);
3210 // Dither the image. It is ok to lose the original here.
3211 if (palette
== null) {
3212 palette
= new SixelPalette();
3213 // TODO: make this an option (shared palette or not)
3214 palette
.emitPalette(sb
, null);
3216 image
= palette
.ditherImage(image
);
3218 // Collect the raster information
3219 int rasterHeight
= 0;
3220 int rasterWidth
= image
.getWidth();
3224 // TODO: make this an option (shared palette or not)
3226 // Emit the palette, but only for the colors actually used by these
3228 boolean [] usedColors = new boolean[sixelPaletteSize];
3229 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3230 for (int imageY = 0; imageY < image.getHeight(); imageY++) {
3231 usedColors[image.getRGB(imageX, imageY)] = true;
3234 palette.emitPalette(sb, usedColors);
3237 // Render the entire row of cells.
3238 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
3239 int [][] sixels
= new int[image
.getWidth()][6];
3241 // See which colors are actually used in this band of sixels.
3242 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3243 for (int imageY
= 0;
3244 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
3247 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
3248 assert (colorIdx
>= 0);
3249 assert (colorIdx
< sixelPaletteSize
);
3251 sixels
[imageX
][imageY
] = colorIdx
;
3255 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
3256 boolean isUsed
= false;
3257 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3258 for (int j
= 0; j
< 6; j
++) {
3259 if (sixels
[imageX
][j
] == i
) {
3264 if (isUsed
== false) {
3268 // Set to the beginning of scan line for the next set of
3269 // colored pixels, and select the color.
3270 sb
.append(String
.format("$#%d", i
));
3273 int oldDataCount
= 0;
3274 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3276 // Add up all the pixels that match this color.
3279 (j
< 6) && (currentRow
+ j
< fullHeight
);
3282 if (sixels
[imageX
][j
] == i
) {
3303 if ((currentRow
+ j
+ 1) > rasterHeight
) {
3304 rasterHeight
= currentRow
+ j
+ 1;
3312 if (data
== oldData
) {
3315 if (oldDataCount
== 1) {
3316 sb
.append((char) oldData
);
3317 } else if (oldDataCount
> 1) {
3318 sb
.append(String
.format("!%d", oldDataCount
));
3319 sb
.append((char) oldData
);
3325 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
3327 // Emit the last sequence.
3328 if (oldDataCount
== 1) {
3329 sb
.append((char) oldData
);
3330 } else if (oldDataCount
> 1) {
3331 sb
.append(String
.format("!%d", oldDataCount
));
3332 sb
.append((char) oldData
);
3335 } // for (int i = 0; i < sixelPaletteSize; i++)
3337 // Advance to the next scan line.
3340 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3342 // Kill the very last "-", because it is unnecessary.
3343 sb
.deleteCharAt(sb
.length() - 1);
3345 // Add the raster information
3346 sb
.insert(0, String
.format("\"1;1;%d;%d", rasterWidth
, rasterHeight
));
3349 // This row is OK to save into the cache.
3350 sixelCache
.put(cells
, sb
.toString());
3353 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3357 * Get the sixel support flag.
3359 * @return true if this terminal is emitting sixel
3361 public boolean hasSixel() {
3365 // ------------------------------------------------------------------------
3366 // End sixel output support -----------------------------------------------
3367 // ------------------------------------------------------------------------
3369 // ------------------------------------------------------------------------
3370 // iTerm2 image output support --------------------------------------------
3371 // ------------------------------------------------------------------------
3374 * Create an iTerm2 images string representing a row of several cells
3375 * containing bitmap data.
3377 * @param x column coordinate. 0 is the left-most column.
3378 * @param y row coordinate. 0 is the top-most row.
3379 * @param cells the cells containing the bitmap data
3380 * @return the string to emit to an ANSI / ECMA-style terminal
3382 private String
toIterm2Image(final int x
, final int y
,
3383 final ArrayList
<Cell
> cells
) {
3385 StringBuilder sb
= new StringBuilder();
3387 assert (cells
!= null);
3388 assert (cells
.size() > 0);
3389 assert (cells
.get(0).getImage() != null);
3391 if (iterm2Images
== false) {
3392 sb
.append(normal());
3393 sb
.append(gotoXY(x
, y
));
3394 for (int i
= 0; i
< cells
.size(); i
++) {
3397 return sb
.toString();
3400 if (iterm2Cache
== null) {
3401 iterm2Cache
= new ImageCache(height
* 10);
3402 base64
= java
.util
.Base64
.getEncoder();
3405 // Save and get rows to/from the cache that do NOT have inverted
3407 boolean saveInCache
= true;
3408 for (Cell cell
: cells
) {
3409 if (cell
.isInvertedImage()) {
3410 saveInCache
= false;
3414 String cachedResult
= iterm2Cache
.get(cells
);
3415 if (cachedResult
!= null) {
3416 // System.err.println("CACHE HIT");
3417 sb
.append(gotoXY(x
, y
));
3418 sb
.append(cachedResult
);
3419 return sb
.toString();
3421 // System.err.println("CACHE MISS");
3424 int imageWidth
= cells
.get(0).getImage().getWidth();
3425 int imageHeight
= cells
.get(0).getImage().getHeight();
3427 // cells.get(x).getImage() has a dithered bitmap containing indexes
3428 // into the color palette. Piece these together into one larger
3429 // image for final rendering.
3431 int fullWidth
= cells
.size() * getTextWidth();
3432 int fullHeight
= getTextHeight();
3433 for (int i
= 0; i
< cells
.size(); i
++) {
3434 totalWidth
+= cells
.get(i
).getImage().getWidth();
3437 BufferedImage image
= new BufferedImage(fullWidth
,
3438 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3441 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3442 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
3444 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
3446 if (false && cells
.get(i
).isInvertedImage()) {
3447 // I used to put an all-white cell over the cursor, don't do
3449 rgbArray
= new int[imageWidth
* imageHeight
];
3450 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3451 rgbArray
[j
] = 0xFFFFFF;
3455 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3456 tileWidth
, tileHeight
, null, 0, tileWidth
);
3457 } catch (Exception e
) {
3458 throw new RuntimeException("image " + imageWidth
+ "x" +
3460 "tile " + tileWidth
+ "x" +
3462 " cells.get(i).getImage() " +
3463 cells
.get(i
).getImage() +
3465 " fullWidth " + fullWidth
+
3466 " fullHeight " + fullHeight
, e
);
3471 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3472 i * imageWidth, 0, imageWidth, imageHeight,
3474 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3475 fullWidth, fullHeight, cells.size(), getTextWidth());
3478 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3479 rgbArray
, 0, tileWidth
);
3480 if (tileHeight
< fullHeight
) {
3481 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3482 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3483 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3486 image
.setRGB(imageX
, imageY
, backgroundColor
);
3491 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3492 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3493 // I used to put an all-white cell over the cursor, don't do that
3495 rgbArray
= new int[totalWidth
* imageHeight
];
3496 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3497 rgbArray
[j
] = 0xFFFFFF;
3501 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3502 totalWidth
, imageHeight
, null, 0, totalWidth
);
3503 } catch (Exception e
) {
3504 throw new RuntimeException("image " + imageWidth
+ "x" +
3505 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3506 cells
.get(cells
.size() - 1).getImage(), e
);
3509 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3510 imageHeight
, rgbArray
, 0, totalWidth
);
3512 if (totalWidth
< getTextWidth()) {
3513 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3515 for (int imageX
= image
.getWidth() - totalWidth
;
3516 imageX
< image
.getWidth(); imageX
++) {
3518 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3519 image
.setRGB(imageX
, imageY
, backgroundColor
);
3525 * From https://iterm2.com/documentation-images.html:
3529 * iTerm2 extends the xterm protocol with a set of proprietary escape
3530 * sequences. In general, the pattern is:
3532 * ESC ] 1337 ; key = value ^G
3534 * Whitespace is shown here for ease of reading: in practice, no
3535 * spaces should be used.
3537 * For file transfer and inline images, the code is:
3539 * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
3541 * The optional arguments are formatted as key=value with a semicolon
3542 * between each key-value pair. They are described below:
3544 * Key Description of value
3545 * name base-64 encoded filename. Defaults to "Unnamed file".
3546 * size File size in bytes. Optional; this is only used by the
3547 * progress indicator.
3548 * width Width to render. See notes below.
3549 * height Height to render. See notes below.
3550 * preserveAspectRatio If set to 0, then the image's inherent aspect
3551 * ratio will not be respected; otherwise, it
3552 * will fill the specified width and height as
3553 * much as possible without stretching. Defaults
3555 * inline If set to 1, the file will be displayed inline. Otherwise,
3556 * it will be downloaded with no visual representation in the
3557 * terminal session. Defaults to 0.
3559 * The width and height are given as a number followed by a unit, or
3562 * N: N character cells.
3564 * N%: N percent of the session's width or height.
3565 * auto: The image's inherent size will be used to determine an
3566 * appropriate dimension.
3570 // File contents can be several image formats. We will use PNG.
3571 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3573 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3574 Math
.min(image
.getHeight(), fullHeight
)),
3575 "PNG", pngOutputStream
)
3577 // We failed to render image, bail out.
3580 } catch (IOException e
) {
3581 // We failed to render image, bail out.
3585 // iTerm2 does not advance the cursor automatically, so place it
3587 sb
.append("\033]1337;File=");
3589 sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
3593 sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
3594 image.getWidth(), Math.min(image.getHeight(),
3597 sb
.append("inline=1:");
3598 sb
.append(base64
.encodeToString(pngOutputStream
.toByteArray()));
3602 // This row is OK to save into the cache.
3603 iterm2Cache
.put(cells
, sb
.toString());
3606 return (gotoXY(x
, y
) + sb
.toString());
3610 * Get the iTerm2 images support flag.
3612 * @return true if this terminal is emitting iTerm2 images
3614 public boolean hasIterm2Images() {
3615 return iterm2Images
;
3618 // ------------------------------------------------------------------------
3619 // End iTerm2 image output support ----------------------------------------
3620 // ------------------------------------------------------------------------
3622 // ------------------------------------------------------------------------
3623 // Jexer image output support ---------------------------------------------
3624 // ------------------------------------------------------------------------
3627 * Create a Jexer images string representing a row of several cells
3628 * containing bitmap data.
3630 * @param x column coordinate. 0 is the left-most column.
3631 * @param y row coordinate. 0 is the top-most row.
3632 * @param cells the cells containing the bitmap data
3633 * @return the string to emit to an ANSI / ECMA-style terminal
3635 private String
toJexerImage(final int x
, final int y
,
3636 final ArrayList
<Cell
> cells
) {
3638 StringBuilder sb
= new StringBuilder();
3640 assert (cells
!= null);
3641 assert (cells
.size() > 0);
3642 assert (cells
.get(0).getImage() != null);
3644 if (jexerImages
== false) {
3645 sb
.append(normal());
3646 sb
.append(gotoXY(x
, y
));
3647 for (int i
= 0; i
< cells
.size(); i
++) {
3650 return sb
.toString();
3653 if (jexerCache
== null) {
3654 jexerCache
= new ImageCache(height
* 10);
3655 base64
= java
.util
.Base64
.getEncoder();
3658 // Save and get rows to/from the cache that do NOT have inverted
3660 boolean saveInCache
= true;
3661 for (Cell cell
: cells
) {
3662 if (cell
.isInvertedImage()) {
3663 saveInCache
= false;
3667 String cachedResult
= jexerCache
.get(cells
);
3668 if (cachedResult
!= null) {
3669 // System.err.println("CACHE HIT");
3670 sb
.append(gotoXY(x
, y
));
3671 sb
.append(cachedResult
);
3672 return sb
.toString();
3674 // System.err.println("CACHE MISS");
3677 int imageWidth
= cells
.get(0).getImage().getWidth();
3678 int imageHeight
= cells
.get(0).getImage().getHeight();
3680 // cells.get(x).getImage() has a dithered bitmap containing indexes
3681 // into the color palette. Piece these together into one larger
3682 // image for final rendering.
3684 int fullWidth
= cells
.size() * getTextWidth();
3685 int fullHeight
= getTextHeight();
3686 for (int i
= 0; i
< cells
.size(); i
++) {
3687 totalWidth
+= cells
.get(i
).getImage().getWidth();
3690 BufferedImage image
= new BufferedImage(fullWidth
,
3691 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3694 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3695 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
3697 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
3699 if (false && cells
.get(i
).isInvertedImage()) {
3700 // I used to put an all-white cell over the cursor, don't do
3702 rgbArray
= new int[imageWidth
* imageHeight
];
3703 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3704 rgbArray
[j
] = 0xFFFFFF;
3708 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3709 tileWidth
, tileHeight
, null, 0, tileWidth
);
3710 } catch (Exception e
) {
3711 throw new RuntimeException("image " + imageWidth
+ "x" +
3713 "tile " + tileWidth
+ "x" +
3715 " cells.get(i).getImage() " +
3716 cells
.get(i
).getImage() +
3718 " fullWidth " + fullWidth
+
3719 " fullHeight " + fullHeight
, e
);
3724 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3725 i * imageWidth, 0, imageWidth, imageHeight,
3727 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3728 fullWidth, fullHeight, cells.size(), getTextWidth());
3731 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3732 rgbArray
, 0, tileWidth
);
3733 if (tileHeight
< fullHeight
) {
3734 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3735 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3736 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3739 image
.setRGB(imageX
, imageY
, backgroundColor
);
3744 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3745 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3746 // I used to put an all-white cell over the cursor, don't do that
3748 rgbArray
= new int[totalWidth
* imageHeight
];
3749 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3750 rgbArray
[j
] = 0xFFFFFF;
3754 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3755 totalWidth
, imageHeight
, null, 0, totalWidth
);
3756 } catch (Exception e
) {
3757 throw new RuntimeException("image " + imageWidth
+ "x" +
3758 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3759 cells
.get(cells
.size() - 1).getImage(), e
);
3762 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3763 imageHeight
, rgbArray
, 0, totalWidth
);
3765 if (totalWidth
< getTextWidth()) {
3766 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3768 for (int imageX
= image
.getWidth() - totalWidth
;
3769 imageX
< image
.getWidth(); imageX
++) {
3771 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3772 image
.setRGB(imageX
, imageY
, backgroundColor
);
3777 sb
.append(String
.format("\033]444;%d;%d;0;", image
.getWidth(),
3778 Math
.min(image
.getHeight(), fullHeight
)));
3780 byte [] bytes
= new byte[image
.getWidth() * image
.getHeight() * 3];
3781 int stride
= image
.getWidth();
3782 for (int px
= 0; px
< stride
; px
++) {
3783 for (int py
= 0; py
< image
.getHeight(); py
++) {
3784 int rgb
= image
.getRGB(px
, py
);
3785 bytes
[(py
* stride
* 3) + (px
* 3)] = (byte) ((rgb
>>> 16) & 0xFF);
3786 bytes
[(py
* stride
* 3) + (px
* 3) + 1] = (byte) ((rgb
>>> 8) & 0xFF);
3787 bytes
[(py
* stride
* 3) + (px
* 3) + 2] = (byte) ( rgb
& 0xFF);
3790 sb
.append(base64
.encodeToString(bytes
));
3794 // This row is OK to save into the cache.
3795 jexerCache
.put(cells
, sb
.toString());
3798 return (gotoXY(x
, y
) + sb
.toString());
3802 * Get the Jexer images support flag.
3804 * @return true if this terminal is emitting Jexer images
3806 public boolean hasJexerImages() {
3810 // ------------------------------------------------------------------------
3811 // End Jexer image output support -----------------------------------------
3812 // ------------------------------------------------------------------------
3815 * Setup system colors to match DOS color palette.
3817 private void setDOSColors() {
3818 MYBLACK
= new java
.awt
.Color(0x00, 0x00, 0x00);
3819 MYRED
= new java
.awt
.Color(0xa8, 0x00, 0x00);
3820 MYGREEN
= new java
.awt
.Color(0x00, 0xa8, 0x00);
3821 MYYELLOW
= new java
.awt
.Color(0xa8, 0x54, 0x00);
3822 MYBLUE
= new java
.awt
.Color(0x00, 0x00, 0xa8);
3823 MYMAGENTA
= new java
.awt
.Color(0xa8, 0x00, 0xa8);
3824 MYCYAN
= new java
.awt
.Color(0x00, 0xa8, 0xa8);
3825 MYWHITE
= new java
.awt
.Color(0xa8, 0xa8, 0xa8);
3826 MYBOLD_BLACK
= new java
.awt
.Color(0x54, 0x54, 0x54);
3827 MYBOLD_RED
= new java
.awt
.Color(0xfc, 0x54, 0x54);
3828 MYBOLD_GREEN
= new java
.awt
.Color(0x54, 0xfc, 0x54);
3829 MYBOLD_YELLOW
= new java
.awt
.Color(0xfc, 0xfc, 0x54);
3830 MYBOLD_BLUE
= new java
.awt
.Color(0x54, 0x54, 0xfc);
3831 MYBOLD_MAGENTA
= new java
.awt
.Color(0xfc, 0x54, 0xfc);
3832 MYBOLD_CYAN
= new java
.awt
.Color(0x54, 0xfc, 0xfc);
3833 MYBOLD_WHITE
= new java
.awt
.Color(0xfc, 0xfc, 0xfc);
3837 * Setup ECMA48 colors to match those provided in system properties.
3839 private void setCustomSystemColors() {
3842 MYBLACK
= getCustomColor("jexer.ECMA48.color0", MYBLACK
);
3843 MYRED
= getCustomColor("jexer.ECMA48.color1", MYRED
);
3844 MYGREEN
= getCustomColor("jexer.ECMA48.color2", MYGREEN
);
3845 MYYELLOW
= getCustomColor("jexer.ECMA48.color3", MYYELLOW
);
3846 MYBLUE
= getCustomColor("jexer.ECMA48.color4", MYBLUE
);
3847 MYMAGENTA
= getCustomColor("jexer.ECMA48.color5", MYMAGENTA
);
3848 MYCYAN
= getCustomColor("jexer.ECMA48.color6", MYCYAN
);
3849 MYWHITE
= getCustomColor("jexer.ECMA48.color7", MYWHITE
);
3850 MYBOLD_BLACK
= getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK
);
3851 MYBOLD_RED
= getCustomColor("jexer.ECMA48.color9", MYBOLD_RED
);
3852 MYBOLD_GREEN
= getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN
);
3853 MYBOLD_YELLOW
= getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW
);
3854 MYBOLD_BLUE
= getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE
);
3855 MYBOLD_MAGENTA
= getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA
);
3856 MYBOLD_CYAN
= getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN
);
3857 MYBOLD_WHITE
= getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE
);
3861 * Setup one system color to match the RGB value provided in system
3864 * @param key the system property key
3865 * @param defaultColor the default color to return if key is not set, or
3867 * @return a color from the RGB string, or defaultColor
3869 private java
.awt
.Color
getCustomColor(final String key
,
3870 final java
.awt
.Color defaultColor
) {
3872 String rgb
= System
.getProperty(key
);
3874 return defaultColor
;
3876 if (rgb
.startsWith("#")) {
3877 rgb
= rgb
.substring(1);
3881 rgbInt
= Integer
.parseInt(rgb
, 16);
3882 } catch (NumberFormatException e
) {
3883 return defaultColor
;
3885 java
.awt
.Color color
= new java
.awt
.Color((rgbInt
& 0xFF0000) >>> 16,
3886 (rgbInt
& 0x00FF00) >>> 8,
3887 (rgbInt
& 0x0000FF));
3893 * Create a T.416 RGB parameter sequence for a custom system color.
3895 * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
3896 * @return the color portion of the string to emit to an ANSI /
3897 * ECMA-style terminal
3899 private String
systemColorRGB(final java
.awt
.Color color
) {
3900 return String
.format("%d;%d;%d", color
.getRed(), color
.getGreen(),
3905 * Create a SGR parameter sequence for a single color change.
3907 * @param bold if true, set bold
3908 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3909 * @param foreground if true, this is a foreground color
3910 * @return the string to emit to an ANSI / ECMA-style terminal,
3913 private String
color(final boolean bold
, final Color color
,
3914 final boolean foreground
) {
3915 return color(color
, foreground
, true) +
3916 rgbColor(bold
, color
, foreground
);
3920 * Create a T.416 RGB parameter sequence for a single color change.
3922 * @param colorRGB a 24-bit RGB value for foreground color
3923 * @param foreground if true, this is a foreground color
3924 * @return the string to emit to an ANSI / ECMA-style terminal,
3927 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
3929 int colorRed
= (colorRGB
>>> 16) & 0xFF;
3930 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
3931 int colorBlue
= colorRGB
& 0xFF;
3933 StringBuilder sb
= new StringBuilder();
3935 sb
.append("\033[38;2;");
3937 sb
.append("\033[48;2;");
3939 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
3940 return sb
.toString();
3944 * Create a T.416 RGB parameter sequence for both foreground and
3945 * background color change.
3947 * @param foreColorRGB a 24-bit RGB value for foreground color
3948 * @param backColorRGB a 24-bit RGB value for foreground color
3949 * @return the string to emit to an ANSI / ECMA-style terminal,
3952 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
3953 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3954 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3955 int foreColorBlue
= foreColorRGB
& 0xFF;
3956 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3957 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3958 int backColorBlue
= backColorRGB
& 0xFF;
3960 StringBuilder sb
= new StringBuilder();
3961 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
3962 foreColorRed
, foreColorGreen
, foreColorBlue
));
3963 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
3964 backColorRed
, backColorGreen
, backColorBlue
));
3965 return sb
.toString();
3969 * Create a T.416 RGB parameter sequence for a single color change.
3971 * @param bold if true, set bold
3972 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3973 * @param foreground if true, this is a foreground color
3974 * @return the string to emit to an xterm terminal with RGB support,
3975 * e.g. "\033[38;2;RR;GG;BBm"
3977 private String
rgbColor(final boolean bold
, final Color color
,
3978 final boolean foreground
) {
3979 if (doRgbColor
== false) {
3982 StringBuilder sb
= new StringBuilder("\033[");
3984 // Bold implies foreground only
3986 if (color
.equals(Color
.BLACK
)) {
3987 sb
.append(systemColorRGB(MYBOLD_BLACK
));
3988 } else if (color
.equals(Color
.RED
)) {
3989 sb
.append(systemColorRGB(MYBOLD_RED
));
3990 } else if (color
.equals(Color
.GREEN
)) {
3991 sb
.append(systemColorRGB(MYBOLD_GREEN
));
3992 } else if (color
.equals(Color
.YELLOW
)) {
3993 sb
.append(systemColorRGB(MYBOLD_YELLOW
));
3994 } else if (color
.equals(Color
.BLUE
)) {
3995 sb
.append(systemColorRGB(MYBOLD_BLUE
));
3996 } else if (color
.equals(Color
.MAGENTA
)) {
3997 sb
.append(systemColorRGB(MYBOLD_MAGENTA
));
3998 } else if (color
.equals(Color
.CYAN
)) {
3999 sb
.append(systemColorRGB(MYBOLD_CYAN
));
4000 } else if (color
.equals(Color
.WHITE
)) {
4001 sb
.append(systemColorRGB(MYBOLD_WHITE
));
4009 if (color
.equals(Color
.BLACK
)) {
4010 sb
.append(systemColorRGB(MYBLACK
));
4011 } else if (color
.equals(Color
.RED
)) {
4012 sb
.append(systemColorRGB(MYRED
));
4013 } else if (color
.equals(Color
.GREEN
)) {
4014 sb
.append(systemColorRGB(MYGREEN
));
4015 } else if (color
.equals(Color
.YELLOW
)) {
4016 sb
.append(systemColorRGB(MYYELLOW
));
4017 } else if (color
.equals(Color
.BLUE
)) {
4018 sb
.append(systemColorRGB(MYBLUE
));
4019 } else if (color
.equals(Color
.MAGENTA
)) {
4020 sb
.append(systemColorRGB(MYMAGENTA
));
4021 } else if (color
.equals(Color
.CYAN
)) {
4022 sb
.append(systemColorRGB(MYCYAN
));
4023 } else if (color
.equals(Color
.WHITE
)) {
4024 sb
.append(systemColorRGB(MYWHITE
));
4028 return sb
.toString();
4032 * Create a T.416 RGB parameter sequence for both foreground and
4033 * background color change.
4035 * @param bold if true, set bold
4036 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4037 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4038 * @return the string to emit to an xterm terminal with RGB support,
4039 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
4041 private String
rgbColor(final boolean bold
, final Color foreColor
,
4042 final Color backColor
) {
4043 if (doRgbColor
== false) {
4047 return rgbColor(bold
, foreColor
, true) +
4048 rgbColor(false, backColor
, false);
4052 * Create a SGR parameter sequence for a single color change.
4054 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4055 * @param foreground if true, this is a foreground color
4056 * @param header if true, make the full header, otherwise just emit the
4057 * color parameter e.g. "42;"
4058 * @return the string to emit to an ANSI / ECMA-style terminal,
4061 private String
color(final Color color
, final boolean foreground
,
4062 final boolean header
) {
4064 int ecmaColor
= color
.getValue();
4066 // Convert Color.* values to SGR numerics
4074 return String
.format("\033[%dm", ecmaColor
);
4076 return String
.format("%d;", ecmaColor
);
4081 * Create a SGR parameter sequence for both foreground and background
4084 * @param bold if true, set bold
4085 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4086 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4087 * @return the string to emit to an ANSI / ECMA-style terminal,
4088 * e.g. "\033[31;42m"
4090 private String
color(final boolean bold
, final Color foreColor
,
4091 final Color backColor
) {
4092 return color(foreColor
, backColor
, true) +
4093 rgbColor(bold
, foreColor
, backColor
);
4097 * Create a SGR parameter sequence for both foreground and
4098 * background color change.
4100 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4101 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4102 * @param header if true, make the full header, otherwise just emit the
4103 * color parameter e.g. "31;42;"
4104 * @return the string to emit to an ANSI / ECMA-style terminal,
4105 * e.g. "\033[31;42m"
4107 private String
color(final Color foreColor
, final Color backColor
,
4108 final boolean header
) {
4110 int ecmaForeColor
= foreColor
.getValue();
4111 int ecmaBackColor
= backColor
.getValue();
4113 // Convert Color.* values to SGR numerics
4114 ecmaBackColor
+= 40;
4115 ecmaForeColor
+= 30;
4118 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
4120 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
4125 * Create a SGR parameter sequence for foreground, background, and
4126 * several attributes. This sequence first resets all attributes to
4127 * default, then sets attributes as per the parameters.
4129 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4130 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4131 * @param bold if true, set bold
4132 * @param reverse if true, set reverse
4133 * @param blink if true, set blink
4134 * @param underline if true, set underline
4135 * @return the string to emit to an ANSI / ECMA-style terminal,
4136 * e.g. "\033[0;1;31;42m"
4138 private String
color(final Color foreColor
, final Color backColor
,
4139 final boolean bold
, final boolean reverse
, final boolean blink
,
4140 final boolean underline
) {
4142 int ecmaForeColor
= foreColor
.getValue();
4143 int ecmaBackColor
= backColor
.getValue();
4145 // Convert Color.* values to SGR numerics
4146 ecmaBackColor
+= 40;
4147 ecmaForeColor
+= 30;
4149 StringBuilder sb
= new StringBuilder();
4150 if ( bold
&& reverse
&& blink
&& !underline
) {
4151 sb
.append("\033[0;1;7;5;");
4152 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4153 sb
.append("\033[0;1;7;");
4154 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4155 sb
.append("\033[0;7;5;");
4156 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4157 sb
.append("\033[0;1;5;");
4158 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4159 sb
.append("\033[0;1;");
4160 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4161 sb
.append("\033[0;7;");
4162 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4163 sb
.append("\033[0;5;");
4164 } else if ( bold
&& reverse
&& blink
&& underline
) {
4165 sb
.append("\033[0;1;7;5;4;");
4166 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4167 sb
.append("\033[0;1;7;4;");
4168 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4169 sb
.append("\033[0;7;5;4;");
4170 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4171 sb
.append("\033[0;1;5;4;");
4172 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4173 sb
.append("\033[0;1;4;");
4174 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4175 sb
.append("\033[0;7;4;");
4176 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4177 sb
.append("\033[0;5;4;");
4178 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4179 sb
.append("\033[0;4;");
4181 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4182 sb
.append("\033[0;");
4184 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
4185 sb
.append(rgbColor(bold
, foreColor
, backColor
));
4186 return sb
.toString();
4190 * Create a SGR parameter sequence for foreground, background, and
4191 * several attributes. This sequence first resets all attributes to
4192 * default, then sets attributes as per the parameters.
4194 * @param foreColorRGB a 24-bit RGB value for foreground color
4195 * @param backColorRGB a 24-bit RGB value for foreground color
4196 * @param bold if true, set bold
4197 * @param reverse if true, set reverse
4198 * @param blink if true, set blink
4199 * @param underline if true, set underline
4200 * @return the string to emit to an ANSI / ECMA-style terminal,
4201 * e.g. "\033[0;1;31;42m"
4203 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
4204 final boolean bold
, final boolean reverse
, final boolean blink
,
4205 final boolean underline
) {
4207 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
4208 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
4209 int foreColorBlue
= foreColorRGB
& 0xFF;
4210 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
4211 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
4212 int backColorBlue
= backColorRGB
& 0xFF;
4214 StringBuilder sb
= new StringBuilder();
4215 if ( bold
&& reverse
&& blink
&& !underline
) {
4216 sb
.append("\033[0;1;7;5;");
4217 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4218 sb
.append("\033[0;1;7;");
4219 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4220 sb
.append("\033[0;7;5;");
4221 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4222 sb
.append("\033[0;1;5;");
4223 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4224 sb
.append("\033[0;1;");
4225 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4226 sb
.append("\033[0;7;");
4227 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4228 sb
.append("\033[0;5;");
4229 } else if ( bold
&& reverse
&& blink
&& underline
) {
4230 sb
.append("\033[0;1;7;5;4;");
4231 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4232 sb
.append("\033[0;1;7;4;");
4233 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4234 sb
.append("\033[0;7;5;4;");
4235 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4236 sb
.append("\033[0;1;5;4;");
4237 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4238 sb
.append("\033[0;1;4;");
4239 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4240 sb
.append("\033[0;7;4;");
4241 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4242 sb
.append("\033[0;5;4;");
4243 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4244 sb
.append("\033[0;4;");
4246 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4247 sb
.append("\033[0;");
4250 sb
.append("m\033[38;2;");
4251 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
4253 sb
.append("m\033[48;2;");
4254 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
4257 return sb
.toString();
4261 * Create a SGR parameter sequence to reset to VT100 defaults.
4263 * @return the string to emit to an ANSI / ECMA-style terminal,
4266 private String
normal() {
4267 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
4271 * Create a SGR parameter sequence to reset to ECMA-48 default
4272 * foreground/background.
4274 * @return the string to emit to an ANSI / ECMA-style terminal,
4277 private String
defaultColor() {
4280 * Normal (neither bold nor faint).
4283 * Steady (not blinking).
4284 * Positive (not inverse).
4285 * Visible (not hidden).
4287 * Default foreground color.
4288 * Default background color.
4290 return "\033[0;22;23;24;25;27;28;29;39;49m";
4294 * Create a SGR parameter sequence to reset to defaults.
4296 * @param header if true, make the full header, otherwise just emit the
4297 * bare parameter e.g. "0;"
4298 * @return the string to emit to an ANSI / ECMA-style terminal,
4301 private String
normal(final boolean header
) {
4303 return "\033[0;37;40m";
4309 * Create a SGR parameter sequence for enabling the visible cursor.
4311 * @param on if true, turn on cursor
4312 * @return the string to emit to an ANSI / ECMA-style terminal
4314 private String
cursor(final boolean on
) {
4315 if (on
&& !cursorOn
) {
4319 if (!on
&& cursorOn
) {
4327 * Clear the entire screen. Because some terminals use back-color-erase,
4328 * set the color to white-on-black beforehand.
4330 * @return the string to emit to an ANSI / ECMA-style terminal
4332 private String
clearAll() {
4333 return "\033[0;37;40m\033[2J";
4337 * Clear the line from the cursor (inclusive) to the end of the screen.
4338 * Because some terminals use back-color-erase, set the color to
4339 * white-on-black beforehand.
4341 * @return the string to emit to an ANSI / ECMA-style terminal
4343 private String
clearRemainingLine() {
4344 return "\033[0;37;40m\033[K";
4348 * Move the cursor to (x, y).
4350 * @param x column coordinate. 0 is the left-most column.
4351 * @param y row coordinate. 0 is the top-most row.
4352 * @return the string to emit to an ANSI / ECMA-style terminal
4354 private String
gotoXY(final int x
, final int y
) {
4355 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
4359 * Tell (u)xterm that we want to receive mouse events based on "Any event
4360 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
4361 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
4363 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
4365 * Note that this also sets the alternate/primary screen buffer.
4367 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
4368 * mean "hide the mouse pointer." We have to use our own sequence to do
4369 * this because there is no standard in xterm for unilaterally hiding the
4370 * pointer all the time (regardless of typing).
4372 * @param on If true, enable mouse report and use the alternate screen
4373 * buffer. If false disable mouse reporting and use the primary screen
4375 * @return the string to emit to xterm
4377 private String
mouse(final boolean on
) {
4379 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
4381 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";