2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.backend
;
31 import java
.awt
.image
.BufferedImage
;
32 import java
.io
.BufferedReader
;
33 import java
.io
.ByteArrayOutputStream
;
34 import java
.io
.FileDescriptor
;
35 import java
.io
.FileInputStream
;
36 import java
.io
.InputStream
;
37 import java
.io
.InputStreamReader
;
38 import java
.io
.IOException
;
39 import java
.io
.OutputStream
;
40 import java
.io
.OutputStreamWriter
;
41 import java
.io
.PrintWriter
;
42 import java
.io
.Reader
;
43 import java
.io
.UnsupportedEncodingException
;
44 import java
.util
.ArrayList
;
45 import java
.util
.Collections
;
46 import java
.util
.HashMap
;
47 import java
.util
.List
;
48 import javax
.imageio
.ImageIO
;
51 import jexer
.bits
.Cell
;
52 import jexer
.bits
.CellAttributes
;
53 import jexer
.bits
.Color
;
54 import jexer
.event
.TCommandEvent
;
55 import jexer
.event
.TInputEvent
;
56 import jexer
.event
.TKeypressEvent
;
57 import jexer
.event
.TMouseEvent
;
58 import jexer
.event
.TResizeEvent
;
59 import static jexer
.TCommand
.*;
60 import static jexer
.TKeypress
.*;
63 * This class reads keystrokes and mouse events and emits output to ANSI
64 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
66 public class ECMA48Terminal
extends LogicalScreen
67 implements TerminalReader
, Runnable
{
69 // ------------------------------------------------------------------------
70 // Constants --------------------------------------------------------------
71 // ------------------------------------------------------------------------
74 * States in the input parser.
76 private enum ParseState
{
86 // ------------------------------------------------------------------------
87 // Variables --------------------------------------------------------------
88 // ------------------------------------------------------------------------
91 * Emit debugging to stderr.
93 private boolean debugToStderr
= false;
96 * If true, emit T.416-style RGB colors for normal system colors. This
97 * is a) expensive in bandwidth, and b) potentially terrible looking for
100 private static boolean doRgbColor
= false;
103 * The session information.
105 private SessionInfo sessionInfo
;
108 * The event queue, filled up by a thread reading on input.
110 private List
<TInputEvent
> eventQueue
;
113 * If true, we want the reader thread to exit gracefully.
115 private boolean stopReaderThread
;
120 private Thread readerThread
;
123 * Parameters being collected. E.g. if the string is \033[1;3m, then
124 * params[0] will be 1 and params[1] will be 3.
126 private List
<String
> params
;
129 * Current parsing state.
131 private ParseState state
;
134 * The time we entered ESCAPE. If we get a bare escape without a code
135 * following it, this is used to return that bare escape.
137 private long escapeTime
;
140 * The time we last checked the window size. We try not to spawn stty
141 * more than once per second.
143 private long windowSizeTime
;
146 * true if mouse1 was down. Used to report mouse1 on the release event.
148 private boolean mouse1
;
151 * true if mouse2 was down. Used to report mouse2 on the release event.
153 private boolean mouse2
;
156 * true if mouse3 was down. Used to report mouse3 on the release event.
158 private boolean mouse3
;
161 * Cache the cursor visibility value so we only emit the sequence when we
164 private boolean cursorOn
= true;
167 * Cache the last window size to figure out if a TResizeEvent needs to be
170 private TResizeEvent windowResize
= null;
173 * If true, emit wide-char (CJK/Emoji) characters as sixel images.
175 private boolean wideCharImages
= true;
178 * Window width in pixels. Used for sixel support.
180 private int widthPixels
= 640;
183 * Window height in pixels. Used for sixel support.
185 private int heightPixels
= 400;
188 * If true, emit image data via sixel.
190 private boolean sixel
= true;
193 * The sixel palette handler.
195 private SixelPalette palette
= null;
198 * The sixel post-rendered string cache.
200 private ImageCache sixelCache
= null;
203 * Number of colors in the sixel palette. Xterm 335 defines the max as
204 * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
207 private int sixelPaletteSize
= 1024;
210 * If true, emit image data via iTerm2 image protocol.
212 private boolean iterm2Images
= false;
215 * The iTerm2 post-rendered string cache.
217 private ImageCache iterm2Cache
= null;
220 * Base64 encoder used by iTerm2 images.
222 private java
.util
.Base64
.Encoder base64
= null;
225 * If true, then we changed System.in and need to change it back.
227 private boolean setRawMode
= false;
230 * The terminal's input. If an InputStream is not specified in the
231 * constructor, then this InputStreamReader will be bound to System.in
232 * with UTF-8 encoding.
234 private Reader input
;
237 * The terminal's raw InputStream. If an InputStream is not specified in
238 * the constructor, then this InputReader will be bound to System.in.
239 * This is used by run() to see if bytes are available() before calling
240 * (Reader)input.read().
242 private InputStream inputStream
;
245 * The terminal's output. If an OutputStream is not specified in the
246 * constructor, then this PrintWriter will be bound to System.out with
249 private PrintWriter output
;
252 * The listening object that run() wakes up on new input.
254 private Object listener
;
256 // Colors to map DOS colors to AWT colors.
257 private static java
.awt
.Color MYBLACK
;
258 private static java
.awt
.Color MYRED
;
259 private static java
.awt
.Color MYGREEN
;
260 private static java
.awt
.Color MYYELLOW
;
261 private static java
.awt
.Color MYBLUE
;
262 private static java
.awt
.Color MYMAGENTA
;
263 private static java
.awt
.Color MYCYAN
;
264 private static java
.awt
.Color MYWHITE
;
265 private static java
.awt
.Color MYBOLD_BLACK
;
266 private static java
.awt
.Color MYBOLD_RED
;
267 private static java
.awt
.Color MYBOLD_GREEN
;
268 private static java
.awt
.Color MYBOLD_YELLOW
;
269 private static java
.awt
.Color MYBOLD_BLUE
;
270 private static java
.awt
.Color MYBOLD_MAGENTA
;
271 private static java
.awt
.Color MYBOLD_CYAN
;
272 private static java
.awt
.Color MYBOLD_WHITE
;
275 * SixelPalette is used to manage the conversion of images between 24-bit
276 * RGB color and a palette of sixelPaletteSize colors.
278 private class SixelPalette
{
281 * Color palette for sixel output, sorted low to high.
283 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
286 * Map of color palette index for sixel output, from the order it was
287 * generated by makePalette() to rgbColors.
289 private int [] rgbSortedIndex
= new int[sixelPaletteSize
];
292 * The color palette, organized by hue, saturation, and luminance.
293 * This is used for a fast color match.
295 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
298 * Number of bits for hue.
300 private int hueBits
= -1;
303 * Number of bits for saturation.
305 private int satBits
= -1;
308 * Number of bits for luminance.
310 private int lumBits
= -1;
313 * Step size for hue bins.
315 private int hueStep
= -1;
318 * Step size for saturation bins.
320 private int satStep
= -1;
323 * Cached RGB to HSL result.
325 private int hsl
[] = new int[3];
328 * ColorIdx records a RGB color and its palette index.
330 private class ColorIdx
{
332 * The 24-bit RGB color.
337 * The palette index for this color.
342 * Public constructor.
344 * @param color the 24-bit RGB color
345 * @param index the palette index for this color
347 public ColorIdx(final int color
, final int index
) {
354 * Public constructor.
356 public SixelPalette() {
361 * Find the nearest match for a color in the palette.
363 * @param color the RGB color
364 * @return the index in rgbColors that is closest to color
366 public int matchColor(final int color
) {
371 * matchColor() is a critical performance bottleneck. To make it
372 * decent, we do the following:
374 * 1. Find the nearest two hues that bracket this color.
376 * 2. Find the nearest two saturations that bracket this color.
378 * 3. Iterate within these four bands of luminance values,
379 * returning the closest color by Euclidean distance.
381 * This strategy reduces the search space by about 97%.
383 int red
= (color
>>> 16) & 0xFF;
384 int green
= (color
>>> 8) & 0xFF;
385 int blue
= color
& 0xFF;
387 if (sixelPaletteSize
== 2) {
388 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
397 rgbToHsl(red
, green
, blue
, hsl
);
401 // System.err.printf("%d %d %d\n", hue, sat, lum);
403 double diff
= Double
.MAX_VALUE
;
406 int hue1
= hue
/ (360/hueStep
);
408 if (hue1
>= hslColors
.size() - 1) {
409 // Bracket pure red from above.
410 hue1
= hslColors
.size() - 1;
412 } else if (hue1
== 0) {
413 // Bracket pure red from below.
414 hue2
= hslColors
.size() - 1;
417 for (int hI
= hue1
; hI
!= -1;) {
418 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
421 } else if (hI
== hue2
) {
425 int sMin
= (sat
/ satStep
) - 1;
430 } else if (sMin
== sats
.size() - 1) {
435 assert (sMax
- sMin
== 1);
438 // int sMax = sats.size() - 1;
440 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
441 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
443 // True 3D colorspace match for the remaining values
444 for (ColorIdx c
: lums
) {
445 int rgbColor
= c
.color
;
447 int red2
= (rgbColor
>>> 16) & 0xFF;
448 int green2
= (rgbColor
>>> 8) & 0xFF;
449 int blue2
= rgbColor
& 0xFF;
450 newDiff
+= Math
.pow(red2
- red
, 2);
451 newDiff
+= Math
.pow(green2
- green
, 2);
452 newDiff
+= Math
.pow(blue2
- blue
, 2);
453 if (newDiff
< diff
) {
454 idx
= rgbSortedIndex
[c
.index
];
461 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
462 // Black is a closer match.
464 } else if ((((255 - red
) * (255 - red
)) +
465 ((255 - green
) * (255 - green
)) +
466 ((255 - blue
) * (255 - blue
))) < diff
) {
468 // White is a closer match.
469 idx
= sixelPaletteSize
- 1;
476 * Clamp an int value to [0, 255].
478 * @param x the int value
479 * @return an int between 0 and 255.
481 private int clamp(final int x
) {
492 * Dither an image to a sixelPaletteSize palette. The dithered
493 * image cells will contain indexes into the palette.
495 * @param image the image to dither
496 * @return the dithered image. Every pixel is an index into the
499 public BufferedImage
ditherImage(final BufferedImage image
) {
501 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
502 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
504 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
505 image
.getHeight(), null, 0, image
.getWidth());
506 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
507 rgbArray
, 0, image
.getWidth());
509 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
510 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
511 int oldPixel
= ditheredImage
.getRGB(imageX
,
513 int colorIdx
= matchColor(oldPixel
);
514 assert (colorIdx
>= 0);
515 assert (colorIdx
< sixelPaletteSize
);
516 int newPixel
= rgbColors
.get(colorIdx
);
517 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
519 int oldRed
= (oldPixel
>>> 16) & 0xFF;
520 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
521 int oldBlue
= oldPixel
& 0xFF;
523 int newRed
= (newPixel
>>> 16) & 0xFF;
524 int newGreen
= (newPixel
>>> 8) & 0xFF;
525 int newBlue
= newPixel
& 0xFF;
527 int redError
= (oldRed
- newRed
) / 16;
528 int greenError
= (oldGreen
- newGreen
) / 16;
529 int blueError
= (oldBlue
- newBlue
) / 16;
531 int red
, green
, blue
;
532 if (imageX
< image
.getWidth() - 1) {
533 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
534 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
535 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
536 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
538 green
= clamp(green
);
540 pXpY
= ((red
& 0xFF) << 16);
541 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
542 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
544 if (imageY
< image
.getHeight() - 1) {
545 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
547 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
548 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
549 blue
= ( pXpYp
& 0xFF) + blueError
;
551 green
= clamp(green
);
553 pXpYp
= ((red
& 0xFF) << 16);
554 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
555 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
557 } else if (imageY
< image
.getHeight() - 1) {
558 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
560 int pXYp
= ditheredImage
.getRGB(imageX
,
563 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
564 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
565 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
567 green
= clamp(green
);
569 pXmYp
= ((red
& 0xFF) << 16);
570 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
571 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
573 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
574 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
575 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
577 green
= clamp(green
);
579 pXYp
= ((red
& 0xFF) << 16);
580 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
581 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
583 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
584 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
586 return ditheredImage
;
590 * Convert an RGB color to HSL.
592 * @param red red color, between 0 and 255
593 * @param green green color, between 0 and 255
594 * @param blue blue color, between 0 and 255
595 * @param hsl the hsl color as [hue, saturation, luminance]
597 private void rgbToHsl(final int red
, final int green
,
598 final int blue
, final int [] hsl
) {
600 assert ((red
>= 0) && (red
<= 255));
601 assert ((green
>= 0) && (green
<= 255));
602 assert ((blue
>= 0) && (blue
<= 255));
604 double R
= red
/ 255.0;
605 double G
= green
/ 255.0;
606 double B
= blue
/ 255.0;
607 boolean Rmax
= false;
608 boolean Gmax
= false;
609 boolean Bmax
= false;
610 double min
= (R
< G ? R
: G
);
611 min
= (min
< B ? min
: B
);
613 if ((R
>= G
) && (R
>= B
)) {
616 } else if ((G
>= R
) && (G
>= B
)) {
619 } else if ((B
>= G
) && (B
>= R
)) {
624 double L
= (min
+ max
) / 2.0;
629 S
= (max
- min
) / (max
+ min
);
631 S
= (max
- min
) / (2.0 - max
- min
);
635 assert (Gmax
== false);
636 assert (Bmax
== false);
637 H
= (G
- B
) / (max
- min
);
639 assert (Rmax
== false);
640 assert (Bmax
== false);
641 H
= 2.0 + (B
- R
) / (max
- min
);
643 assert (Rmax
== false);
644 assert (Gmax
== false);
645 H
= 4.0 + (R
- G
) / (max
- min
);
650 hsl
[0] = (int) (H
* 60.0);
651 hsl
[1] = (int) (S
* 100.0);
652 hsl
[2] = (int) (L
* 100.0);
654 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
655 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
656 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
660 * Convert a HSL color to RGB.
662 * @param hue hue, between 0 and 359
663 * @param sat saturation, between 0 and 100
664 * @param lum luminance, between 0 and 100
665 * @return the rgb color as 0x00RRGGBB
667 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
668 assert ((hue
>= 0) && (hue
<= 360));
669 assert ((sat
>= 0) && (sat
<= 100));
670 assert ((lum
>= 0) && (lum
<= 100));
672 double S
= sat
/ 100.0;
673 double L
= lum
/ 100.0;
674 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
675 double Hp
= hue
/ 60.0;
676 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
683 } else if (Hp
<= 2.0) {
686 } else if (Hp
<= 3.0) {
689 } else if (Hp
<= 4.0) {
692 } else if (Hp
<= 5.0) {
695 } else if (Hp
<= 6.0) {
699 double m
= L
- (C
/ 2.0);
700 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
701 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
702 int blue
= (int) ((Bp
+ m
) * 255.0);
704 return (red
| green
| blue
);
708 * Create the sixel palette.
710 private void makePalette() {
711 // Generate the sixel palette. Because we have no idea at this
712 // layer which image(s) will be shown, we have to use a common
713 // palette with sixelPaletteSize colors for everything, and
714 // map the BufferedImage colors to their nearest neighbor in RGB
717 if (sixelPaletteSize
== 2) {
719 rgbColors
.add(0xFFFFFF);
720 rgbSortedIndex
[0] = 0;
721 rgbSortedIndex
[1] = 1;
725 // We build a palette using the Hue-Saturation-Luminence model,
726 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
727 // Luminance. We convert these colors to 24-bit RGB, sort them
728 // ascending, and steal the first index for pure black and the
729 // last for pure white. The 8-bit final palette favors bright
730 // colors, somewhere between pastel and classic television
731 // technicolor. 9- and 10-bit palettes are more uniform.
733 // Default at 256 colors.
738 assert (sixelPaletteSize
>= 256);
739 assert ((sixelPaletteSize
== 256)
740 || (sixelPaletteSize
== 512)
741 || (sixelPaletteSize
== 1024)
742 || (sixelPaletteSize
== 2048));
744 switch (sixelPaletteSize
) {
761 hueStep
= (int) (Math
.pow(2, hueBits
));
762 satStep
= (int) (100 / Math
.pow(2, satBits
));
763 // 1 bit for luminance: 40 and 70.
768 // 2 bits: 20, 40, 60, 80
773 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
779 // System.err.printf("<html><body>\n");
780 // Hue is evenly spaced around the wheel.
781 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
783 final boolean DEBUG
= false;
784 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
786 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
787 hue
+= (360/hueStep
)) {
789 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
790 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
791 hslColors
.add(satList
);
793 // Saturation is linearly spaced between pastel and pure.
794 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
796 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
797 satList
.add(lumList
);
799 // Luminance brackets the pure color, but leaning toward
801 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
803 System.err.printf("<font style = \"color:");
804 System.err.printf("hsl(%d, %d%%, %d%%)",
806 System.err.printf(";\">=</font>\n");
808 int rgbColor
= hslToRgb(hue
, sat
, lum
);
809 rgbColors
.add(rgbColor
);
810 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
811 rgbColors
.size() - 1);
812 lumList
.add(colorIdx
);
814 rawRgbList
.add(rgbColor
);
816 int red
= (rgbColor
>>> 16) & 0xFF;
817 int green
= (rgbColor
>>> 8) & 0xFF;
818 int blue
= rgbColor
& 0xFF;
819 int [] backToHsl
= new int[3];
820 rgbToHsl(red
, green
, blue
, backToHsl
);
821 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
822 hue
, backToHsl
[0], sat
, backToHsl
[1],
828 // System.err.printf("\n</body></html>\n");
830 assert (rgbColors
.size() == sixelPaletteSize
);
833 * We need to sort rgbColors, so that toSixel() can know where
834 * BLACK and WHITE are in it. But we also need to be able to
835 * find the sorted values using the old unsorted indexes. So we
836 * will sort it, put all the indexes into a HashMap, and then
837 * build rgbSortedIndex[].
839 Collections
.sort(rgbColors
);
840 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
841 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
842 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
843 rgbColorIndices
.put(rgbColors
.get(i
), i
);
845 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
846 int rawColor
= rawRgbList
.get(i
);
847 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
850 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
851 assert (rawRgbList
!= null);
852 int idx
= rgbSortedIndex
[i
];
853 int rgbColor
= rgbColors
.get(idx
);
854 if ((idx
!= 0) && (idx
!= sixelPaletteSize
- 1)) {
856 System.err.printf("%d %06x --> %d %06x\n",
857 i, rawRgbList.get(i), idx, rgbColors.get(idx));
859 assert (rgbColor
== rawRgbList
.get(i
));
864 // Set the dimmest color as true black, and the brightest as true
867 rgbColors
.set(sixelPaletteSize
- 1, 0xFFFFFF);
870 System.err.printf("<html><body>\n");
871 for (Integer rgb: rgbColors) {
872 System.err.printf("<font style = \"color:");
873 System.err.printf("#%06x", rgb);
874 System.err.printf(";\">=</font>\n");
876 System.err.printf("\n</body></html>\n");
882 * Emit the sixel palette.
884 * @param sb the StringBuilder to append to
885 * @param used array of booleans set to true for each color actually
886 * used in this cell, or null to emit the entire palette
887 * @return the string to emit to an ANSI / ECMA-style terminal
889 public String
emitPalette(final StringBuilder sb
,
890 final boolean [] used
) {
892 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
893 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
894 int rgbColor
= rgbColors
.get(i
);
895 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
896 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
897 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
898 ( rgbColor
& 0xFF) * 100 / 255));
901 return sb
.toString();
906 * ImageCache is a least-recently-used cache that hangs on to the
907 * post-rendered sixel or iTerm2 string for a particular set of cells.
909 private class ImageCache
{
912 * Maximum size of the cache.
914 private int maxSize
= 100;
917 * The entries stored in the cache.
919 private HashMap
<String
, CacheEntry
> cache
= null;
922 * CacheEntry is one entry in the cache.
924 private class CacheEntry
{
936 * The last time this entry was used.
938 public long millis
= 0;
941 * Public constructor.
943 * @param key the cache entry key
944 * @param data the cache entry data
946 public CacheEntry(final String key
, final String data
) {
949 this.millis
= System
.currentTimeMillis();
954 * Public constructor.
956 * @param maxSize the maximum size of the cache
958 public ImageCache(final int maxSize
) {
959 this.maxSize
= maxSize
;
960 cache
= new HashMap
<String
, CacheEntry
>();
964 * Make a unique key for a list of cells.
966 * @param cells the cells
969 private String
makeKey(final ArrayList
<Cell
> cells
) {
970 StringBuilder sb
= new StringBuilder();
971 for (Cell cell
: cells
) {
972 sb
.append(cell
.hashCode());
974 return sb
.toString();
978 * Get an entry from the cache.
980 * @param cells the list of cells that are the cache key
981 * @return the sixel string representing these cells, or null if this
982 * list of cells is not in the cache
984 public String
get(final ArrayList
<Cell
> cells
) {
985 CacheEntry entry
= cache
.get(makeKey(cells
));
989 entry
.millis
= System
.currentTimeMillis();
994 * Put an entry into the cache.
996 * @param cells the list of cells that are the cache key
997 * @param data the sixel string representing these cells
999 public void put(final ArrayList
<Cell
> cells
, final String data
) {
1000 String key
= makeKey(cells
);
1002 // System.err.println("put() " + key + " size " + cache.size());
1004 assert (!cache
.containsKey(key
));
1006 assert (cache
.size() <= maxSize
);
1007 if (cache
.size() == maxSize
) {
1008 // Cache is at limit, evict oldest entry.
1009 long oldestTime
= Long
.MAX_VALUE
;
1010 String keyToRemove
= null;
1011 for (CacheEntry entry
: cache
.values()) {
1012 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
1013 keyToRemove
= entry
.key
;
1014 oldestTime
= entry
.millis
;
1018 System.err.println("put() remove key = " + keyToRemove +
1019 " size " + cache.size());
1021 assert (keyToRemove
!= null);
1022 cache
.remove(keyToRemove
);
1024 System.err.println("put() removed, size " + cache.size());
1027 assert (cache
.size() <= maxSize
);
1028 CacheEntry entry
= new CacheEntry(key
, data
);
1029 assert (key
.equals(entry
.key
));
1030 cache
.put(key
, entry
);
1032 System.err.println("put() added key " + key + " " +
1033 " size " + cache.size());
1039 // ------------------------------------------------------------------------
1040 // Constructors -----------------------------------------------------------
1041 // ------------------------------------------------------------------------
1044 * Constructor sets up state for getEvent(). If either windowWidth or
1045 * windowHeight are less than 1, the terminal is not resized.
1047 * @param listener the object this backend needs to wake up when new
1049 * @param input an InputStream connected to the remote user, or null for
1050 * System.in. If System.in is used, then on non-Windows systems it will
1051 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1052 * cooked mode. input is always converted to a Reader with UTF-8
1054 * @param output an OutputStream connected to the remote user, or null
1055 * for System.out. output is always converted to a Writer with UTF-8
1057 * @param windowWidth the number of text columns to start with
1058 * @param windowHeight the number of text rows to start with
1059 * @throws UnsupportedEncodingException if an exception is thrown when
1060 * creating the InputStreamReader
1062 public ECMA48Terminal(final Object listener
, final InputStream input
,
1063 final OutputStream output
, final int windowWidth
,
1064 final int windowHeight
) throws UnsupportedEncodingException
{
1066 this(listener
, input
, output
);
1068 // Send dtterm/xterm sequences, which will probably not work because
1069 // allowWindowOps is defaulted to false.
1070 if ((windowWidth
> 0) && (windowHeight
> 0)) {
1071 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1073 this.output
.write(resizeString
);
1074 this.output
.flush();
1079 * Constructor sets up state for getEvent().
1081 * @param listener the object this backend needs to wake up when new
1083 * @param input an InputStream connected to the remote user, or null for
1084 * System.in. If System.in is used, then on non-Windows systems it will
1085 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1086 * cooked mode. input is always converted to a Reader with UTF-8
1088 * @param output an OutputStream connected to the remote user, or null
1089 * for System.out. output is always converted to a Writer with UTF-8
1091 * @throws UnsupportedEncodingException if an exception is thrown when
1092 * creating the InputStreamReader
1094 public ECMA48Terminal(final Object listener
, final InputStream input
,
1095 final OutputStream output
) throws UnsupportedEncodingException
{
1101 stopReaderThread
= false;
1102 this.listener
= listener
;
1104 if (input
== null) {
1105 // inputStream = System.in;
1106 inputStream
= new FileInputStream(FileDescriptor
.in
);
1110 inputStream
= input
;
1112 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1114 if (input
instanceof SessionInfo
) {
1115 // This is a TelnetInputStream that exposes window size and
1116 // environment variables from the telnet layer.
1117 sessionInfo
= (SessionInfo
) input
;
1119 if (sessionInfo
== null) {
1120 if (input
== null) {
1121 // Reading right off the tty
1122 sessionInfo
= new TTYSessionInfo();
1124 sessionInfo
= new TSessionInfo();
1128 if (output
== null) {
1129 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1132 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1136 // Request xterm report window/cell dimensions in pixels
1137 this.output
.printf("%s", xtermReportPixelDimensions());
1139 // Enable mouse reporting and metaSendsEscape
1140 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1141 this.output
.flush();
1143 // Request xterm use the sixel settings we want
1144 this.output
.printf("%s", xtermSetSixelSettings());
1146 // Query the screen size
1147 sessionInfo
.queryWindowSize();
1148 setDimensions(sessionInfo
.getWindowWidth(),
1149 sessionInfo
.getWindowHeight());
1151 // Hang onto the window size
1152 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1153 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1157 // Spin up the input reader
1158 eventQueue
= new ArrayList
<TInputEvent
>();
1159 readerThread
= new Thread(this);
1160 readerThread
.start();
1163 this.output
.write(clearAll());
1164 this.output
.flush();
1168 * Constructor sets up state for getEvent().
1170 * @param listener the object this backend needs to wake up when new
1172 * @param input the InputStream underlying 'reader'. Its available()
1173 * method is used to determine if reader.read() will block or not.
1174 * @param reader a Reader connected to the remote user.
1175 * @param writer a PrintWriter connected to the remote user.
1176 * @param setRawMode if true, set System.in into raw mode with stty.
1177 * This should in general not be used. It is here solely for Demo3,
1178 * which uses System.in.
1179 * @throws IllegalArgumentException if input, reader, or writer are null.
1181 public ECMA48Terminal(final Object listener
, final InputStream input
,
1182 final Reader reader
, final PrintWriter writer
,
1183 final boolean setRawMode
) {
1185 if (input
== null) {
1186 throw new IllegalArgumentException("InputStream must be specified");
1188 if (reader
== null) {
1189 throw new IllegalArgumentException("Reader must be specified");
1191 if (writer
== null) {
1192 throw new IllegalArgumentException("Writer must be specified");
1198 stopReaderThread
= false;
1199 this.listener
= listener
;
1201 inputStream
= input
;
1202 this.input
= reader
;
1204 if (setRawMode
== true) {
1207 this.setRawMode
= setRawMode
;
1209 if (input
instanceof SessionInfo
) {
1210 // This is a TelnetInputStream that exposes window size and
1211 // environment variables from the telnet layer.
1212 sessionInfo
= (SessionInfo
) input
;
1214 if (sessionInfo
== null) {
1215 if (setRawMode
== true) {
1216 // Reading right off the tty
1217 sessionInfo
= new TTYSessionInfo();
1219 sessionInfo
= new TSessionInfo();
1223 this.output
= writer
;
1225 // Request xterm report window/cell dimensions in pixels
1226 this.output
.printf("%s", xtermReportPixelDimensions());
1228 // Enable mouse reporting and metaSendsEscape
1229 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1230 this.output
.flush();
1232 // Request xterm use the sixel settings we want
1233 this.output
.printf("%s", xtermSetSixelSettings());
1235 // Query the screen size
1236 sessionInfo
.queryWindowSize();
1237 setDimensions(sessionInfo
.getWindowWidth(),
1238 sessionInfo
.getWindowHeight());
1240 // Hang onto the window size
1241 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1242 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1246 // Spin up the input reader
1247 eventQueue
= new ArrayList
<TInputEvent
>();
1248 readerThread
= new Thread(this);
1249 readerThread
.start();
1252 this.output
.write(clearAll());
1253 this.output
.flush();
1257 * Constructor sets up state for getEvent().
1259 * @param listener the object this backend needs to wake up when new
1261 * @param input the InputStream underlying 'reader'. Its available()
1262 * method is used to determine if reader.read() will block or not.
1263 * @param reader a Reader connected to the remote user.
1264 * @param writer a PrintWriter connected to the remote user.
1265 * @throws IllegalArgumentException if input, reader, or writer are null.
1267 public ECMA48Terminal(final Object listener
, final InputStream input
,
1268 final Reader reader
, final PrintWriter writer
) {
1270 this(listener
, input
, reader
, writer
, false);
1273 // ------------------------------------------------------------------------
1274 // LogicalScreen ----------------------------------------------------------
1275 // ------------------------------------------------------------------------
1278 * Set the window title.
1280 * @param title the new title
1283 public void setTitle(final String title
) {
1284 output
.write(getSetTitleString(title
));
1289 * Push the logical screen to the physical device.
1292 public void flushPhysical() {
1293 StringBuilder sb
= new StringBuilder();
1297 && (cursorY
<= height
- 1)
1298 && (cursorX
<= width
- 1)
1301 sb
.append(cursor(true));
1302 sb
.append(gotoXY(cursorX
, cursorY
));
1304 sb
.append(cursor(false));
1307 output
.write(sb
.toString());
1312 * Resize the physical screen to match the logical screen dimensions.
1315 public void resizeToScreen() {
1316 // Send dtterm/xterm sequences, which will probably not work because
1317 // allowWindowOps is defaulted to false.
1318 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1320 this.output
.write(resizeString
);
1321 this.output
.flush();
1324 // ------------------------------------------------------------------------
1325 // TerminalReader ---------------------------------------------------------
1326 // ------------------------------------------------------------------------
1329 * Check if there are events in the queue.
1331 * @return if true, getEvents() has something to return to the backend
1333 public boolean hasEvents() {
1334 synchronized (eventQueue
) {
1335 return (eventQueue
.size() > 0);
1340 * Return any events in the IO queue.
1342 * @param queue list to append new events to
1344 public void getEvents(final List
<TInputEvent
> queue
) {
1345 synchronized (eventQueue
) {
1346 if (eventQueue
.size() > 0) {
1347 synchronized (queue
) {
1348 queue
.addAll(eventQueue
);
1356 * Restore terminal to normal state.
1358 public void closeTerminal() {
1360 // System.err.println("=== closeTerminal() ==="); System.err.flush();
1362 // Tell the reader thread to stop looking at input
1363 stopReaderThread
= true;
1365 readerThread
.join();
1366 } catch (InterruptedException e
) {
1367 if (debugToStderr
) {
1368 e
.printStackTrace();
1372 // Disable mouse reporting and show cursor. Defensive null check
1373 // here in case closeTerminal() is called twice.
1374 if (output
!= null) {
1375 output
.printf("%s%s%s%s", mouse(false), cursor(true),
1376 defaultColor(), xtermResetSixelSettings());
1383 // We don't close System.in/out
1385 // Shut down the streams, this should wake up the reader thread
1386 // and make it exit.
1387 if (input
!= null) {
1390 } catch (IOException e
) {
1395 if (output
!= null) {
1403 * Set listener to a different Object.
1405 * @param listener the new listening object that run() wakes up on new
1408 public void setListener(final Object listener
) {
1409 this.listener
= listener
;
1413 * Reload options from System properties.
1415 public void reloadOptions() {
1416 // Permit RGB colors only if externally requested.
1417 if (System
.getProperty("jexer.ECMA48.rgbColor",
1418 "false").equals("true")
1425 // Default to using images for full-width characters.
1426 if (System
.getProperty("jexer.ECMA48.wideCharImages",
1427 "true").equals("true")) {
1428 wideCharImages
= true;
1430 wideCharImages
= false;
1433 // Pull the system properties for sixel output.
1434 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1441 int paletteSize
= 1024;
1443 paletteSize
= Integer
.parseInt(System
.getProperty(
1444 "jexer.ECMA48.sixelPaletteSize", "1024"));
1445 switch (paletteSize
) {
1451 sixelPaletteSize
= paletteSize
;
1457 } catch (NumberFormatException e
) {
1461 // Default to using images for full-width characters.
1462 if (System
.getProperty("jexer.ECMA48.iTerm2Images",
1463 "false").equals("true")) {
1464 iterm2Images
= true;
1466 iterm2Images
= false;
1469 // Set custom colors
1470 setCustomSystemColors();
1473 // ------------------------------------------------------------------------
1474 // Runnable ---------------------------------------------------------------
1475 // ------------------------------------------------------------------------
1478 * Read function runs on a separate thread.
1481 boolean done
= false;
1482 // available() will often return > 1, so we need to read in chunks to
1484 char [] readBuffer
= new char[128];
1485 List
<TInputEvent
> events
= new ArrayList
<TInputEvent
>();
1487 while (!done
&& !stopReaderThread
) {
1489 // We assume that if inputStream has bytes available, then
1490 // input won't block on read().
1491 int n
= inputStream
.available();
1494 System.err.printf("inputStream.available(): %d\n", n);
1499 if (readBuffer
.length
< n
) {
1500 // The buffer wasn't big enough, make it huger
1501 readBuffer
= new char[readBuffer
.length
* 2];
1504 // System.err.printf("BEFORE read()\n"); System.err.flush();
1506 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1509 System.err.printf("AFTER read() %d\n", rc);
1517 for (int i
= 0; i
< rc
; i
++) {
1518 int ch
= readBuffer
[i
];
1519 processChar(events
, (char)ch
);
1521 getIdleEvents(events
);
1522 if (events
.size() > 0) {
1523 // Add to the queue for the backend thread to
1524 // be able to obtain.
1525 synchronized (eventQueue
) {
1526 eventQueue
.addAll(events
);
1528 if (listener
!= null) {
1529 synchronized (listener
) {
1530 listener
.notifyAll();
1537 getIdleEvents(events
);
1538 if (events
.size() > 0) {
1539 synchronized (eventQueue
) {
1540 eventQueue
.addAll(events
);
1542 if (listener
!= null) {
1543 synchronized (listener
) {
1544 listener
.notifyAll();
1550 if (output
.checkError()) {
1555 // Wait 20 millis for more data
1558 // System.err.println("end while loop"); System.err.flush();
1559 } catch (InterruptedException e
) {
1561 } catch (IOException e
) {
1562 e
.printStackTrace();
1565 } // while ((done == false) && (stopReaderThread == false))
1567 // Pass an event up to TApplication to tell it this Backend is done.
1568 synchronized (eventQueue
) {
1569 eventQueue
.add(new TCommandEvent(cmBackendDisconnect
));
1571 if (listener
!= null) {
1572 synchronized (listener
) {
1573 listener
.notifyAll();
1577 // System.err.println("*** run() exiting..."); System.err.flush();
1580 // ------------------------------------------------------------------------
1581 // ECMA48Terminal ---------------------------------------------------------
1582 // ------------------------------------------------------------------------
1585 * Get the width of a character cell in pixels.
1587 * @return the width in pixels of a character cell
1589 public int getTextWidth() {
1590 return (widthPixels
/ sessionInfo
.getWindowWidth());
1594 * Get the height of a character cell in pixels.
1596 * @return the height in pixels of a character cell
1598 public int getTextHeight() {
1599 return (heightPixels
/ sessionInfo
.getWindowHeight());
1603 * Getter for sessionInfo.
1605 * @return the SessionInfo
1607 public SessionInfo
getSessionInfo() {
1612 * Get the output writer.
1614 * @return the Writer
1616 public PrintWriter
getOutput() {
1621 * Call 'stty' to set cooked mode.
1623 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1625 private void sttyCooked() {
1630 * Call 'stty' to set raw mode.
1632 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1633 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1634 * -parenb cs8 min 1 < /dev/tty'
1636 private void sttyRaw() {
1641 * Call 'stty' to set raw or cooked mode.
1643 * @param mode if true, set raw mode, otherwise set cooked mode
1645 private void doStty(final boolean mode
) {
1646 String
[] cmdRaw
= {
1647 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1649 String
[] cmdCooked
= {
1650 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1655 process
= Runtime
.getRuntime().exec(cmdRaw
);
1657 process
= Runtime
.getRuntime().exec(cmdCooked
);
1659 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1660 String line
= in
.readLine();
1661 if ((line
!= null) && (line
.length() > 0)) {
1662 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1665 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1666 line
= err
.readLine();
1667 if ((line
!= null) && (line
.length() > 0)) {
1668 System
.err
.println("Error output from stty: " + line
);
1673 } catch (InterruptedException e
) {
1674 if (debugToStderr
) {
1675 e
.printStackTrace();
1679 int rc
= process
.exitValue();
1681 System
.err
.println("stty returned error code: " + rc
);
1683 } catch (IOException e
) {
1684 e
.printStackTrace();
1691 public void flush() {
1696 * Perform a somewhat-optimal rendering of a line.
1698 * @param y row coordinate. 0 is the top-most row.
1699 * @param sb StringBuilder to write escape sequences to
1700 * @param lastAttr cell attributes from the last call to flushLine
1702 private void flushLine(final int y
, final StringBuilder sb
,
1703 CellAttributes lastAttr
) {
1707 for (int x
= 0; x
< width
; x
++) {
1708 Cell lCell
= logical
[x
][y
];
1709 if (!lCell
.isBlank()) {
1713 // Push textEnd to first column beyond the text area
1717 // reallyCleared = true;
1719 boolean hasImage
= false;
1721 for (int x
= 0; x
< width
; x
++) {
1722 Cell lCell
= logical
[x
][y
];
1723 Cell pCell
= physical
[x
][y
];
1725 if (!lCell
.equals(pCell
) || reallyCleared
) {
1727 if (debugToStderr
) {
1728 System
.err
.printf("\n--\n");
1729 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1730 System
.err
.printf(" lCell: %s\n", lCell
);
1731 System
.err
.printf(" pCell: %s\n", pCell
);
1732 System
.err
.printf(" ==== \n");
1735 if (lastAttr
== null) {
1736 lastAttr
= new CellAttributes();
1737 sb
.append(normal());
1741 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1742 // Advancing at least one cell, or the first gotoXY
1743 sb
.append(gotoXY(x
, y
));
1746 assert (lastAttr
!= null);
1748 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1749 assert (lCell
.isBlank());
1751 for (int i
= x
; i
< width
; i
++) {
1752 assert (logical
[i
][y
].isBlank());
1753 // Physical is always updated
1754 physical
[i
][y
].reset();
1757 // Clear remaining line
1758 sb
.append(clearRemainingLine());
1763 // Image cell: bypass the rest of the loop, it is not
1765 if ((wideCharImages
&& lCell
.isImage())
1768 && (lCell
.getWidth() == Cell
.Width
.SINGLE
))
1772 // Save the last rendered cell
1775 // Physical is always updated
1776 physical
[x
][y
].setTo(lCell
);
1780 assert ((wideCharImages
&& !lCell
.isImage())
1782 && (!lCell
.isImage()
1784 && (lCell
.getWidth() != Cell
.Width
.SINGLE
)))));
1786 if (!wideCharImages
&& (lCell
.getWidth() == Cell
.Width
.RIGHT
)) {
1792 sb
.append(gotoXY(x
, y
));
1795 // Now emit only the modified attributes
1796 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1797 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1799 && (lCell
.isBold() == lastAttr
.isBold())
1800 && (lCell
.isReverse() == lastAttr
.isReverse())
1801 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1802 && (lCell
.isBlink() == lastAttr
.isBlink())
1804 // Both colors changed, attributes the same
1805 sb
.append(color(lCell
.isBold(),
1806 lCell
.getForeColor(), lCell
.getBackColor()));
1808 if (debugToStderr
) {
1809 System
.err
.printf("1 Change only fore/back colors\n");
1812 } else if (lCell
.isRGB()
1813 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1814 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1815 && (lCell
.isBold() == lastAttr
.isBold())
1816 && (lCell
.isReverse() == lastAttr
.isReverse())
1817 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1818 && (lCell
.isBlink() == lastAttr
.isBlink())
1820 // Both colors changed, attributes the same
1821 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1822 lCell
.getBackColorRGB()));
1824 if (debugToStderr
) {
1825 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1827 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1828 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1830 && (lCell
.isBold() != lastAttr
.isBold())
1831 && (lCell
.isReverse() != lastAttr
.isReverse())
1832 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1833 && (lCell
.isBlink() != lastAttr
.isBlink())
1835 // Everything is different
1836 sb
.append(color(lCell
.getForeColor(),
1837 lCell
.getBackColor(),
1838 lCell
.isBold(), lCell
.isReverse(),
1840 lCell
.isUnderline()));
1842 if (debugToStderr
) {
1843 System
.err
.printf("2 Set all attributes\n");
1845 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1846 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1848 && (lCell
.isBold() == lastAttr
.isBold())
1849 && (lCell
.isReverse() == lastAttr
.isReverse())
1850 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1851 && (lCell
.isBlink() == lastAttr
.isBlink())
1854 // Attributes same, foreColor different
1855 sb
.append(color(lCell
.isBold(),
1856 lCell
.getForeColor(), true));
1858 if (debugToStderr
) {
1859 System
.err
.printf("3 Change foreColor\n");
1861 } else if (lCell
.isRGB()
1862 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1863 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1864 && (lCell
.getForeColorRGB() >= 0)
1865 && (lCell
.getBackColorRGB() >= 0)
1866 && (lCell
.isBold() == lastAttr
.isBold())
1867 && (lCell
.isReverse() == lastAttr
.isReverse())
1868 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1869 && (lCell
.isBlink() == lastAttr
.isBlink())
1871 // Attributes same, foreColor different
1872 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1874 if (debugToStderr
) {
1875 System
.err
.printf("3 Change foreColor (RGB)\n");
1877 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1878 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1880 && (lCell
.isBold() == lastAttr
.isBold())
1881 && (lCell
.isReverse() == lastAttr
.isReverse())
1882 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1883 && (lCell
.isBlink() == lastAttr
.isBlink())
1885 // Attributes same, backColor different
1886 sb
.append(color(lCell
.isBold(),
1887 lCell
.getBackColor(), false));
1889 if (debugToStderr
) {
1890 System
.err
.printf("4 Change backColor\n");
1892 } else if (lCell
.isRGB()
1893 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1894 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1895 && (lCell
.isBold() == lastAttr
.isBold())
1896 && (lCell
.isReverse() == lastAttr
.isReverse())
1897 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1898 && (lCell
.isBlink() == lastAttr
.isBlink())
1900 // Attributes same, foreColor different
1901 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1903 if (debugToStderr
) {
1904 System
.err
.printf("4 Change backColor (RGB)\n");
1906 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1907 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1908 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1909 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1910 && (lCell
.isBold() == lastAttr
.isBold())
1911 && (lCell
.isReverse() == lastAttr
.isReverse())
1912 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1913 && (lCell
.isBlink() == lastAttr
.isBlink())
1916 // All attributes the same, just print the char
1919 if (debugToStderr
) {
1920 System
.err
.printf("5 Only emit character\n");
1923 // Just reset everything again
1924 if (!lCell
.isRGB()) {
1925 sb
.append(color(lCell
.getForeColor(),
1926 lCell
.getBackColor(),
1930 lCell
.isUnderline()));
1932 if (debugToStderr
) {
1933 System
.err
.printf("6 Change all attributes\n");
1936 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1937 lCell
.getBackColorRGB(),
1941 lCell
.isUnderline()));
1942 if (debugToStderr
) {
1943 System
.err
.printf("6 Change all attributes (RGB)\n");
1948 // Emit the character
1950 // Don't emit the right-half of full-width chars.
1952 && (lCell
.getWidth() != Cell
.Width
.RIGHT
))
1954 sb
.append(Character
.toChars(lCell
.getChar()));
1957 // Save the last rendered cell
1959 lastAttr
.setTo(lCell
);
1961 // Physical is always updated
1962 physical
[x
][y
].setTo(lCell
);
1964 } // if (!lCell.equals(pCell) || (reallyCleared == true))
1966 } // for (int x = 0; x < width; x++)
1970 * Render the screen to a string that can be emitted to something that
1971 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
1973 * @param sb StringBuilder to write escape sequences to
1974 * @return escape sequences string that provides the updates to the
1977 private String
flushString(final StringBuilder sb
) {
1978 CellAttributes attr
= null;
1980 if (reallyCleared
) {
1981 attr
= new CellAttributes();
1982 sb
.append(clearAll());
1986 * For images support, draw all of the image output first, and then
1987 * draw everything else afterwards. This works OK, but performance
1988 * is still a drag on larger pictures.
1990 for (int y
= 0; y
< height
; y
++) {
1991 for (int x
= 0; x
< width
; x
++) {
1992 // If physical had non-image data that is now image data, the
1993 // entire row must be redrawn.
1994 Cell lCell
= logical
[x
][y
];
1995 Cell pCell
= physical
[x
][y
];
1996 if (lCell
.isImage() && !pCell
.isImage()) {
2002 for (int y
= 0; y
< height
; y
++) {
2003 for (int x
= 0; x
< width
; x
++) {
2004 Cell lCell
= logical
[x
][y
];
2005 Cell pCell
= physical
[x
][y
];
2007 if (!lCell
.isImage()
2009 && (lCell
.getWidth() != Cell
.Width
.SINGLE
))
2016 while ((right
< width
)
2017 && (logical
[right
][y
].isImage())
2018 && (!logical
[right
][y
].equals(physical
[right
][y
])
2023 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
2024 for (int i
= 0; i
< (right
- x
); i
++) {
2025 assert (logical
[x
+ i
][y
].isImage());
2026 cellsToDraw
.add(logical
[x
+ i
][y
]);
2028 // Physical is always updated.
2029 physical
[x
+ i
][y
].setTo(lCell
);
2031 if (cellsToDraw
.size() > 0) {
2033 sb
.append(toIterm2Image(x
, y
, cellsToDraw
));
2035 sb
.append(toSixel(x
, y
, cellsToDraw
));
2043 // Draw the text part now.
2044 for (int y
= 0; y
< height
; y
++) {
2045 flushLine(y
, sb
, attr
);
2048 reallyCleared
= false;
2050 String result
= sb
.toString();
2051 if (debugToStderr
) {
2052 System
.err
.printf("flushString(): %s\n", result
);
2058 * Reset keyboard/mouse input parser.
2060 private void resetParser() {
2061 state
= ParseState
.GROUND
;
2062 params
= new ArrayList
<String
>();
2068 * Produce a control character or one of the special ones (ENTER, TAB,
2071 * @param ch Unicode code point
2072 * @param alt if true, set alt on the TKeypress
2073 * @return one TKeypress event, either a control character (e.g. isKey ==
2074 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
2077 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
2078 // System.err.printf("controlChar: %02x\n", ch);
2082 // Carriage return --> ENTER
2083 return new TKeypressEvent(kbEnter
, alt
, false, false);
2085 // Linefeed --> ENTER
2086 return new TKeypressEvent(kbEnter
, alt
, false, false);
2089 return new TKeypressEvent(kbEsc
, alt
, false, false);
2092 return new TKeypressEvent(kbTab
, alt
, false, false);
2094 // Make all other control characters come back as the alphabetic
2095 // character with the ctrl field set. So SOH would be 'A' +
2097 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
2103 * Produce special key from CSI Pn ; Pm ; ... ~
2105 * @return one KEYPRESS event representing a special key
2107 private TInputEvent
csiFnKey() {
2109 if (params
.size() > 0) {
2110 key
= Integer
.parseInt(params
.get(0));
2112 boolean alt
= false;
2113 boolean ctrl
= false;
2114 boolean shift
= false;
2115 if (params
.size() > 1) {
2116 shift
= csiIsShift(params
.get(1));
2117 alt
= csiIsAlt(params
.get(1));
2118 ctrl
= csiIsCtrl(params
.get(1));
2123 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2125 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2127 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2129 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2131 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2133 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2135 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2137 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2139 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2141 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2143 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2145 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2147 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2149 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2157 * Produce mouse events based on "Any event tracking" and UTF-8
2159 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2161 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2163 private TInputEvent
parseMouse() {
2164 int buttons
= params
.get(0).charAt(0) - 32;
2165 int x
= params
.get(0).charAt(1) - 32 - 1;
2166 int y
= params
.get(0).charAt(2) - 32 - 1;
2168 // Clamp X and Y to the physical screen coordinates.
2169 if (x
>= windowResize
.getWidth()) {
2170 x
= windowResize
.getWidth() - 1;
2172 if (y
>= windowResize
.getHeight()) {
2173 y
= windowResize
.getHeight() - 1;
2176 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2177 boolean eventMouse1
= false;
2178 boolean eventMouse2
= false;
2179 boolean eventMouse3
= false;
2180 boolean eventMouseWheelUp
= false;
2181 boolean eventMouseWheelDown
= false;
2183 // System.err.printf("buttons: %04x\r\n", buttons);
2200 if (!mouse1
&& !mouse2
&& !mouse3
) {
2201 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2203 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2220 // Dragging with mouse1 down
2223 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2227 // Dragging with mouse2 down
2230 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2234 // Dragging with mouse3 down
2237 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2241 // Dragging with mouse2 down after wheelUp
2244 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2248 // Dragging with mouse2 down after wheelDown
2251 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2255 eventMouseWheelUp
= true;
2259 eventMouseWheelDown
= true;
2263 // Unknown, just make it motion
2264 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2267 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2268 eventMouse1
, eventMouse2
, eventMouse3
,
2269 eventMouseWheelUp
, eventMouseWheelDown
);
2273 * Produce mouse events based on "Any event tracking" and SGR
2275 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2277 * @param release if true, this was a release ('m')
2278 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2280 private TInputEvent
parseMouseSGR(final boolean release
) {
2281 // SGR extended coordinates - mode 1006
2282 if (params
.size() < 3) {
2283 // Invalid position, bail out.
2286 int buttons
= Integer
.parseInt(params
.get(0));
2287 int x
= Integer
.parseInt(params
.get(1)) - 1;
2288 int y
= Integer
.parseInt(params
.get(2)) - 1;
2290 // Clamp X and Y to the physical screen coordinates.
2291 if (x
>= windowResize
.getWidth()) {
2292 x
= windowResize
.getWidth() - 1;
2294 if (y
>= windowResize
.getHeight()) {
2295 y
= windowResize
.getHeight() - 1;
2298 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2299 boolean eventMouse1
= false;
2300 boolean eventMouse2
= false;
2301 boolean eventMouse3
= false;
2302 boolean eventMouseWheelUp
= false;
2303 boolean eventMouseWheelDown
= false;
2306 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2320 // Motion only, no buttons down
2321 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2325 // Dragging with mouse1 down
2327 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2331 // Dragging with mouse2 down
2333 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2337 // Dragging with mouse3 down
2339 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2343 // Dragging with mouse2 down after wheelUp
2345 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2349 // Dragging with mouse2 down after wheelDown
2351 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2355 eventMouseWheelUp
= true;
2359 eventMouseWheelDown
= true;
2363 // Unknown, bail out
2366 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2367 eventMouse1
, eventMouse2
, eventMouse3
,
2368 eventMouseWheelUp
, eventMouseWheelDown
);
2372 * Return any events in the IO queue due to timeout.
2374 * @param queue list to append new events to
2376 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2377 long nowTime
= System
.currentTimeMillis();
2379 // Check for new window size
2380 long windowSizeDelay
= nowTime
- windowSizeTime
;
2381 if (windowSizeDelay
> 1000) {
2382 int oldTextWidth
= getTextWidth();
2383 int oldTextHeight
= getTextHeight();
2385 sessionInfo
.queryWindowSize();
2386 int newWidth
= sessionInfo
.getWindowWidth();
2387 int newHeight
= sessionInfo
.getWindowHeight();
2389 if ((newWidth
!= windowResize
.getWidth())
2390 || (newHeight
!= windowResize
.getHeight())
2393 // Request xterm report window dimensions in pixels again.
2394 // Between now and then, ensure that the reported text cell
2395 // size is the same by setting widthPixels and heightPixels
2396 // to match the new dimensions.
2397 widthPixels
= oldTextWidth
* newWidth
;
2398 heightPixels
= oldTextHeight
* newHeight
;
2400 if (debugToStderr
) {
2401 System
.err
.println("Screen size changed, old size " +
2403 System
.err
.println(" new size " +
2404 newWidth
+ " x " + newHeight
);
2405 System
.err
.println(" old pixels " +
2406 oldTextWidth
+ " x " + oldTextHeight
);
2407 System
.err
.println(" new pixels " +
2408 getTextWidth() + " x " + getTextHeight());
2411 this.output
.printf("%s", xtermReportPixelDimensions());
2412 this.output
.flush();
2414 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2415 newWidth
, newHeight
);
2416 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2417 newWidth
, newHeight
);
2420 windowSizeTime
= nowTime
;
2423 // ESCDELAY type timeout
2424 if (state
== ParseState
.ESCAPE
) {
2425 long escDelay
= nowTime
- escapeTime
;
2426 if (escDelay
> 100) {
2427 // After 0.1 seconds, assume a true escape character
2428 queue
.add(controlChar((char)0x1B, false));
2435 * Returns true if the CSI parameter for a keyboard command means that
2438 private boolean csiIsShift(final String x
) {
2450 * Returns true if the CSI parameter for a keyboard command means that
2453 private boolean csiIsAlt(final String x
) {
2465 * Returns true if the CSI parameter for a keyboard command means that
2468 private boolean csiIsCtrl(final String x
) {
2480 * Parses the next character of input to see if an InputEvent is
2483 * @param events list to append new events to
2484 * @param ch Unicode code point
2486 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2488 // ESCDELAY type timeout
2489 long nowTime
= System
.currentTimeMillis();
2490 if (state
== ParseState
.ESCAPE
) {
2491 long escDelay
= nowTime
- escapeTime
;
2492 if (escDelay
> 250) {
2493 // After 0.25 seconds, assume a true escape character
2494 events
.add(controlChar((char)0x1B, false));
2500 boolean ctrl
= false;
2501 boolean alt
= false;
2502 boolean shift
= false;
2504 // System.err.printf("state: %s ch %c\r\n", state, ch);
2510 state
= ParseState
.ESCAPE
;
2511 escapeTime
= nowTime
;
2516 // Control character
2517 events
.add(controlChar(ch
, false));
2524 events
.add(new TKeypressEvent(false, 0, ch
,
2525 false, false, false));
2534 // ALT-Control character
2535 events
.add(controlChar(ch
, true));
2541 // This will be one of the function keys
2542 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2546 // '[' goes to CSI_ENTRY
2548 state
= ParseState
.CSI_ENTRY
;
2552 // Everything else is assumed to be Alt-keystroke
2553 if ((ch
>= 'A') && (ch
<= 'Z')) {
2557 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2561 case ESCAPE_INTERMEDIATE
:
2562 if ((ch
>= 'P') && (ch
<= 'S')) {
2566 events
.add(new TKeypressEvent(kbF1
));
2569 events
.add(new TKeypressEvent(kbF2
));
2572 events
.add(new TKeypressEvent(kbF3
));
2575 events
.add(new TKeypressEvent(kbF4
));
2584 // Unknown keystroke, ignore
2589 // Numbers - parameter values
2590 if ((ch
>= '0') && (ch
<= '9')) {
2591 params
.set(params
.size() - 1,
2592 params
.get(params
.size() - 1) + ch
);
2593 state
= ParseState
.CSI_PARAM
;
2596 // Parameter separator
2602 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2606 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2611 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2616 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2621 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2626 events
.add(new TKeypressEvent(kbHome
));
2631 events
.add(new TKeypressEvent(kbEnd
));
2635 // CBT - Cursor backward X tab stops (default 1)
2636 events
.add(new TKeypressEvent(kbBackTab
));
2641 state
= ParseState
.MOUSE
;
2644 // Mouse position, SGR (1006) coordinates
2645 state
= ParseState
.MOUSE_SGR
;
2652 // Unknown keystroke, ignore
2657 // Numbers - parameter values
2658 if ((ch
>= '0') && (ch
<= '9')) {
2659 params
.set(params
.size() - 1,
2660 params
.get(params
.size() - 1) + ch
);
2663 // Parameter separator
2671 // Generate a mouse press event
2672 TInputEvent event
= parseMouseSGR(false);
2673 if (event
!= null) {
2679 // Generate a mouse release event
2680 event
= parseMouseSGR(true);
2681 if (event
!= null) {
2690 // Unknown keystroke, ignore
2695 // Numbers - parameter values
2696 if ((ch
>= '0') && (ch
<= '9')) {
2697 params
.set(params
.size() - 1,
2698 params
.get(params
.size() - 1) + ch
);
2699 state
= ParseState
.CSI_PARAM
;
2702 // Parameter separator
2709 events
.add(csiFnKey());
2714 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2718 if (params
.size() > 1) {
2719 shift
= csiIsShift(params
.get(1));
2720 alt
= csiIsAlt(params
.get(1));
2721 ctrl
= csiIsCtrl(params
.get(1));
2723 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2728 if (params
.size() > 1) {
2729 shift
= csiIsShift(params
.get(1));
2730 alt
= csiIsAlt(params
.get(1));
2731 ctrl
= csiIsCtrl(params
.get(1));
2733 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2738 if (params
.size() > 1) {
2739 shift
= csiIsShift(params
.get(1));
2740 alt
= csiIsAlt(params
.get(1));
2741 ctrl
= csiIsCtrl(params
.get(1));
2743 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2748 if (params
.size() > 1) {
2749 shift
= csiIsShift(params
.get(1));
2750 alt
= csiIsAlt(params
.get(1));
2751 ctrl
= csiIsCtrl(params
.get(1));
2753 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2758 if (params
.size() > 1) {
2759 shift
= csiIsShift(params
.get(1));
2760 alt
= csiIsAlt(params
.get(1));
2761 ctrl
= csiIsCtrl(params
.get(1));
2763 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2768 if (params
.size() > 1) {
2769 shift
= csiIsShift(params
.get(1));
2770 alt
= csiIsAlt(params
.get(1));
2771 ctrl
= csiIsCtrl(params
.get(1));
2773 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2778 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2779 if (debugToStderr
) {
2780 System
.err
.printf("windowOp pixels: " +
2781 "height %s width %s\n",
2782 params
.get(1), params
.get(2));
2785 widthPixels
= Integer
.parseInt(params
.get(2));
2786 heightPixels
= Integer
.parseInt(params
.get(1));
2787 } catch (NumberFormatException e
) {
2788 if (debugToStderr
) {
2789 e
.printStackTrace();
2792 if (widthPixels
<= 0) {
2795 if (heightPixels
<= 0) {
2799 if ((params
.size() > 2) && (params
.get(0).equals("6"))) {
2800 if (debugToStderr
) {
2801 System
.err
.printf("windowOp text cell pixels: " +
2802 "height %s width %s\n",
2803 params
.get(1), params
.get(2));
2806 widthPixels
= width
* Integer
.parseInt(params
.get(2));
2807 heightPixels
= height
* Integer
.parseInt(params
.get(1));
2808 } catch (NumberFormatException e
) {
2809 if (debugToStderr
) {
2810 e
.printStackTrace();
2813 if (widthPixels
<= 0) {
2816 if (heightPixels
<= 0) {
2827 // Unknown keystroke, ignore
2832 params
.set(0, params
.get(params
.size() - 1) + ch
);
2833 if (params
.get(0).length() == 3) {
2834 // We have enough to generate a mouse event
2835 events
.add(parseMouse());
2844 // This "should" be impossible to reach
2849 * Request (u)xterm to use the sixel settings we need:
2851 * - enable sixel scrolling
2853 * - disable private color registers (so that we can use one common
2856 * @return the string to emit to xterm
2858 private String
xtermSetSixelSettings() {
2859 return "\033[?80h\033[?1070l";
2863 * Restore (u)xterm its default sixel settings:
2865 * - enable sixel scrolling
2867 * - enable private color registers
2869 * @return the string to emit to xterm
2871 private String
xtermResetSixelSettings() {
2872 return "\033[?80h\033[?1070h";
2876 * Request (u)xterm to report the current window and cell size dimensions
2879 * @return the string to emit to xterm
2881 private String
xtermReportPixelDimensions() {
2882 // We will ask for both window and text cell dimensions, and
2883 // hopefully one of them will work.
2884 return "\033[14t\033[16t";
2888 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2889 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2892 * @param on if true, enable metaSendsEscape
2893 * @return the string to emit to xterm
2895 private String
xtermMetaSendsEscape(final boolean on
) {
2897 return "\033[?1036h\033[?1034l";
2899 return "\033[?1036l";
2903 * Create an xterm OSC sequence to change the window title.
2905 * @param title the new title
2906 * @return the string to emit to xterm
2908 private String
getSetTitleString(final String title
) {
2909 return "\033]2;" + title
+ "\007";
2912 // ------------------------------------------------------------------------
2913 // Sixel output support ---------------------------------------------------
2914 // ------------------------------------------------------------------------
2917 * Get the number of colors in the sixel palette.
2919 * @return the palette size
2921 public int getSixelPaletteSize() {
2922 return sixelPaletteSize
;
2926 * Set the number of colors in the sixel palette.
2928 * @param paletteSize the new palette size
2930 public void setSixelPaletteSize(final int paletteSize
) {
2931 if (paletteSize
== sixelPaletteSize
) {
2935 switch (paletteSize
) {
2943 throw new IllegalArgumentException("Unsupported sixel palette " +
2944 " size: " + paletteSize
);
2947 // Don't step on the screen refresh thread.
2948 synchronized (this) {
2949 sixelPaletteSize
= paletteSize
;
2957 * Start a sixel string for display one row's worth of bitmap data.
2959 * @param x column coordinate. 0 is the left-most column.
2960 * @param y row coordinate. 0 is the top-most row.
2961 * @return the string to emit to an ANSI / ECMA-style terminal
2963 private String
startSixel(final int x
, final int y
) {
2964 StringBuilder sb
= new StringBuilder();
2966 assert (sixel
== true);
2969 sb
.append(gotoXY(x
, y
));
2972 sb
.append("\033Pq");
2974 if (palette
== null) {
2975 palette
= new SixelPalette();
2976 // TODO: make this an option (shared palette or not)
2977 palette
.emitPalette(sb
, null);
2980 return sb
.toString();
2984 * End a sixel string for display one row's worth of bitmap data.
2986 * @return the string to emit to an ANSI / ECMA-style terminal
2988 private String
endSixel() {
2989 assert (sixel
== true);
2996 * Create a sixel string representing a row of several cells containing
2999 * @param x column coordinate. 0 is the left-most column.
3000 * @param y row coordinate. 0 is the top-most row.
3001 * @param cells the cells containing the bitmap data
3002 * @return the string to emit to an ANSI / ECMA-style terminal
3004 private String
toSixel(final int x
, final int y
,
3005 final ArrayList
<Cell
> cells
) {
3007 StringBuilder sb
= new StringBuilder();
3009 assert (cells
!= null);
3010 assert (cells
.size() > 0);
3011 assert (cells
.get(0).getImage() != null);
3013 if (sixel
== false) {
3014 sb
.append(normal());
3015 sb
.append(gotoXY(x
, y
));
3016 for (int i
= 0; i
< cells
.size(); i
++) {
3019 return sb
.toString();
3022 if (y
== height
- 1) {
3023 // We are on the bottom row. If scrolling mode is enabled
3024 // (default), then VT320/xterm will scroll the entire screen if
3025 // we draw any pixels here.
3027 // TODO: support sixel scrolling mode disabled as an option.
3028 sb
.append(normal());
3029 sb
.append(gotoXY(x
, y
));
3030 for (int j
= 0; j
< cells
.size(); j
++) {
3033 return sb
.toString();
3036 if (sixelCache
== null) {
3037 sixelCache
= new ImageCache(height
* 10);
3040 // Save and get rows to/from the cache that do NOT have inverted
3042 boolean saveInCache
= true;
3043 for (Cell cell
: cells
) {
3044 if (cell
.isInvertedImage()) {
3045 saveInCache
= false;
3049 String cachedResult
= sixelCache
.get(cells
);
3050 if (cachedResult
!= null) {
3051 // System.err.println("CACHE HIT");
3052 sb
.append(startSixel(x
, y
));
3053 sb
.append(cachedResult
);
3054 sb
.append(endSixel());
3055 return sb
.toString();
3057 // System.err.println("CACHE MISS");
3060 int imageWidth
= cells
.get(0).getImage().getWidth();
3061 int imageHeight
= cells
.get(0).getImage().getHeight();
3063 // cells.get(x).getImage() has a dithered bitmap containing indexes
3064 // into the color palette. Piece these together into one larger
3065 // image for final rendering.
3067 int fullWidth
= cells
.size() * getTextWidth();
3068 int fullHeight
= getTextHeight();
3069 for (int i
= 0; i
< cells
.size(); i
++) {
3070 totalWidth
+= cells
.get(i
).getImage().getWidth();
3073 BufferedImage image
= new BufferedImage(fullWidth
,
3074 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3077 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3078 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
3080 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
3083 if (false && cells
.get(i
).isInvertedImage()) {
3084 // I used to put an all-white cell over the cursor, don't do
3086 rgbArray
= new int[imageWidth
* imageHeight
];
3087 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3088 rgbArray
[j
] = 0xFFFFFF;
3092 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3093 tileWidth
, tileHeight
, null, 0, tileWidth
);
3094 } catch (Exception e
) {
3095 throw new RuntimeException("image " + imageWidth
+ "x" +
3097 "tile " + tileWidth
+ "x" +
3099 " cells.get(i).getImage() " +
3100 cells
.get(i
).getImage() +
3102 " fullWidth " + fullWidth
+
3103 " fullHeight " + fullHeight
, e
);
3108 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3109 i * imageWidth, 0, imageWidth, imageHeight,
3111 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3112 fullWidth, fullHeight, cells.size(), getTextWidth());
3115 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3116 rgbArray
, 0, tileWidth
);
3117 if (tileHeight
< fullHeight
) {
3118 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3119 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3120 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3123 image
.setRGB(imageX
, imageY
, backgroundColor
);
3128 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3129 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3130 // I used to put an all-white cell over the cursor, don't do that
3132 rgbArray
= new int[totalWidth
* imageHeight
];
3133 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3134 rgbArray
[j
] = 0xFFFFFF;
3138 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3139 totalWidth
, imageHeight
, null, 0, totalWidth
);
3140 } catch (Exception e
) {
3141 throw new RuntimeException("image " + imageWidth
+ "x" +
3142 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3143 cells
.get(cells
.size() - 1).getImage(), e
);
3146 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3147 imageHeight
, rgbArray
, 0, totalWidth
);
3149 if (totalWidth
< getTextWidth()) {
3150 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3152 for (int imageX
= image
.getWidth() - totalWidth
;
3153 imageX
< image
.getWidth(); imageX
++) {
3155 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3156 image
.setRGB(imageX
, imageY
, backgroundColor
);
3161 // Dither the image. It is ok to lose the original here.
3162 if (palette
== null) {
3163 palette
= new SixelPalette();
3164 // TODO: make this an option (shared palette or not)
3165 palette
.emitPalette(sb
, null);
3167 image
= palette
.ditherImage(image
);
3169 // Collect the raster information
3170 int rasterHeight
= 0;
3171 int rasterWidth
= image
.getWidth();
3175 // TODO: make this an option (shared palette or not)
3177 // Emit the palette, but only for the colors actually used by these
3179 boolean [] usedColors = new boolean[sixelPaletteSize];
3180 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3181 for (int imageY = 0; imageY < image.getHeight(); imageY++) {
3182 usedColors[image.getRGB(imageX, imageY)] = true;
3185 palette.emitPalette(sb, usedColors);
3188 // Render the entire row of cells.
3189 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
3190 int [][] sixels
= new int[image
.getWidth()][6];
3192 // See which colors are actually used in this band of sixels.
3193 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3194 for (int imageY
= 0;
3195 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
3198 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
3199 assert (colorIdx
>= 0);
3200 assert (colorIdx
< sixelPaletteSize
);
3202 sixels
[imageX
][imageY
] = colorIdx
;
3206 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
3207 boolean isUsed
= false;
3208 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3209 for (int j
= 0; j
< 6; j
++) {
3210 if (sixels
[imageX
][j
] == i
) {
3215 if (isUsed
== false) {
3219 // Set to the beginning of scan line for the next set of
3220 // colored pixels, and select the color.
3221 sb
.append(String
.format("$#%d", i
));
3224 int oldDataCount
= 0;
3225 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3227 // Add up all the pixels that match this color.
3230 (j
< 6) && (currentRow
+ j
< fullHeight
);
3233 if (sixels
[imageX
][j
] == i
) {
3254 if ((currentRow
+ j
+ 1) > rasterHeight
) {
3255 rasterHeight
= currentRow
+ j
+ 1;
3263 if (data
== oldData
) {
3266 if (oldDataCount
== 1) {
3267 sb
.append((char) oldData
);
3268 } else if (oldDataCount
> 1) {
3269 sb
.append(String
.format("!%d", oldDataCount
));
3270 sb
.append((char) oldData
);
3276 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
3278 // Emit the last sequence.
3279 if (oldDataCount
== 1) {
3280 sb
.append((char) oldData
);
3281 } else if (oldDataCount
> 1) {
3282 sb
.append(String
.format("!%d", oldDataCount
));
3283 sb
.append((char) oldData
);
3286 } // for (int i = 0; i < sixelPaletteSize; i++)
3288 // Advance to the next scan line.
3291 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3293 // Kill the very last "-", because it is unnecessary.
3294 sb
.deleteCharAt(sb
.length() - 1);
3296 // Add the raster information
3297 sb
.insert(0, String
.format("\"1;1;%d;%d", rasterWidth
, rasterHeight
));
3300 // This row is OK to save into the cache.
3301 sixelCache
.put(cells
, sb
.toString());
3304 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3308 * Get the sixel support flag.
3310 * @return true if this terminal is emitting sixel
3312 public boolean hasSixel() {
3316 // ------------------------------------------------------------------------
3317 // End sixel output support -----------------------------------------------
3318 // ------------------------------------------------------------------------
3320 // ------------------------------------------------------------------------
3321 // iTerm2 image output support --------------------------------------------
3322 // ------------------------------------------------------------------------
3325 * Create an iTerm2 images string representing a row of several cells
3326 * containing bitmap data.
3328 * @param x column coordinate. 0 is the left-most column.
3329 * @param y row coordinate. 0 is the top-most row.
3330 * @param cells the cells containing the bitmap data
3331 * @return the string to emit to an ANSI / ECMA-style terminal
3333 private String
toIterm2Image(final int x
, final int y
,
3334 final ArrayList
<Cell
> cells
) {
3336 StringBuilder sb
= new StringBuilder();
3338 assert (cells
!= null);
3339 assert (cells
.size() > 0);
3340 assert (cells
.get(0).getImage() != null);
3342 if (iterm2Images
== false) {
3343 sb
.append(normal());
3344 sb
.append(gotoXY(x
, y
));
3345 for (int i
= 0; i
< cells
.size(); i
++) {
3348 return sb
.toString();
3351 if (iterm2Cache
== null) {
3352 iterm2Cache
= new ImageCache(height
* 10);
3353 base64
= java
.util
.Base64
.getEncoder();
3356 // Save and get rows to/from the cache that do NOT have inverted
3358 boolean saveInCache
= true;
3359 for (Cell cell
: cells
) {
3360 if (cell
.isInvertedImage()) {
3361 saveInCache
= false;
3365 String cachedResult
= iterm2Cache
.get(cells
);
3366 if (cachedResult
!= null) {
3367 // System.err.println("CACHE HIT");
3368 sb
.append(gotoXY(x
, y
));
3369 sb
.append(cachedResult
);
3370 return sb
.toString();
3372 // System.err.println("CACHE MISS");
3375 int imageWidth
= cells
.get(0).getImage().getWidth();
3376 int imageHeight
= cells
.get(0).getImage().getHeight();
3378 // cells.get(x).getImage() has a dithered bitmap containing indexes
3379 // into the color palette. Piece these together into one larger
3380 // image for final rendering.
3382 int fullWidth
= cells
.size() * getTextWidth();
3383 int fullHeight
= getTextHeight();
3384 for (int i
= 0; i
< cells
.size(); i
++) {
3385 totalWidth
+= cells
.get(i
).getImage().getWidth();
3388 BufferedImage image
= new BufferedImage(fullWidth
,
3389 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3392 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3393 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
3395 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
3397 if (false && cells
.get(i
).isInvertedImage()) {
3398 // I used to put an all-white cell over the cursor, don't do
3400 rgbArray
= new int[imageWidth
* imageHeight
];
3401 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3402 rgbArray
[j
] = 0xFFFFFF;
3406 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3407 tileWidth
, tileHeight
, null, 0, tileWidth
);
3408 } catch (Exception e
) {
3409 throw new RuntimeException("image " + imageWidth
+ "x" +
3411 "tile " + tileWidth
+ "x" +
3413 " cells.get(i).getImage() " +
3414 cells
.get(i
).getImage() +
3416 " fullWidth " + fullWidth
+
3417 " fullHeight " + fullHeight
, e
);
3422 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3423 i * imageWidth, 0, imageWidth, imageHeight,
3425 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3426 fullWidth, fullHeight, cells.size(), getTextWidth());
3429 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3430 rgbArray
, 0, tileWidth
);
3431 if (tileHeight
< fullHeight
) {
3432 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3433 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3434 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3437 image
.setRGB(imageX
, imageY
, backgroundColor
);
3442 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3443 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3444 // I used to put an all-white cell over the cursor, don't do that
3446 rgbArray
= new int[totalWidth
* imageHeight
];
3447 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3448 rgbArray
[j
] = 0xFFFFFF;
3452 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3453 totalWidth
, imageHeight
, null, 0, totalWidth
);
3454 } catch (Exception e
) {
3455 throw new RuntimeException("image " + imageWidth
+ "x" +
3456 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3457 cells
.get(cells
.size() - 1).getImage(), e
);
3460 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3461 imageHeight
, rgbArray
, 0, totalWidth
);
3463 if (totalWidth
< getTextWidth()) {
3464 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3466 for (int imageX
= image
.getWidth() - totalWidth
;
3467 imageX
< image
.getWidth(); imageX
++) {
3469 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3470 image
.setRGB(imageX
, imageY
, backgroundColor
);
3476 * From https://iterm2.com/documentation-images.html:
3480 * iTerm2 extends the xterm protocol with a set of proprietary escape
3481 * sequences. In general, the pattern is:
3483 * ESC ] 1337 ; key = value ^G
3485 * Whitespace is shown here for ease of reading: in practice, no
3486 * spaces should be used.
3488 * For file transfer and inline images, the code is:
3490 * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
3492 * The optional arguments are formatted as key=value with a semicolon
3493 * between each key-value pair. They are described below:
3495 * Key Description of value
3496 * name base-64 encoded filename. Defaults to "Unnamed file".
3497 * size File size in bytes. Optional; this is only used by the
3498 * progress indicator.
3499 * width Width to render. See notes below.
3500 * height Height to render. See notes below.
3501 * preserveAspectRatio If set to 0, then the image's inherent aspect
3502 * ratio will not be respected; otherwise, it
3503 * will fill the specified width and height as
3504 * much as possible without stretching. Defaults
3506 * inline If set to 1, the file will be displayed inline. Otherwise,
3507 * it will be downloaded with no visual representation in the
3508 * terminal session. Defaults to 0.
3510 * The width and height are given as a number followed by a unit, or
3513 * N: N character cells.
3515 * N%: N percent of the session's width or height.
3516 * auto: The image's inherent size will be used to determine an
3517 * appropriate dimension.
3521 // File contents can be several image formats. We will use PNG.
3522 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3524 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3525 Math
.min(image
.getHeight(), fullHeight
)),
3526 "PNG", pngOutputStream
)
3528 // We failed to render image, bail out.
3531 } catch (IOException e
) {
3532 // We failed to render image, bail out.
3536 // iTerm2 does not advance the cursor automatically, so place it
3538 sb
.append("\033]1337;File=");
3540 sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
3544 sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
3545 image.getWidth(), Math.min(image.getHeight(),
3548 sb
.append("inline=1:");
3549 sb
.append(base64
.encodeToString(pngOutputStream
.toByteArray()));
3553 // This row is OK to save into the cache.
3554 iterm2Cache
.put(cells
, sb
.toString());
3557 return (gotoXY(x
, y
) + sb
.toString());
3561 * Get the iTerm2 images support flag.
3563 * @return true if this terminal is emitting iTerm2 images
3565 public boolean hasIterm2Images() {
3566 return iterm2Images
;
3569 // ------------------------------------------------------------------------
3570 // End iTerm2 image output support ----------------------------------------
3571 // ------------------------------------------------------------------------
3574 * Setup system colors to match DOS color palette.
3576 private void setDOSColors() {
3577 MYBLACK
= new java
.awt
.Color(0x00, 0x00, 0x00);
3578 MYRED
= new java
.awt
.Color(0xa8, 0x00, 0x00);
3579 MYGREEN
= new java
.awt
.Color(0x00, 0xa8, 0x00);
3580 MYYELLOW
= new java
.awt
.Color(0xa8, 0x54, 0x00);
3581 MYBLUE
= new java
.awt
.Color(0x00, 0x00, 0xa8);
3582 MYMAGENTA
= new java
.awt
.Color(0xa8, 0x00, 0xa8);
3583 MYCYAN
= new java
.awt
.Color(0x00, 0xa8, 0xa8);
3584 MYWHITE
= new java
.awt
.Color(0xa8, 0xa8, 0xa8);
3585 MYBOLD_BLACK
= new java
.awt
.Color(0x54, 0x54, 0x54);
3586 MYBOLD_RED
= new java
.awt
.Color(0xfc, 0x54, 0x54);
3587 MYBOLD_GREEN
= new java
.awt
.Color(0x54, 0xfc, 0x54);
3588 MYBOLD_YELLOW
= new java
.awt
.Color(0xfc, 0xfc, 0x54);
3589 MYBOLD_BLUE
= new java
.awt
.Color(0x54, 0x54, 0xfc);
3590 MYBOLD_MAGENTA
= new java
.awt
.Color(0xfc, 0x54, 0xfc);
3591 MYBOLD_CYAN
= new java
.awt
.Color(0x54, 0xfc, 0xfc);
3592 MYBOLD_WHITE
= new java
.awt
.Color(0xfc, 0xfc, 0xfc);
3596 * Setup ECMA48 colors to match those provided in system properties.
3598 private void setCustomSystemColors() {
3601 MYBLACK
= getCustomColor("jexer.ECMA48.color0", MYBLACK
);
3602 MYRED
= getCustomColor("jexer.ECMA48.color1", MYRED
);
3603 MYGREEN
= getCustomColor("jexer.ECMA48.color2", MYGREEN
);
3604 MYYELLOW
= getCustomColor("jexer.ECMA48.color3", MYYELLOW
);
3605 MYBLUE
= getCustomColor("jexer.ECMA48.color4", MYBLUE
);
3606 MYMAGENTA
= getCustomColor("jexer.ECMA48.color5", MYMAGENTA
);
3607 MYCYAN
= getCustomColor("jexer.ECMA48.color6", MYCYAN
);
3608 MYWHITE
= getCustomColor("jexer.ECMA48.color7", MYWHITE
);
3609 MYBOLD_BLACK
= getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK
);
3610 MYBOLD_RED
= getCustomColor("jexer.ECMA48.color9", MYBOLD_RED
);
3611 MYBOLD_GREEN
= getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN
);
3612 MYBOLD_YELLOW
= getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW
);
3613 MYBOLD_BLUE
= getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE
);
3614 MYBOLD_MAGENTA
= getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA
);
3615 MYBOLD_CYAN
= getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN
);
3616 MYBOLD_WHITE
= getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE
);
3620 * Setup one system color to match the RGB value provided in system
3623 * @param key the system property key
3624 * @param defaultColor the default color to return if key is not set, or
3626 * @return a color from the RGB string, or defaultColor
3628 private java
.awt
.Color
getCustomColor(final String key
,
3629 final java
.awt
.Color defaultColor
) {
3631 String rgb
= System
.getProperty(key
);
3633 return defaultColor
;
3635 if (rgb
.startsWith("#")) {
3636 rgb
= rgb
.substring(1);
3640 rgbInt
= Integer
.parseInt(rgb
, 16);
3641 } catch (NumberFormatException e
) {
3642 return defaultColor
;
3644 java
.awt
.Color color
= new java
.awt
.Color((rgbInt
& 0xFF0000) >>> 16,
3645 (rgbInt
& 0x00FF00) >>> 8,
3646 (rgbInt
& 0x0000FF));
3652 * Create a T.416 RGB parameter sequence for a custom system color.
3654 * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
3655 * @return the color portion of the string to emit to an ANSI /
3656 * ECMA-style terminal
3658 private String
systemColorRGB(final java
.awt
.Color color
) {
3659 return String
.format("%d;%d;%d", color
.getRed(), color
.getGreen(),
3664 * Create a SGR parameter sequence for a single color change.
3666 * @param bold if true, set bold
3667 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3668 * @param foreground if true, this is a foreground color
3669 * @return the string to emit to an ANSI / ECMA-style terminal,
3672 private String
color(final boolean bold
, final Color color
,
3673 final boolean foreground
) {
3674 return color(color
, foreground
, true) +
3675 rgbColor(bold
, color
, foreground
);
3679 * Create a T.416 RGB parameter sequence for a single color change.
3681 * @param colorRGB a 24-bit RGB value for foreground color
3682 * @param foreground if true, this is a foreground color
3683 * @return the string to emit to an ANSI / ECMA-style terminal,
3686 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
3688 int colorRed
= (colorRGB
>>> 16) & 0xFF;
3689 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
3690 int colorBlue
= colorRGB
& 0xFF;
3692 StringBuilder sb
= new StringBuilder();
3694 sb
.append("\033[38;2;");
3696 sb
.append("\033[48;2;");
3698 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
3699 return sb
.toString();
3703 * Create a T.416 RGB parameter sequence for both foreground and
3704 * background color change.
3706 * @param foreColorRGB a 24-bit RGB value for foreground color
3707 * @param backColorRGB a 24-bit RGB value for foreground color
3708 * @return the string to emit to an ANSI / ECMA-style terminal,
3711 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
3712 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3713 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3714 int foreColorBlue
= foreColorRGB
& 0xFF;
3715 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3716 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3717 int backColorBlue
= backColorRGB
& 0xFF;
3719 StringBuilder sb
= new StringBuilder();
3720 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
3721 foreColorRed
, foreColorGreen
, foreColorBlue
));
3722 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
3723 backColorRed
, backColorGreen
, backColorBlue
));
3724 return sb
.toString();
3728 * Create a T.416 RGB parameter sequence for a single color change.
3730 * @param bold if true, set bold
3731 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3732 * @param foreground if true, this is a foreground color
3733 * @return the string to emit to an xterm terminal with RGB support,
3734 * e.g. "\033[38;2;RR;GG;BBm"
3736 private String
rgbColor(final boolean bold
, final Color color
,
3737 final boolean foreground
) {
3738 if (doRgbColor
== false) {
3741 StringBuilder sb
= new StringBuilder("\033[");
3743 // Bold implies foreground only
3745 if (color
.equals(Color
.BLACK
)) {
3746 sb
.append(systemColorRGB(MYBOLD_BLACK
));
3747 } else if (color
.equals(Color
.RED
)) {
3748 sb
.append(systemColorRGB(MYBOLD_RED
));
3749 } else if (color
.equals(Color
.GREEN
)) {
3750 sb
.append(systemColorRGB(MYBOLD_GREEN
));
3751 } else if (color
.equals(Color
.YELLOW
)) {
3752 sb
.append(systemColorRGB(MYBOLD_YELLOW
));
3753 } else if (color
.equals(Color
.BLUE
)) {
3754 sb
.append(systemColorRGB(MYBOLD_BLUE
));
3755 } else if (color
.equals(Color
.MAGENTA
)) {
3756 sb
.append(systemColorRGB(MYBOLD_MAGENTA
));
3757 } else if (color
.equals(Color
.CYAN
)) {
3758 sb
.append(systemColorRGB(MYBOLD_CYAN
));
3759 } else if (color
.equals(Color
.WHITE
)) {
3760 sb
.append(systemColorRGB(MYBOLD_WHITE
));
3768 if (color
.equals(Color
.BLACK
)) {
3769 sb
.append(systemColorRGB(MYBLACK
));
3770 } else if (color
.equals(Color
.RED
)) {
3771 sb
.append(systemColorRGB(MYRED
));
3772 } else if (color
.equals(Color
.GREEN
)) {
3773 sb
.append(systemColorRGB(MYGREEN
));
3774 } else if (color
.equals(Color
.YELLOW
)) {
3775 sb
.append(systemColorRGB(MYYELLOW
));
3776 } else if (color
.equals(Color
.BLUE
)) {
3777 sb
.append(systemColorRGB(MYBLUE
));
3778 } else if (color
.equals(Color
.MAGENTA
)) {
3779 sb
.append(systemColorRGB(MYMAGENTA
));
3780 } else if (color
.equals(Color
.CYAN
)) {
3781 sb
.append(systemColorRGB(MYCYAN
));
3782 } else if (color
.equals(Color
.WHITE
)) {
3783 sb
.append(systemColorRGB(MYWHITE
));
3787 return sb
.toString();
3791 * Create a T.416 RGB parameter sequence for both foreground and
3792 * background color change.
3794 * @param bold if true, set bold
3795 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3796 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3797 * @return the string to emit to an xterm terminal with RGB support,
3798 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
3800 private String
rgbColor(final boolean bold
, final Color foreColor
,
3801 final Color backColor
) {
3802 if (doRgbColor
== false) {
3806 return rgbColor(bold
, foreColor
, true) +
3807 rgbColor(false, backColor
, false);
3811 * Create a SGR parameter sequence for a single color change.
3813 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3814 * @param foreground if true, this is a foreground color
3815 * @param header if true, make the full header, otherwise just emit the
3816 * color parameter e.g. "42;"
3817 * @return the string to emit to an ANSI / ECMA-style terminal,
3820 private String
color(final Color color
, final boolean foreground
,
3821 final boolean header
) {
3823 int ecmaColor
= color
.getValue();
3825 // Convert Color.* values to SGR numerics
3833 return String
.format("\033[%dm", ecmaColor
);
3835 return String
.format("%d;", ecmaColor
);
3840 * Create a SGR parameter sequence for both foreground and background
3843 * @param bold if true, set bold
3844 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3845 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3846 * @return the string to emit to an ANSI / ECMA-style terminal,
3847 * e.g. "\033[31;42m"
3849 private String
color(final boolean bold
, final Color foreColor
,
3850 final Color backColor
) {
3851 return color(foreColor
, backColor
, true) +
3852 rgbColor(bold
, foreColor
, backColor
);
3856 * Create a SGR parameter sequence for both foreground and
3857 * background color change.
3859 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3860 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3861 * @param header if true, make the full header, otherwise just emit the
3862 * color parameter e.g. "31;42;"
3863 * @return the string to emit to an ANSI / ECMA-style terminal,
3864 * e.g. "\033[31;42m"
3866 private String
color(final Color foreColor
, final Color backColor
,
3867 final boolean header
) {
3869 int ecmaForeColor
= foreColor
.getValue();
3870 int ecmaBackColor
= backColor
.getValue();
3872 // Convert Color.* values to SGR numerics
3873 ecmaBackColor
+= 40;
3874 ecmaForeColor
+= 30;
3877 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
3879 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
3884 * Create a SGR parameter sequence for foreground, background, and
3885 * several attributes. This sequence first resets all attributes to
3886 * default, then sets attributes as per the parameters.
3888 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3889 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3890 * @param bold if true, set bold
3891 * @param reverse if true, set reverse
3892 * @param blink if true, set blink
3893 * @param underline if true, set underline
3894 * @return the string to emit to an ANSI / ECMA-style terminal,
3895 * e.g. "\033[0;1;31;42m"
3897 private String
color(final Color foreColor
, final Color backColor
,
3898 final boolean bold
, final boolean reverse
, final boolean blink
,
3899 final boolean underline
) {
3901 int ecmaForeColor
= foreColor
.getValue();
3902 int ecmaBackColor
= backColor
.getValue();
3904 // Convert Color.* values to SGR numerics
3905 ecmaBackColor
+= 40;
3906 ecmaForeColor
+= 30;
3908 StringBuilder sb
= new StringBuilder();
3909 if ( bold
&& reverse
&& blink
&& !underline
) {
3910 sb
.append("\033[0;1;7;5;");
3911 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3912 sb
.append("\033[0;1;7;");
3913 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3914 sb
.append("\033[0;7;5;");
3915 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3916 sb
.append("\033[0;1;5;");
3917 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3918 sb
.append("\033[0;1;");
3919 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3920 sb
.append("\033[0;7;");
3921 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3922 sb
.append("\033[0;5;");
3923 } else if ( bold
&& reverse
&& blink
&& underline
) {
3924 sb
.append("\033[0;1;7;5;4;");
3925 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3926 sb
.append("\033[0;1;7;4;");
3927 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3928 sb
.append("\033[0;7;5;4;");
3929 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3930 sb
.append("\033[0;1;5;4;");
3931 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3932 sb
.append("\033[0;1;4;");
3933 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3934 sb
.append("\033[0;7;4;");
3935 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3936 sb
.append("\033[0;5;4;");
3937 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3938 sb
.append("\033[0;4;");
3940 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3941 sb
.append("\033[0;");
3943 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
3944 sb
.append(rgbColor(bold
, foreColor
, backColor
));
3945 return sb
.toString();
3949 * Create a SGR parameter sequence for foreground, background, and
3950 * several attributes. This sequence first resets all attributes to
3951 * default, then sets attributes as per the parameters.
3953 * @param foreColorRGB a 24-bit RGB value for foreground color
3954 * @param backColorRGB a 24-bit RGB value for foreground color
3955 * @param bold if true, set bold
3956 * @param reverse if true, set reverse
3957 * @param blink if true, set blink
3958 * @param underline if true, set underline
3959 * @return the string to emit to an ANSI / ECMA-style terminal,
3960 * e.g. "\033[0;1;31;42m"
3962 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
3963 final boolean bold
, final boolean reverse
, final boolean blink
,
3964 final boolean underline
) {
3966 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3967 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3968 int foreColorBlue
= foreColorRGB
& 0xFF;
3969 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3970 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3971 int backColorBlue
= backColorRGB
& 0xFF;
3973 StringBuilder sb
= new StringBuilder();
3974 if ( bold
&& reverse
&& blink
&& !underline
) {
3975 sb
.append("\033[0;1;7;5;");
3976 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3977 sb
.append("\033[0;1;7;");
3978 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3979 sb
.append("\033[0;7;5;");
3980 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3981 sb
.append("\033[0;1;5;");
3982 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3983 sb
.append("\033[0;1;");
3984 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3985 sb
.append("\033[0;7;");
3986 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3987 sb
.append("\033[0;5;");
3988 } else if ( bold
&& reverse
&& blink
&& underline
) {
3989 sb
.append("\033[0;1;7;5;4;");
3990 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3991 sb
.append("\033[0;1;7;4;");
3992 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3993 sb
.append("\033[0;7;5;4;");
3994 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3995 sb
.append("\033[0;1;5;4;");
3996 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3997 sb
.append("\033[0;1;4;");
3998 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3999 sb
.append("\033[0;7;4;");
4000 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4001 sb
.append("\033[0;5;4;");
4002 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4003 sb
.append("\033[0;4;");
4005 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4006 sb
.append("\033[0;");
4009 sb
.append("m\033[38;2;");
4010 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
4012 sb
.append("m\033[48;2;");
4013 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
4016 return sb
.toString();
4020 * Create a SGR parameter sequence to reset to VT100 defaults.
4022 * @return the string to emit to an ANSI / ECMA-style terminal,
4025 private String
normal() {
4026 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
4030 * Create a SGR parameter sequence to reset to ECMA-48 default
4031 * foreground/background.
4033 * @return the string to emit to an ANSI / ECMA-style terminal,
4036 private String
defaultColor() {
4039 * Normal (neither bold nor faint).
4042 * Steady (not blinking).
4043 * Positive (not inverse).
4044 * Visible (not hidden).
4046 * Default foreground color.
4047 * Default background color.
4049 return "\033[0;22;23;24;25;27;28;29;39;49m";
4053 * Create a SGR parameter sequence to reset to defaults.
4055 * @param header if true, make the full header, otherwise just emit the
4056 * bare parameter e.g. "0;"
4057 * @return the string to emit to an ANSI / ECMA-style terminal,
4060 private String
normal(final boolean header
) {
4062 return "\033[0;37;40m";
4068 * Create a SGR parameter sequence for enabling the visible cursor.
4070 * @param on if true, turn on cursor
4071 * @return the string to emit to an ANSI / ECMA-style terminal
4073 private String
cursor(final boolean on
) {
4074 if (on
&& !cursorOn
) {
4078 if (!on
&& cursorOn
) {
4086 * Clear the entire screen. Because some terminals use back-color-erase,
4087 * set the color to white-on-black beforehand.
4089 * @return the string to emit to an ANSI / ECMA-style terminal
4091 private String
clearAll() {
4092 return "\033[0;37;40m\033[2J";
4096 * Clear the line from the cursor (inclusive) to the end of the screen.
4097 * Because some terminals use back-color-erase, set the color to
4098 * white-on-black beforehand.
4100 * @return the string to emit to an ANSI / ECMA-style terminal
4102 private String
clearRemainingLine() {
4103 return "\033[0;37;40m\033[K";
4107 * Move the cursor to (x, y).
4109 * @param x column coordinate. 0 is the left-most column.
4110 * @param y row coordinate. 0 is the top-most row.
4111 * @return the string to emit to an ANSI / ECMA-style terminal
4113 private String
gotoXY(final int x
, final int y
) {
4114 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
4118 * Tell (u)xterm that we want to receive mouse events based on "Any event
4119 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
4120 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
4122 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
4124 * Note that this also sets the alternate/primary screen buffer.
4126 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
4127 * mean "hide the mouse pointer." We have to use our own sequence to do
4128 * this because there is no standard in xterm for unilaterally hiding the
4129 * pointer all the time (regardless of typing).
4131 * @param on If true, enable mouse report and use the alternate screen
4132 * buffer. If false disable mouse reporting and use the primary screen
4134 * @return the string to emit to xterm
4136 private String
mouse(final boolean on
) {
4138 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
4140 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";