2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.backend
;
31 import java
.awt
.image
.BufferedImage
;
32 import java
.io
.BufferedReader
;
33 import java
.io
.ByteArrayOutputStream
;
34 import java
.io
.FileDescriptor
;
35 import java
.io
.FileInputStream
;
36 import java
.io
.InputStream
;
37 import java
.io
.InputStreamReader
;
38 import java
.io
.IOException
;
39 import java
.io
.OutputStream
;
40 import java
.io
.OutputStreamWriter
;
41 import java
.io
.PrintWriter
;
42 import java
.io
.Reader
;
43 import java
.io
.UnsupportedEncodingException
;
44 import java
.util
.ArrayList
;
45 import java
.util
.Collections
;
46 import java
.util
.HashMap
;
47 import java
.util
.List
;
48 import javax
.imageio
.ImageIO
;
51 import jexer
.bits
.Cell
;
52 import jexer
.bits
.CellAttributes
;
53 import jexer
.bits
.Color
;
54 import jexer
.bits
.StringUtils
;
55 import jexer
.event
.TCommandEvent
;
56 import jexer
.event
.TInputEvent
;
57 import jexer
.event
.TKeypressEvent
;
58 import jexer
.event
.TMouseEvent
;
59 import jexer
.event
.TResizeEvent
;
60 import static jexer
.TCommand
.*;
61 import static jexer
.TKeypress
.*;
64 * This class reads keystrokes and mouse events and emits output to ANSI
65 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
67 public class ECMA48Terminal
extends LogicalScreen
68 implements TerminalReader
, Runnable
{
70 // ------------------------------------------------------------------------
71 // Constants --------------------------------------------------------------
72 // ------------------------------------------------------------------------
75 * States in the input parser.
77 private enum ParseState
{
88 * Available Jexer images support.
90 private enum JexerImageOption
{
97 // ------------------------------------------------------------------------
98 // Variables --------------------------------------------------------------
99 // ------------------------------------------------------------------------
102 * Emit debugging to stderr.
104 private boolean debugToStderr
= false;
107 * If true, emit T.416-style RGB colors for normal system colors. This
108 * is a) expensive in bandwidth, and b) potentially terrible looking for
111 private static boolean doRgbColor
= false;
114 * The session information.
116 private SessionInfo sessionInfo
;
119 * The event queue, filled up by a thread reading on input.
121 private List
<TInputEvent
> eventQueue
;
124 * If true, we want the reader thread to exit gracefully.
126 private boolean stopReaderThread
;
131 private Thread readerThread
;
134 * Parameters being collected. E.g. if the string is \033[1;3m, then
135 * params[0] will be 1 and params[1] will be 3.
137 private List
<String
> params
;
140 * Current parsing state.
142 private ParseState state
;
145 * The time we entered ESCAPE. If we get a bare escape without a code
146 * following it, this is used to return that bare escape.
148 private long escapeTime
;
151 * The time we last checked the window size. We try not to spawn stty
152 * more than once per second.
154 private long windowSizeTime
;
157 * true if mouse1 was down. Used to report mouse1 on the release event.
159 private boolean mouse1
;
162 * true if mouse2 was down. Used to report mouse2 on the release event.
164 private boolean mouse2
;
167 * true if mouse3 was down. Used to report mouse3 on the release event.
169 private boolean mouse3
;
172 * Cache the cursor visibility value so we only emit the sequence when we
175 private boolean cursorOn
= true;
178 * Cache the last window size to figure out if a TResizeEvent needs to be
181 private TResizeEvent windowResize
= null;
184 * If true, emit wide-char (CJK/Emoji) characters as sixel images.
186 private boolean wideCharImages
= true;
189 * Window width in pixels. Used for sixel support.
191 private int widthPixels
= 640;
194 * Window height in pixels. Used for sixel support.
196 private int heightPixels
= 400;
199 * If true, emit image data via sixel.
201 private boolean sixel
= true;
204 * If true, use a single shared palette for sixel.
206 private boolean sixelSharedPalette
= true;
209 * The sixel palette handler.
211 private SixelPalette palette
= null;
214 * The sixel post-rendered string cache.
216 private ImageCache sixelCache
= null;
219 * Number of colors in the sixel palette. Xterm 335 defines the max as
220 * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
223 private int sixelPaletteSize
= 1024;
226 * If true, emit image data via iTerm2 image protocol.
228 private boolean iterm2Images
= false;
231 * The iTerm2 post-rendered string cache.
233 private ImageCache iterm2Cache
= null;
236 * If not DISABLED, emit image data via Jexer image protocol if the
237 * terminal supports it.
239 private JexerImageOption jexerImageOption
= JexerImageOption
.JPG
;
242 * The Jexer post-rendered string cache.
244 private ImageCache jexerCache
= null;
247 * If true, then we changed System.in and need to change it back.
249 private boolean setRawMode
= false;
252 * If true, '?' was seen in terminal response.
254 private boolean decPrivateModeFlag
= false;
257 * The terminal's input. If an InputStream is not specified in the
258 * constructor, then this InputStreamReader will be bound to System.in
259 * with UTF-8 encoding.
261 private Reader input
;
264 * The terminal's raw InputStream. If an InputStream is not specified in
265 * the constructor, then this InputReader will be bound to System.in.
266 * This is used by run() to see if bytes are available() before calling
267 * (Reader)input.read().
269 private InputStream inputStream
;
272 * The terminal's output. If an OutputStream is not specified in the
273 * constructor, then this PrintWriter will be bound to System.out with
276 private PrintWriter output
;
279 * The listening object that run() wakes up on new input.
281 private Object listener
;
283 // Colors to map DOS colors to AWT colors.
284 private static java
.awt
.Color MYBLACK
;
285 private static java
.awt
.Color MYRED
;
286 private static java
.awt
.Color MYGREEN
;
287 private static java
.awt
.Color MYYELLOW
;
288 private static java
.awt
.Color MYBLUE
;
289 private static java
.awt
.Color MYMAGENTA
;
290 private static java
.awt
.Color MYCYAN
;
291 private static java
.awt
.Color MYWHITE
;
292 private static java
.awt
.Color MYBOLD_BLACK
;
293 private static java
.awt
.Color MYBOLD_RED
;
294 private static java
.awt
.Color MYBOLD_GREEN
;
295 private static java
.awt
.Color MYBOLD_YELLOW
;
296 private static java
.awt
.Color MYBOLD_BLUE
;
297 private static java
.awt
.Color MYBOLD_MAGENTA
;
298 private static java
.awt
.Color MYBOLD_CYAN
;
299 private static java
.awt
.Color MYBOLD_WHITE
;
302 * SixelPalette is used to manage the conversion of images between 24-bit
303 * RGB color and a palette of sixelPaletteSize colors.
305 private class SixelPalette
{
308 * Color palette for sixel output, sorted low to high.
310 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
313 * Map of color palette index for sixel output, from the order it was
314 * generated by makePalette() to rgbColors.
316 private int [] rgbSortedIndex
= new int[sixelPaletteSize
];
319 * The color palette, organized by hue, saturation, and luminance.
320 * This is used for a fast color match.
322 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
325 * Number of bits for hue.
327 private int hueBits
= -1;
330 * Number of bits for saturation.
332 private int satBits
= -1;
335 * Number of bits for luminance.
337 private int lumBits
= -1;
340 * Step size for hue bins.
342 private int hueStep
= -1;
345 * Step size for saturation bins.
347 private int satStep
= -1;
350 * Cached RGB to HSL result.
352 private int hsl
[] = new int[3];
355 * ColorIdx records a RGB color and its palette index.
357 private class ColorIdx
{
359 * The 24-bit RGB color.
364 * The palette index for this color.
369 * Public constructor.
371 * @param color the 24-bit RGB color
372 * @param index the palette index for this color
374 public ColorIdx(final int color
, final int index
) {
381 * Public constructor.
383 public SixelPalette() {
388 * Find the nearest match for a color in the palette.
390 * @param color the RGB color
391 * @return the index in rgbColors that is closest to color
393 public int matchColor(final int color
) {
398 * matchColor() is a critical performance bottleneck. To make it
399 * decent, we do the following:
401 * 1. Find the nearest two hues that bracket this color.
403 * 2. Find the nearest two saturations that bracket this color.
405 * 3. Iterate within these four bands of luminance values,
406 * returning the closest color by Euclidean distance.
408 * This strategy reduces the search space by about 97%.
410 int red
= (color
>>> 16) & 0xFF;
411 int green
= (color
>>> 8) & 0xFF;
412 int blue
= color
& 0xFF;
414 if (sixelPaletteSize
== 2) {
415 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
424 rgbToHsl(red
, green
, blue
, hsl
);
428 // System.err.printf("%d %d %d\n", hue, sat, lum);
430 double diff
= Double
.MAX_VALUE
;
433 int hue1
= hue
/ (360/hueStep
);
435 if (hue1
>= hslColors
.size() - 1) {
436 // Bracket pure red from above.
437 hue1
= hslColors
.size() - 1;
439 } else if (hue1
== 0) {
440 // Bracket pure red from below.
441 hue2
= hslColors
.size() - 1;
444 for (int hI
= hue1
; hI
!= -1;) {
445 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
448 } else if (hI
== hue2
) {
452 int sMin
= (sat
/ satStep
) - 1;
457 } else if (sMin
== sats
.size() - 1) {
462 assert (sMax
- sMin
== 1);
465 // int sMax = sats.size() - 1;
467 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
468 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
470 // True 3D colorspace match for the remaining values
471 for (ColorIdx c
: lums
) {
472 int rgbColor
= c
.color
;
474 int red2
= (rgbColor
>>> 16) & 0xFF;
475 int green2
= (rgbColor
>>> 8) & 0xFF;
476 int blue2
= rgbColor
& 0xFF;
477 newDiff
+= Math
.pow(red2
- red
, 2);
478 newDiff
+= Math
.pow(green2
- green
, 2);
479 newDiff
+= Math
.pow(blue2
- blue
, 2);
480 if (newDiff
< diff
) {
481 idx
= rgbSortedIndex
[c
.index
];
488 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
489 // Black is a closer match.
491 } else if ((((255 - red
) * (255 - red
)) +
492 ((255 - green
) * (255 - green
)) +
493 ((255 - blue
) * (255 - blue
))) < diff
) {
495 // White is a closer match.
496 idx
= sixelPaletteSize
- 1;
503 * Clamp an int value to [0, 255].
505 * @param x the int value
506 * @return an int between 0 and 255.
508 private int clamp(final int x
) {
519 * Dither an image to a sixelPaletteSize palette. The dithered
520 * image cells will contain indexes into the palette.
522 * @param image the image to dither
523 * @return the dithered image. Every pixel is an index into the
526 public BufferedImage
ditherImage(final BufferedImage image
) {
528 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
529 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
531 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
532 image
.getHeight(), null, 0, image
.getWidth());
533 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
534 rgbArray
, 0, image
.getWidth());
536 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
537 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
538 int oldPixel
= ditheredImage
.getRGB(imageX
,
540 int colorIdx
= matchColor(oldPixel
);
541 assert (colorIdx
>= 0);
542 assert (colorIdx
< sixelPaletteSize
);
543 int newPixel
= rgbColors
.get(colorIdx
);
544 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
546 int oldRed
= (oldPixel
>>> 16) & 0xFF;
547 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
548 int oldBlue
= oldPixel
& 0xFF;
550 int newRed
= (newPixel
>>> 16) & 0xFF;
551 int newGreen
= (newPixel
>>> 8) & 0xFF;
552 int newBlue
= newPixel
& 0xFF;
554 int redError
= (oldRed
- newRed
) / 16;
555 int greenError
= (oldGreen
- newGreen
) / 16;
556 int blueError
= (oldBlue
- newBlue
) / 16;
558 int red
, green
, blue
;
559 if (imageX
< image
.getWidth() - 1) {
560 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
561 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
562 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
563 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
565 green
= clamp(green
);
567 pXpY
= ((red
& 0xFF) << 16);
568 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
569 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
571 if (imageY
< image
.getHeight() - 1) {
572 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
574 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
575 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
576 blue
= ( pXpYp
& 0xFF) + blueError
;
578 green
= clamp(green
);
580 pXpYp
= ((red
& 0xFF) << 16);
581 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
582 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
584 } else if (imageY
< image
.getHeight() - 1) {
585 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
587 int pXYp
= ditheredImage
.getRGB(imageX
,
590 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
591 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
592 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
594 green
= clamp(green
);
596 pXmYp
= ((red
& 0xFF) << 16);
597 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
598 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
600 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
601 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
602 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
604 green
= clamp(green
);
606 pXYp
= ((red
& 0xFF) << 16);
607 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
608 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
610 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
611 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
613 return ditheredImage
;
617 * Convert an RGB color to HSL.
619 * @param red red color, between 0 and 255
620 * @param green green color, between 0 and 255
621 * @param blue blue color, between 0 and 255
622 * @param hsl the hsl color as [hue, saturation, luminance]
624 private void rgbToHsl(final int red
, final int green
,
625 final int blue
, final int [] hsl
) {
627 assert ((red
>= 0) && (red
<= 255));
628 assert ((green
>= 0) && (green
<= 255));
629 assert ((blue
>= 0) && (blue
<= 255));
631 double R
= red
/ 255.0;
632 double G
= green
/ 255.0;
633 double B
= blue
/ 255.0;
634 boolean Rmax
= false;
635 boolean Gmax
= false;
636 boolean Bmax
= false;
637 double min
= (R
< G ? R
: G
);
638 min
= (min
< B ? min
: B
);
640 if ((R
>= G
) && (R
>= B
)) {
643 } else if ((G
>= R
) && (G
>= B
)) {
646 } else if ((B
>= G
) && (B
>= R
)) {
651 double L
= (min
+ max
) / 2.0;
656 S
= (max
- min
) / (max
+ min
);
658 S
= (max
- min
) / (2.0 - max
- min
);
662 assert (Gmax
== false);
663 assert (Bmax
== false);
664 H
= (G
- B
) / (max
- min
);
666 assert (Rmax
== false);
667 assert (Bmax
== false);
668 H
= 2.0 + (B
- R
) / (max
- min
);
670 assert (Rmax
== false);
671 assert (Gmax
== false);
672 H
= 4.0 + (R
- G
) / (max
- min
);
677 hsl
[0] = (int) (H
* 60.0);
678 hsl
[1] = (int) (S
* 100.0);
679 hsl
[2] = (int) (L
* 100.0);
681 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
682 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
683 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
687 * Convert a HSL color to RGB.
689 * @param hue hue, between 0 and 359
690 * @param sat saturation, between 0 and 100
691 * @param lum luminance, between 0 and 100
692 * @return the rgb color as 0x00RRGGBB
694 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
695 assert ((hue
>= 0) && (hue
<= 360));
696 assert ((sat
>= 0) && (sat
<= 100));
697 assert ((lum
>= 0) && (lum
<= 100));
699 double S
= sat
/ 100.0;
700 double L
= lum
/ 100.0;
701 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
702 double Hp
= hue
/ 60.0;
703 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
710 } else if (Hp
<= 2.0) {
713 } else if (Hp
<= 3.0) {
716 } else if (Hp
<= 4.0) {
719 } else if (Hp
<= 5.0) {
722 } else if (Hp
<= 6.0) {
726 double m
= L
- (C
/ 2.0);
727 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
728 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
729 int blue
= (int) ((Bp
+ m
) * 255.0);
731 return (red
| green
| blue
);
735 * Create the sixel palette.
737 private void makePalette() {
738 // Generate the sixel palette. Because we have no idea at this
739 // layer which image(s) will be shown, we have to use a common
740 // palette with sixelPaletteSize colors for everything, and
741 // map the BufferedImage colors to their nearest neighbor in RGB
744 if (sixelPaletteSize
== 2) {
746 rgbColors
.add(0xFFFFFF);
747 rgbSortedIndex
[0] = 0;
748 rgbSortedIndex
[1] = 1;
752 // We build a palette using the Hue-Saturation-Luminence model,
753 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
754 // Luminance. We convert these colors to 24-bit RGB, sort them
755 // ascending, and steal the first index for pure black and the
756 // last for pure white. The 8-bit final palette favors bright
757 // colors, somewhere between pastel and classic television
758 // technicolor. 9- and 10-bit palettes are more uniform.
760 // Default at 256 colors.
765 assert (sixelPaletteSize
>= 256);
766 assert ((sixelPaletteSize
== 256)
767 || (sixelPaletteSize
== 512)
768 || (sixelPaletteSize
== 1024)
769 || (sixelPaletteSize
== 2048));
771 switch (sixelPaletteSize
) {
788 hueStep
= (int) (Math
.pow(2, hueBits
));
789 satStep
= (int) (100 / Math
.pow(2, satBits
));
790 // 1 bit for luminance: 40 and 70.
795 // 2 bits: 20, 40, 60, 80
800 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
806 // System.err.printf("<html><body>\n");
807 // Hue is evenly spaced around the wheel.
808 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
810 final boolean DEBUG
= false;
811 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
813 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
814 hue
+= (360/hueStep
)) {
816 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
817 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
818 hslColors
.add(satList
);
820 // Saturation is linearly spaced between pastel and pure.
821 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
823 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
824 satList
.add(lumList
);
826 // Luminance brackets the pure color, but leaning toward
828 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
830 System.err.printf("<font style = \"color:");
831 System.err.printf("hsl(%d, %d%%, %d%%)",
833 System.err.printf(";\">=</font>\n");
835 int rgbColor
= hslToRgb(hue
, sat
, lum
);
836 rgbColors
.add(rgbColor
);
837 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
838 rgbColors
.size() - 1);
839 lumList
.add(colorIdx
);
841 rawRgbList
.add(rgbColor
);
843 int red
= (rgbColor
>>> 16) & 0xFF;
844 int green
= (rgbColor
>>> 8) & 0xFF;
845 int blue
= rgbColor
& 0xFF;
846 int [] backToHsl
= new int[3];
847 rgbToHsl(red
, green
, blue
, backToHsl
);
848 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
849 hue
, backToHsl
[0], sat
, backToHsl
[1],
855 // System.err.printf("\n</body></html>\n");
857 assert (rgbColors
.size() == sixelPaletteSize
);
860 * We need to sort rgbColors, so that toSixel() can know where
861 * BLACK and WHITE are in it. But we also need to be able to
862 * find the sorted values using the old unsorted indexes. So we
863 * will sort it, put all the indexes into a HashMap, and then
864 * build rgbSortedIndex[].
866 Collections
.sort(rgbColors
);
867 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
868 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
869 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
870 rgbColorIndices
.put(rgbColors
.get(i
), i
);
872 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
873 int rawColor
= rawRgbList
.get(i
);
874 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
877 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
878 assert (rawRgbList
!= null);
879 int idx
= rgbSortedIndex
[i
];
880 int rgbColor
= rgbColors
.get(idx
);
881 if ((idx
!= 0) && (idx
!= sixelPaletteSize
- 1)) {
883 System.err.printf("%d %06x --> %d %06x\n",
884 i, rawRgbList.get(i), idx, rgbColors.get(idx));
886 assert (rgbColor
== rawRgbList
.get(i
));
891 // Set the dimmest color as true black, and the brightest as true
894 rgbColors
.set(sixelPaletteSize
- 1, 0xFFFFFF);
897 System.err.printf("<html><body>\n");
898 for (Integer rgb: rgbColors) {
899 System.err.printf("<font style = \"color:");
900 System.err.printf("#%06x", rgb);
901 System.err.printf(";\">=</font>\n");
903 System.err.printf("\n</body></html>\n");
909 * Emit the sixel palette.
911 * @param sb the StringBuilder to append to
912 * @param used array of booleans set to true for each color actually
913 * used in this cell, or null to emit the entire palette
914 * @return the string to emit to an ANSI / ECMA-style terminal
916 public String
emitPalette(final StringBuilder sb
,
917 final boolean [] used
) {
919 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
920 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
921 int rgbColor
= rgbColors
.get(i
);
922 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
923 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
924 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
925 ( rgbColor
& 0xFF) * 100 / 255));
928 return sb
.toString();
933 * ImageCache is a least-recently-used cache that hangs on to the
934 * post-rendered sixel or iTerm2 string for a particular set of cells.
936 private class ImageCache
{
939 * Maximum size of the cache.
941 private int maxSize
= 100;
944 * The entries stored in the cache.
946 private HashMap
<String
, CacheEntry
> cache
= null;
949 * CacheEntry is one entry in the cache.
951 private class CacheEntry
{
963 * The last time this entry was used.
965 public long millis
= 0;
968 * Public constructor.
970 * @param key the cache entry key
971 * @param data the cache entry data
973 public CacheEntry(final String key
, final String data
) {
976 this.millis
= System
.currentTimeMillis();
981 * Public constructor.
983 * @param maxSize the maximum size of the cache
985 public ImageCache(final int maxSize
) {
986 this.maxSize
= maxSize
;
987 cache
= new HashMap
<String
, CacheEntry
>();
991 * Make a unique key for a list of cells.
993 * @param cells the cells
996 private String
makeKey(final ArrayList
<Cell
> cells
) {
997 StringBuilder sb
= new StringBuilder();
998 for (Cell cell
: cells
) {
999 sb
.append(cell
.hashCode());
1001 return sb
.toString();
1005 * Get an entry from the cache.
1007 * @param cells the list of cells that are the cache key
1008 * @return the sixel string representing these cells, or null if this
1009 * list of cells is not in the cache
1011 public String
get(final ArrayList
<Cell
> cells
) {
1012 CacheEntry entry
= cache
.get(makeKey(cells
));
1013 if (entry
== null) {
1016 entry
.millis
= System
.currentTimeMillis();
1021 * Put an entry into the cache.
1023 * @param cells the list of cells that are the cache key
1024 * @param data the sixel string representing these cells
1026 public void put(final ArrayList
<Cell
> cells
, final String data
) {
1027 String key
= makeKey(cells
);
1029 // System.err.println("put() " + key + " size " + cache.size());
1031 assert (!cache
.containsKey(key
));
1033 assert (cache
.size() <= maxSize
);
1034 if (cache
.size() == maxSize
) {
1035 // Cache is at limit, evict oldest entry.
1036 long oldestTime
= Long
.MAX_VALUE
;
1037 String keyToRemove
= null;
1038 for (CacheEntry entry
: cache
.values()) {
1039 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
1040 keyToRemove
= entry
.key
;
1041 oldestTime
= entry
.millis
;
1045 System.err.println("put() remove key = " + keyToRemove +
1046 " size " + cache.size());
1048 assert (keyToRemove
!= null);
1049 cache
.remove(keyToRemove
);
1051 System.err.println("put() removed, size " + cache.size());
1054 assert (cache
.size() <= maxSize
);
1055 CacheEntry entry
= new CacheEntry(key
, data
);
1056 assert (key
.equals(entry
.key
));
1057 cache
.put(key
, entry
);
1059 System.err.println("put() added key " + key + " " +
1060 " size " + cache.size());
1066 // ------------------------------------------------------------------------
1067 // Constructors -----------------------------------------------------------
1068 // ------------------------------------------------------------------------
1071 * Constructor sets up state for getEvent(). If either windowWidth or
1072 * windowHeight are less than 1, the terminal is not resized.
1074 * @param listener the object this backend needs to wake up when new
1076 * @param input an InputStream connected to the remote user, or null for
1077 * System.in. If System.in is used, then on non-Windows systems it will
1078 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1079 * cooked mode. input is always converted to a Reader with UTF-8
1081 * @param output an OutputStream connected to the remote user, or null
1082 * for System.out. output is always converted to a Writer with UTF-8
1084 * @param windowWidth the number of text columns to start with
1085 * @param windowHeight the number of text rows to start with
1086 * @throws UnsupportedEncodingException if an exception is thrown when
1087 * creating the InputStreamReader
1089 public ECMA48Terminal(final Object listener
, final InputStream input
,
1090 final OutputStream output
, final int windowWidth
,
1091 final int windowHeight
) throws UnsupportedEncodingException
{
1093 this(listener
, input
, output
);
1095 // Send dtterm/xterm sequences, which will probably not work because
1096 // allowWindowOps is defaulted to false.
1097 if ((windowWidth
> 0) && (windowHeight
> 0)) {
1098 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1100 this.output
.write(resizeString
);
1101 this.output
.flush();
1106 * Constructor sets up state for getEvent().
1108 * @param listener the object this backend needs to wake up when new
1110 * @param input an InputStream connected to the remote user, or null for
1111 * System.in. If System.in is used, then on non-Windows systems it will
1112 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1113 * cooked mode. input is always converted to a Reader with UTF-8
1115 * @param output an OutputStream connected to the remote user, or null
1116 * for System.out. output is always converted to a Writer with UTF-8
1118 * @throws UnsupportedEncodingException if an exception is thrown when
1119 * creating the InputStreamReader
1121 public ECMA48Terminal(final Object listener
, final InputStream input
,
1122 final OutputStream output
) throws UnsupportedEncodingException
{
1128 stopReaderThread
= false;
1129 this.listener
= listener
;
1131 if (input
== null) {
1132 // inputStream = System.in;
1133 inputStream
= new FileInputStream(FileDescriptor
.in
);
1137 inputStream
= input
;
1139 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1141 if (input
instanceof SessionInfo
) {
1142 // This is a TelnetInputStream that exposes window size and
1143 // environment variables from the telnet layer.
1144 sessionInfo
= (SessionInfo
) input
;
1146 if (sessionInfo
== null) {
1147 if (input
== null) {
1148 // Reading right off the tty
1149 sessionInfo
= new TTYSessionInfo();
1151 sessionInfo
= new TSessionInfo();
1155 if (output
== null) {
1156 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1159 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1163 // Request Device Attributes
1164 this.output
.printf("\033[c");
1166 // Request xterm report window/cell dimensions in pixels
1167 this.output
.printf("%s", xtermReportPixelDimensions());
1169 // Enable mouse reporting and metaSendsEscape
1170 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1172 // Request xterm use the sixel settings we want
1173 this.output
.printf("%s", xtermSetSixelSettings());
1175 this.output
.flush();
1177 // Query the screen size
1178 sessionInfo
.queryWindowSize();
1179 setDimensions(sessionInfo
.getWindowWidth(),
1180 sessionInfo
.getWindowHeight());
1182 // Hang onto the window size
1183 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1184 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1188 // Spin up the input reader
1189 eventQueue
= new ArrayList
<TInputEvent
>();
1190 readerThread
= new Thread(this);
1191 readerThread
.start();
1194 this.output
.write(clearAll());
1195 this.output
.flush();
1199 * Constructor sets up state for getEvent().
1201 * @param listener the object this backend needs to wake up when new
1203 * @param input the InputStream underlying 'reader'. Its available()
1204 * method is used to determine if reader.read() will block or not.
1205 * @param reader a Reader connected to the remote user.
1206 * @param writer a PrintWriter connected to the remote user.
1207 * @param setRawMode if true, set System.in into raw mode with stty.
1208 * This should in general not be used. It is here solely for Demo3,
1209 * which uses System.in.
1210 * @throws IllegalArgumentException if input, reader, or writer are null.
1212 public ECMA48Terminal(final Object listener
, final InputStream input
,
1213 final Reader reader
, final PrintWriter writer
,
1214 final boolean setRawMode
) {
1216 if (input
== null) {
1217 throw new IllegalArgumentException("InputStream must be specified");
1219 if (reader
== null) {
1220 throw new IllegalArgumentException("Reader must be specified");
1222 if (writer
== null) {
1223 throw new IllegalArgumentException("Writer must be specified");
1229 stopReaderThread
= false;
1230 this.listener
= listener
;
1232 inputStream
= input
;
1233 this.input
= reader
;
1235 if (setRawMode
== true) {
1238 this.setRawMode
= setRawMode
;
1240 if (input
instanceof SessionInfo
) {
1241 // This is a TelnetInputStream that exposes window size and
1242 // environment variables from the telnet layer.
1243 sessionInfo
= (SessionInfo
) input
;
1245 if (sessionInfo
== null) {
1246 if (setRawMode
== true) {
1247 // Reading right off the tty
1248 sessionInfo
= new TTYSessionInfo();
1250 sessionInfo
= new TSessionInfo();
1254 this.output
= writer
;
1256 // Request Device Attributes
1257 this.output
.printf("\033[c");
1259 // Request xterm report window/cell dimensions in pixels
1260 this.output
.printf("%s", xtermReportPixelDimensions());
1262 // Enable mouse reporting and metaSendsEscape
1263 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1265 // Request xterm use the sixel settings we want
1266 this.output
.printf("%s", xtermSetSixelSettings());
1268 this.output
.flush();
1270 // Query the screen size
1271 sessionInfo
.queryWindowSize();
1272 setDimensions(sessionInfo
.getWindowWidth(),
1273 sessionInfo
.getWindowHeight());
1275 // Hang onto the window size
1276 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1277 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1281 // Spin up the input reader
1282 eventQueue
= new ArrayList
<TInputEvent
>();
1283 readerThread
= new Thread(this);
1284 readerThread
.start();
1287 this.output
.write(clearAll());
1288 this.output
.flush();
1292 * Constructor sets up state for getEvent().
1294 * @param listener the object this backend needs to wake up when new
1296 * @param input the InputStream underlying 'reader'. Its available()
1297 * method is used to determine if reader.read() will block or not.
1298 * @param reader a Reader connected to the remote user.
1299 * @param writer a PrintWriter connected to the remote user.
1300 * @throws IllegalArgumentException if input, reader, or writer are null.
1302 public ECMA48Terminal(final Object listener
, final InputStream input
,
1303 final Reader reader
, final PrintWriter writer
) {
1305 this(listener
, input
, reader
, writer
, false);
1308 // ------------------------------------------------------------------------
1309 // LogicalScreen ----------------------------------------------------------
1310 // ------------------------------------------------------------------------
1313 * Set the window title.
1315 * @param title the new title
1318 public void setTitle(final String title
) {
1319 output
.write(getSetTitleString(title
));
1324 * Push the logical screen to the physical device.
1327 public void flushPhysical() {
1328 StringBuilder sb
= new StringBuilder();
1332 && (cursorY
<= height
- 1)
1333 && (cursorX
<= width
- 1)
1336 sb
.append(cursor(true));
1337 sb
.append(gotoXY(cursorX
, cursorY
));
1339 sb
.append(cursor(false));
1342 output
.write(sb
.toString());
1347 * Resize the physical screen to match the logical screen dimensions.
1350 public void resizeToScreen() {
1351 // Send dtterm/xterm sequences, which will probably not work because
1352 // allowWindowOps is defaulted to false.
1353 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1355 this.output
.write(resizeString
);
1356 this.output
.flush();
1359 // ------------------------------------------------------------------------
1360 // TerminalReader ---------------------------------------------------------
1361 // ------------------------------------------------------------------------
1364 * Check if there are events in the queue.
1366 * @return if true, getEvents() has something to return to the backend
1368 public boolean hasEvents() {
1369 synchronized (eventQueue
) {
1370 return (eventQueue
.size() > 0);
1375 * Return any events in the IO queue.
1377 * @param queue list to append new events to
1379 public void getEvents(final List
<TInputEvent
> queue
) {
1380 synchronized (eventQueue
) {
1381 if (eventQueue
.size() > 0) {
1382 synchronized (queue
) {
1383 queue
.addAll(eventQueue
);
1391 * Restore terminal to normal state.
1393 public void closeTerminal() {
1395 // System.err.println("=== closeTerminal() ==="); System.err.flush();
1397 // Tell the reader thread to stop looking at input
1398 stopReaderThread
= true;
1400 readerThread
.join();
1401 } catch (InterruptedException e
) {
1402 if (debugToStderr
) {
1403 e
.printStackTrace();
1407 // Disable mouse reporting and show cursor. Defensive null check
1408 // here in case closeTerminal() is called twice.
1409 if (output
!= null) {
1410 output
.printf("%s%s%s%s", mouse(false), cursor(true),
1411 defaultColor(), xtermResetSixelSettings());
1418 // We don't close System.in/out
1420 // Shut down the streams, this should wake up the reader thread
1421 // and make it exit.
1422 if (input
!= null) {
1425 } catch (IOException e
) {
1430 if (output
!= null) {
1438 * Set listener to a different Object.
1440 * @param listener the new listening object that run() wakes up on new
1443 public void setListener(final Object listener
) {
1444 this.listener
= listener
;
1448 * Reload options from System properties.
1450 public void reloadOptions() {
1451 // Permit RGB colors only if externally requested.
1452 if (System
.getProperty("jexer.ECMA48.rgbColor",
1453 "false").equals("true")
1460 // Default to using images for full-width characters.
1461 if (System
.getProperty("jexer.ECMA48.wideCharImages",
1462 "true").equals("true")) {
1463 wideCharImages
= true;
1465 wideCharImages
= false;
1468 // Pull the system properties for sixel output.
1469 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1476 int paletteSize
= 1024;
1478 paletteSize
= Integer
.parseInt(System
.getProperty(
1479 "jexer.ECMA48.sixelPaletteSize", "1024"));
1480 switch (paletteSize
) {
1486 sixelPaletteSize
= paletteSize
;
1492 } catch (NumberFormatException e
) {
1497 if (System
.getProperty("jexer.ECMA48.sixelSharedPalette",
1498 "true").equals("false")) {
1499 sixelSharedPalette
= false;
1501 sixelSharedPalette
= true;
1504 // Default to not supporting iTerm2 images.
1505 if (System
.getProperty("jexer.ECMA48.iTerm2Images",
1506 "false").equals("true")) {
1507 iterm2Images
= true;
1509 iterm2Images
= false;
1512 // Default to using JPG Jexer images if terminal supports it.
1513 String jexerImageStr
= System
.getProperty("jexer.ECMA48.jexerImages",
1514 "jpg").toLowerCase();
1515 if (jexerImageStr
.equals("false")) {
1516 jexerImageOption
= JexerImageOption
.DISABLED
;
1517 } else if (jexerImageStr
.equals("jpg")) {
1518 jexerImageOption
= JexerImageOption
.JPG
;
1519 } else if (jexerImageStr
.equals("png")) {
1520 jexerImageOption
= JexerImageOption
.PNG
;
1521 } else if (jexerImageStr
.equals("rgb")) {
1522 jexerImageOption
= JexerImageOption
.RGB
;
1525 // Set custom colors
1526 setCustomSystemColors();
1529 // ------------------------------------------------------------------------
1530 // Runnable ---------------------------------------------------------------
1531 // ------------------------------------------------------------------------
1534 * Read function runs on a separate thread.
1537 boolean done
= false;
1538 // available() will often return > 1, so we need to read in chunks to
1540 char [] readBuffer
= new char[128];
1541 List
<TInputEvent
> events
= new ArrayList
<TInputEvent
>();
1543 while (!done
&& !stopReaderThread
) {
1545 // We assume that if inputStream has bytes available, then
1546 // input won't block on read().
1547 int n
= inputStream
.available();
1550 System.err.printf("inputStream.available(): %d\n", n);
1555 if (readBuffer
.length
< n
) {
1556 // The buffer wasn't big enough, make it huger
1557 readBuffer
= new char[readBuffer
.length
* 2];
1560 // System.err.printf("BEFORE read()\n"); System.err.flush();
1562 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1565 System.err.printf("AFTER read() %d\n", rc);
1573 for (int i
= 0; i
< rc
; i
++) {
1574 int ch
= readBuffer
[i
];
1575 processChar(events
, (char)ch
);
1577 getIdleEvents(events
);
1578 if (events
.size() > 0) {
1579 // Add to the queue for the backend thread to
1580 // be able to obtain.
1581 synchronized (eventQueue
) {
1582 eventQueue
.addAll(events
);
1584 if (listener
!= null) {
1585 synchronized (listener
) {
1586 listener
.notifyAll();
1593 getIdleEvents(events
);
1594 if (events
.size() > 0) {
1595 synchronized (eventQueue
) {
1596 eventQueue
.addAll(events
);
1598 if (listener
!= null) {
1599 synchronized (listener
) {
1600 listener
.notifyAll();
1606 if (output
.checkError()) {
1611 // Wait 20 millis for more data
1614 // System.err.println("end while loop"); System.err.flush();
1615 } catch (InterruptedException e
) {
1617 } catch (IOException e
) {
1618 e
.printStackTrace();
1621 } // while ((done == false) && (stopReaderThread == false))
1623 // Pass an event up to TApplication to tell it this Backend is done.
1624 synchronized (eventQueue
) {
1625 eventQueue
.add(new TCommandEvent(cmBackendDisconnect
));
1627 if (listener
!= null) {
1628 synchronized (listener
) {
1629 listener
.notifyAll();
1633 // System.err.println("*** run() exiting..."); System.err.flush();
1636 // ------------------------------------------------------------------------
1637 // ECMA48Terminal ---------------------------------------------------------
1638 // ------------------------------------------------------------------------
1641 * Get the width of a character cell in pixels.
1643 * @return the width in pixels of a character cell
1645 public int getTextWidth() {
1646 if (sessionInfo
.getWindowWidth() > 0) {
1647 return (widthPixels
/ sessionInfo
.getWindowWidth());
1653 * Get the height of a character cell in pixels.
1655 * @return the height in pixels of a character cell
1657 public int getTextHeight() {
1658 if (sessionInfo
.getWindowHeight() > 0) {
1659 return (heightPixels
/ sessionInfo
.getWindowHeight());
1665 * Getter for sessionInfo.
1667 * @return the SessionInfo
1669 public SessionInfo
getSessionInfo() {
1674 * Get the output writer.
1676 * @return the Writer
1678 public PrintWriter
getOutput() {
1683 * Call 'stty' to set cooked mode.
1685 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1687 private void sttyCooked() {
1692 * Call 'stty' to set raw mode.
1694 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1695 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1696 * -parenb cs8 min 1 < /dev/tty'
1698 private void sttyRaw() {
1703 * Call 'stty' to set raw or cooked mode.
1705 * @param mode if true, set raw mode, otherwise set cooked mode
1707 private void doStty(final boolean mode
) {
1708 String
[] cmdRaw
= {
1709 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1711 String
[] cmdCooked
= {
1712 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1717 process
= Runtime
.getRuntime().exec(cmdRaw
);
1719 process
= Runtime
.getRuntime().exec(cmdCooked
);
1721 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1722 String line
= in
.readLine();
1723 if ((line
!= null) && (line
.length() > 0)) {
1724 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1727 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1728 line
= err
.readLine();
1729 if ((line
!= null) && (line
.length() > 0)) {
1730 System
.err
.println("Error output from stty: " + line
);
1735 } catch (InterruptedException e
) {
1736 if (debugToStderr
) {
1737 e
.printStackTrace();
1741 int rc
= process
.exitValue();
1743 System
.err
.println("stty returned error code: " + rc
);
1745 } catch (IOException e
) {
1746 e
.printStackTrace();
1753 public void flush() {
1758 * Perform a somewhat-optimal rendering of a line.
1760 * @param y row coordinate. 0 is the top-most row.
1761 * @param sb StringBuilder to write escape sequences to
1762 * @param lastAttr cell attributes from the last call to flushLine
1764 private void flushLine(final int y
, final StringBuilder sb
,
1765 CellAttributes lastAttr
) {
1769 for (int x
= 0; x
< width
; x
++) {
1770 Cell lCell
= logical
[x
][y
];
1771 if (!lCell
.isBlank()) {
1775 // Push textEnd to first column beyond the text area
1779 // reallyCleared = true;
1781 boolean hasImage
= false;
1783 for (int x
= 0; x
< width
; x
++) {
1784 Cell lCell
= logical
[x
][y
];
1785 Cell pCell
= physical
[x
][y
];
1787 if (!lCell
.equals(pCell
) || reallyCleared
) {
1789 if (debugToStderr
) {
1790 System
.err
.printf("\n--\n");
1791 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1792 System
.err
.printf(" lCell: %s\n", lCell
);
1793 System
.err
.printf(" pCell: %s\n", pCell
);
1794 System
.err
.printf(" ==== \n");
1797 if (lastAttr
== null) {
1798 lastAttr
= new CellAttributes();
1799 sb
.append(normal());
1803 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1804 // Advancing at least one cell, or the first gotoXY
1805 sb
.append(gotoXY(x
, y
));
1808 assert (lastAttr
!= null);
1810 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1811 assert (lCell
.isBlank());
1813 for (int i
= x
; i
< width
; i
++) {
1814 assert (logical
[i
][y
].isBlank());
1815 // Physical is always updated
1816 physical
[i
][y
].reset();
1819 // Clear remaining line
1820 sb
.append(clearRemainingLine());
1825 // Image cell: bypass the rest of the loop, it is not
1827 if ((wideCharImages
&& lCell
.isImage())
1830 && (lCell
.getWidth() == Cell
.Width
.SINGLE
))
1834 // Save the last rendered cell
1837 // Physical is always updated
1838 physical
[x
][y
].setTo(lCell
);
1842 assert ((wideCharImages
&& !lCell
.isImage())
1844 && (!lCell
.isImage()
1846 && (lCell
.getWidth() != Cell
.Width
.SINGLE
)))));
1848 if (!wideCharImages
&& (lCell
.getWidth() == Cell
.Width
.RIGHT
)) {
1854 sb
.append(gotoXY(x
, y
));
1857 // Now emit only the modified attributes
1858 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1859 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1861 && (lCell
.isBold() == lastAttr
.isBold())
1862 && (lCell
.isReverse() == lastAttr
.isReverse())
1863 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1864 && (lCell
.isBlink() == lastAttr
.isBlink())
1866 // Both colors changed, attributes the same
1867 sb
.append(color(lCell
.isBold(),
1868 lCell
.getForeColor(), lCell
.getBackColor()));
1870 if (debugToStderr
) {
1871 System
.err
.printf("1 Change only fore/back colors\n");
1874 } else if (lCell
.isRGB()
1875 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1876 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1877 && (lCell
.isBold() == lastAttr
.isBold())
1878 && (lCell
.isReverse() == lastAttr
.isReverse())
1879 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1880 && (lCell
.isBlink() == lastAttr
.isBlink())
1882 // Both colors changed, attributes the same
1883 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1884 lCell
.getBackColorRGB()));
1886 if (debugToStderr
) {
1887 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1889 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1890 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1892 && (lCell
.isBold() != lastAttr
.isBold())
1893 && (lCell
.isReverse() != lastAttr
.isReverse())
1894 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1895 && (lCell
.isBlink() != lastAttr
.isBlink())
1897 // Everything is different
1898 sb
.append(color(lCell
.getForeColor(),
1899 lCell
.getBackColor(),
1900 lCell
.isBold(), lCell
.isReverse(),
1902 lCell
.isUnderline()));
1904 if (debugToStderr
) {
1905 System
.err
.printf("2 Set all attributes\n");
1907 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1908 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1910 && (lCell
.isBold() == lastAttr
.isBold())
1911 && (lCell
.isReverse() == lastAttr
.isReverse())
1912 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1913 && (lCell
.isBlink() == lastAttr
.isBlink())
1916 // Attributes same, foreColor different
1917 sb
.append(color(lCell
.isBold(),
1918 lCell
.getForeColor(), true));
1920 if (debugToStderr
) {
1921 System
.err
.printf("3 Change foreColor\n");
1923 } else if (lCell
.isRGB()
1924 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1925 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1926 && (lCell
.getForeColorRGB() >= 0)
1927 && (lCell
.getBackColorRGB() >= 0)
1928 && (lCell
.isBold() == lastAttr
.isBold())
1929 && (lCell
.isReverse() == lastAttr
.isReverse())
1930 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1931 && (lCell
.isBlink() == lastAttr
.isBlink())
1933 // Attributes same, foreColor different
1934 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1936 if (debugToStderr
) {
1937 System
.err
.printf("3 Change foreColor (RGB)\n");
1939 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1940 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1942 && (lCell
.isBold() == lastAttr
.isBold())
1943 && (lCell
.isReverse() == lastAttr
.isReverse())
1944 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1945 && (lCell
.isBlink() == lastAttr
.isBlink())
1947 // Attributes same, backColor different
1948 sb
.append(color(lCell
.isBold(),
1949 lCell
.getBackColor(), false));
1951 if (debugToStderr
) {
1952 System
.err
.printf("4 Change backColor\n");
1954 } else if (lCell
.isRGB()
1955 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1956 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1957 && (lCell
.isBold() == lastAttr
.isBold())
1958 && (lCell
.isReverse() == lastAttr
.isReverse())
1959 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1960 && (lCell
.isBlink() == lastAttr
.isBlink())
1962 // Attributes same, foreColor different
1963 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1965 if (debugToStderr
) {
1966 System
.err
.printf("4 Change backColor (RGB)\n");
1968 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1969 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1970 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1971 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1972 && (lCell
.isBold() == lastAttr
.isBold())
1973 && (lCell
.isReverse() == lastAttr
.isReverse())
1974 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1975 && (lCell
.isBlink() == lastAttr
.isBlink())
1978 // All attributes the same, just print the char
1981 if (debugToStderr
) {
1982 System
.err
.printf("5 Only emit character\n");
1985 // Just reset everything again
1986 if (!lCell
.isRGB()) {
1987 sb
.append(color(lCell
.getForeColor(),
1988 lCell
.getBackColor(),
1992 lCell
.isUnderline()));
1994 if (debugToStderr
) {
1995 System
.err
.printf("6 Change all attributes\n");
1998 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1999 lCell
.getBackColorRGB(),
2003 lCell
.isUnderline()));
2004 if (debugToStderr
) {
2005 System
.err
.printf("6 Change all attributes (RGB)\n");
2010 // Emit the character
2012 // Don't emit the right-half of full-width chars.
2014 && (lCell
.getWidth() != Cell
.Width
.RIGHT
))
2016 sb
.append(Character
.toChars(lCell
.getChar()));
2019 // Save the last rendered cell
2021 lastAttr
.setTo(lCell
);
2023 // Physical is always updated
2024 physical
[x
][y
].setTo(lCell
);
2026 } // if (!lCell.equals(pCell) || (reallyCleared == true))
2028 } // for (int x = 0; x < width; x++)
2032 * Render the screen to a string that can be emitted to something that
2033 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
2035 * @param sb StringBuilder to write escape sequences to
2036 * @return escape sequences string that provides the updates to the
2039 private String
flushString(final StringBuilder sb
) {
2040 CellAttributes attr
= null;
2042 if (reallyCleared
) {
2043 attr
= new CellAttributes();
2044 sb
.append(clearAll());
2048 * For images support, draw all of the image output first, and then
2049 * draw everything else afterwards. This works OK, but performance
2050 * is still a drag on larger pictures.
2052 for (int y
= 0; y
< height
; y
++) {
2053 for (int x
= 0; x
< width
; x
++) {
2054 // If physical had non-image data that is now image data, the
2055 // entire row must be redrawn.
2056 Cell lCell
= logical
[x
][y
];
2057 Cell pCell
= physical
[x
][y
];
2058 if (lCell
.isImage() && !pCell
.isImage()) {
2064 for (int y
= 0; y
< height
; y
++) {
2065 for (int x
= 0; x
< width
; x
++) {
2066 Cell lCell
= logical
[x
][y
];
2067 Cell pCell
= physical
[x
][y
];
2069 if (!lCell
.isImage()
2071 && (lCell
.getWidth() != Cell
.Width
.SINGLE
))
2078 while ((right
< width
)
2079 && (logical
[right
][y
].isImage())
2080 && (!logical
[right
][y
].equals(physical
[right
][y
])
2085 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
2086 for (int i
= 0; i
< (right
- x
); i
++) {
2087 assert (logical
[x
+ i
][y
].isImage());
2088 cellsToDraw
.add(logical
[x
+ i
][y
]);
2090 // Physical is always updated.
2091 physical
[x
+ i
][y
].setTo(lCell
);
2093 if (cellsToDraw
.size() > 0) {
2095 sb
.append(toIterm2Image(x
, y
, cellsToDraw
));
2096 } else if (jexerImageOption
!= JexerImageOption
.DISABLED
) {
2097 sb
.append(toJexerImage(x
, y
, cellsToDraw
));
2099 sb
.append(toSixel(x
, y
, cellsToDraw
));
2107 // Draw the text part now.
2108 for (int y
= 0; y
< height
; y
++) {
2109 flushLine(y
, sb
, attr
);
2112 reallyCleared
= false;
2114 String result
= sb
.toString();
2115 if (debugToStderr
) {
2116 System
.err
.printf("flushString(): %s\n", result
);
2122 * Reset keyboard/mouse input parser.
2124 private void resetParser() {
2125 state
= ParseState
.GROUND
;
2126 params
= new ArrayList
<String
>();
2129 decPrivateModeFlag
= false;
2133 * Produce a control character or one of the special ones (ENTER, TAB,
2136 * @param ch Unicode code point
2137 * @param alt if true, set alt on the TKeypress
2138 * @return one TKeypress event, either a control character (e.g. isKey ==
2139 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
2142 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
2143 // System.err.printf("controlChar: %02x\n", ch);
2147 // Carriage return --> ENTER
2148 return new TKeypressEvent(kbEnter
, alt
, false, false);
2150 // Linefeed --> ENTER
2151 return new TKeypressEvent(kbEnter
, alt
, false, false);
2154 return new TKeypressEvent(kbEsc
, alt
, false, false);
2157 return new TKeypressEvent(kbTab
, alt
, false, false);
2159 // Make all other control characters come back as the alphabetic
2160 // character with the ctrl field set. So SOH would be 'A' +
2162 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
2168 * Produce special key from CSI Pn ; Pm ; ... ~
2170 * @return one KEYPRESS event representing a special key
2172 private TInputEvent
csiFnKey() {
2174 if (params
.size() > 0) {
2175 key
= Integer
.parseInt(params
.get(0));
2177 boolean alt
= false;
2178 boolean ctrl
= false;
2179 boolean shift
= false;
2180 if (params
.size() > 1) {
2181 shift
= csiIsShift(params
.get(1));
2182 alt
= csiIsAlt(params
.get(1));
2183 ctrl
= csiIsCtrl(params
.get(1));
2188 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2190 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2192 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2194 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2196 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2198 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2200 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2202 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2204 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2206 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2208 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2210 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2212 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2214 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2222 * Produce mouse events based on "Any event tracking" and UTF-8
2224 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2226 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2228 private TInputEvent
parseMouse() {
2229 int buttons
= params
.get(0).charAt(0) - 32;
2230 int x
= params
.get(0).charAt(1) - 32 - 1;
2231 int y
= params
.get(0).charAt(2) - 32 - 1;
2233 // Clamp X and Y to the physical screen coordinates.
2234 if (x
>= windowResize
.getWidth()) {
2235 x
= windowResize
.getWidth() - 1;
2237 if (y
>= windowResize
.getHeight()) {
2238 y
= windowResize
.getHeight() - 1;
2241 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2242 boolean eventMouse1
= false;
2243 boolean eventMouse2
= false;
2244 boolean eventMouse3
= false;
2245 boolean eventMouseWheelUp
= false;
2246 boolean eventMouseWheelDown
= false;
2247 boolean eventAlt
= false;
2248 boolean eventCtrl
= false;
2249 boolean eventShift
= false;
2251 // System.err.printf("buttons: %04x\r\n", buttons);
2253 switch (buttons
& 0xE3) {
2268 if (!mouse1
&& !mouse2
&& !mouse3
) {
2269 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2271 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2288 // Dragging with mouse1 down
2291 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2295 // Dragging with mouse2 down
2298 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2302 // Dragging with mouse3 down
2305 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2309 // Dragging with mouse2 down after wheelUp
2312 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2316 // Dragging with mouse2 down after wheelDown
2319 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2323 eventMouseWheelUp
= true;
2327 eventMouseWheelDown
= true;
2331 // Unknown, just make it motion
2332 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2336 if ((buttons
& 0x04) != 0) {
2339 if ((buttons
& 0x08) != 0) {
2342 if ((buttons
& 0x10) != 0) {
2346 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2347 eventMouse1
, eventMouse2
, eventMouse3
,
2348 eventMouseWheelUp
, eventMouseWheelDown
,
2349 eventAlt
, eventCtrl
, eventShift
);
2353 * Produce mouse events based on "Any event tracking" and SGR
2355 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2357 * @param release if true, this was a release ('m')
2358 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2360 private TInputEvent
parseMouseSGR(final boolean release
) {
2361 // SGR extended coordinates - mode 1006
2362 if (params
.size() < 3) {
2363 // Invalid position, bail out.
2366 int buttons
= Integer
.parseInt(params
.get(0));
2367 int x
= Integer
.parseInt(params
.get(1)) - 1;
2368 int y
= Integer
.parseInt(params
.get(2)) - 1;
2370 // Clamp X and Y to the physical screen coordinates.
2371 if (x
>= windowResize
.getWidth()) {
2372 x
= windowResize
.getWidth() - 1;
2374 if (y
>= windowResize
.getHeight()) {
2375 y
= windowResize
.getHeight() - 1;
2378 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2379 boolean eventMouse1
= false;
2380 boolean eventMouse2
= false;
2381 boolean eventMouse3
= false;
2382 boolean eventMouseWheelUp
= false;
2383 boolean eventMouseWheelDown
= false;
2384 boolean eventAlt
= false;
2385 boolean eventCtrl
= false;
2386 boolean eventShift
= false;
2389 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2392 switch (buttons
& 0xE3) {
2403 // Motion only, no buttons down
2404 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2408 // Dragging with mouse1 down
2410 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2414 // Dragging with mouse2 down
2416 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2420 // Dragging with mouse3 down
2422 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2426 // Dragging with mouse2 down after wheelUp
2428 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2432 // Dragging with mouse2 down after wheelDown
2434 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2438 eventMouseWheelUp
= true;
2442 eventMouseWheelDown
= true;
2446 // Unknown, bail out
2450 if ((buttons
& 0x04) != 0) {
2453 if ((buttons
& 0x08) != 0) {
2456 if ((buttons
& 0x10) != 0) {
2460 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2461 eventMouse1
, eventMouse2
, eventMouse3
,
2462 eventMouseWheelUp
, eventMouseWheelDown
,
2463 eventAlt
, eventCtrl
, eventShift
);
2467 * Return any events in the IO queue due to timeout.
2469 * @param queue list to append new events to
2471 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2472 long nowTime
= System
.currentTimeMillis();
2474 // Check for new window size
2475 long windowSizeDelay
= nowTime
- windowSizeTime
;
2476 if (windowSizeDelay
> 1000) {
2477 int oldTextWidth
= getTextWidth();
2478 int oldTextHeight
= getTextHeight();
2480 sessionInfo
.queryWindowSize();
2481 int newWidth
= sessionInfo
.getWindowWidth();
2482 int newHeight
= sessionInfo
.getWindowHeight();
2484 if ((newWidth
!= windowResize
.getWidth())
2485 || (newHeight
!= windowResize
.getHeight())
2488 // Request xterm report window dimensions in pixels again.
2489 // Between now and then, ensure that the reported text cell
2490 // size is the same by setting widthPixels and heightPixels
2491 // to match the new dimensions.
2492 widthPixels
= oldTextWidth
* newWidth
;
2493 heightPixels
= oldTextHeight
* newHeight
;
2495 if (debugToStderr
) {
2496 System
.err
.println("Screen size changed, old size " +
2498 System
.err
.println(" new size " +
2499 newWidth
+ " x " + newHeight
);
2500 System
.err
.println(" old pixels " +
2501 oldTextWidth
+ " x " + oldTextHeight
);
2502 System
.err
.println(" new pixels " +
2503 getTextWidth() + " x " + getTextHeight());
2506 this.output
.printf("%s", xtermReportPixelDimensions());
2507 this.output
.flush();
2509 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2510 newWidth
, newHeight
);
2511 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2512 newWidth
, newHeight
);
2515 windowSizeTime
= nowTime
;
2518 // ESCDELAY type timeout
2519 if (state
== ParseState
.ESCAPE
) {
2520 long escDelay
= nowTime
- escapeTime
;
2521 if (escDelay
> 100) {
2522 // After 0.1 seconds, assume a true escape character
2523 queue
.add(controlChar((char)0x1B, false));
2530 * Returns true if the CSI parameter for a keyboard command means that
2533 private boolean csiIsShift(final String x
) {
2545 * Returns true if the CSI parameter for a keyboard command means that
2548 private boolean csiIsAlt(final String x
) {
2560 * Returns true if the CSI parameter for a keyboard command means that
2563 private boolean csiIsCtrl(final String x
) {
2575 * Parses the next character of input to see if an InputEvent is
2578 * @param events list to append new events to
2579 * @param ch Unicode code point
2581 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2583 // ESCDELAY type timeout
2584 long nowTime
= System
.currentTimeMillis();
2585 if (state
== ParseState
.ESCAPE
) {
2586 long escDelay
= nowTime
- escapeTime
;
2587 if (escDelay
> 250) {
2588 // After 0.25 seconds, assume a true escape character
2589 events
.add(controlChar((char)0x1B, false));
2595 boolean ctrl
= false;
2596 boolean alt
= false;
2597 boolean shift
= false;
2599 // System.err.printf("state: %s ch %c\r\n", state, ch);
2605 state
= ParseState
.ESCAPE
;
2606 escapeTime
= nowTime
;
2611 // Control character
2612 events
.add(controlChar(ch
, false));
2619 events
.add(new TKeypressEvent(false, 0, ch
,
2620 false, false, false));
2629 // ALT-Control character
2630 events
.add(controlChar(ch
, true));
2636 // This will be one of the function keys
2637 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2641 // '[' goes to CSI_ENTRY
2643 state
= ParseState
.CSI_ENTRY
;
2647 // Everything else is assumed to be Alt-keystroke
2648 if ((ch
>= 'A') && (ch
<= 'Z')) {
2652 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2656 case ESCAPE_INTERMEDIATE
:
2657 if ((ch
>= 'P') && (ch
<= 'S')) {
2661 events
.add(new TKeypressEvent(kbF1
));
2664 events
.add(new TKeypressEvent(kbF2
));
2667 events
.add(new TKeypressEvent(kbF3
));
2670 events
.add(new TKeypressEvent(kbF4
));
2679 // Unknown keystroke, ignore
2684 // Numbers - parameter values
2685 if ((ch
>= '0') && (ch
<= '9')) {
2686 params
.set(params
.size() - 1,
2687 params
.get(params
.size() - 1) + ch
);
2688 state
= ParseState
.CSI_PARAM
;
2691 // Parameter separator
2697 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2701 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2706 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2711 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2716 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2721 events
.add(new TKeypressEvent(kbHome
));
2726 events
.add(new TKeypressEvent(kbEnd
));
2730 // CBT - Cursor backward X tab stops (default 1)
2731 events
.add(new TKeypressEvent(kbBackTab
));
2736 state
= ParseState
.MOUSE
;
2739 // Mouse position, SGR (1006) coordinates
2740 state
= ParseState
.MOUSE_SGR
;
2743 // DEC private mode flag
2744 decPrivateModeFlag
= true;
2751 // Unknown keystroke, ignore
2756 // Numbers - parameter values
2757 if ((ch
>= '0') && (ch
<= '9')) {
2758 params
.set(params
.size() - 1,
2759 params
.get(params
.size() - 1) + ch
);
2762 // Parameter separator
2770 // Generate a mouse press event
2771 TInputEvent event
= parseMouseSGR(false);
2772 if (event
!= null) {
2778 // Generate a mouse release event
2779 event
= parseMouseSGR(true);
2780 if (event
!= null) {
2789 // Unknown keystroke, ignore
2794 // Numbers - parameter values
2795 if ((ch
>= '0') && (ch
<= '9')) {
2796 params
.set(params
.size() - 1,
2797 params
.get(params
.size() - 1) + ch
);
2798 state
= ParseState
.CSI_PARAM
;
2801 // Parameter separator
2808 events
.add(csiFnKey());
2813 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2817 if (params
.size() > 1) {
2818 shift
= csiIsShift(params
.get(1));
2819 alt
= csiIsAlt(params
.get(1));
2820 ctrl
= csiIsCtrl(params
.get(1));
2822 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2827 if (params
.size() > 1) {
2828 shift
= csiIsShift(params
.get(1));
2829 alt
= csiIsAlt(params
.get(1));
2830 ctrl
= csiIsCtrl(params
.get(1));
2832 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2837 if (params
.size() > 1) {
2838 shift
= csiIsShift(params
.get(1));
2839 alt
= csiIsAlt(params
.get(1));
2840 ctrl
= csiIsCtrl(params
.get(1));
2842 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2847 if (params
.size() > 1) {
2848 shift
= csiIsShift(params
.get(1));
2849 alt
= csiIsAlt(params
.get(1));
2850 ctrl
= csiIsCtrl(params
.get(1));
2852 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2857 if (params
.size() > 1) {
2858 shift
= csiIsShift(params
.get(1));
2859 alt
= csiIsAlt(params
.get(1));
2860 ctrl
= csiIsCtrl(params
.get(1));
2862 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2867 if (params
.size() > 1) {
2868 shift
= csiIsShift(params
.get(1));
2869 alt
= csiIsAlt(params
.get(1));
2870 ctrl
= csiIsCtrl(params
.get(1));
2872 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2876 // Device Attributes
2877 if (decPrivateModeFlag
== false) {
2880 boolean jexerImages
= false;
2881 for (String x
: params
) {
2882 if (x
.equals("4")) {
2883 // Terminal reports sixel support
2884 if (debugToStderr
) {
2885 System
.err
.println("Device Attributes: sixel");
2888 if (x
.equals("444")) {
2889 // Terminal reports Jexer images support
2890 if (debugToStderr
) {
2891 System
.err
.println("Device Attributes: Jexer images");
2895 if (x
.equals("1337")) {
2896 // Terminal reports iTerm2 images support
2897 if (debugToStderr
) {
2898 System
.err
.println("Device Attributes: iTerm2 images");
2900 iterm2Images
= true;
2903 if (jexerImages
== false) {
2904 // Terminal does not support Jexer images, disable
2906 jexerImageOption
= JexerImageOption
.DISABLED
;
2912 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2913 if (debugToStderr
) {
2914 System
.err
.printf("windowOp pixels: " +
2915 "height %s width %s\n",
2916 params
.get(1), params
.get(2));
2919 widthPixels
= Integer
.parseInt(params
.get(2));
2920 heightPixels
= Integer
.parseInt(params
.get(1));
2921 } catch (NumberFormatException e
) {
2922 if (debugToStderr
) {
2923 e
.printStackTrace();
2926 if (widthPixels
<= 0) {
2929 if (heightPixels
<= 0) {
2933 if ((params
.size() > 2) && (params
.get(0).equals("6"))) {
2934 if (debugToStderr
) {
2935 System
.err
.printf("windowOp text cell pixels: " +
2936 "height %s width %s\n",
2937 params
.get(1), params
.get(2));
2940 widthPixels
= width
* Integer
.parseInt(params
.get(2));
2941 heightPixels
= height
* Integer
.parseInt(params
.get(1));
2942 } catch (NumberFormatException e
) {
2943 if (debugToStderr
) {
2944 e
.printStackTrace();
2947 if (widthPixels
<= 0) {
2950 if (heightPixels
<= 0) {
2961 // Unknown keystroke, ignore
2966 params
.set(0, params
.get(params
.size() - 1) + ch
);
2967 if (params
.get(0).length() == 3) {
2968 // We have enough to generate a mouse event
2969 events
.add(parseMouse());
2978 // This "should" be impossible to reach
2983 * Request (u)xterm to use the sixel settings we need:
2985 * - enable sixel scrolling
2987 * - disable private color registers (so that we can use one common
2988 * palette) if sixelSharedPalette is set
2990 * @return the string to emit to xterm
2992 private String
xtermSetSixelSettings() {
2993 if (sixelSharedPalette
== true) {
2994 return "\033[?80h\033[?1070l";
2996 return "\033[?80h\033[?1070h";
3001 * Restore (u)xterm its default sixel settings:
3003 * - enable sixel scrolling
3005 * - enable private color registers
3007 * @return the string to emit to xterm
3009 private String
xtermResetSixelSettings() {
3010 return "\033[?80h\033[?1070h";
3014 * Request (u)xterm to report the current window and cell size dimensions
3017 * @return the string to emit to xterm
3019 private String
xtermReportPixelDimensions() {
3020 // We will ask for both window and text cell dimensions, and
3021 // hopefully one of them will work.
3022 return "\033[14t\033[16t";
3026 * Tell (u)xterm that we want alt- keystrokes to send escape + character
3027 * rather than set the 8th bit. Anyone who wants UTF8 should want this
3030 * @param on if true, enable metaSendsEscape
3031 * @return the string to emit to xterm
3033 private String
xtermMetaSendsEscape(final boolean on
) {
3035 return "\033[?1036h\033[?1034l";
3037 return "\033[?1036l";
3041 * Create an xterm OSC sequence to change the window title.
3043 * @param title the new title
3044 * @return the string to emit to xterm
3046 private String
getSetTitleString(final String title
) {
3047 return "\033]2;" + title
+ "\007";
3050 // ------------------------------------------------------------------------
3051 // Sixel output support ---------------------------------------------------
3052 // ------------------------------------------------------------------------
3055 * Get the number of colors in the sixel palette.
3057 * @return the palette size
3059 public int getSixelPaletteSize() {
3060 return sixelPaletteSize
;
3064 * Set the number of colors in the sixel palette.
3066 * @param paletteSize the new palette size
3068 public void setSixelPaletteSize(final int paletteSize
) {
3069 if (paletteSize
== sixelPaletteSize
) {
3073 switch (paletteSize
) {
3081 throw new IllegalArgumentException("Unsupported sixel palette " +
3082 " size: " + paletteSize
);
3085 // Don't step on the screen refresh thread.
3086 synchronized (this) {
3087 sixelPaletteSize
= paletteSize
;
3095 * Start a sixel string for display one row's worth of bitmap data.
3097 * @param x column coordinate. 0 is the left-most column.
3098 * @param y row coordinate. 0 is the top-most row.
3099 * @return the string to emit to an ANSI / ECMA-style terminal
3101 private String
startSixel(final int x
, final int y
) {
3102 StringBuilder sb
= new StringBuilder();
3104 assert (sixel
== true);
3107 sb
.append(gotoXY(x
, y
));
3110 sb
.append("\033Pq");
3112 if (palette
== null) {
3113 palette
= new SixelPalette();
3114 if (sixelSharedPalette
== true) {
3115 palette
.emitPalette(sb
, null);
3119 return sb
.toString();
3123 * End a sixel string for display one row's worth of bitmap data.
3125 * @return the string to emit to an ANSI / ECMA-style terminal
3127 private String
endSixel() {
3128 assert (sixel
== true);
3135 * Create a sixel string representing a row of several cells containing
3138 * @param x column coordinate. 0 is the left-most column.
3139 * @param y row coordinate. 0 is the top-most row.
3140 * @param cells the cells containing the bitmap data
3141 * @return the string to emit to an ANSI / ECMA-style terminal
3143 private String
toSixel(final int x
, final int y
,
3144 final ArrayList
<Cell
> cells
) {
3146 StringBuilder sb
= new StringBuilder();
3148 assert (cells
!= null);
3149 assert (cells
.size() > 0);
3150 assert (cells
.get(0).getImage() != null);
3152 if (sixel
== false) {
3153 sb
.append(normal());
3154 sb
.append(gotoXY(x
, y
));
3155 for (int i
= 0; i
< cells
.size(); i
++) {
3158 return sb
.toString();
3161 if (y
== height
- 1) {
3162 // We are on the bottom row. If scrolling mode is enabled
3163 // (default), then VT320/xterm will scroll the entire screen if
3164 // we draw any pixels here. Do not draw the image, bail out
3166 sb
.append(normal());
3167 sb
.append(gotoXY(x
, y
));
3168 for (int j
= 0; j
< cells
.size(); j
++) {
3171 return sb
.toString();
3174 if (sixelCache
== null) {
3175 sixelCache
= new ImageCache(height
* 10);
3178 // Save and get rows to/from the cache that do NOT have inverted
3180 boolean saveInCache
= true;
3181 for (Cell cell
: cells
) {
3182 if (cell
.isInvertedImage()) {
3183 saveInCache
= false;
3187 String cachedResult
= sixelCache
.get(cells
);
3188 if (cachedResult
!= null) {
3189 // System.err.println("CACHE HIT");
3190 sb
.append(startSixel(x
, y
));
3191 sb
.append(cachedResult
);
3192 sb
.append(endSixel());
3193 return sb
.toString();
3195 // System.err.println("CACHE MISS");
3198 int imageWidth
= cells
.get(0).getImage().getWidth();
3199 int imageHeight
= cells
.get(0).getImage().getHeight();
3201 // Piece these together into one larger image for final rendering.
3203 int fullWidth
= cells
.size() * imageWidth
;
3204 int fullHeight
= imageHeight
;
3205 for (int i
= 0; i
< cells
.size(); i
++) {
3206 totalWidth
+= cells
.get(i
).getImage().getWidth();
3209 BufferedImage image
= new BufferedImage(fullWidth
,
3210 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3213 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3214 int tileWidth
= imageWidth
;
3215 int tileHeight
= imageHeight
;
3217 if (false && cells
.get(i
).isInvertedImage()) {
3218 // I used to put an all-white cell over the cursor, don't do
3220 rgbArray
= new int[imageWidth
* imageHeight
];
3221 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3222 rgbArray
[j
] = 0xFFFFFF;
3226 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3227 tileWidth
, tileHeight
, null, 0, tileWidth
);
3228 } catch (Exception e
) {
3229 throw new RuntimeException("image " + imageWidth
+ "x" +
3231 "tile " + tileWidth
+ "x" +
3233 " cells.get(i).getImage() " +
3234 cells
.get(i
).getImage() +
3236 " fullWidth " + fullWidth
+
3237 " fullHeight " + fullHeight
, e
);
3242 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3243 i * imageWidth, 0, imageWidth, imageHeight,
3245 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3246 fullWidth, fullHeight, cells.size(), getTextWidth());
3249 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3250 rgbArray
, 0, tileWidth
);
3251 if (tileHeight
< fullHeight
) {
3252 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3253 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3254 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3257 image
.setRGB(imageX
, imageY
, backgroundColor
);
3262 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3263 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3264 // I used to put an all-white cell over the cursor, don't do that
3266 rgbArray
= new int[totalWidth
* imageHeight
];
3267 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3268 rgbArray
[j
] = 0xFFFFFF;
3272 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3273 totalWidth
, imageHeight
, null, 0, totalWidth
);
3274 } catch (Exception e
) {
3275 throw new RuntimeException("image " + imageWidth
+ "x" +
3276 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3277 cells
.get(cells
.size() - 1).getImage(), e
);
3280 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3281 imageHeight
, rgbArray
, 0, totalWidth
);
3283 if (totalWidth
< imageWidth
) {
3284 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3286 for (int imageX
= image
.getWidth() - totalWidth
;
3287 imageX
< image
.getWidth(); imageX
++) {
3289 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3290 image
.setRGB(imageX
, imageY
, backgroundColor
);
3295 if ((image
.getWidth() != cells
.size() * getTextWidth())
3296 || (image
.getHeight() != getTextHeight())
3298 // Rescale the image to fit the text cells it is going into.
3299 BufferedImage newImage
;
3300 newImage
= new BufferedImage(cells
.size() * getTextWidth(),
3301 getTextHeight(), BufferedImage
.TYPE_INT_ARGB
);
3303 java
.awt
.Graphics gr
= newImage
.getGraphics();
3304 gr
.drawImage(image
, 0, 0, newImage
.getWidth(),
3305 newImage
.getHeight(), null, null);
3308 fullHeight
= image
.getHeight();
3311 // Dither the image. It is ok to lose the original here.
3312 if (palette
== null) {
3313 palette
= new SixelPalette();
3314 if (sixelSharedPalette
== true) {
3315 palette
.emitPalette(sb
, null);
3318 image
= palette
.ditherImage(image
);
3320 // Collect the raster information
3321 int rasterHeight
= 0;
3322 int rasterWidth
= image
.getWidth();
3324 if (sixelSharedPalette
== false) {
3325 // Emit the palette, but only for the colors actually used by
3327 boolean [] usedColors
= new boolean[sixelPaletteSize
];
3328 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3329 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
3330 usedColors
[image
.getRGB(imageX
, imageY
)] = true;
3333 palette
.emitPalette(sb
, usedColors
);
3336 // Render the entire row of cells.
3337 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
3338 int [][] sixels
= new int[image
.getWidth()][6];
3340 // See which colors are actually used in this band of sixels.
3341 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3342 for (int imageY
= 0;
3343 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
3346 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
3347 assert (colorIdx
>= 0);
3348 assert (colorIdx
< sixelPaletteSize
);
3350 sixels
[imageX
][imageY
] = colorIdx
;
3354 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
3355 boolean isUsed
= false;
3356 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3357 for (int j
= 0; j
< 6; j
++) {
3358 if (sixels
[imageX
][j
] == i
) {
3363 if (isUsed
== false) {
3367 // Set to the beginning of scan line for the next set of
3368 // colored pixels, and select the color.
3369 sb
.append(String
.format("$#%d", i
));
3372 int oldDataCount
= 0;
3373 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3375 // Add up all the pixels that match this color.
3378 (j
< 6) && (currentRow
+ j
< fullHeight
);
3381 if (sixels
[imageX
][j
] == i
) {
3402 if ((currentRow
+ j
+ 1) > rasterHeight
) {
3403 rasterHeight
= currentRow
+ j
+ 1;
3411 if (data
== oldData
) {
3414 if (oldDataCount
== 1) {
3415 sb
.append((char) oldData
);
3416 } else if (oldDataCount
> 1) {
3417 sb
.append(String
.format("!%d", oldDataCount
));
3418 sb
.append((char) oldData
);
3424 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
3426 // Emit the last sequence.
3427 if (oldDataCount
== 1) {
3428 sb
.append((char) oldData
);
3429 } else if (oldDataCount
> 1) {
3430 sb
.append(String
.format("!%d", oldDataCount
));
3431 sb
.append((char) oldData
);
3434 } // for (int i = 0; i < sixelPaletteSize; i++)
3436 // Advance to the next scan line.
3439 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3441 // Kill the very last "-", because it is unnecessary.
3442 sb
.deleteCharAt(sb
.length() - 1);
3444 // Add the raster information
3445 sb
.insert(0, String
.format("\"1;1;%d;%d", rasterWidth
, rasterHeight
));
3448 // This row is OK to save into the cache.
3449 sixelCache
.put(cells
, sb
.toString());
3452 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3456 * Get the sixel support flag.
3458 * @return true if this terminal is emitting sixel
3460 public boolean hasSixel() {
3464 // ------------------------------------------------------------------------
3465 // End sixel output support -----------------------------------------------
3466 // ------------------------------------------------------------------------
3468 // ------------------------------------------------------------------------
3469 // iTerm2 image output support --------------------------------------------
3470 // ------------------------------------------------------------------------
3473 * Create an iTerm2 images string representing a row of several cells
3474 * containing bitmap data.
3476 * @param x column coordinate. 0 is the left-most column.
3477 * @param y row coordinate. 0 is the top-most row.
3478 * @param cells the cells containing the bitmap data
3479 * @return the string to emit to an ANSI / ECMA-style terminal
3481 private String
toIterm2Image(final int x
, final int y
,
3482 final ArrayList
<Cell
> cells
) {
3484 StringBuilder sb
= new StringBuilder();
3486 assert (cells
!= null);
3487 assert (cells
.size() > 0);
3488 assert (cells
.get(0).getImage() != null);
3490 if (iterm2Images
== false) {
3491 sb
.append(normal());
3492 sb
.append(gotoXY(x
, y
));
3493 for (int i
= 0; i
< cells
.size(); i
++) {
3496 return sb
.toString();
3499 if (iterm2Cache
== null) {
3500 iterm2Cache
= new ImageCache(height
* 10);
3503 // Save and get rows to/from the cache that do NOT have inverted
3505 boolean saveInCache
= true;
3506 for (Cell cell
: cells
) {
3507 if (cell
.isInvertedImage()) {
3508 saveInCache
= false;
3512 String cachedResult
= iterm2Cache
.get(cells
);
3513 if (cachedResult
!= null) {
3514 // System.err.println("CACHE HIT");
3515 sb
.append(gotoXY(x
, y
));
3516 sb
.append(cachedResult
);
3517 return sb
.toString();
3519 // System.err.println("CACHE MISS");
3522 int imageWidth
= cells
.get(0).getImage().getWidth();
3523 int imageHeight
= cells
.get(0).getImage().getHeight();
3525 // Piece cells.get(x).getImage() pieces together into one larger
3526 // image for final rendering.
3528 int fullWidth
= cells
.size() * imageWidth
;
3529 int fullHeight
= imageHeight
;
3530 for (int i
= 0; i
< cells
.size(); i
++) {
3531 totalWidth
+= cells
.get(i
).getImage().getWidth();
3534 BufferedImage image
= new BufferedImage(fullWidth
,
3535 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3538 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3539 int tileWidth
= imageWidth
;
3540 int tileHeight
= imageHeight
;
3541 if (false && cells
.get(i
).isInvertedImage()) {
3542 // I used to put an all-white cell over the cursor, don't do
3544 rgbArray
= new int[imageWidth
* imageHeight
];
3545 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3546 rgbArray
[j
] = 0xFFFFFF;
3550 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3551 tileWidth
, tileHeight
, null, 0, tileWidth
);
3552 } catch (Exception e
) {
3553 throw new RuntimeException("image " + imageWidth
+ "x" +
3555 "tile " + tileWidth
+ "x" +
3557 " cells.get(i).getImage() " +
3558 cells
.get(i
).getImage() +
3560 " fullWidth " + fullWidth
+
3561 " fullHeight " + fullHeight
, e
);
3566 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3567 i * imageWidth, 0, imageWidth, imageHeight,
3569 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3570 fullWidth, fullHeight, cells.size(), getTextWidth());
3573 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3574 rgbArray
, 0, tileWidth
);
3575 if (tileHeight
< fullHeight
) {
3576 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3577 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3578 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3581 image
.setRGB(imageX
, imageY
, backgroundColor
);
3586 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3587 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3588 // I used to put an all-white cell over the cursor, don't do that
3590 rgbArray
= new int[totalWidth
* imageHeight
];
3591 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3592 rgbArray
[j
] = 0xFFFFFF;
3596 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3597 totalWidth
, imageHeight
, null, 0, totalWidth
);
3598 } catch (Exception e
) {
3599 throw new RuntimeException("image " + imageWidth
+ "x" +
3600 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3601 cells
.get(cells
.size() - 1).getImage(), e
);
3604 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3605 imageHeight
, rgbArray
, 0, totalWidth
);
3607 if (totalWidth
< imageWidth
) {
3608 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3610 for (int imageX
= image
.getWidth() - totalWidth
;
3611 imageX
< image
.getWidth(); imageX
++) {
3613 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3614 image
.setRGB(imageX
, imageY
, backgroundColor
);
3619 if ((image
.getWidth() != cells
.size() * getTextWidth())
3620 || (image
.getHeight() != getTextHeight())
3622 // Rescale the image to fit the text cells it is going into.
3623 BufferedImage newImage
;
3624 newImage
= new BufferedImage(cells
.size() * getTextWidth(),
3625 getTextHeight(), BufferedImage
.TYPE_INT_ARGB
);
3627 java
.awt
.Graphics gr
= newImage
.getGraphics();
3628 gr
.drawImage(image
, 0, 0, newImage
.getWidth(),
3629 newImage
.getHeight(), null, null);
3632 fullHeight
= image
.getHeight();
3636 * From https://iterm2.com/documentation-images.html:
3640 * iTerm2 extends the xterm protocol with a set of proprietary escape
3641 * sequences. In general, the pattern is:
3643 * ESC ] 1337 ; key = value ^G
3645 * Whitespace is shown here for ease of reading: in practice, no
3646 * spaces should be used.
3648 * For file transfer and inline images, the code is:
3650 * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
3652 * The optional arguments are formatted as key=value with a semicolon
3653 * between each key-value pair. They are described below:
3655 * Key Description of value
3656 * name base-64 encoded filename. Defaults to "Unnamed file".
3657 * size File size in bytes. Optional; this is only used by the
3658 * progress indicator.
3659 * width Width to render. See notes below.
3660 * height Height to render. See notes below.
3661 * preserveAspectRatio If set to 0, then the image's inherent aspect
3662 * ratio will not be respected; otherwise, it
3663 * will fill the specified width and height as
3664 * much as possible without stretching. Defaults
3666 * inline If set to 1, the file will be displayed inline. Otherwise,
3667 * it will be downloaded with no visual representation in the
3668 * terminal session. Defaults to 0.
3670 * The width and height are given as a number followed by a unit, or
3673 * N: N character cells.
3675 * N%: N percent of the session's width or height.
3676 * auto: The image's inherent size will be used to determine an
3677 * appropriate dimension.
3681 // File contents can be several image formats. We will use PNG.
3682 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3684 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3685 Math
.min(image
.getHeight(), fullHeight
)),
3686 "PNG", pngOutputStream
)
3688 // We failed to render image, bail out.
3691 } catch (IOException e
) {
3692 // We failed to render image, bail out.
3696 sb
.append("\033]1337;File=");
3698 sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
3702 sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
3703 image.getWidth(), Math.min(image.getHeight(),
3706 sb
.append("inline=1:");
3707 sb
.append(StringUtils
.toBase64(pngOutputStream
.toByteArray()));
3711 // This row is OK to save into the cache.
3712 iterm2Cache
.put(cells
, sb
.toString());
3715 return (gotoXY(x
, y
) + sb
.toString());
3719 * Get the iTerm2 images support flag.
3721 * @return true if this terminal is emitting iTerm2 images
3723 public boolean hasIterm2Images() {
3724 return iterm2Images
;
3727 // ------------------------------------------------------------------------
3728 // End iTerm2 image output support ----------------------------------------
3729 // ------------------------------------------------------------------------
3731 // ------------------------------------------------------------------------
3732 // Jexer image output support ---------------------------------------------
3733 // ------------------------------------------------------------------------
3736 * Create a Jexer images string representing a row of several cells
3737 * containing bitmap data.
3739 * @param x column coordinate. 0 is the left-most column.
3740 * @param y row coordinate. 0 is the top-most row.
3741 * @param cells the cells containing the bitmap data
3742 * @return the string to emit to an ANSI / ECMA-style terminal
3744 private String
toJexerImage(final int x
, final int y
,
3745 final ArrayList
<Cell
> cells
) {
3747 StringBuilder sb
= new StringBuilder();
3749 assert (cells
!= null);
3750 assert (cells
.size() > 0);
3751 assert (cells
.get(0).getImage() != null);
3753 if (jexerImageOption
== JexerImageOption
.DISABLED
) {
3754 sb
.append(normal());
3755 sb
.append(gotoXY(x
, y
));
3756 for (int i
= 0; i
< cells
.size(); i
++) {
3759 return sb
.toString();
3762 if (jexerCache
== null) {
3763 jexerCache
= new ImageCache(height
* 10);
3766 // Save and get rows to/from the cache that do NOT have inverted
3768 boolean saveInCache
= true;
3769 for (Cell cell
: cells
) {
3770 if (cell
.isInvertedImage()) {
3771 saveInCache
= false;
3775 String cachedResult
= jexerCache
.get(cells
);
3776 if (cachedResult
!= null) {
3777 // System.err.println("CACHE HIT");
3778 sb
.append(gotoXY(x
, y
));
3779 sb
.append(cachedResult
);
3780 return sb
.toString();
3782 // System.err.println("CACHE MISS");
3785 int imageWidth
= cells
.get(0).getImage().getWidth();
3786 int imageHeight
= cells
.get(0).getImage().getHeight();
3788 // Piece cells.get(x).getImage() pieces together into one larger
3789 // image for final rendering.
3791 int fullWidth
= cells
.size() * imageWidth
;
3792 int fullHeight
= imageHeight
;
3793 for (int i
= 0; i
< cells
.size(); i
++) {
3794 totalWidth
+= cells
.get(i
).getImage().getWidth();
3797 BufferedImage image
= new BufferedImage(fullWidth
,
3798 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3801 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3802 int tileWidth
= imageWidth
;
3803 int tileHeight
= imageHeight
;
3804 if (false && cells
.get(i
).isInvertedImage()) {
3805 // I used to put an all-white cell over the cursor, don't do
3807 rgbArray
= new int[imageWidth
* imageHeight
];
3808 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3809 rgbArray
[j
] = 0xFFFFFF;
3813 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3814 tileWidth
, tileHeight
, null, 0, tileWidth
);
3815 } catch (Exception e
) {
3816 throw new RuntimeException("image " + imageWidth
+ "x" +
3818 "tile " + tileWidth
+ "x" +
3820 " cells.get(i).getImage() " +
3821 cells
.get(i
).getImage() +
3823 " fullWidth " + fullWidth
+
3824 " fullHeight " + fullHeight
, e
);
3829 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3830 i * imageWidth, 0, imageWidth, imageHeight,
3832 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3833 fullWidth, fullHeight, cells.size(), getTextWidth());
3836 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3837 rgbArray
, 0, tileWidth
);
3838 if (tileHeight
< fullHeight
) {
3839 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3840 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3841 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3844 image
.setRGB(imageX
, imageY
, backgroundColor
);
3849 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3850 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3851 // I used to put an all-white cell over the cursor, don't do that
3853 rgbArray
= new int[totalWidth
* imageHeight
];
3854 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3855 rgbArray
[j
] = 0xFFFFFF;
3859 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3860 totalWidth
, imageHeight
, null, 0, totalWidth
);
3861 } catch (Exception e
) {
3862 throw new RuntimeException("image " + imageWidth
+ "x" +
3863 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3864 cells
.get(cells
.size() - 1).getImage(), e
);
3867 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3868 imageHeight
, rgbArray
, 0, totalWidth
);
3870 if (totalWidth
< imageWidth
) {
3871 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3873 for (int imageX
= image
.getWidth() - totalWidth
;
3874 imageX
< image
.getWidth(); imageX
++) {
3876 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3877 image
.setRGB(imageX
, imageY
, backgroundColor
);
3882 if ((image
.getWidth() != cells
.size() * getTextWidth())
3883 || (image
.getHeight() != getTextHeight())
3885 // Rescale the image to fit the text cells it is going into.
3886 BufferedImage newImage
;
3887 newImage
= new BufferedImage(cells
.size() * getTextWidth(),
3888 getTextHeight(), BufferedImage
.TYPE_INT_ARGB
);
3890 java
.awt
.Graphics gr
= newImage
.getGraphics();
3891 gr
.drawImage(image
, 0, 0, newImage
.getWidth(),
3892 newImage
.getHeight(), null, null);
3895 fullHeight
= image
.getHeight();
3898 if (jexerImageOption
== JexerImageOption
.PNG
) {
3900 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3902 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3903 Math
.min(image
.getHeight(), fullHeight
)),
3904 "PNG", pngOutputStream
)
3906 // We failed to render image, bail out.
3909 } catch (IOException e
) {
3910 // We failed to render image, bail out.
3914 sb
.append("\033]444;1;0;");
3915 sb
.append(StringUtils
.toBase64(pngOutputStream
.toByteArray()));
3918 } else if (jexerImageOption
== JexerImageOption
.JPG
) {
3921 ByteArrayOutputStream jpgOutputStream
= new ByteArrayOutputStream(1024);
3923 // Convert from ARGB to RGB, otherwise the JPG encode will fail.
3924 BufferedImage jpgImage
= new BufferedImage(image
.getWidth(),
3925 image
.getHeight(), BufferedImage
.TYPE_INT_RGB
);
3926 int [] pixels
= new int[image
.getWidth() * image
.getHeight()];
3927 image
.getRGB(0, 0, image
.getWidth(), image
.getHeight(), pixels
,
3928 0, image
.getWidth());
3929 jpgImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(), pixels
,
3930 0, image
.getWidth());
3933 if (!ImageIO
.write(jpgImage
.getSubimage(0, 0,
3934 jpgImage
.getWidth(),
3935 Math
.min(jpgImage
.getHeight(), fullHeight
)),
3936 "JPG", jpgOutputStream
)
3938 // We failed to render image, bail out.
3941 } catch (IOException e
) {
3942 // We failed to render image, bail out.
3946 sb
.append("\033]444;2;0;");
3947 sb
.append(StringUtils
.toBase64(jpgOutputStream
.toByteArray()));
3950 } else if (jexerImageOption
== JexerImageOption
.RGB
) {
3953 sb
.append(String
.format("\033]444;0;%d;%d;0;", image
.getWidth(),
3954 Math
.min(image
.getHeight(), fullHeight
)));
3956 byte [] bytes
= new byte[image
.getWidth() * image
.getHeight() * 3];
3957 int stride
= image
.getWidth();
3958 for (int px
= 0; px
< stride
; px
++) {
3959 for (int py
= 0; py
< image
.getHeight(); py
++) {
3960 int rgb
= image
.getRGB(px
, py
);
3961 bytes
[(py
* stride
* 3) + (px
* 3)] = (byte) ((rgb
>>> 16) & 0xFF);
3962 bytes
[(py
* stride
* 3) + (px
* 3) + 1] = (byte) ((rgb
>>> 8) & 0xFF);
3963 bytes
[(py
* stride
* 3) + (px
* 3) + 2] = (byte) ( rgb
& 0xFF);
3966 sb
.append(StringUtils
.toBase64(bytes
));
3971 // This row is OK to save into the cache.
3972 jexerCache
.put(cells
, sb
.toString());
3975 return (gotoXY(x
, y
) + sb
.toString());
3979 * Get the Jexer images support flag.
3981 * @return true if this terminal is emitting Jexer images
3983 public boolean hasJexerImages() {
3984 return (jexerImageOption
!= JexerImageOption
.DISABLED
);
3987 // ------------------------------------------------------------------------
3988 // End Jexer image output support -----------------------------------------
3989 // ------------------------------------------------------------------------
3992 * Setup system colors to match DOS color palette.
3994 private void setDOSColors() {
3995 MYBLACK
= new java
.awt
.Color(0x00, 0x00, 0x00);
3996 MYRED
= new java
.awt
.Color(0xa8, 0x00, 0x00);
3997 MYGREEN
= new java
.awt
.Color(0x00, 0xa8, 0x00);
3998 MYYELLOW
= new java
.awt
.Color(0xa8, 0x54, 0x00);
3999 MYBLUE
= new java
.awt
.Color(0x00, 0x00, 0xa8);
4000 MYMAGENTA
= new java
.awt
.Color(0xa8, 0x00, 0xa8);
4001 MYCYAN
= new java
.awt
.Color(0x00, 0xa8, 0xa8);
4002 MYWHITE
= new java
.awt
.Color(0xa8, 0xa8, 0xa8);
4003 MYBOLD_BLACK
= new java
.awt
.Color(0x54, 0x54, 0x54);
4004 MYBOLD_RED
= new java
.awt
.Color(0xfc, 0x54, 0x54);
4005 MYBOLD_GREEN
= new java
.awt
.Color(0x54, 0xfc, 0x54);
4006 MYBOLD_YELLOW
= new java
.awt
.Color(0xfc, 0xfc, 0x54);
4007 MYBOLD_BLUE
= new java
.awt
.Color(0x54, 0x54, 0xfc);
4008 MYBOLD_MAGENTA
= new java
.awt
.Color(0xfc, 0x54, 0xfc);
4009 MYBOLD_CYAN
= new java
.awt
.Color(0x54, 0xfc, 0xfc);
4010 MYBOLD_WHITE
= new java
.awt
.Color(0xfc, 0xfc, 0xfc);
4014 * Setup ECMA48 colors to match those provided in system properties.
4016 private void setCustomSystemColors() {
4019 MYBLACK
= getCustomColor("jexer.ECMA48.color0", MYBLACK
);
4020 MYRED
= getCustomColor("jexer.ECMA48.color1", MYRED
);
4021 MYGREEN
= getCustomColor("jexer.ECMA48.color2", MYGREEN
);
4022 MYYELLOW
= getCustomColor("jexer.ECMA48.color3", MYYELLOW
);
4023 MYBLUE
= getCustomColor("jexer.ECMA48.color4", MYBLUE
);
4024 MYMAGENTA
= getCustomColor("jexer.ECMA48.color5", MYMAGENTA
);
4025 MYCYAN
= getCustomColor("jexer.ECMA48.color6", MYCYAN
);
4026 MYWHITE
= getCustomColor("jexer.ECMA48.color7", MYWHITE
);
4027 MYBOLD_BLACK
= getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK
);
4028 MYBOLD_RED
= getCustomColor("jexer.ECMA48.color9", MYBOLD_RED
);
4029 MYBOLD_GREEN
= getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN
);
4030 MYBOLD_YELLOW
= getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW
);
4031 MYBOLD_BLUE
= getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE
);
4032 MYBOLD_MAGENTA
= getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA
);
4033 MYBOLD_CYAN
= getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN
);
4034 MYBOLD_WHITE
= getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE
);
4038 * Setup one system color to match the RGB value provided in system
4041 * @param key the system property key
4042 * @param defaultColor the default color to return if key is not set, or
4044 * @return a color from the RGB string, or defaultColor
4046 private java
.awt
.Color
getCustomColor(final String key
,
4047 final java
.awt
.Color defaultColor
) {
4049 String rgb
= System
.getProperty(key
);
4051 return defaultColor
;
4053 if (rgb
.startsWith("#")) {
4054 rgb
= rgb
.substring(1);
4058 rgbInt
= Integer
.parseInt(rgb
, 16);
4059 } catch (NumberFormatException e
) {
4060 return defaultColor
;
4062 java
.awt
.Color color
= new java
.awt
.Color((rgbInt
& 0xFF0000) >>> 16,
4063 (rgbInt
& 0x00FF00) >>> 8,
4064 (rgbInt
& 0x0000FF));
4070 * Create a T.416 RGB parameter sequence for a custom system color.
4072 * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
4073 * @return the color portion of the string to emit to an ANSI /
4074 * ECMA-style terminal
4076 private String
systemColorRGB(final java
.awt
.Color color
) {
4077 return String
.format("%d;%d;%d", color
.getRed(), color
.getGreen(),
4082 * Create a SGR parameter sequence for a single color change.
4084 * @param bold if true, set bold
4085 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4086 * @param foreground if true, this is a foreground color
4087 * @return the string to emit to an ANSI / ECMA-style terminal,
4090 private String
color(final boolean bold
, final Color color
,
4091 final boolean foreground
) {
4092 return color(color
, foreground
, true) +
4093 rgbColor(bold
, color
, foreground
);
4097 * Create a T.416 RGB parameter sequence for a single color change.
4099 * @param colorRGB a 24-bit RGB value for foreground color
4100 * @param foreground if true, this is a foreground color
4101 * @return the string to emit to an ANSI / ECMA-style terminal,
4104 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
4106 int colorRed
= (colorRGB
>>> 16) & 0xFF;
4107 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
4108 int colorBlue
= colorRGB
& 0xFF;
4110 StringBuilder sb
= new StringBuilder();
4112 sb
.append("\033[38;2;");
4114 sb
.append("\033[48;2;");
4116 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
4117 return sb
.toString();
4121 * Create a T.416 RGB parameter sequence for both foreground and
4122 * background color change.
4124 * @param foreColorRGB a 24-bit RGB value for foreground color
4125 * @param backColorRGB a 24-bit RGB value for foreground color
4126 * @return the string to emit to an ANSI / ECMA-style terminal,
4129 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
4130 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
4131 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
4132 int foreColorBlue
= foreColorRGB
& 0xFF;
4133 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
4134 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
4135 int backColorBlue
= backColorRGB
& 0xFF;
4137 StringBuilder sb
= new StringBuilder();
4138 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
4139 foreColorRed
, foreColorGreen
, foreColorBlue
));
4140 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
4141 backColorRed
, backColorGreen
, backColorBlue
));
4142 return sb
.toString();
4146 * Create a T.416 RGB parameter sequence for a single color change.
4148 * @param bold if true, set bold
4149 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4150 * @param foreground if true, this is a foreground color
4151 * @return the string to emit to an xterm terminal with RGB support,
4152 * e.g. "\033[38;2;RR;GG;BBm"
4154 private String
rgbColor(final boolean bold
, final Color color
,
4155 final boolean foreground
) {
4156 if (doRgbColor
== false) {
4159 StringBuilder sb
= new StringBuilder("\033[");
4161 // Bold implies foreground only
4163 if (color
.equals(Color
.BLACK
)) {
4164 sb
.append(systemColorRGB(MYBOLD_BLACK
));
4165 } else if (color
.equals(Color
.RED
)) {
4166 sb
.append(systemColorRGB(MYBOLD_RED
));
4167 } else if (color
.equals(Color
.GREEN
)) {
4168 sb
.append(systemColorRGB(MYBOLD_GREEN
));
4169 } else if (color
.equals(Color
.YELLOW
)) {
4170 sb
.append(systemColorRGB(MYBOLD_YELLOW
));
4171 } else if (color
.equals(Color
.BLUE
)) {
4172 sb
.append(systemColorRGB(MYBOLD_BLUE
));
4173 } else if (color
.equals(Color
.MAGENTA
)) {
4174 sb
.append(systemColorRGB(MYBOLD_MAGENTA
));
4175 } else if (color
.equals(Color
.CYAN
)) {
4176 sb
.append(systemColorRGB(MYBOLD_CYAN
));
4177 } else if (color
.equals(Color
.WHITE
)) {
4178 sb
.append(systemColorRGB(MYBOLD_WHITE
));
4186 if (color
.equals(Color
.BLACK
)) {
4187 sb
.append(systemColorRGB(MYBLACK
));
4188 } else if (color
.equals(Color
.RED
)) {
4189 sb
.append(systemColorRGB(MYRED
));
4190 } else if (color
.equals(Color
.GREEN
)) {
4191 sb
.append(systemColorRGB(MYGREEN
));
4192 } else if (color
.equals(Color
.YELLOW
)) {
4193 sb
.append(systemColorRGB(MYYELLOW
));
4194 } else if (color
.equals(Color
.BLUE
)) {
4195 sb
.append(systemColorRGB(MYBLUE
));
4196 } else if (color
.equals(Color
.MAGENTA
)) {
4197 sb
.append(systemColorRGB(MYMAGENTA
));
4198 } else if (color
.equals(Color
.CYAN
)) {
4199 sb
.append(systemColorRGB(MYCYAN
));
4200 } else if (color
.equals(Color
.WHITE
)) {
4201 sb
.append(systemColorRGB(MYWHITE
));
4205 return sb
.toString();
4209 * Create a T.416 RGB parameter sequence for both foreground and
4210 * background color change.
4212 * @param bold if true, set bold
4213 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4214 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4215 * @return the string to emit to an xterm terminal with RGB support,
4216 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
4218 private String
rgbColor(final boolean bold
, final Color foreColor
,
4219 final Color backColor
) {
4220 if (doRgbColor
== false) {
4224 return rgbColor(bold
, foreColor
, true) +
4225 rgbColor(false, backColor
, false);
4229 * Create a SGR parameter sequence for a single color change.
4231 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4232 * @param foreground if true, this is a foreground color
4233 * @param header if true, make the full header, otherwise just emit the
4234 * color parameter e.g. "42;"
4235 * @return the string to emit to an ANSI / ECMA-style terminal,
4238 private String
color(final Color color
, final boolean foreground
,
4239 final boolean header
) {
4241 int ecmaColor
= color
.getValue();
4243 // Convert Color.* values to SGR numerics
4251 return String
.format("\033[%dm", ecmaColor
);
4253 return String
.format("%d;", ecmaColor
);
4258 * Create a SGR parameter sequence for both foreground and background
4261 * @param bold if true, set bold
4262 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4263 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4264 * @return the string to emit to an ANSI / ECMA-style terminal,
4265 * e.g. "\033[31;42m"
4267 private String
color(final boolean bold
, final Color foreColor
,
4268 final Color backColor
) {
4269 return color(foreColor
, backColor
, true) +
4270 rgbColor(bold
, foreColor
, backColor
);
4274 * Create a SGR parameter sequence for both foreground and
4275 * background color change.
4277 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4278 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4279 * @param header if true, make the full header, otherwise just emit the
4280 * color parameter e.g. "31;42;"
4281 * @return the string to emit to an ANSI / ECMA-style terminal,
4282 * e.g. "\033[31;42m"
4284 private String
color(final Color foreColor
, final Color backColor
,
4285 final boolean header
) {
4287 int ecmaForeColor
= foreColor
.getValue();
4288 int ecmaBackColor
= backColor
.getValue();
4290 // Convert Color.* values to SGR numerics
4291 ecmaBackColor
+= 40;
4292 ecmaForeColor
+= 30;
4295 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
4297 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
4302 * Create a SGR parameter sequence for foreground, background, and
4303 * several attributes. This sequence first resets all attributes to
4304 * default, then sets attributes as per the parameters.
4306 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4307 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4308 * @param bold if true, set bold
4309 * @param reverse if true, set reverse
4310 * @param blink if true, set blink
4311 * @param underline if true, set underline
4312 * @return the string to emit to an ANSI / ECMA-style terminal,
4313 * e.g. "\033[0;1;31;42m"
4315 private String
color(final Color foreColor
, final Color backColor
,
4316 final boolean bold
, final boolean reverse
, final boolean blink
,
4317 final boolean underline
) {
4319 int ecmaForeColor
= foreColor
.getValue();
4320 int ecmaBackColor
= backColor
.getValue();
4322 // Convert Color.* values to SGR numerics
4323 ecmaBackColor
+= 40;
4324 ecmaForeColor
+= 30;
4326 StringBuilder sb
= new StringBuilder();
4327 if ( bold
&& reverse
&& blink
&& !underline
) {
4328 sb
.append("\033[0;1;7;5;");
4329 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4330 sb
.append("\033[0;1;7;");
4331 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4332 sb
.append("\033[0;7;5;");
4333 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4334 sb
.append("\033[0;1;5;");
4335 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4336 sb
.append("\033[0;1;");
4337 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4338 sb
.append("\033[0;7;");
4339 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4340 sb
.append("\033[0;5;");
4341 } else if ( bold
&& reverse
&& blink
&& underline
) {
4342 sb
.append("\033[0;1;7;5;4;");
4343 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4344 sb
.append("\033[0;1;7;4;");
4345 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4346 sb
.append("\033[0;7;5;4;");
4347 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4348 sb
.append("\033[0;1;5;4;");
4349 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4350 sb
.append("\033[0;1;4;");
4351 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4352 sb
.append("\033[0;7;4;");
4353 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4354 sb
.append("\033[0;5;4;");
4355 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4356 sb
.append("\033[0;4;");
4358 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4359 sb
.append("\033[0;");
4361 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
4362 sb
.append(rgbColor(bold
, foreColor
, backColor
));
4363 return sb
.toString();
4367 * Create a SGR parameter sequence for foreground, background, and
4368 * several attributes. This sequence first resets all attributes to
4369 * default, then sets attributes as per the parameters.
4371 * @param foreColorRGB a 24-bit RGB value for foreground color
4372 * @param backColorRGB a 24-bit RGB value for foreground color
4373 * @param bold if true, set bold
4374 * @param reverse if true, set reverse
4375 * @param blink if true, set blink
4376 * @param underline if true, set underline
4377 * @return the string to emit to an ANSI / ECMA-style terminal,
4378 * e.g. "\033[0;1;31;42m"
4380 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
4381 final boolean bold
, final boolean reverse
, final boolean blink
,
4382 final boolean underline
) {
4384 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
4385 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
4386 int foreColorBlue
= foreColorRGB
& 0xFF;
4387 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
4388 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
4389 int backColorBlue
= backColorRGB
& 0xFF;
4391 StringBuilder sb
= new StringBuilder();
4392 if ( bold
&& reverse
&& blink
&& !underline
) {
4393 sb
.append("\033[0;1;7;5;");
4394 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4395 sb
.append("\033[0;1;7;");
4396 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4397 sb
.append("\033[0;7;5;");
4398 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4399 sb
.append("\033[0;1;5;");
4400 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4401 sb
.append("\033[0;1;");
4402 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4403 sb
.append("\033[0;7;");
4404 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4405 sb
.append("\033[0;5;");
4406 } else if ( bold
&& reverse
&& blink
&& underline
) {
4407 sb
.append("\033[0;1;7;5;4;");
4408 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4409 sb
.append("\033[0;1;7;4;");
4410 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4411 sb
.append("\033[0;7;5;4;");
4412 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4413 sb
.append("\033[0;1;5;4;");
4414 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4415 sb
.append("\033[0;1;4;");
4416 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4417 sb
.append("\033[0;7;4;");
4418 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4419 sb
.append("\033[0;5;4;");
4420 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4421 sb
.append("\033[0;4;");
4423 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4424 sb
.append("\033[0;");
4427 sb
.append("m\033[38;2;");
4428 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
4430 sb
.append("m\033[48;2;");
4431 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
4434 return sb
.toString();
4438 * Create a SGR parameter sequence to reset to VT100 defaults.
4440 * @return the string to emit to an ANSI / ECMA-style terminal,
4443 private String
normal() {
4444 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
4448 * Create a SGR parameter sequence to reset to ECMA-48 default
4449 * foreground/background.
4451 * @return the string to emit to an ANSI / ECMA-style terminal,
4454 private String
defaultColor() {
4457 * Normal (neither bold nor faint).
4460 * Steady (not blinking).
4461 * Positive (not inverse).
4462 * Visible (not hidden).
4464 * Default foreground color.
4465 * Default background color.
4467 return "\033[0;22;23;24;25;27;28;29;39;49m";
4471 * Create a SGR parameter sequence to reset to defaults.
4473 * @param header if true, make the full header, otherwise just emit the
4474 * bare parameter e.g. "0;"
4475 * @return the string to emit to an ANSI / ECMA-style terminal,
4478 private String
normal(final boolean header
) {
4480 return "\033[0;37;40m";
4486 * Create a SGR parameter sequence for enabling the visible cursor.
4488 * @param on if true, turn on cursor
4489 * @return the string to emit to an ANSI / ECMA-style terminal
4491 private String
cursor(final boolean on
) {
4492 if (on
&& !cursorOn
) {
4496 if (!on
&& cursorOn
) {
4504 * Clear the entire screen. Because some terminals use back-color-erase,
4505 * set the color to white-on-black beforehand.
4507 * @return the string to emit to an ANSI / ECMA-style terminal
4509 private String
clearAll() {
4510 return "\033[0;37;40m\033[2J";
4514 * Clear the line from the cursor (inclusive) to the end of the screen.
4515 * Because some terminals use back-color-erase, set the color to
4516 * white-on-black beforehand.
4518 * @return the string to emit to an ANSI / ECMA-style terminal
4520 private String
clearRemainingLine() {
4521 return "\033[0;37;40m\033[K";
4525 * Move the cursor to (x, y).
4527 * @param x column coordinate. 0 is the left-most column.
4528 * @param y row coordinate. 0 is the top-most row.
4529 * @return the string to emit to an ANSI / ECMA-style terminal
4531 private String
gotoXY(final int x
, final int y
) {
4532 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
4536 * Tell (u)xterm that we want to receive mouse events based on "Any event
4537 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
4538 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
4540 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
4542 * Note that this also sets the alternate/primary screen buffer.
4544 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
4545 * mean "hide the mouse pointer." We have to use our own sequence to do
4546 * this because there is no standard in xterm for unilaterally hiding the
4547 * pointer all the time (regardless of typing).
4549 * @param on If true, enable mouse report and use the alternate screen
4550 * buffer. If false disable mouse reporting and use the primary screen
4552 * @return the string to emit to xterm
4554 private String
mouse(final boolean on
) {
4556 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
4558 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";