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
.Graphics
;
32 import java
.awt
.Graphics2D
;
33 import java
.awt
.RenderingHints
;
34 import java
.awt
.image
.BufferedImage
;
35 import java
.io
.BufferedReader
;
36 import java
.io
.ByteArrayOutputStream
;
37 import java
.io
.FileDescriptor
;
38 import java
.io
.FileInputStream
;
39 import java
.io
.InputStream
;
40 import java
.io
.InputStreamReader
;
41 import java
.io
.IOException
;
42 import java
.io
.OutputStream
;
43 import java
.io
.OutputStreamWriter
;
44 import java
.io
.PrintWriter
;
45 import java
.io
.Reader
;
46 import java
.io
.UnsupportedEncodingException
;
47 import java
.util
.ArrayList
;
48 import java
.util
.Collections
;
49 import java
.util
.HashMap
;
50 import java
.util
.List
;
51 import javax
.imageio
.ImageIO
;
54 import jexer
.bits
.Cell
;
55 import jexer
.bits
.CellAttributes
;
56 import jexer
.bits
.Color
;
57 import jexer
.bits
.StringUtils
;
58 import jexer
.event
.TCommandEvent
;
59 import jexer
.event
.TInputEvent
;
60 import jexer
.event
.TKeypressEvent
;
61 import jexer
.event
.TMouseEvent
;
62 import jexer
.event
.TResizeEvent
;
63 import static jexer
.TCommand
.*;
64 import static jexer
.TKeypress
.*;
67 * This class reads keystrokes and mouse events and emits output to ANSI
68 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
70 public class ECMA48Terminal
extends LogicalScreen
71 implements TerminalReader
, Runnable
{
73 // ------------------------------------------------------------------------
74 // Constants --------------------------------------------------------------
75 // ------------------------------------------------------------------------
78 * States in the input parser.
80 private enum ParseState
{
91 * Available Jexer images support.
93 private enum JexerImageOption
{
100 // ------------------------------------------------------------------------
101 // Variables --------------------------------------------------------------
102 // ------------------------------------------------------------------------
105 * Emit debugging to stderr.
107 private boolean debugToStderr
= false;
110 * If true, emit T.416-style RGB colors for normal system colors. This
111 * is a) expensive in bandwidth, and b) potentially terrible looking for
114 private static boolean doRgbColor
= false;
117 * The session information.
119 private SessionInfo sessionInfo
;
122 * The event queue, filled up by a thread reading on input.
124 private List
<TInputEvent
> eventQueue
;
127 * If true, we want the reader thread to exit gracefully.
129 private boolean stopReaderThread
;
134 private Thread readerThread
;
137 * Parameters being collected. E.g. if the string is \033[1;3m, then
138 * params[0] will be 1 and params[1] will be 3.
140 private List
<String
> params
;
143 * Current parsing state.
145 private ParseState state
;
148 * The time we entered ESCAPE. If we get a bare escape without a code
149 * following it, this is used to return that bare escape.
151 private long escapeTime
;
154 * The time we last checked the window size. We try not to spawn stty
155 * more than once per second.
157 private long windowSizeTime
;
160 * true if mouse1 was down. Used to report mouse1 on the release event.
162 private boolean mouse1
;
165 * true if mouse2 was down. Used to report mouse2 on the release event.
167 private boolean mouse2
;
170 * true if mouse3 was down. Used to report mouse3 on the release event.
172 private boolean mouse3
;
175 * Cache the cursor visibility value so we only emit the sequence when we
178 private boolean cursorOn
= true;
181 * Cache the last window size to figure out if a TResizeEvent needs to be
184 private TResizeEvent windowResize
= null;
187 * If true, emit wide-char (CJK/Emoji) characters as sixel images.
189 private boolean wideCharImages
= true;
192 * Window width in pixels. Used for sixel support.
194 private int widthPixels
= 640;
197 * Window height in pixels. Used for sixel support.
199 private int heightPixels
= 400;
202 * If true, emit image data via sixel.
204 private boolean sixel
= true;
207 * If true, use a single shared palette for sixel.
209 private boolean sixelSharedPalette
= true;
212 * The sixel palette handler.
214 private SixelPalette palette
= null;
217 * The sixel post-rendered string cache.
219 private ImageCache sixelCache
= null;
222 * Number of colors in the sixel palette. Xterm 335 defines the max as
223 * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
226 private int sixelPaletteSize
= 1024;
229 * If true, emit image data via iTerm2 image protocol.
231 private boolean iterm2Images
= false;
234 * The iTerm2 post-rendered string cache.
236 private ImageCache iterm2Cache
= null;
239 * If not DISABLED, emit image data via Jexer image protocol if the
240 * terminal supports it.
242 private JexerImageOption jexerImageOption
= JexerImageOption
.JPG
;
245 * The Jexer post-rendered string cache.
247 private ImageCache jexerCache
= null;
250 * If true, then we changed System.in and need to change it back.
252 private boolean setRawMode
= false;
255 * If true, '?' was seen in terminal response.
257 private boolean decPrivateModeFlag
= false;
260 * The terminal's input. If an InputStream is not specified in the
261 * constructor, then this InputStreamReader will be bound to System.in
262 * with UTF-8 encoding.
264 private Reader input
;
267 * The terminal's raw InputStream. If an InputStream is not specified in
268 * the constructor, then this InputReader will be bound to System.in.
269 * This is used by run() to see if bytes are available() before calling
270 * (Reader)input.read().
272 private InputStream inputStream
;
275 * The terminal's output. If an OutputStream is not specified in the
276 * constructor, then this PrintWriter will be bound to System.out with
279 private PrintWriter output
;
282 * The listening object that run() wakes up on new input.
284 private Object listener
;
286 // Colors to map DOS colors to AWT colors.
287 private static java
.awt
.Color MYBLACK
;
288 private static java
.awt
.Color MYRED
;
289 private static java
.awt
.Color MYGREEN
;
290 private static java
.awt
.Color MYYELLOW
;
291 private static java
.awt
.Color MYBLUE
;
292 private static java
.awt
.Color MYMAGENTA
;
293 private static java
.awt
.Color MYCYAN
;
294 private static java
.awt
.Color MYWHITE
;
295 private static java
.awt
.Color MYBOLD_BLACK
;
296 private static java
.awt
.Color MYBOLD_RED
;
297 private static java
.awt
.Color MYBOLD_GREEN
;
298 private static java
.awt
.Color MYBOLD_YELLOW
;
299 private static java
.awt
.Color MYBOLD_BLUE
;
300 private static java
.awt
.Color MYBOLD_MAGENTA
;
301 private static java
.awt
.Color MYBOLD_CYAN
;
302 private static java
.awt
.Color MYBOLD_WHITE
;
305 * SixelPalette is used to manage the conversion of images between 24-bit
306 * RGB color and a palette of sixelPaletteSize colors.
308 private class SixelPalette
{
311 * Color palette for sixel output, sorted low to high.
313 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
316 * Map of color palette index for sixel output, from the order it was
317 * generated by makePalette() to rgbColors.
319 private int [] rgbSortedIndex
= new int[sixelPaletteSize
];
322 * The color palette, organized by hue, saturation, and luminance.
323 * This is used for a fast color match.
325 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
328 * Number of bits for hue.
330 private int hueBits
= -1;
333 * Number of bits for saturation.
335 private int satBits
= -1;
338 * Number of bits for luminance.
340 private int lumBits
= -1;
343 * Step size for hue bins.
345 private int hueStep
= -1;
348 * Step size for saturation bins.
350 private int satStep
= -1;
353 * Cached RGB to HSL result.
355 private int hsl
[] = new int[3];
358 * ColorIdx records a RGB color and its palette index.
360 private class ColorIdx
{
362 * The 24-bit RGB color.
367 * The palette index for this color.
372 * Public constructor.
374 * @param color the 24-bit RGB color
375 * @param index the palette index for this color
377 public ColorIdx(final int color
, final int index
) {
384 * Public constructor.
386 public SixelPalette() {
391 * Find the nearest match for a color in the palette.
393 * @param color the RGB color
394 * @return the index in rgbColors that is closest to color
396 public int matchColor(final int color
) {
401 * matchColor() is a critical performance bottleneck. To make it
402 * decent, we do the following:
404 * 1. Find the nearest two hues that bracket this color.
406 * 2. Find the nearest two saturations that bracket this color.
408 * 3. Iterate within these four bands of luminance values,
409 * returning the closest color by Euclidean distance.
411 * This strategy reduces the search space by about 97%.
413 int red
= (color
>>> 16) & 0xFF;
414 int green
= (color
>>> 8) & 0xFF;
415 int blue
= color
& 0xFF;
417 if (sixelPaletteSize
== 2) {
418 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
427 rgbToHsl(red
, green
, blue
, hsl
);
431 // System.err.printf("%d %d %d\n", hue, sat, lum);
433 double diff
= Double
.MAX_VALUE
;
436 int hue1
= hue
/ (360/hueStep
);
438 if (hue1
>= hslColors
.size() - 1) {
439 // Bracket pure red from above.
440 hue1
= hslColors
.size() - 1;
442 } else if (hue1
== 0) {
443 // Bracket pure red from below.
444 hue2
= hslColors
.size() - 1;
447 for (int hI
= hue1
; hI
!= -1;) {
448 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
451 } else if (hI
== hue2
) {
455 int sMin
= (sat
/ satStep
) - 1;
460 } else if (sMin
== sats
.size() - 1) {
465 assert (sMax
- sMin
== 1);
468 // int sMax = sats.size() - 1;
470 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
471 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
473 // True 3D colorspace match for the remaining values
474 for (ColorIdx c
: lums
) {
475 int rgbColor
= c
.color
;
477 int red2
= (rgbColor
>>> 16) & 0xFF;
478 int green2
= (rgbColor
>>> 8) & 0xFF;
479 int blue2
= rgbColor
& 0xFF;
480 newDiff
+= Math
.pow(red2
- red
, 2);
481 newDiff
+= Math
.pow(green2
- green
, 2);
482 newDiff
+= Math
.pow(blue2
- blue
, 2);
483 if (newDiff
< diff
) {
484 idx
= rgbSortedIndex
[c
.index
];
491 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
492 // Black is a closer match.
494 } else if ((((255 - red
) * (255 - red
)) +
495 ((255 - green
) * (255 - green
)) +
496 ((255 - blue
) * (255 - blue
))) < diff
) {
498 // White is a closer match.
499 idx
= sixelPaletteSize
- 1;
506 * Clamp an int value to [0, 255].
508 * @param x the int value
509 * @return an int between 0 and 255.
511 private int clamp(final int x
) {
522 * Dither an image to a sixelPaletteSize palette. The dithered
523 * image cells will contain indexes into the palette.
525 * @param image the image to dither
526 * @return the dithered image. Every pixel is an index into the
529 public BufferedImage
ditherImage(final BufferedImage image
) {
531 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
532 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
534 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
535 image
.getHeight(), null, 0, image
.getWidth());
536 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
537 rgbArray
, 0, image
.getWidth());
539 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
540 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
541 int oldPixel
= ditheredImage
.getRGB(imageX
,
543 int colorIdx
= matchColor(oldPixel
);
544 assert (colorIdx
>= 0);
545 assert (colorIdx
< sixelPaletteSize
);
546 int newPixel
= rgbColors
.get(colorIdx
);
547 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
549 int oldRed
= (oldPixel
>>> 16) & 0xFF;
550 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
551 int oldBlue
= oldPixel
& 0xFF;
553 int newRed
= (newPixel
>>> 16) & 0xFF;
554 int newGreen
= (newPixel
>>> 8) & 0xFF;
555 int newBlue
= newPixel
& 0xFF;
557 int redError
= (oldRed
- newRed
) / 16;
558 int greenError
= (oldGreen
- newGreen
) / 16;
559 int blueError
= (oldBlue
- newBlue
) / 16;
561 int red
, green
, blue
;
562 if (imageX
< image
.getWidth() - 1) {
563 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
564 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
565 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
566 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
568 green
= clamp(green
);
570 pXpY
= ((red
& 0xFF) << 16);
571 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
572 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
574 if (imageY
< image
.getHeight() - 1) {
575 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
577 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
578 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
579 blue
= ( pXpYp
& 0xFF) + blueError
;
581 green
= clamp(green
);
583 pXpYp
= ((red
& 0xFF) << 16);
584 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
585 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
587 } else if (imageY
< image
.getHeight() - 1) {
588 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
590 int pXYp
= ditheredImage
.getRGB(imageX
,
593 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
594 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
595 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
597 green
= clamp(green
);
599 pXmYp
= ((red
& 0xFF) << 16);
600 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
601 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
603 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
604 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
605 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
607 green
= clamp(green
);
609 pXYp
= ((red
& 0xFF) << 16);
610 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
611 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
613 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
614 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
616 return ditheredImage
;
620 * Convert an RGB color to HSL.
622 * @param red red color, between 0 and 255
623 * @param green green color, between 0 and 255
624 * @param blue blue color, between 0 and 255
625 * @param hsl the hsl color as [hue, saturation, luminance]
627 private void rgbToHsl(final int red
, final int green
,
628 final int blue
, final int [] hsl
) {
630 assert ((red
>= 0) && (red
<= 255));
631 assert ((green
>= 0) && (green
<= 255));
632 assert ((blue
>= 0) && (blue
<= 255));
634 double R
= red
/ 255.0;
635 double G
= green
/ 255.0;
636 double B
= blue
/ 255.0;
637 boolean Rmax
= false;
638 boolean Gmax
= false;
639 boolean Bmax
= false;
640 double min
= (R
< G ? R
: G
);
641 min
= (min
< B ? min
: B
);
643 if ((R
>= G
) && (R
>= B
)) {
646 } else if ((G
>= R
) && (G
>= B
)) {
649 } else if ((B
>= G
) && (B
>= R
)) {
654 double L
= (min
+ max
) / 2.0;
659 S
= (max
- min
) / (max
+ min
);
661 S
= (max
- min
) / (2.0 - max
- min
);
665 assert (Gmax
== false);
666 assert (Bmax
== false);
667 H
= (G
- B
) / (max
- min
);
669 assert (Rmax
== false);
670 assert (Bmax
== false);
671 H
= 2.0 + (B
- R
) / (max
- min
);
673 assert (Rmax
== false);
674 assert (Gmax
== false);
675 H
= 4.0 + (R
- G
) / (max
- min
);
680 hsl
[0] = (int) (H
* 60.0);
681 hsl
[1] = (int) (S
* 100.0);
682 hsl
[2] = (int) (L
* 100.0);
684 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
685 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
686 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
690 * Convert a HSL color to RGB.
692 * @param hue hue, between 0 and 359
693 * @param sat saturation, between 0 and 100
694 * @param lum luminance, between 0 and 100
695 * @return the rgb color as 0x00RRGGBB
697 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
698 assert ((hue
>= 0) && (hue
<= 360));
699 assert ((sat
>= 0) && (sat
<= 100));
700 assert ((lum
>= 0) && (lum
<= 100));
702 double S
= sat
/ 100.0;
703 double L
= lum
/ 100.0;
704 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
705 double Hp
= hue
/ 60.0;
706 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
713 } else if (Hp
<= 2.0) {
716 } else if (Hp
<= 3.0) {
719 } else if (Hp
<= 4.0) {
722 } else if (Hp
<= 5.0) {
725 } else if (Hp
<= 6.0) {
729 double m
= L
- (C
/ 2.0);
730 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
731 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
732 int blue
= (int) ((Bp
+ m
) * 255.0);
734 return (red
| green
| blue
);
738 * Create the sixel palette.
740 private void makePalette() {
741 // Generate the sixel palette. Because we have no idea at this
742 // layer which image(s) will be shown, we have to use a common
743 // palette with sixelPaletteSize colors for everything, and
744 // map the BufferedImage colors to their nearest neighbor in RGB
747 if (sixelPaletteSize
== 2) {
749 rgbColors
.add(0xFFFFFF);
750 rgbSortedIndex
[0] = 0;
751 rgbSortedIndex
[1] = 1;
755 // We build a palette using the Hue-Saturation-Luminence model,
756 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
757 // Luminance. We convert these colors to 24-bit RGB, sort them
758 // ascending, and steal the first index for pure black and the
759 // last for pure white. The 8-bit final palette favors bright
760 // colors, somewhere between pastel and classic television
761 // technicolor. 9- and 10-bit palettes are more uniform.
763 // Default at 256 colors.
768 assert (sixelPaletteSize
>= 256);
769 assert ((sixelPaletteSize
== 256)
770 || (sixelPaletteSize
== 512)
771 || (sixelPaletteSize
== 1024)
772 || (sixelPaletteSize
== 2048));
774 switch (sixelPaletteSize
) {
791 hueStep
= (int) (Math
.pow(2, hueBits
));
792 satStep
= (int) (100 / Math
.pow(2, satBits
));
793 // 1 bit for luminance: 40 and 70.
798 // 2 bits: 20, 40, 60, 80
803 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
809 // System.err.printf("<html><body>\n");
810 // Hue is evenly spaced around the wheel.
811 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
813 final boolean DEBUG
= false;
814 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
816 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
817 hue
+= (360/hueStep
)) {
819 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
820 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
821 hslColors
.add(satList
);
823 // Saturation is linearly spaced between pastel and pure.
824 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
826 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
827 satList
.add(lumList
);
829 // Luminance brackets the pure color, but leaning toward
831 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
833 System.err.printf("<font style = \"color:");
834 System.err.printf("hsl(%d, %d%%, %d%%)",
836 System.err.printf(";\">=</font>\n");
838 int rgbColor
= hslToRgb(hue
, sat
, lum
);
839 rgbColors
.add(rgbColor
);
840 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
841 rgbColors
.size() - 1);
842 lumList
.add(colorIdx
);
844 rawRgbList
.add(rgbColor
);
846 int red
= (rgbColor
>>> 16) & 0xFF;
847 int green
= (rgbColor
>>> 8) & 0xFF;
848 int blue
= rgbColor
& 0xFF;
849 int [] backToHsl
= new int[3];
850 rgbToHsl(red
, green
, blue
, backToHsl
);
851 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
852 hue
, backToHsl
[0], sat
, backToHsl
[1],
858 // System.err.printf("\n</body></html>\n");
860 assert (rgbColors
.size() == sixelPaletteSize
);
863 * We need to sort rgbColors, so that toSixel() can know where
864 * BLACK and WHITE are in it. But we also need to be able to
865 * find the sorted values using the old unsorted indexes. So we
866 * will sort it, put all the indexes into a HashMap, and then
867 * build rgbSortedIndex[].
869 Collections
.sort(rgbColors
);
870 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
871 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
872 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
873 rgbColorIndices
.put(rgbColors
.get(i
), i
);
875 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
876 int rawColor
= rawRgbList
.get(i
);
877 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
880 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
881 assert (rawRgbList
!= null);
882 int idx
= rgbSortedIndex
[i
];
883 int rgbColor
= rgbColors
.get(idx
);
884 if ((idx
!= 0) && (idx
!= sixelPaletteSize
- 1)) {
886 System.err.printf("%d %06x --> %d %06x\n",
887 i, rawRgbList.get(i), idx, rgbColors.get(idx));
889 assert (rgbColor
== rawRgbList
.get(i
));
894 // Set the dimmest color as true black, and the brightest as true
897 rgbColors
.set(sixelPaletteSize
- 1, 0xFFFFFF);
900 System.err.printf("<html><body>\n");
901 for (Integer rgb: rgbColors) {
902 System.err.printf("<font style = \"color:");
903 System.err.printf("#%06x", rgb);
904 System.err.printf(";\">=</font>\n");
906 System.err.printf("\n</body></html>\n");
912 * Emit the sixel palette.
914 * @param sb the StringBuilder to append to
915 * @param used array of booleans set to true for each color actually
916 * used in this cell, or null to emit the entire palette
917 * @return the string to emit to an ANSI / ECMA-style terminal
919 public String
emitPalette(final StringBuilder sb
,
920 final boolean [] used
) {
922 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
923 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
924 int rgbColor
= rgbColors
.get(i
);
925 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
926 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
927 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
928 ( rgbColor
& 0xFF) * 100 / 255));
931 return sb
.toString();
936 * ImageCache is a least-recently-used cache that hangs on to the
937 * post-rendered sixel or iTerm2 string for a particular set of cells.
939 private class ImageCache
{
942 * Maximum size of the cache.
944 private int maxSize
= 100;
947 * The entries stored in the cache.
949 private HashMap
<String
, CacheEntry
> cache
= null;
952 * CacheEntry is one entry in the cache.
954 private class CacheEntry
{
966 * The last time this entry was used.
968 public long millis
= 0;
971 * Public constructor.
973 * @param key the cache entry key
974 * @param data the cache entry data
976 public CacheEntry(final String key
, final String data
) {
979 this.millis
= System
.currentTimeMillis();
984 * Public constructor.
986 * @param maxSize the maximum size of the cache
988 public ImageCache(final int maxSize
) {
989 this.maxSize
= maxSize
;
990 cache
= new HashMap
<String
, CacheEntry
>();
994 * Make a unique key for a list of cells.
996 * @param cells the cells
999 private String
makeKey(final ArrayList
<Cell
> cells
) {
1000 StringBuilder sb
= new StringBuilder();
1001 for (Cell cell
: cells
) {
1002 sb
.append(cell
.hashCode());
1004 return sb
.toString();
1008 * Get an entry from the cache.
1010 * @param cells the list of cells that are the cache key
1011 * @return the sixel string representing these cells, or null if this
1012 * list of cells is not in the cache
1014 public String
get(final ArrayList
<Cell
> cells
) {
1015 CacheEntry entry
= cache
.get(makeKey(cells
));
1016 if (entry
== null) {
1019 entry
.millis
= System
.currentTimeMillis();
1024 * Put an entry into the cache.
1026 * @param cells the list of cells that are the cache key
1027 * @param data the sixel string representing these cells
1029 public void put(final ArrayList
<Cell
> cells
, final String data
) {
1030 String key
= makeKey(cells
);
1032 // System.err.println("put() " + key + " size " + cache.size());
1034 assert (!cache
.containsKey(key
));
1036 assert (cache
.size() <= maxSize
);
1037 if (cache
.size() == maxSize
) {
1038 // Cache is at limit, evict oldest entry.
1039 long oldestTime
= Long
.MAX_VALUE
;
1040 String keyToRemove
= null;
1041 for (CacheEntry entry
: cache
.values()) {
1042 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
1043 keyToRemove
= entry
.key
;
1044 oldestTime
= entry
.millis
;
1048 System.err.println("put() remove key = " + keyToRemove +
1049 " size " + cache.size());
1051 assert (keyToRemove
!= null);
1052 cache
.remove(keyToRemove
);
1054 System.err.println("put() removed, size " + cache.size());
1057 assert (cache
.size() <= maxSize
);
1058 CacheEntry entry
= new CacheEntry(key
, data
);
1059 assert (key
.equals(entry
.key
));
1060 cache
.put(key
, entry
);
1062 System.err.println("put() added key " + key + " " +
1063 " size " + cache.size());
1069 // ------------------------------------------------------------------------
1070 // Constructors -----------------------------------------------------------
1071 // ------------------------------------------------------------------------
1074 * Constructor sets up state for getEvent(). If either windowWidth or
1075 * windowHeight are less than 1, the terminal is not resized.
1077 * @param listener the object this backend needs to wake up when new
1079 * @param input an InputStream connected to the remote user, or null for
1080 * System.in. If System.in is used, then on non-Windows systems it will
1081 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1082 * cooked mode. input is always converted to a Reader with UTF-8
1084 * @param output an OutputStream connected to the remote user, or null
1085 * for System.out. output is always converted to a Writer with UTF-8
1087 * @param windowWidth the number of text columns to start with
1088 * @param windowHeight the number of text rows to start with
1089 * @throws UnsupportedEncodingException if an exception is thrown when
1090 * creating the InputStreamReader
1092 public ECMA48Terminal(final Object listener
, final InputStream input
,
1093 final OutputStream output
, final int windowWidth
,
1094 final int windowHeight
) throws UnsupportedEncodingException
{
1096 this(listener
, input
, output
);
1098 // Send dtterm/xterm sequences, which will probably not work because
1099 // allowWindowOps is defaulted to false.
1100 if ((windowWidth
> 0) && (windowHeight
> 0)) {
1101 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1103 this.output
.write(resizeString
);
1104 this.output
.flush();
1109 * Constructor sets up state for getEvent().
1111 * @param listener the object this backend needs to wake up when new
1113 * @param input an InputStream connected to the remote user, or null for
1114 * System.in. If System.in is used, then on non-Windows systems it will
1115 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1116 * cooked mode. input is always converted to a Reader with UTF-8
1118 * @param output an OutputStream connected to the remote user, or null
1119 * for System.out. output is always converted to a Writer with UTF-8
1121 * @throws UnsupportedEncodingException if an exception is thrown when
1122 * creating the InputStreamReader
1124 public ECMA48Terminal(final Object listener
, final InputStream input
,
1125 final OutputStream output
) throws UnsupportedEncodingException
{
1131 stopReaderThread
= false;
1132 this.listener
= listener
;
1134 if (input
== null) {
1135 // inputStream = System.in;
1136 inputStream
= new FileInputStream(FileDescriptor
.in
);
1140 inputStream
= input
;
1142 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1144 if (input
instanceof SessionInfo
) {
1145 // This is a TelnetInputStream that exposes window size and
1146 // environment variables from the telnet layer.
1147 sessionInfo
= (SessionInfo
) input
;
1149 if (sessionInfo
== null) {
1150 if (input
== null) {
1151 // Reading right off the tty
1152 sessionInfo
= new TTYSessionInfo();
1154 sessionInfo
= new TSessionInfo();
1158 if (output
== null) {
1159 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1162 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1166 // Request Device Attributes
1167 this.output
.printf("\033[c");
1169 // Request xterm report window/cell dimensions in pixels
1170 this.output
.printf("%s", xtermReportPixelDimensions());
1172 // Enable mouse reporting and metaSendsEscape
1173 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1175 // Request xterm use the sixel settings we want
1176 this.output
.printf("%s", xtermSetSixelSettings());
1178 this.output
.flush();
1180 // Query the screen size
1181 sessionInfo
.queryWindowSize();
1182 setDimensions(sessionInfo
.getWindowWidth(),
1183 sessionInfo
.getWindowHeight());
1185 // Hang onto the window size
1186 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1187 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1191 // Spin up the input reader
1192 eventQueue
= new ArrayList
<TInputEvent
>();
1193 readerThread
= new Thread(this);
1194 readerThread
.start();
1197 this.output
.write(clearAll());
1198 this.output
.flush();
1202 * Constructor sets up state for getEvent().
1204 * @param listener the object this backend needs to wake up when new
1206 * @param input the InputStream underlying 'reader'. Its available()
1207 * method is used to determine if reader.read() will block or not.
1208 * @param reader a Reader connected to the remote user.
1209 * @param writer a PrintWriter connected to the remote user.
1210 * @param setRawMode if true, set System.in into raw mode with stty.
1211 * This should in general not be used. It is here solely for Demo3,
1212 * which uses System.in.
1213 * @throws IllegalArgumentException if input, reader, or writer are null.
1215 public ECMA48Terminal(final Object listener
, final InputStream input
,
1216 final Reader reader
, final PrintWriter writer
,
1217 final boolean setRawMode
) {
1219 if (input
== null) {
1220 throw new IllegalArgumentException("InputStream must be specified");
1222 if (reader
== null) {
1223 throw new IllegalArgumentException("Reader must be specified");
1225 if (writer
== null) {
1226 throw new IllegalArgumentException("Writer must be specified");
1232 stopReaderThread
= false;
1233 this.listener
= listener
;
1235 inputStream
= input
;
1236 this.input
= reader
;
1238 if (setRawMode
== true) {
1241 this.setRawMode
= setRawMode
;
1243 if (input
instanceof SessionInfo
) {
1244 // This is a TelnetInputStream that exposes window size and
1245 // environment variables from the telnet layer.
1246 sessionInfo
= (SessionInfo
) input
;
1248 if (sessionInfo
== null) {
1249 if (setRawMode
== true) {
1250 // Reading right off the tty
1251 sessionInfo
= new TTYSessionInfo();
1253 sessionInfo
= new TSessionInfo();
1257 this.output
= writer
;
1259 // Request Device Attributes
1260 this.output
.printf("\033[c");
1262 // Request xterm report window/cell dimensions in pixels
1263 this.output
.printf("%s", xtermReportPixelDimensions());
1265 // Enable mouse reporting and metaSendsEscape
1266 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1268 // Request xterm use the sixel settings we want
1269 this.output
.printf("%s", xtermSetSixelSettings());
1271 this.output
.flush();
1273 // Query the screen size
1274 sessionInfo
.queryWindowSize();
1275 setDimensions(sessionInfo
.getWindowWidth(),
1276 sessionInfo
.getWindowHeight());
1278 // Hang onto the window size
1279 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1280 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1284 // Spin up the input reader
1285 eventQueue
= new ArrayList
<TInputEvent
>();
1286 readerThread
= new Thread(this);
1287 readerThread
.start();
1290 this.output
.write(clearAll());
1291 this.output
.flush();
1295 * Constructor sets up state for getEvent().
1297 * @param listener the object this backend needs to wake up when new
1299 * @param input the InputStream underlying 'reader'. Its available()
1300 * method is used to determine if reader.read() will block or not.
1301 * @param reader a Reader connected to the remote user.
1302 * @param writer a PrintWriter connected to the remote user.
1303 * @throws IllegalArgumentException if input, reader, or writer are null.
1305 public ECMA48Terminal(final Object listener
, final InputStream input
,
1306 final Reader reader
, final PrintWriter writer
) {
1308 this(listener
, input
, reader
, writer
, false);
1311 // ------------------------------------------------------------------------
1312 // LogicalScreen ----------------------------------------------------------
1313 // ------------------------------------------------------------------------
1316 * Set the window title.
1318 * @param title the new title
1321 public void setTitle(final String title
) {
1322 output
.write(getSetTitleString(title
));
1327 * Push the logical screen to the physical device.
1330 public void flushPhysical() {
1331 StringBuilder sb
= new StringBuilder();
1335 && (cursorY
<= height
- 1)
1336 && (cursorX
<= width
- 1)
1339 sb
.append(cursor(true));
1340 sb
.append(gotoXY(cursorX
, cursorY
));
1342 sb
.append(cursor(false));
1345 output
.write(sb
.toString());
1350 * Resize the physical screen to match the logical screen dimensions.
1353 public void resizeToScreen() {
1354 // Send dtterm/xterm sequences, which will probably not work because
1355 // allowWindowOps is defaulted to false.
1356 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1358 this.output
.write(resizeString
);
1359 this.output
.flush();
1362 // ------------------------------------------------------------------------
1363 // TerminalReader ---------------------------------------------------------
1364 // ------------------------------------------------------------------------
1367 * Check if there are events in the queue.
1369 * @return if true, getEvents() has something to return to the backend
1371 public boolean hasEvents() {
1372 synchronized (eventQueue
) {
1373 return (eventQueue
.size() > 0);
1378 * Return any events in the IO queue.
1380 * @param queue list to append new events to
1382 public void getEvents(final List
<TInputEvent
> queue
) {
1383 synchronized (eventQueue
) {
1384 if (eventQueue
.size() > 0) {
1385 synchronized (queue
) {
1386 queue
.addAll(eventQueue
);
1394 * Restore terminal to normal state.
1396 public void closeTerminal() {
1398 // System.err.println("=== closeTerminal() ==="); System.err.flush();
1400 // Tell the reader thread to stop looking at input
1401 stopReaderThread
= true;
1403 readerThread
.join();
1404 } catch (InterruptedException e
) {
1405 if (debugToStderr
) {
1406 e
.printStackTrace();
1410 // Disable mouse reporting and show cursor. Defensive null check
1411 // here in case closeTerminal() is called twice.
1412 if (output
!= null) {
1413 output
.printf("%s%s%s%s", mouse(false), cursor(true),
1414 defaultColor(), xtermResetSixelSettings());
1421 // We don't close System.in/out
1423 // Shut down the streams, this should wake up the reader thread
1424 // and make it exit.
1425 if (input
!= null) {
1428 } catch (IOException e
) {
1433 if (output
!= null) {
1441 * Set listener to a different Object.
1443 * @param listener the new listening object that run() wakes up on new
1446 public void setListener(final Object listener
) {
1447 this.listener
= listener
;
1451 * Reload options from System properties.
1453 public void reloadOptions() {
1454 // Permit RGB colors only if externally requested.
1455 if (System
.getProperty("jexer.ECMA48.rgbColor",
1456 "false").equals("true")
1463 // Default to using images for full-width characters.
1464 if (System
.getProperty("jexer.ECMA48.wideCharImages",
1465 "true").equals("true")) {
1466 wideCharImages
= true;
1468 wideCharImages
= false;
1471 // Pull the system properties for sixel output.
1472 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1479 int paletteSize
= 1024;
1481 paletteSize
= Integer
.parseInt(System
.getProperty(
1482 "jexer.ECMA48.sixelPaletteSize", "1024"));
1483 switch (paletteSize
) {
1489 sixelPaletteSize
= paletteSize
;
1495 } catch (NumberFormatException e
) {
1500 if (System
.getProperty("jexer.ECMA48.sixelSharedPalette",
1501 "true").equals("false")) {
1502 sixelSharedPalette
= false;
1504 sixelSharedPalette
= true;
1507 // Default to not supporting iTerm2 images.
1508 if (System
.getProperty("jexer.ECMA48.iTerm2Images",
1509 "false").equals("true")) {
1510 iterm2Images
= true;
1512 iterm2Images
= false;
1515 // Default to using JPG Jexer images if terminal supports it.
1516 String jexerImageStr
= System
.getProperty("jexer.ECMA48.jexerImages",
1517 "jpg").toLowerCase();
1518 if (jexerImageStr
.equals("false")) {
1519 jexerImageOption
= JexerImageOption
.DISABLED
;
1520 } else if (jexerImageStr
.equals("jpg")) {
1521 jexerImageOption
= JexerImageOption
.JPG
;
1522 } else if (jexerImageStr
.equals("png")) {
1523 jexerImageOption
= JexerImageOption
.PNG
;
1524 } else if (jexerImageStr
.equals("rgb")) {
1525 jexerImageOption
= JexerImageOption
.RGB
;
1528 // Set custom colors
1529 setCustomSystemColors();
1532 // ------------------------------------------------------------------------
1533 // Runnable ---------------------------------------------------------------
1534 // ------------------------------------------------------------------------
1537 * Read function runs on a separate thread.
1540 boolean done
= false;
1541 // available() will often return > 1, so we need to read in chunks to
1543 char [] readBuffer
= new char[128];
1544 List
<TInputEvent
> events
= new ArrayList
<TInputEvent
>();
1546 while (!done
&& !stopReaderThread
) {
1548 // We assume that if inputStream has bytes available, then
1549 // input won't block on read().
1550 int n
= inputStream
.available();
1553 System.err.printf("inputStream.available(): %d\n", n);
1558 if (readBuffer
.length
< n
) {
1559 // The buffer wasn't big enough, make it huger
1560 readBuffer
= new char[readBuffer
.length
* 2];
1563 // System.err.printf("BEFORE read()\n"); System.err.flush();
1565 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1568 System.err.printf("AFTER read() %d\n", rc);
1576 for (int i
= 0; i
< rc
; i
++) {
1577 int ch
= readBuffer
[i
];
1578 processChar(events
, (char)ch
);
1580 getIdleEvents(events
);
1581 if (events
.size() > 0) {
1582 // Add to the queue for the backend thread to
1583 // be able to obtain.
1584 synchronized (eventQueue
) {
1585 eventQueue
.addAll(events
);
1587 if (listener
!= null) {
1588 synchronized (listener
) {
1589 listener
.notifyAll();
1596 getIdleEvents(events
);
1597 if (events
.size() > 0) {
1598 synchronized (eventQueue
) {
1599 eventQueue
.addAll(events
);
1601 if (listener
!= null) {
1602 synchronized (listener
) {
1603 listener
.notifyAll();
1609 if (output
.checkError()) {
1614 // Wait 20 millis for more data
1617 // System.err.println("end while loop"); System.err.flush();
1618 } catch (InterruptedException e
) {
1620 } catch (IOException e
) {
1621 e
.printStackTrace();
1624 } // while ((done == false) && (stopReaderThread == false))
1626 // Pass an event up to TApplication to tell it this Backend is done.
1627 synchronized (eventQueue
) {
1628 eventQueue
.add(new TCommandEvent(cmBackendDisconnect
));
1630 if (listener
!= null) {
1631 synchronized (listener
) {
1632 listener
.notifyAll();
1636 // System.err.println("*** run() exiting..."); System.err.flush();
1639 // ------------------------------------------------------------------------
1640 // ECMA48Terminal ---------------------------------------------------------
1641 // ------------------------------------------------------------------------
1644 * Get the width of a character cell in pixels.
1646 * @return the width in pixels of a character cell
1648 public int getTextWidth() {
1649 if (sessionInfo
.getWindowWidth() > 0) {
1650 return (widthPixels
/ sessionInfo
.getWindowWidth());
1656 * Get the height of a character cell in pixels.
1658 * @return the height in pixels of a character cell
1660 public int getTextHeight() {
1661 if (sessionInfo
.getWindowHeight() > 0) {
1662 return (heightPixels
/ sessionInfo
.getWindowHeight());
1668 * Getter for sessionInfo.
1670 * @return the SessionInfo
1672 public SessionInfo
getSessionInfo() {
1677 * Get the output writer.
1679 * @return the Writer
1681 public PrintWriter
getOutput() {
1686 * Call 'stty' to set cooked mode.
1688 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1690 private void sttyCooked() {
1695 * Call 'stty' to set raw mode.
1697 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1698 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1699 * -parenb cs8 min 1 < /dev/tty'
1701 private void sttyRaw() {
1706 * Call 'stty' to set raw or cooked mode.
1708 * @param mode if true, set raw mode, otherwise set cooked mode
1710 private void doStty(final boolean mode
) {
1711 String
[] cmdRaw
= {
1712 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1714 String
[] cmdCooked
= {
1715 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1720 process
= Runtime
.getRuntime().exec(cmdRaw
);
1722 process
= Runtime
.getRuntime().exec(cmdCooked
);
1724 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1725 String line
= in
.readLine();
1726 if ((line
!= null) && (line
.length() > 0)) {
1727 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1730 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1731 line
= err
.readLine();
1732 if ((line
!= null) && (line
.length() > 0)) {
1733 System
.err
.println("Error output from stty: " + line
);
1738 } catch (InterruptedException e
) {
1739 if (debugToStderr
) {
1740 e
.printStackTrace();
1744 int rc
= process
.exitValue();
1746 System
.err
.println("stty returned error code: " + rc
);
1748 } catch (IOException e
) {
1749 e
.printStackTrace();
1756 public void flush() {
1761 * Perform a somewhat-optimal rendering of a line.
1763 * @param y row coordinate. 0 is the top-most row.
1764 * @param sb StringBuilder to write escape sequences to
1765 * @param lastAttr cell attributes from the last call to flushLine
1767 private void flushLine(final int y
, final StringBuilder sb
,
1768 CellAttributes lastAttr
) {
1772 for (int x
= 0; x
< width
; x
++) {
1773 Cell lCell
= logical
[x
][y
];
1774 if (!lCell
.isBlank()) {
1778 // Push textEnd to first column beyond the text area
1782 // reallyCleared = true;
1784 boolean hasImage
= false;
1786 for (int x
= 0; x
< width
; x
++) {
1787 Cell lCell
= logical
[x
][y
];
1788 Cell pCell
= physical
[x
][y
];
1790 if (!lCell
.equals(pCell
) || reallyCleared
) {
1792 if (debugToStderr
) {
1793 System
.err
.printf("\n--\n");
1794 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1795 System
.err
.printf(" lCell: %s\n", lCell
);
1796 System
.err
.printf(" pCell: %s\n", pCell
);
1797 System
.err
.printf(" ==== \n");
1800 if (lastAttr
== null) {
1801 lastAttr
= new CellAttributes();
1802 sb
.append(normal());
1806 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1807 // Advancing at least one cell, or the first gotoXY
1808 sb
.append(gotoXY(x
, y
));
1811 assert (lastAttr
!= null);
1813 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1814 assert (lCell
.isBlank());
1816 for (int i
= x
; i
< width
; i
++) {
1817 assert (logical
[i
][y
].isBlank());
1818 // Physical is always updated
1819 physical
[i
][y
].reset();
1822 // Clear remaining line
1823 sb
.append(clearRemainingLine());
1828 // Image cell: bypass the rest of the loop, it is not
1830 if ((wideCharImages
&& lCell
.isImage())
1833 && (lCell
.getWidth() == Cell
.Width
.SINGLE
))
1837 // Save the last rendered cell
1840 // Physical is always updated
1841 physical
[x
][y
].setTo(lCell
);
1845 assert ((wideCharImages
&& !lCell
.isImage())
1847 && (!lCell
.isImage()
1849 && (lCell
.getWidth() != Cell
.Width
.SINGLE
)))));
1851 if (!wideCharImages
&& (lCell
.getWidth() == Cell
.Width
.RIGHT
)) {
1857 sb
.append(gotoXY(x
, y
));
1860 // Now emit only the modified attributes
1861 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1862 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1864 && (lCell
.isBold() == lastAttr
.isBold())
1865 && (lCell
.isReverse() == lastAttr
.isReverse())
1866 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1867 && (lCell
.isBlink() == lastAttr
.isBlink())
1869 // Both colors changed, attributes the same
1870 sb
.append(color(lCell
.isBold(),
1871 lCell
.getForeColor(), lCell
.getBackColor()));
1873 if (debugToStderr
) {
1874 System
.err
.printf("1 Change only fore/back colors\n");
1877 } else if (lCell
.isRGB()
1878 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1879 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1880 && (lCell
.isBold() == lastAttr
.isBold())
1881 && (lCell
.isReverse() == lastAttr
.isReverse())
1882 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1883 && (lCell
.isBlink() == lastAttr
.isBlink())
1885 // Both colors changed, attributes the same
1886 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1887 lCell
.getBackColorRGB()));
1889 if (debugToStderr
) {
1890 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1892 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1893 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1895 && (lCell
.isBold() != lastAttr
.isBold())
1896 && (lCell
.isReverse() != lastAttr
.isReverse())
1897 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1898 && (lCell
.isBlink() != lastAttr
.isBlink())
1900 // Everything is different
1901 sb
.append(color(lCell
.getForeColor(),
1902 lCell
.getBackColor(),
1903 lCell
.isBold(), lCell
.isReverse(),
1905 lCell
.isUnderline()));
1907 if (debugToStderr
) {
1908 System
.err
.printf("2 Set all attributes\n");
1910 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1911 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1913 && (lCell
.isBold() == lastAttr
.isBold())
1914 && (lCell
.isReverse() == lastAttr
.isReverse())
1915 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1916 && (lCell
.isBlink() == lastAttr
.isBlink())
1919 // Attributes same, foreColor different
1920 sb
.append(color(lCell
.isBold(),
1921 lCell
.getForeColor(), true));
1923 if (debugToStderr
) {
1924 System
.err
.printf("3 Change foreColor\n");
1926 } else if (lCell
.isRGB()
1927 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1928 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1929 && (lCell
.getForeColorRGB() >= 0)
1930 && (lCell
.getBackColorRGB() >= 0)
1931 && (lCell
.isBold() == lastAttr
.isBold())
1932 && (lCell
.isReverse() == lastAttr
.isReverse())
1933 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1934 && (lCell
.isBlink() == lastAttr
.isBlink())
1936 // Attributes same, foreColor different
1937 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1939 if (debugToStderr
) {
1940 System
.err
.printf("3 Change foreColor (RGB)\n");
1942 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1943 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1945 && (lCell
.isBold() == lastAttr
.isBold())
1946 && (lCell
.isReverse() == lastAttr
.isReverse())
1947 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1948 && (lCell
.isBlink() == lastAttr
.isBlink())
1950 // Attributes same, backColor different
1951 sb
.append(color(lCell
.isBold(),
1952 lCell
.getBackColor(), false));
1954 if (debugToStderr
) {
1955 System
.err
.printf("4 Change backColor\n");
1957 } else if (lCell
.isRGB()
1958 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1959 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1960 && (lCell
.isBold() == lastAttr
.isBold())
1961 && (lCell
.isReverse() == lastAttr
.isReverse())
1962 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1963 && (lCell
.isBlink() == lastAttr
.isBlink())
1965 // Attributes same, foreColor different
1966 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1968 if (debugToStderr
) {
1969 System
.err
.printf("4 Change backColor (RGB)\n");
1971 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1972 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1973 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1974 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1975 && (lCell
.isBold() == lastAttr
.isBold())
1976 && (lCell
.isReverse() == lastAttr
.isReverse())
1977 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1978 && (lCell
.isBlink() == lastAttr
.isBlink())
1981 // All attributes the same, just print the char
1984 if (debugToStderr
) {
1985 System
.err
.printf("5 Only emit character\n");
1988 // Just reset everything again
1989 if (!lCell
.isRGB()) {
1990 sb
.append(color(lCell
.getForeColor(),
1991 lCell
.getBackColor(),
1995 lCell
.isUnderline()));
1997 if (debugToStderr
) {
1998 System
.err
.printf("6 Change all attributes\n");
2001 sb
.append(colorRGB(lCell
.getForeColorRGB(),
2002 lCell
.getBackColorRGB(),
2006 lCell
.isUnderline()));
2007 if (debugToStderr
) {
2008 System
.err
.printf("6 Change all attributes (RGB)\n");
2013 // Emit the character
2015 // Don't emit the right-half of full-width chars.
2017 && (lCell
.getWidth() != Cell
.Width
.RIGHT
))
2019 sb
.append(Character
.toChars(lCell
.getChar()));
2022 // Save the last rendered cell
2024 lastAttr
.setTo(lCell
);
2026 // Physical is always updated
2027 physical
[x
][y
].setTo(lCell
);
2029 } // if (!lCell.equals(pCell) || (reallyCleared == true))
2031 } // for (int x = 0; x < width; x++)
2035 * Render the screen to a string that can be emitted to something that
2036 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
2038 * @param sb StringBuilder to write escape sequences to
2039 * @return escape sequences string that provides the updates to the
2042 private String
flushString(final StringBuilder sb
) {
2043 CellAttributes attr
= null;
2045 if (reallyCleared
) {
2046 attr
= new CellAttributes();
2047 sb
.append(clearAll());
2051 * For images support, draw all of the image output first, and then
2052 * draw everything else afterwards. This works OK, but performance
2053 * is still a drag on larger pictures.
2055 for (int y
= 0; y
< height
; y
++) {
2056 for (int x
= 0; x
< width
; x
++) {
2057 // If physical had non-image data that is now image data, the
2058 // entire row must be redrawn.
2059 Cell lCell
= logical
[x
][y
];
2060 Cell pCell
= physical
[x
][y
];
2061 if (lCell
.isImage() && !pCell
.isImage()) {
2067 for (int y
= 0; y
< height
; y
++) {
2068 for (int x
= 0; x
< width
; x
++) {
2069 Cell lCell
= logical
[x
][y
];
2070 Cell pCell
= physical
[x
][y
];
2072 if (!lCell
.isImage()
2074 && (lCell
.getWidth() != Cell
.Width
.SINGLE
))
2081 while ((right
< width
)
2082 && (logical
[right
][y
].isImage())
2083 && (!logical
[right
][y
].equals(physical
[right
][y
])
2088 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
2089 for (int i
= 0; i
< (right
- x
); i
++) {
2090 assert (logical
[x
+ i
][y
].isImage());
2091 cellsToDraw
.add(logical
[x
+ i
][y
]);
2093 // Physical is always updated.
2094 physical
[x
+ i
][y
].setTo(lCell
);
2096 if (cellsToDraw
.size() > 0) {
2098 sb
.append(toIterm2Image(x
, y
, cellsToDraw
));
2099 } else if (jexerImageOption
!= JexerImageOption
.DISABLED
) {
2100 sb
.append(toJexerImage(x
, y
, cellsToDraw
));
2102 sb
.append(toSixel(x
, y
, cellsToDraw
));
2110 // Draw the text part now.
2111 for (int y
= 0; y
< height
; y
++) {
2112 flushLine(y
, sb
, attr
);
2115 reallyCleared
= false;
2117 String result
= sb
.toString();
2118 if (debugToStderr
) {
2119 System
.err
.printf("flushString(): %s\n", result
);
2125 * Reset keyboard/mouse input parser.
2127 private void resetParser() {
2128 state
= ParseState
.GROUND
;
2129 params
= new ArrayList
<String
>();
2132 decPrivateModeFlag
= false;
2136 * Produce a control character or one of the special ones (ENTER, TAB,
2139 * @param ch Unicode code point
2140 * @param alt if true, set alt on the TKeypress
2141 * @return one TKeypress event, either a control character (e.g. isKey ==
2142 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
2145 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
2146 // System.err.printf("controlChar: %02x\n", ch);
2150 // Carriage return --> ENTER
2151 return new TKeypressEvent(kbEnter
, alt
, false, false);
2153 // Linefeed --> ENTER
2154 return new TKeypressEvent(kbEnter
, alt
, false, false);
2157 return new TKeypressEvent(kbEsc
, alt
, false, false);
2160 return new TKeypressEvent(kbTab
, alt
, false, false);
2162 // Make all other control characters come back as the alphabetic
2163 // character with the ctrl field set. So SOH would be 'A' +
2165 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
2171 * Produce special key from CSI Pn ; Pm ; ... ~
2173 * @return one KEYPRESS event representing a special key
2175 private TInputEvent
csiFnKey() {
2177 if (params
.size() > 0) {
2178 key
= Integer
.parseInt(params
.get(0));
2180 boolean alt
= false;
2181 boolean ctrl
= false;
2182 boolean shift
= false;
2183 if (params
.size() > 1) {
2184 shift
= csiIsShift(params
.get(1));
2185 alt
= csiIsAlt(params
.get(1));
2186 ctrl
= csiIsCtrl(params
.get(1));
2191 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2193 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2195 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2197 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2199 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2201 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2203 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2205 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2207 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2209 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2211 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2213 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2215 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2217 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2225 * Produce mouse events based on "Any event tracking" and UTF-8
2227 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2229 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2231 private TInputEvent
parseMouse() {
2232 int buttons
= params
.get(0).charAt(0) - 32;
2233 int x
= params
.get(0).charAt(1) - 32 - 1;
2234 int y
= params
.get(0).charAt(2) - 32 - 1;
2236 // Clamp X and Y to the physical screen coordinates.
2237 if (x
>= windowResize
.getWidth()) {
2238 x
= windowResize
.getWidth() - 1;
2240 if (y
>= windowResize
.getHeight()) {
2241 y
= windowResize
.getHeight() - 1;
2244 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2245 boolean eventMouse1
= false;
2246 boolean eventMouse2
= false;
2247 boolean eventMouse3
= false;
2248 boolean eventMouseWheelUp
= false;
2249 boolean eventMouseWheelDown
= false;
2250 boolean eventAlt
= false;
2251 boolean eventCtrl
= false;
2252 boolean eventShift
= false;
2254 // System.err.printf("buttons: %04x\r\n", buttons);
2256 switch (buttons
& 0xE3) {
2271 if (!mouse1
&& !mouse2
&& !mouse3
) {
2272 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2274 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2291 // Dragging with mouse1 down
2294 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2298 // Dragging with mouse2 down
2301 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2305 // Dragging with mouse3 down
2308 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2312 // Dragging with mouse2 down after wheelUp
2315 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2319 // Dragging with mouse2 down after wheelDown
2322 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2326 eventMouseWheelUp
= true;
2330 eventMouseWheelDown
= true;
2334 // Unknown, just make it motion
2335 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2339 if ((buttons
& 0x04) != 0) {
2342 if ((buttons
& 0x08) != 0) {
2345 if ((buttons
& 0x10) != 0) {
2349 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2350 eventMouse1
, eventMouse2
, eventMouse3
,
2351 eventMouseWheelUp
, eventMouseWheelDown
,
2352 eventAlt
, eventCtrl
, eventShift
);
2356 * Produce mouse events based on "Any event tracking" and SGR
2358 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2360 * @param release if true, this was a release ('m')
2361 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2363 private TInputEvent
parseMouseSGR(final boolean release
) {
2364 // SGR extended coordinates - mode 1006
2365 if (params
.size() < 3) {
2366 // Invalid position, bail out.
2369 int buttons
= Integer
.parseInt(params
.get(0));
2370 int x
= Integer
.parseInt(params
.get(1)) - 1;
2371 int y
= Integer
.parseInt(params
.get(2)) - 1;
2373 // Clamp X and Y to the physical screen coordinates.
2374 if (x
>= windowResize
.getWidth()) {
2375 x
= windowResize
.getWidth() - 1;
2377 if (y
>= windowResize
.getHeight()) {
2378 y
= windowResize
.getHeight() - 1;
2381 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2382 boolean eventMouse1
= false;
2383 boolean eventMouse2
= false;
2384 boolean eventMouse3
= false;
2385 boolean eventMouseWheelUp
= false;
2386 boolean eventMouseWheelDown
= false;
2387 boolean eventAlt
= false;
2388 boolean eventCtrl
= false;
2389 boolean eventShift
= false;
2392 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2395 switch (buttons
& 0xE3) {
2406 // Motion only, no buttons down
2407 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2411 // Dragging with mouse1 down
2413 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2417 // Dragging with mouse2 down
2419 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2423 // Dragging with mouse3 down
2425 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2429 // Dragging with mouse2 down after wheelUp
2431 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2435 // Dragging with mouse2 down after wheelDown
2437 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2441 eventMouseWheelUp
= true;
2445 eventMouseWheelDown
= true;
2449 // Unknown, bail out
2453 if ((buttons
& 0x04) != 0) {
2456 if ((buttons
& 0x08) != 0) {
2459 if ((buttons
& 0x10) != 0) {
2463 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2464 eventMouse1
, eventMouse2
, eventMouse3
,
2465 eventMouseWheelUp
, eventMouseWheelDown
,
2466 eventAlt
, eventCtrl
, eventShift
);
2470 * Return any events in the IO queue due to timeout.
2472 * @param queue list to append new events to
2474 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2475 long nowTime
= System
.currentTimeMillis();
2477 // Check for new window size
2478 long windowSizeDelay
= nowTime
- windowSizeTime
;
2479 if (windowSizeDelay
> 1000) {
2480 int oldTextWidth
= getTextWidth();
2481 int oldTextHeight
= getTextHeight();
2483 sessionInfo
.queryWindowSize();
2484 int newWidth
= sessionInfo
.getWindowWidth();
2485 int newHeight
= sessionInfo
.getWindowHeight();
2487 if ((newWidth
!= windowResize
.getWidth())
2488 || (newHeight
!= windowResize
.getHeight())
2491 // Request xterm report window dimensions in pixels again.
2492 // Between now and then, ensure that the reported text cell
2493 // size is the same by setting widthPixels and heightPixels
2494 // to match the new dimensions.
2495 widthPixels
= oldTextWidth
* newWidth
;
2496 heightPixels
= oldTextHeight
* newHeight
;
2498 if (debugToStderr
) {
2499 System
.err
.println("Screen size changed, old size " +
2501 System
.err
.println(" new size " +
2502 newWidth
+ " x " + newHeight
);
2503 System
.err
.println(" old pixels " +
2504 oldTextWidth
+ " x " + oldTextHeight
);
2505 System
.err
.println(" new pixels " +
2506 getTextWidth() + " x " + getTextHeight());
2509 this.output
.printf("%s", xtermReportPixelDimensions());
2510 this.output
.flush();
2512 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2513 newWidth
, newHeight
);
2514 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2515 newWidth
, newHeight
);
2518 windowSizeTime
= nowTime
;
2521 // ESCDELAY type timeout
2522 if (state
== ParseState
.ESCAPE
) {
2523 long escDelay
= nowTime
- escapeTime
;
2524 if (escDelay
> 100) {
2525 // After 0.1 seconds, assume a true escape character
2526 queue
.add(controlChar((char)0x1B, false));
2533 * Returns true if the CSI parameter for a keyboard command means that
2536 private boolean csiIsShift(final String x
) {
2548 * Returns true if the CSI parameter for a keyboard command means that
2551 private boolean csiIsAlt(final String x
) {
2563 * Returns true if the CSI parameter for a keyboard command means that
2566 private boolean csiIsCtrl(final String x
) {
2578 * Parses the next character of input to see if an InputEvent is
2581 * @param events list to append new events to
2582 * @param ch Unicode code point
2584 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2586 // ESCDELAY type timeout
2587 long nowTime
= System
.currentTimeMillis();
2588 if (state
== ParseState
.ESCAPE
) {
2589 long escDelay
= nowTime
- escapeTime
;
2590 if (escDelay
> 250) {
2591 // After 0.25 seconds, assume a true escape character
2592 events
.add(controlChar((char)0x1B, false));
2598 boolean ctrl
= false;
2599 boolean alt
= false;
2600 boolean shift
= false;
2602 // System.err.printf("state: %s ch %c\r\n", state, ch);
2608 state
= ParseState
.ESCAPE
;
2609 escapeTime
= nowTime
;
2614 // Control character
2615 events
.add(controlChar(ch
, false));
2622 events
.add(new TKeypressEvent(false, 0, ch
,
2623 false, false, false));
2632 // ALT-Control character
2633 events
.add(controlChar(ch
, true));
2639 // This will be one of the function keys
2640 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2644 // '[' goes to CSI_ENTRY
2646 state
= ParseState
.CSI_ENTRY
;
2650 // Everything else is assumed to be Alt-keystroke
2651 if ((ch
>= 'A') && (ch
<= 'Z')) {
2655 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2659 case ESCAPE_INTERMEDIATE
:
2660 if ((ch
>= 'P') && (ch
<= 'S')) {
2664 events
.add(new TKeypressEvent(kbF1
));
2667 events
.add(new TKeypressEvent(kbF2
));
2670 events
.add(new TKeypressEvent(kbF3
));
2673 events
.add(new TKeypressEvent(kbF4
));
2682 // Unknown keystroke, ignore
2687 // Numbers - parameter values
2688 if ((ch
>= '0') && (ch
<= '9')) {
2689 params
.set(params
.size() - 1,
2690 params
.get(params
.size() - 1) + ch
);
2691 state
= ParseState
.CSI_PARAM
;
2694 // Parameter separator
2700 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2704 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2709 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2714 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2719 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2724 events
.add(new TKeypressEvent(kbHome
));
2729 events
.add(new TKeypressEvent(kbEnd
));
2733 // CBT - Cursor backward X tab stops (default 1)
2734 events
.add(new TKeypressEvent(kbBackTab
));
2739 state
= ParseState
.MOUSE
;
2742 // Mouse position, SGR (1006) coordinates
2743 state
= ParseState
.MOUSE_SGR
;
2746 // DEC private mode flag
2747 decPrivateModeFlag
= true;
2754 // Unknown keystroke, ignore
2759 // Numbers - parameter values
2760 if ((ch
>= '0') && (ch
<= '9')) {
2761 params
.set(params
.size() - 1,
2762 params
.get(params
.size() - 1) + ch
);
2765 // Parameter separator
2773 // Generate a mouse press event
2774 TInputEvent event
= parseMouseSGR(false);
2775 if (event
!= null) {
2781 // Generate a mouse release event
2782 event
= parseMouseSGR(true);
2783 if (event
!= null) {
2792 // Unknown keystroke, ignore
2797 // Numbers - parameter values
2798 if ((ch
>= '0') && (ch
<= '9')) {
2799 params
.set(params
.size() - 1,
2800 params
.get(params
.size() - 1) + ch
);
2801 state
= ParseState
.CSI_PARAM
;
2804 // Parameter separator
2811 events
.add(csiFnKey());
2816 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2820 if (params
.size() > 1) {
2821 shift
= csiIsShift(params
.get(1));
2822 alt
= csiIsAlt(params
.get(1));
2823 ctrl
= csiIsCtrl(params
.get(1));
2825 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2830 if (params
.size() > 1) {
2831 shift
= csiIsShift(params
.get(1));
2832 alt
= csiIsAlt(params
.get(1));
2833 ctrl
= csiIsCtrl(params
.get(1));
2835 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2840 if (params
.size() > 1) {
2841 shift
= csiIsShift(params
.get(1));
2842 alt
= csiIsAlt(params
.get(1));
2843 ctrl
= csiIsCtrl(params
.get(1));
2845 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2850 if (params
.size() > 1) {
2851 shift
= csiIsShift(params
.get(1));
2852 alt
= csiIsAlt(params
.get(1));
2853 ctrl
= csiIsCtrl(params
.get(1));
2855 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2860 if (params
.size() > 1) {
2861 shift
= csiIsShift(params
.get(1));
2862 alt
= csiIsAlt(params
.get(1));
2863 ctrl
= csiIsCtrl(params
.get(1));
2865 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2870 if (params
.size() > 1) {
2871 shift
= csiIsShift(params
.get(1));
2872 alt
= csiIsAlt(params
.get(1));
2873 ctrl
= csiIsCtrl(params
.get(1));
2875 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2879 // Device Attributes
2880 if (decPrivateModeFlag
== false) {
2883 boolean jexerImages
= false;
2884 for (String x
: params
) {
2885 if (x
.equals("4")) {
2886 // Terminal reports sixel support
2887 if (debugToStderr
) {
2888 System
.err
.println("Device Attributes: sixel");
2891 if (x
.equals("444")) {
2892 // Terminal reports Jexer images support
2893 if (debugToStderr
) {
2894 System
.err
.println("Device Attributes: Jexer images");
2898 if (x
.equals("1337")) {
2899 // Terminal reports iTerm2 images support
2900 if (debugToStderr
) {
2901 System
.err
.println("Device Attributes: iTerm2 images");
2903 iterm2Images
= true;
2906 if (jexerImages
== false) {
2907 // Terminal does not support Jexer images, disable
2909 jexerImageOption
= JexerImageOption
.DISABLED
;
2915 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2916 if (debugToStderr
) {
2917 System
.err
.printf("windowOp pixels: " +
2918 "height %s width %s\n",
2919 params
.get(1), params
.get(2));
2922 widthPixels
= Integer
.parseInt(params
.get(2));
2923 heightPixels
= Integer
.parseInt(params
.get(1));
2924 } catch (NumberFormatException e
) {
2925 if (debugToStderr
) {
2926 e
.printStackTrace();
2929 if (widthPixels
<= 0) {
2932 if (heightPixels
<= 0) {
2936 if ((params
.size() > 2) && (params
.get(0).equals("6"))) {
2937 if (debugToStderr
) {
2938 System
.err
.printf("windowOp text cell pixels: " +
2939 "height %s width %s\n",
2940 params
.get(1), params
.get(2));
2943 widthPixels
= width
* Integer
.parseInt(params
.get(2));
2944 heightPixels
= height
* Integer
.parseInt(params
.get(1));
2945 } catch (NumberFormatException e
) {
2946 if (debugToStderr
) {
2947 e
.printStackTrace();
2950 if (widthPixels
<= 0) {
2953 if (heightPixels
<= 0) {
2964 // Unknown keystroke, ignore
2969 params
.set(0, params
.get(params
.size() - 1) + ch
);
2970 if (params
.get(0).length() == 3) {
2971 // We have enough to generate a mouse event
2972 events
.add(parseMouse());
2981 // This "should" be impossible to reach
2986 * Request (u)xterm to use the sixel settings we need:
2988 * - enable sixel scrolling
2990 * - disable private color registers (so that we can use one common
2991 * palette) if sixelSharedPalette is set
2993 * @return the string to emit to xterm
2995 private String
xtermSetSixelSettings() {
2996 if (sixelSharedPalette
== true) {
2997 return "\033[?80h\033[?1070l";
2999 return "\033[?80h\033[?1070h";
3004 * Restore (u)xterm its default sixel settings:
3006 * - enable sixel scrolling
3008 * - enable private color registers
3010 * @return the string to emit to xterm
3012 private String
xtermResetSixelSettings() {
3013 return "\033[?80h\033[?1070h";
3017 * Request (u)xterm to report the current window and cell size dimensions
3020 * @return the string to emit to xterm
3022 private String
xtermReportPixelDimensions() {
3023 // We will ask for both window and text cell dimensions, and
3024 // hopefully one of them will work.
3025 return "\033[14t\033[16t";
3029 * Tell (u)xterm that we want alt- keystrokes to send escape + character
3030 * rather than set the 8th bit. Anyone who wants UTF8 should want this
3033 * @param on if true, enable metaSendsEscape
3034 * @return the string to emit to xterm
3036 private String
xtermMetaSendsEscape(final boolean on
) {
3038 return "\033[?1036h\033[?1034l";
3040 return "\033[?1036l";
3044 * Create an xterm OSC sequence to change the window title.
3046 * @param title the new title
3047 * @return the string to emit to xterm
3049 private String
getSetTitleString(final String title
) {
3050 return "\033]2;" + title
+ "\007";
3053 // ------------------------------------------------------------------------
3054 // Sixel output support ---------------------------------------------------
3055 // ------------------------------------------------------------------------
3058 * Get the number of colors in the sixel palette.
3060 * @return the palette size
3062 public int getSixelPaletteSize() {
3063 return sixelPaletteSize
;
3067 * Set the number of colors in the sixel palette.
3069 * @param paletteSize the new palette size
3071 public void setSixelPaletteSize(final int paletteSize
) {
3072 if (paletteSize
== sixelPaletteSize
) {
3076 switch (paletteSize
) {
3084 throw new IllegalArgumentException("Unsupported sixel palette " +
3085 " size: " + paletteSize
);
3088 // Don't step on the screen refresh thread.
3089 synchronized (this) {
3090 sixelPaletteSize
= paletteSize
;
3098 * Start a sixel string for display one row's worth of bitmap data.
3100 * @param x column coordinate. 0 is the left-most column.
3101 * @param y row coordinate. 0 is the top-most row.
3102 * @return the string to emit to an ANSI / ECMA-style terminal
3104 private String
startSixel(final int x
, final int y
) {
3105 StringBuilder sb
= new StringBuilder();
3107 assert (sixel
== true);
3110 sb
.append(gotoXY(x
, y
));
3113 sb
.append("\033Pq");
3115 if (palette
== null) {
3116 palette
= new SixelPalette();
3117 if (sixelSharedPalette
== true) {
3118 palette
.emitPalette(sb
, null);
3122 return sb
.toString();
3126 * End a sixel string for display one row's worth of bitmap data.
3128 * @return the string to emit to an ANSI / ECMA-style terminal
3130 private String
endSixel() {
3131 assert (sixel
== true);
3138 * Create a sixel string representing a row of several cells containing
3141 * @param x column coordinate. 0 is the left-most column.
3142 * @param y row coordinate. 0 is the top-most row.
3143 * @param cells the cells containing the bitmap data
3144 * @return the string to emit to an ANSI / ECMA-style terminal
3146 private String
toSixel(final int x
, final int y
,
3147 final ArrayList
<Cell
> cells
) {
3149 StringBuilder sb
= new StringBuilder();
3151 assert (cells
!= null);
3152 assert (cells
.size() > 0);
3153 assert (cells
.get(0).getImage() != null);
3155 if (sixel
== false) {
3156 sb
.append(normal());
3157 sb
.append(gotoXY(x
, y
));
3158 for (int i
= 0; i
< cells
.size(); i
++) {
3161 return sb
.toString();
3164 if (y
== height
- 1) {
3165 // We are on the bottom row. If scrolling mode is enabled
3166 // (default), then VT320/xterm will scroll the entire screen if
3167 // we draw any pixels here. Do not draw the image, bail out
3169 sb
.append(normal());
3170 sb
.append(gotoXY(x
, y
));
3171 for (int j
= 0; j
< cells
.size(); j
++) {
3174 return sb
.toString();
3177 if (sixelCache
== null) {
3178 sixelCache
= new ImageCache(height
* 10);
3181 // Save and get rows to/from the cache that do NOT have inverted
3183 boolean saveInCache
= true;
3184 for (Cell cell
: cells
) {
3185 if (cell
.isInvertedImage()) {
3186 saveInCache
= false;
3190 String cachedResult
= sixelCache
.get(cells
);
3191 if (cachedResult
!= null) {
3192 // System.err.println("CACHE HIT");
3193 sb
.append(startSixel(x
, y
));
3194 sb
.append(cachedResult
);
3195 sb
.append(endSixel());
3196 return sb
.toString();
3198 // System.err.println("CACHE MISS");
3201 int imageWidth
= cells
.get(0).getImage().getWidth();
3202 int imageHeight
= cells
.get(0).getImage().getHeight();
3204 // Piece these together into one larger image for final rendering.
3206 int fullWidth
= cells
.size() * imageWidth
;
3207 int fullHeight
= imageHeight
;
3208 for (int i
= 0; i
< cells
.size(); i
++) {
3209 totalWidth
+= cells
.get(i
).getImage().getWidth();
3212 BufferedImage image
= new BufferedImage(fullWidth
,
3213 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3216 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3217 int tileWidth
= imageWidth
;
3218 int tileHeight
= imageHeight
;
3220 if (false && cells
.get(i
).isInvertedImage()) {
3221 // I used to put an all-white cell over the cursor, don't do
3223 rgbArray
= new int[imageWidth
* imageHeight
];
3224 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3225 rgbArray
[j
] = 0xFFFFFF;
3229 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3230 tileWidth
, tileHeight
, null, 0, tileWidth
);
3231 } catch (Exception e
) {
3232 throw new RuntimeException("image " + imageWidth
+ "x" +
3234 "tile " + tileWidth
+ "x" +
3236 " cells.get(i).getImage() " +
3237 cells
.get(i
).getImage() +
3239 " fullWidth " + fullWidth
+
3240 " fullHeight " + fullHeight
, e
);
3245 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3246 i * imageWidth, 0, imageWidth, imageHeight,
3248 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3249 fullWidth, fullHeight, cells.size(), getTextWidth());
3252 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3253 rgbArray
, 0, tileWidth
);
3254 if (tileHeight
< fullHeight
) {
3255 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3256 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3257 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3260 image
.setRGB(imageX
, imageY
, backgroundColor
);
3265 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3266 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3267 // I used to put an all-white cell over the cursor, don't do that
3269 rgbArray
= new int[totalWidth
* imageHeight
];
3270 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3271 rgbArray
[j
] = 0xFFFFFF;
3275 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3276 totalWidth
, imageHeight
, null, 0, totalWidth
);
3277 } catch (Exception e
) {
3278 throw new RuntimeException("image " + imageWidth
+ "x" +
3279 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3280 cells
.get(cells
.size() - 1).getImage(), e
);
3283 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3284 imageHeight
, rgbArray
, 0, totalWidth
);
3286 if (totalWidth
< imageWidth
) {
3287 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3289 for (int imageX
= image
.getWidth() - totalWidth
;
3290 imageX
< image
.getWidth(); imageX
++) {
3292 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3293 image
.setRGB(imageX
, imageY
, backgroundColor
);
3298 if ((image
.getWidth() != cells
.size() * getTextWidth())
3299 || (image
.getHeight() != getTextHeight())
3301 // Rescale the image to fit the text cells it is going into.
3302 BufferedImage newImage
;
3303 newImage
= new BufferedImage(cells
.size() * getTextWidth(),
3304 getTextHeight(), BufferedImage
.TYPE_INT_ARGB
);
3306 Graphics gr
= newImage
.getGraphics();
3307 if (gr
instanceof Graphics2D
) {
3308 ((Graphics2D
) gr
).setRenderingHint(RenderingHints
.KEY_ANTIALIASING
,
3309 RenderingHints
.VALUE_ANTIALIAS_ON
);
3310 ((Graphics2D
) gr
).setRenderingHint(RenderingHints
.KEY_RENDERING
,
3311 RenderingHints
.VALUE_RENDER_QUALITY
);
3313 gr
.drawImage(image
, 0, 0, newImage
.getWidth(),
3314 newImage
.getHeight(), null, null);
3317 fullHeight
= image
.getHeight();
3320 // Dither the image. It is ok to lose the original here.
3321 if (palette
== null) {
3322 palette
= new SixelPalette();
3323 if (sixelSharedPalette
== true) {
3324 palette
.emitPalette(sb
, null);
3327 image
= palette
.ditherImage(image
);
3329 // Collect the raster information
3330 int rasterHeight
= 0;
3331 int rasterWidth
= image
.getWidth();
3333 if (sixelSharedPalette
== false) {
3334 // Emit the palette, but only for the colors actually used by
3336 boolean [] usedColors
= new boolean[sixelPaletteSize
];
3337 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3338 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
3339 usedColors
[image
.getRGB(imageX
, imageY
)] = true;
3342 palette
.emitPalette(sb
, usedColors
);
3345 // Render the entire row of cells.
3346 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
3347 int [][] sixels
= new int[image
.getWidth()][6];
3349 // See which colors are actually used in this band of sixels.
3350 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3351 for (int imageY
= 0;
3352 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
3355 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
3356 assert (colorIdx
>= 0);
3357 assert (colorIdx
< sixelPaletteSize
);
3359 sixels
[imageX
][imageY
] = colorIdx
;
3363 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
3364 boolean isUsed
= false;
3365 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3366 for (int j
= 0; j
< 6; j
++) {
3367 if (sixels
[imageX
][j
] == i
) {
3372 if (isUsed
== false) {
3376 // Set to the beginning of scan line for the next set of
3377 // colored pixels, and select the color.
3378 sb
.append(String
.format("$#%d", i
));
3381 int oldDataCount
= 0;
3382 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3384 // Add up all the pixels that match this color.
3387 (j
< 6) && (currentRow
+ j
< fullHeight
);
3390 if (sixels
[imageX
][j
] == i
) {
3411 if ((currentRow
+ j
+ 1) > rasterHeight
) {
3412 rasterHeight
= currentRow
+ j
+ 1;
3420 if (data
== oldData
) {
3423 if (oldDataCount
== 1) {
3424 sb
.append((char) oldData
);
3425 } else if (oldDataCount
> 1) {
3426 sb
.append(String
.format("!%d", oldDataCount
));
3427 sb
.append((char) oldData
);
3433 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
3435 // Emit the last sequence.
3436 if (oldDataCount
== 1) {
3437 sb
.append((char) oldData
);
3438 } else if (oldDataCount
> 1) {
3439 sb
.append(String
.format("!%d", oldDataCount
));
3440 sb
.append((char) oldData
);
3443 } // for (int i = 0; i < sixelPaletteSize; i++)
3445 // Advance to the next scan line.
3448 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3450 // Kill the very last "-", because it is unnecessary.
3451 sb
.deleteCharAt(sb
.length() - 1);
3453 // Add the raster information
3454 sb
.insert(0, String
.format("\"1;1;%d;%d", rasterWidth
, rasterHeight
));
3457 // This row is OK to save into the cache.
3458 sixelCache
.put(cells
, sb
.toString());
3461 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3465 * Get the sixel support flag.
3467 * @return true if this terminal is emitting sixel
3469 public boolean hasSixel() {
3473 // ------------------------------------------------------------------------
3474 // End sixel output support -----------------------------------------------
3475 // ------------------------------------------------------------------------
3477 // ------------------------------------------------------------------------
3478 // iTerm2 image output support --------------------------------------------
3479 // ------------------------------------------------------------------------
3482 * Create an iTerm2 images string representing a row of several cells
3483 * containing bitmap data.
3485 * @param x column coordinate. 0 is the left-most column.
3486 * @param y row coordinate. 0 is the top-most row.
3487 * @param cells the cells containing the bitmap data
3488 * @return the string to emit to an ANSI / ECMA-style terminal
3490 private String
toIterm2Image(final int x
, final int y
,
3491 final ArrayList
<Cell
> cells
) {
3493 StringBuilder sb
= new StringBuilder();
3495 assert (cells
!= null);
3496 assert (cells
.size() > 0);
3497 assert (cells
.get(0).getImage() != null);
3499 if (iterm2Images
== false) {
3500 sb
.append(normal());
3501 sb
.append(gotoXY(x
, y
));
3502 for (int i
= 0; i
< cells
.size(); i
++) {
3505 return sb
.toString();
3508 if (iterm2Cache
== null) {
3509 iterm2Cache
= new ImageCache(height
* 10);
3512 // Save and get rows to/from the cache that do NOT have inverted
3514 boolean saveInCache
= true;
3515 for (Cell cell
: cells
) {
3516 if (cell
.isInvertedImage()) {
3517 saveInCache
= false;
3521 String cachedResult
= iterm2Cache
.get(cells
);
3522 if (cachedResult
!= null) {
3523 // System.err.println("CACHE HIT");
3524 sb
.append(gotoXY(x
, y
));
3525 sb
.append(cachedResult
);
3526 return sb
.toString();
3528 // System.err.println("CACHE MISS");
3531 int imageWidth
= cells
.get(0).getImage().getWidth();
3532 int imageHeight
= cells
.get(0).getImage().getHeight();
3534 // Piece cells.get(x).getImage() pieces together into one larger
3535 // image for final rendering.
3537 int fullWidth
= cells
.size() * imageWidth
;
3538 int fullHeight
= imageHeight
;
3539 for (int i
= 0; i
< cells
.size(); i
++) {
3540 totalWidth
+= cells
.get(i
).getImage().getWidth();
3543 BufferedImage image
= new BufferedImage(fullWidth
,
3544 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3547 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3548 int tileWidth
= imageWidth
;
3549 int tileHeight
= imageHeight
;
3550 if (false && cells
.get(i
).isInvertedImage()) {
3551 // I used to put an all-white cell over the cursor, don't do
3553 rgbArray
= new int[imageWidth
* imageHeight
];
3554 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3555 rgbArray
[j
] = 0xFFFFFF;
3559 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3560 tileWidth
, tileHeight
, null, 0, tileWidth
);
3561 } catch (Exception e
) {
3562 throw new RuntimeException("image " + imageWidth
+ "x" +
3564 "tile " + tileWidth
+ "x" +
3566 " cells.get(i).getImage() " +
3567 cells
.get(i
).getImage() +
3569 " fullWidth " + fullWidth
+
3570 " fullHeight " + fullHeight
, e
);
3575 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3576 i * imageWidth, 0, imageWidth, imageHeight,
3578 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3579 fullWidth, fullHeight, cells.size(), getTextWidth());
3582 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3583 rgbArray
, 0, tileWidth
);
3584 if (tileHeight
< fullHeight
) {
3585 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3586 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3587 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3590 image
.setRGB(imageX
, imageY
, backgroundColor
);
3595 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3596 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3597 // I used to put an all-white cell over the cursor, don't do that
3599 rgbArray
= new int[totalWidth
* imageHeight
];
3600 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3601 rgbArray
[j
] = 0xFFFFFF;
3605 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3606 totalWidth
, imageHeight
, null, 0, totalWidth
);
3607 } catch (Exception e
) {
3608 throw new RuntimeException("image " + imageWidth
+ "x" +
3609 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3610 cells
.get(cells
.size() - 1).getImage(), e
);
3613 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3614 imageHeight
, rgbArray
, 0, totalWidth
);
3616 if (totalWidth
< imageWidth
) {
3617 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3619 for (int imageX
= image
.getWidth() - totalWidth
;
3620 imageX
< image
.getWidth(); imageX
++) {
3622 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3623 image
.setRGB(imageX
, imageY
, backgroundColor
);
3628 if ((image
.getWidth() != cells
.size() * getTextWidth())
3629 || (image
.getHeight() != getTextHeight())
3631 // Rescale the image to fit the text cells it is going into.
3632 BufferedImage newImage
;
3633 newImage
= new BufferedImage(cells
.size() * getTextWidth(),
3634 getTextHeight(), BufferedImage
.TYPE_INT_ARGB
);
3636 Graphics gr
= newImage
.getGraphics();
3637 if (gr
instanceof Graphics2D
) {
3638 ((Graphics2D
) gr
).setRenderingHint(RenderingHints
.KEY_ANTIALIASING
,
3639 RenderingHints
.VALUE_ANTIALIAS_ON
);
3640 ((Graphics2D
) gr
).setRenderingHint(RenderingHints
.KEY_RENDERING
,
3641 RenderingHints
.VALUE_RENDER_QUALITY
);
3643 gr
.drawImage(image
, 0, 0, newImage
.getWidth(),
3644 newImage
.getHeight(), null, null);
3647 fullHeight
= image
.getHeight();
3651 * From https://iterm2.com/documentation-images.html:
3655 * iTerm2 extends the xterm protocol with a set of proprietary escape
3656 * sequences. In general, the pattern is:
3658 * ESC ] 1337 ; key = value ^G
3660 * Whitespace is shown here for ease of reading: in practice, no
3661 * spaces should be used.
3663 * For file transfer and inline images, the code is:
3665 * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
3667 * The optional arguments are formatted as key=value with a semicolon
3668 * between each key-value pair. They are described below:
3670 * Key Description of value
3671 * name base-64 encoded filename. Defaults to "Unnamed file".
3672 * size File size in bytes. Optional; this is only used by the
3673 * progress indicator.
3674 * width Width to render. See notes below.
3675 * height Height to render. See notes below.
3676 * preserveAspectRatio If set to 0, then the image's inherent aspect
3677 * ratio will not be respected; otherwise, it
3678 * will fill the specified width and height as
3679 * much as possible without stretching. Defaults
3681 * inline If set to 1, the file will be displayed inline. Otherwise,
3682 * it will be downloaded with no visual representation in the
3683 * terminal session. Defaults to 0.
3685 * The width and height are given as a number followed by a unit, or
3688 * N: N character cells.
3690 * N%: N percent of the session's width or height.
3691 * auto: The image's inherent size will be used to determine an
3692 * appropriate dimension.
3696 // File contents can be several image formats. We will use PNG.
3697 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3699 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3700 Math
.min(image
.getHeight(), fullHeight
)),
3701 "PNG", pngOutputStream
)
3703 // We failed to render image, bail out.
3706 } catch (IOException e
) {
3707 // We failed to render image, bail out.
3711 sb
.append("\033]1337;File=");
3713 sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
3717 sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
3718 image.getWidth(), Math.min(image.getHeight(),
3721 sb
.append("inline=1:");
3722 sb
.append(StringUtils
.toBase64(pngOutputStream
.toByteArray()));
3726 // This row is OK to save into the cache.
3727 iterm2Cache
.put(cells
, sb
.toString());
3730 return (gotoXY(x
, y
) + sb
.toString());
3734 * Get the iTerm2 images support flag.
3736 * @return true if this terminal is emitting iTerm2 images
3738 public boolean hasIterm2Images() {
3739 return iterm2Images
;
3742 // ------------------------------------------------------------------------
3743 // End iTerm2 image output support ----------------------------------------
3744 // ------------------------------------------------------------------------
3746 // ------------------------------------------------------------------------
3747 // Jexer image output support ---------------------------------------------
3748 // ------------------------------------------------------------------------
3751 * Create a Jexer images string representing a row of several cells
3752 * containing bitmap data.
3754 * @param x column coordinate. 0 is the left-most column.
3755 * @param y row coordinate. 0 is the top-most row.
3756 * @param cells the cells containing the bitmap data
3757 * @return the string to emit to an ANSI / ECMA-style terminal
3759 private String
toJexerImage(final int x
, final int y
,
3760 final ArrayList
<Cell
> cells
) {
3762 StringBuilder sb
= new StringBuilder();
3764 assert (cells
!= null);
3765 assert (cells
.size() > 0);
3766 assert (cells
.get(0).getImage() != null);
3768 if (jexerImageOption
== JexerImageOption
.DISABLED
) {
3769 sb
.append(normal());
3770 sb
.append(gotoXY(x
, y
));
3771 for (int i
= 0; i
< cells
.size(); i
++) {
3774 return sb
.toString();
3777 if (jexerCache
== null) {
3778 jexerCache
= new ImageCache(height
* 10);
3781 // Save and get rows to/from the cache that do NOT have inverted
3783 boolean saveInCache
= true;
3784 for (Cell cell
: cells
) {
3785 if (cell
.isInvertedImage()) {
3786 saveInCache
= false;
3790 String cachedResult
= jexerCache
.get(cells
);
3791 if (cachedResult
!= null) {
3792 // System.err.println("CACHE HIT");
3793 sb
.append(gotoXY(x
, y
));
3794 sb
.append(cachedResult
);
3795 return sb
.toString();
3797 // System.err.println("CACHE MISS");
3800 int imageWidth
= cells
.get(0).getImage().getWidth();
3801 int imageHeight
= cells
.get(0).getImage().getHeight();
3803 // Piece cells.get(x).getImage() pieces together into one larger
3804 // image for final rendering.
3806 int fullWidth
= cells
.size() * imageWidth
;
3807 int fullHeight
= imageHeight
;
3808 for (int i
= 0; i
< cells
.size(); i
++) {
3809 totalWidth
+= cells
.get(i
).getImage().getWidth();
3812 BufferedImage image
= new BufferedImage(fullWidth
,
3813 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3816 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3817 int tileWidth
= imageWidth
;
3818 int tileHeight
= imageHeight
;
3819 if (false && cells
.get(i
).isInvertedImage()) {
3820 // I used to put an all-white cell over the cursor, don't do
3822 rgbArray
= new int[imageWidth
* imageHeight
];
3823 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3824 rgbArray
[j
] = 0xFFFFFF;
3828 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3829 tileWidth
, tileHeight
, null, 0, tileWidth
);
3830 } catch (Exception e
) {
3831 throw new RuntimeException("image " + imageWidth
+ "x" +
3833 "tile " + tileWidth
+ "x" +
3835 " cells.get(i).getImage() " +
3836 cells
.get(i
).getImage() +
3838 " fullWidth " + fullWidth
+
3839 " fullHeight " + fullHeight
, e
);
3844 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3845 i * imageWidth, 0, imageWidth, imageHeight,
3847 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3848 fullWidth, fullHeight, cells.size(), getTextWidth());
3851 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3852 rgbArray
, 0, tileWidth
);
3853 if (tileHeight
< fullHeight
) {
3854 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3855 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3856 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3859 image
.setRGB(imageX
, imageY
, backgroundColor
);
3864 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3865 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3866 // I used to put an all-white cell over the cursor, don't do that
3868 rgbArray
= new int[totalWidth
* imageHeight
];
3869 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3870 rgbArray
[j
] = 0xFFFFFF;
3874 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3875 totalWidth
, imageHeight
, null, 0, totalWidth
);
3876 } catch (Exception e
) {
3877 throw new RuntimeException("image " + imageWidth
+ "x" +
3878 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3879 cells
.get(cells
.size() - 1).getImage(), e
);
3882 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3883 imageHeight
, rgbArray
, 0, totalWidth
);
3885 if (totalWidth
< imageWidth
) {
3886 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3888 for (int imageX
= image
.getWidth() - totalWidth
;
3889 imageX
< image
.getWidth(); imageX
++) {
3891 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3892 image
.setRGB(imageX
, imageY
, backgroundColor
);
3897 if ((image
.getWidth() != cells
.size() * getTextWidth())
3898 || (image
.getHeight() != getTextHeight())
3900 // Rescale the image to fit the text cells it is going into.
3901 BufferedImage newImage
;
3902 newImage
= new BufferedImage(cells
.size() * getTextWidth(),
3903 getTextHeight(), BufferedImage
.TYPE_INT_ARGB
);
3905 Graphics gr
= newImage
.getGraphics();
3906 if (gr
instanceof Graphics2D
) {
3907 ((Graphics2D
) gr
).setRenderingHint(RenderingHints
.KEY_ANTIALIASING
,
3908 RenderingHints
.VALUE_ANTIALIAS_ON
);
3909 ((Graphics2D
) gr
).setRenderingHint(RenderingHints
.KEY_RENDERING
,
3910 RenderingHints
.VALUE_RENDER_QUALITY
);
3912 gr
.drawImage(image
, 0, 0, newImage
.getWidth(),
3913 newImage
.getHeight(), null, null);
3916 fullHeight
= image
.getHeight();
3919 if (jexerImageOption
== JexerImageOption
.PNG
) {
3921 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3923 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3924 Math
.min(image
.getHeight(), fullHeight
)),
3925 "PNG", pngOutputStream
)
3927 // We failed to render image, bail out.
3930 } catch (IOException e
) {
3931 // We failed to render image, bail out.
3935 sb
.append("\033]444;1;0;");
3936 sb
.append(StringUtils
.toBase64(pngOutputStream
.toByteArray()));
3939 } else if (jexerImageOption
== JexerImageOption
.JPG
) {
3942 ByteArrayOutputStream jpgOutputStream
= new ByteArrayOutputStream(1024);
3944 // Convert from ARGB to RGB, otherwise the JPG encode will fail.
3945 BufferedImage jpgImage
= new BufferedImage(image
.getWidth(),
3946 image
.getHeight(), BufferedImage
.TYPE_INT_RGB
);
3947 int [] pixels
= new int[image
.getWidth() * image
.getHeight()];
3948 image
.getRGB(0, 0, image
.getWidth(), image
.getHeight(), pixels
,
3949 0, image
.getWidth());
3950 jpgImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(), pixels
,
3951 0, image
.getWidth());
3954 if (!ImageIO
.write(jpgImage
.getSubimage(0, 0,
3955 jpgImage
.getWidth(),
3956 Math
.min(jpgImage
.getHeight(), fullHeight
)),
3957 "JPG", jpgOutputStream
)
3959 // We failed to render image, bail out.
3962 } catch (IOException e
) {
3963 // We failed to render image, bail out.
3967 sb
.append("\033]444;2;0;");
3968 sb
.append(StringUtils
.toBase64(jpgOutputStream
.toByteArray()));
3971 } else if (jexerImageOption
== JexerImageOption
.RGB
) {
3974 sb
.append(String
.format("\033]444;0;%d;%d;0;", image
.getWidth(),
3975 Math
.min(image
.getHeight(), fullHeight
)));
3977 byte [] bytes
= new byte[image
.getWidth() * image
.getHeight() * 3];
3978 int stride
= image
.getWidth();
3979 for (int px
= 0; px
< stride
; px
++) {
3980 for (int py
= 0; py
< image
.getHeight(); py
++) {
3981 int rgb
= image
.getRGB(px
, py
);
3982 bytes
[(py
* stride
* 3) + (px
* 3)] = (byte) ((rgb
>>> 16) & 0xFF);
3983 bytes
[(py
* stride
* 3) + (px
* 3) + 1] = (byte) ((rgb
>>> 8) & 0xFF);
3984 bytes
[(py
* stride
* 3) + (px
* 3) + 2] = (byte) ( rgb
& 0xFF);
3987 sb
.append(StringUtils
.toBase64(bytes
));
3992 // This row is OK to save into the cache.
3993 jexerCache
.put(cells
, sb
.toString());
3996 return (gotoXY(x
, y
) + sb
.toString());
4000 * Get the Jexer images support flag.
4002 * @return true if this terminal is emitting Jexer images
4004 public boolean hasJexerImages() {
4005 return (jexerImageOption
!= JexerImageOption
.DISABLED
);
4008 // ------------------------------------------------------------------------
4009 // End Jexer image output support -----------------------------------------
4010 // ------------------------------------------------------------------------
4013 * Setup system colors to match DOS color palette.
4015 private void setDOSColors() {
4016 MYBLACK
= new java
.awt
.Color(0x00, 0x00, 0x00);
4017 MYRED
= new java
.awt
.Color(0xa8, 0x00, 0x00);
4018 MYGREEN
= new java
.awt
.Color(0x00, 0xa8, 0x00);
4019 MYYELLOW
= new java
.awt
.Color(0xa8, 0x54, 0x00);
4020 MYBLUE
= new java
.awt
.Color(0x00, 0x00, 0xa8);
4021 MYMAGENTA
= new java
.awt
.Color(0xa8, 0x00, 0xa8);
4022 MYCYAN
= new java
.awt
.Color(0x00, 0xa8, 0xa8);
4023 MYWHITE
= new java
.awt
.Color(0xa8, 0xa8, 0xa8);
4024 MYBOLD_BLACK
= new java
.awt
.Color(0x54, 0x54, 0x54);
4025 MYBOLD_RED
= new java
.awt
.Color(0xfc, 0x54, 0x54);
4026 MYBOLD_GREEN
= new java
.awt
.Color(0x54, 0xfc, 0x54);
4027 MYBOLD_YELLOW
= new java
.awt
.Color(0xfc, 0xfc, 0x54);
4028 MYBOLD_BLUE
= new java
.awt
.Color(0x54, 0x54, 0xfc);
4029 MYBOLD_MAGENTA
= new java
.awt
.Color(0xfc, 0x54, 0xfc);
4030 MYBOLD_CYAN
= new java
.awt
.Color(0x54, 0xfc, 0xfc);
4031 MYBOLD_WHITE
= new java
.awt
.Color(0xfc, 0xfc, 0xfc);
4035 * Setup ECMA48 colors to match those provided in system properties.
4037 private void setCustomSystemColors() {
4040 MYBLACK
= getCustomColor("jexer.ECMA48.color0", MYBLACK
);
4041 MYRED
= getCustomColor("jexer.ECMA48.color1", MYRED
);
4042 MYGREEN
= getCustomColor("jexer.ECMA48.color2", MYGREEN
);
4043 MYYELLOW
= getCustomColor("jexer.ECMA48.color3", MYYELLOW
);
4044 MYBLUE
= getCustomColor("jexer.ECMA48.color4", MYBLUE
);
4045 MYMAGENTA
= getCustomColor("jexer.ECMA48.color5", MYMAGENTA
);
4046 MYCYAN
= getCustomColor("jexer.ECMA48.color6", MYCYAN
);
4047 MYWHITE
= getCustomColor("jexer.ECMA48.color7", MYWHITE
);
4048 MYBOLD_BLACK
= getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK
);
4049 MYBOLD_RED
= getCustomColor("jexer.ECMA48.color9", MYBOLD_RED
);
4050 MYBOLD_GREEN
= getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN
);
4051 MYBOLD_YELLOW
= getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW
);
4052 MYBOLD_BLUE
= getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE
);
4053 MYBOLD_MAGENTA
= getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA
);
4054 MYBOLD_CYAN
= getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN
);
4055 MYBOLD_WHITE
= getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE
);
4059 * Setup one system color to match the RGB value provided in system
4062 * @param key the system property key
4063 * @param defaultColor the default color to return if key is not set, or
4065 * @return a color from the RGB string, or defaultColor
4067 private java
.awt
.Color
getCustomColor(final String key
,
4068 final java
.awt
.Color defaultColor
) {
4070 String rgb
= System
.getProperty(key
);
4072 return defaultColor
;
4074 if (rgb
.startsWith("#")) {
4075 rgb
= rgb
.substring(1);
4079 rgbInt
= Integer
.parseInt(rgb
, 16);
4080 } catch (NumberFormatException e
) {
4081 return defaultColor
;
4083 java
.awt
.Color color
= new java
.awt
.Color((rgbInt
& 0xFF0000) >>> 16,
4084 (rgbInt
& 0x00FF00) >>> 8,
4085 (rgbInt
& 0x0000FF));
4091 * Create a T.416 RGB parameter sequence for a custom system color.
4093 * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
4094 * @return the color portion of the string to emit to an ANSI /
4095 * ECMA-style terminal
4097 private String
systemColorRGB(final java
.awt
.Color color
) {
4098 return String
.format("%d;%d;%d", color
.getRed(), color
.getGreen(),
4103 * Create a SGR parameter sequence for a single color change.
4105 * @param bold if true, set bold
4106 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4107 * @param foreground if true, this is a foreground color
4108 * @return the string to emit to an ANSI / ECMA-style terminal,
4111 private String
color(final boolean bold
, final Color color
,
4112 final boolean foreground
) {
4113 return color(color
, foreground
, true) +
4114 rgbColor(bold
, color
, foreground
);
4118 * Create a T.416 RGB parameter sequence for a single color change.
4120 * @param colorRGB a 24-bit RGB value for foreground color
4121 * @param foreground if true, this is a foreground color
4122 * @return the string to emit to an ANSI / ECMA-style terminal,
4125 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
4127 int colorRed
= (colorRGB
>>> 16) & 0xFF;
4128 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
4129 int colorBlue
= colorRGB
& 0xFF;
4131 StringBuilder sb
= new StringBuilder();
4133 sb
.append("\033[38;2;");
4135 sb
.append("\033[48;2;");
4137 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
4138 return sb
.toString();
4142 * Create a T.416 RGB parameter sequence for both foreground and
4143 * background color change.
4145 * @param foreColorRGB a 24-bit RGB value for foreground color
4146 * @param backColorRGB a 24-bit RGB value for foreground color
4147 * @return the string to emit to an ANSI / ECMA-style terminal,
4150 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
4151 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
4152 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
4153 int foreColorBlue
= foreColorRGB
& 0xFF;
4154 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
4155 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
4156 int backColorBlue
= backColorRGB
& 0xFF;
4158 StringBuilder sb
= new StringBuilder();
4159 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
4160 foreColorRed
, foreColorGreen
, foreColorBlue
));
4161 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
4162 backColorRed
, backColorGreen
, backColorBlue
));
4163 return sb
.toString();
4167 * Create a T.416 RGB parameter sequence for a single color change.
4169 * @param bold if true, set bold
4170 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4171 * @param foreground if true, this is a foreground color
4172 * @return the string to emit to an xterm terminal with RGB support,
4173 * e.g. "\033[38;2;RR;GG;BBm"
4175 private String
rgbColor(final boolean bold
, final Color color
,
4176 final boolean foreground
) {
4177 if (doRgbColor
== false) {
4180 StringBuilder sb
= new StringBuilder("\033[");
4182 // Bold implies foreground only
4184 if (color
.equals(Color
.BLACK
)) {
4185 sb
.append(systemColorRGB(MYBOLD_BLACK
));
4186 } else if (color
.equals(Color
.RED
)) {
4187 sb
.append(systemColorRGB(MYBOLD_RED
));
4188 } else if (color
.equals(Color
.GREEN
)) {
4189 sb
.append(systemColorRGB(MYBOLD_GREEN
));
4190 } else if (color
.equals(Color
.YELLOW
)) {
4191 sb
.append(systemColorRGB(MYBOLD_YELLOW
));
4192 } else if (color
.equals(Color
.BLUE
)) {
4193 sb
.append(systemColorRGB(MYBOLD_BLUE
));
4194 } else if (color
.equals(Color
.MAGENTA
)) {
4195 sb
.append(systemColorRGB(MYBOLD_MAGENTA
));
4196 } else if (color
.equals(Color
.CYAN
)) {
4197 sb
.append(systemColorRGB(MYBOLD_CYAN
));
4198 } else if (color
.equals(Color
.WHITE
)) {
4199 sb
.append(systemColorRGB(MYBOLD_WHITE
));
4207 if (color
.equals(Color
.BLACK
)) {
4208 sb
.append(systemColorRGB(MYBLACK
));
4209 } else if (color
.equals(Color
.RED
)) {
4210 sb
.append(systemColorRGB(MYRED
));
4211 } else if (color
.equals(Color
.GREEN
)) {
4212 sb
.append(systemColorRGB(MYGREEN
));
4213 } else if (color
.equals(Color
.YELLOW
)) {
4214 sb
.append(systemColorRGB(MYYELLOW
));
4215 } else if (color
.equals(Color
.BLUE
)) {
4216 sb
.append(systemColorRGB(MYBLUE
));
4217 } else if (color
.equals(Color
.MAGENTA
)) {
4218 sb
.append(systemColorRGB(MYMAGENTA
));
4219 } else if (color
.equals(Color
.CYAN
)) {
4220 sb
.append(systemColorRGB(MYCYAN
));
4221 } else if (color
.equals(Color
.WHITE
)) {
4222 sb
.append(systemColorRGB(MYWHITE
));
4226 return sb
.toString();
4230 * Create a T.416 RGB parameter sequence for both foreground and
4231 * background color change.
4233 * @param bold if true, set bold
4234 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4235 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4236 * @return the string to emit to an xterm terminal with RGB support,
4237 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
4239 private String
rgbColor(final boolean bold
, final Color foreColor
,
4240 final Color backColor
) {
4241 if (doRgbColor
== false) {
4245 return rgbColor(bold
, foreColor
, true) +
4246 rgbColor(false, backColor
, false);
4250 * Create a SGR parameter sequence for a single color change.
4252 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4253 * @param foreground if true, this is a foreground color
4254 * @param header if true, make the full header, otherwise just emit the
4255 * color parameter e.g. "42;"
4256 * @return the string to emit to an ANSI / ECMA-style terminal,
4259 private String
color(final Color color
, final boolean foreground
,
4260 final boolean header
) {
4262 int ecmaColor
= color
.getValue();
4264 // Convert Color.* values to SGR numerics
4272 return String
.format("\033[%dm", ecmaColor
);
4274 return String
.format("%d;", ecmaColor
);
4279 * Create a SGR parameter sequence for both foreground and background
4282 * @param bold if true, set bold
4283 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4284 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4285 * @return the string to emit to an ANSI / ECMA-style terminal,
4286 * e.g. "\033[31;42m"
4288 private String
color(final boolean bold
, final Color foreColor
,
4289 final Color backColor
) {
4290 return color(foreColor
, backColor
, true) +
4291 rgbColor(bold
, foreColor
, backColor
);
4295 * Create a SGR parameter sequence for both foreground and
4296 * background color change.
4298 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4299 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4300 * @param header if true, make the full header, otherwise just emit the
4301 * color parameter e.g. "31;42;"
4302 * @return the string to emit to an ANSI / ECMA-style terminal,
4303 * e.g. "\033[31;42m"
4305 private String
color(final Color foreColor
, final Color backColor
,
4306 final boolean header
) {
4308 int ecmaForeColor
= foreColor
.getValue();
4309 int ecmaBackColor
= backColor
.getValue();
4311 // Convert Color.* values to SGR numerics
4312 ecmaBackColor
+= 40;
4313 ecmaForeColor
+= 30;
4316 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
4318 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
4323 * Create a SGR parameter sequence for foreground, background, and
4324 * several attributes. This sequence first resets all attributes to
4325 * default, then sets attributes as per the parameters.
4327 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4328 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4329 * @param bold if true, set bold
4330 * @param reverse if true, set reverse
4331 * @param blink if true, set blink
4332 * @param underline if true, set underline
4333 * @return the string to emit to an ANSI / ECMA-style terminal,
4334 * e.g. "\033[0;1;31;42m"
4336 private String
color(final Color foreColor
, final Color backColor
,
4337 final boolean bold
, final boolean reverse
, final boolean blink
,
4338 final boolean underline
) {
4340 int ecmaForeColor
= foreColor
.getValue();
4341 int ecmaBackColor
= backColor
.getValue();
4343 // Convert Color.* values to SGR numerics
4344 ecmaBackColor
+= 40;
4345 ecmaForeColor
+= 30;
4347 StringBuilder sb
= new StringBuilder();
4348 if ( bold
&& reverse
&& blink
&& !underline
) {
4349 sb
.append("\033[0;1;7;5;");
4350 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4351 sb
.append("\033[0;1;7;");
4352 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4353 sb
.append("\033[0;7;5;");
4354 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4355 sb
.append("\033[0;1;5;");
4356 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4357 sb
.append("\033[0;1;");
4358 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4359 sb
.append("\033[0;7;");
4360 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4361 sb
.append("\033[0;5;");
4362 } else if ( bold
&& reverse
&& blink
&& underline
) {
4363 sb
.append("\033[0;1;7;5;4;");
4364 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4365 sb
.append("\033[0;1;7;4;");
4366 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4367 sb
.append("\033[0;7;5;4;");
4368 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4369 sb
.append("\033[0;1;5;4;");
4370 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4371 sb
.append("\033[0;1;4;");
4372 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4373 sb
.append("\033[0;7;4;");
4374 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4375 sb
.append("\033[0;5;4;");
4376 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4377 sb
.append("\033[0;4;");
4379 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4380 sb
.append("\033[0;");
4382 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
4383 sb
.append(rgbColor(bold
, foreColor
, backColor
));
4384 return sb
.toString();
4388 * Create a SGR parameter sequence for foreground, background, and
4389 * several attributes. This sequence first resets all attributes to
4390 * default, then sets attributes as per the parameters.
4392 * @param foreColorRGB a 24-bit RGB value for foreground color
4393 * @param backColorRGB a 24-bit RGB value for foreground color
4394 * @param bold if true, set bold
4395 * @param reverse if true, set reverse
4396 * @param blink if true, set blink
4397 * @param underline if true, set underline
4398 * @return the string to emit to an ANSI / ECMA-style terminal,
4399 * e.g. "\033[0;1;31;42m"
4401 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
4402 final boolean bold
, final boolean reverse
, final boolean blink
,
4403 final boolean underline
) {
4405 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
4406 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
4407 int foreColorBlue
= foreColorRGB
& 0xFF;
4408 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
4409 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
4410 int backColorBlue
= backColorRGB
& 0xFF;
4412 StringBuilder sb
= new StringBuilder();
4413 if ( bold
&& reverse
&& blink
&& !underline
) {
4414 sb
.append("\033[0;1;7;5;");
4415 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4416 sb
.append("\033[0;1;7;");
4417 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4418 sb
.append("\033[0;7;5;");
4419 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4420 sb
.append("\033[0;1;5;");
4421 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4422 sb
.append("\033[0;1;");
4423 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4424 sb
.append("\033[0;7;");
4425 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4426 sb
.append("\033[0;5;");
4427 } else if ( bold
&& reverse
&& blink
&& underline
) {
4428 sb
.append("\033[0;1;7;5;4;");
4429 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4430 sb
.append("\033[0;1;7;4;");
4431 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4432 sb
.append("\033[0;7;5;4;");
4433 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4434 sb
.append("\033[0;1;5;4;");
4435 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4436 sb
.append("\033[0;1;4;");
4437 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4438 sb
.append("\033[0;7;4;");
4439 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4440 sb
.append("\033[0;5;4;");
4441 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4442 sb
.append("\033[0;4;");
4444 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4445 sb
.append("\033[0;");
4448 sb
.append("m\033[38;2;");
4449 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
4451 sb
.append("m\033[48;2;");
4452 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
4455 return sb
.toString();
4459 * Create a SGR parameter sequence to reset to VT100 defaults.
4461 * @return the string to emit to an ANSI / ECMA-style terminal,
4464 private String
normal() {
4465 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
4469 * Create a SGR parameter sequence to reset to ECMA-48 default
4470 * foreground/background.
4472 * @return the string to emit to an ANSI / ECMA-style terminal,
4475 private String
defaultColor() {
4478 * Normal (neither bold nor faint).
4481 * Steady (not blinking).
4482 * Positive (not inverse).
4483 * Visible (not hidden).
4485 * Default foreground color.
4486 * Default background color.
4488 return "\033[0;22;23;24;25;27;28;29;39;49m";
4492 * Create a SGR parameter sequence to reset to defaults.
4494 * @param header if true, make the full header, otherwise just emit the
4495 * bare parameter e.g. "0;"
4496 * @return the string to emit to an ANSI / ECMA-style terminal,
4499 private String
normal(final boolean header
) {
4501 return "\033[0;37;40m";
4507 * Create a SGR parameter sequence for enabling the visible cursor.
4509 * @param on if true, turn on cursor
4510 * @return the string to emit to an ANSI / ECMA-style terminal
4512 private String
cursor(final boolean on
) {
4513 if (on
&& !cursorOn
) {
4517 if (!on
&& cursorOn
) {
4525 * Clear the entire screen. Because some terminals use back-color-erase,
4526 * set the color to white-on-black beforehand.
4528 * @return the string to emit to an ANSI / ECMA-style terminal
4530 private String
clearAll() {
4531 return "\033[0;37;40m\033[2J";
4535 * Clear the line from the cursor (inclusive) to the end of the screen.
4536 * Because some terminals use back-color-erase, set the color to
4537 * white-on-black beforehand.
4539 * @return the string to emit to an ANSI / ECMA-style terminal
4541 private String
clearRemainingLine() {
4542 return "\033[0;37;40m\033[K";
4546 * Move the cursor to (x, y).
4548 * @param x column coordinate. 0 is the left-most column.
4549 * @param y row coordinate. 0 is the top-most row.
4550 * @return the string to emit to an ANSI / ECMA-style terminal
4552 private String
gotoXY(final int x
, final int y
) {
4553 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
4557 * Tell (u)xterm that we want to receive mouse events based on "Any event
4558 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
4559 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
4561 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
4563 * Note that this also sets the alternate/primary screen buffer.
4565 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
4566 * mean "hide the mouse pointer." We have to use our own sequence to do
4567 * this because there is no standard in xterm for unilaterally hiding the
4568 * pointer all the time (regardless of typing).
4570 * @param on If true, enable mouse report and use the alternate screen
4571 * buffer. If false disable mouse reporting and use the primary screen
4573 * @return the string to emit to xterm
4575 private String
mouse(final boolean on
) {
4577 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
4579 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";