2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.backend
;
31 import java
.awt
.Graphics
;
32 import java
.awt
.Graphics2D
;
33 import java
.awt
.RenderingHints
;
34 import java
.awt
.image
.BufferedImage
;
35 import java
.io
.BufferedReader
;
36 import java
.io
.ByteArrayOutputStream
;
37 import java
.io
.FileDescriptor
;
38 import java
.io
.FileInputStream
;
39 import java
.io
.InputStream
;
40 import java
.io
.InputStreamReader
;
41 import java
.io
.IOException
;
42 import java
.io
.OutputStream
;
43 import java
.io
.OutputStreamWriter
;
44 import java
.io
.PrintWriter
;
45 import java
.io
.Reader
;
46 import java
.io
.UnsupportedEncodingException
;
47 import java
.util
.ArrayList
;
48 import java
.util
.Collections
;
49 import java
.util
.HashMap
;
50 import java
.util
.List
;
51 import javax
.imageio
.ImageIO
;
53 import jexer
.bits
.Cell
;
54 import jexer
.bits
.CellAttributes
;
55 import jexer
.bits
.Color
;
56 import jexer
.bits
.StringUtils
;
57 import jexer
.event
.TCommandEvent
;
58 import jexer
.event
.TInputEvent
;
59 import jexer
.event
.TKeypressEvent
;
60 import jexer
.event
.TMouseEvent
;
61 import jexer
.event
.TResizeEvent
;
62 import static jexer
.TCommand
.*;
63 import static jexer
.TKeypress
.*;
66 * This class reads keystrokes and mouse events and emits output to ANSI
67 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
69 public class ECMA48Terminal
extends LogicalScreen
70 implements TerminalReader
, Runnable
{
72 // ------------------------------------------------------------------------
73 // Constants --------------------------------------------------------------
74 // ------------------------------------------------------------------------
77 * States in the input parser.
79 private enum ParseState
{
90 * Available Jexer images support.
92 private enum JexerImageOption
{
99 // ------------------------------------------------------------------------
100 // Variables --------------------------------------------------------------
101 // ------------------------------------------------------------------------
104 * Emit debugging to stderr.
106 private boolean debugToStderr
= false;
109 * If true, emit T.416-style RGB colors for normal system colors. This
110 * is a) expensive in bandwidth, and b) potentially terrible looking for
113 private static boolean doRgbColor
= false;
116 * The session information.
118 private SessionInfo sessionInfo
;
121 * The event queue, filled up by a thread reading on input.
123 private List
<TInputEvent
> eventQueue
;
126 * If true, we want the reader thread to exit gracefully.
128 private boolean stopReaderThread
;
133 private Thread readerThread
;
136 * Parameters being collected. E.g. if the string is \033[1;3m, then
137 * params[0] will be 1 and params[1] will be 3.
139 private List
<String
> params
;
142 * Current parsing state.
144 private ParseState state
;
147 * The time we entered ESCAPE. If we get a bare escape without a code
148 * following it, this is used to return that bare escape.
150 private long escapeTime
;
153 * The time we last checked the window size. We try not to spawn stty
154 * more than once per second.
156 private long windowSizeTime
;
159 * true if mouse1 was down. Used to report mouse1 on the release event.
161 private boolean mouse1
;
164 * true if mouse2 was down. Used to report mouse2 on the release event.
166 private boolean mouse2
;
169 * true if mouse3 was down. Used to report mouse3 on the release event.
171 private boolean mouse3
;
174 * Cache the cursor visibility value so we only emit the sequence when we
177 private boolean cursorOn
= true;
180 * Cache the last window size to figure out if a TResizeEvent needs to be
183 private TResizeEvent windowResize
= null;
186 * If true, emit wide-char (CJK/Emoji) characters as sixel images.
188 private boolean wideCharImages
= true;
191 * Window width in pixels. Used for sixel support.
193 private int widthPixels
= 640;
196 * Window height in pixels. Used for sixel support.
198 private int heightPixels
= 400;
201 * If true, emit image data via sixel.
203 private boolean sixel
= true;
206 * If true, use a single shared palette for sixel.
208 private boolean sixelSharedPalette
= true;
211 * The sixel palette handler.
213 private SixelPalette palette
= null;
216 * The sixel post-rendered string cache.
218 private ImageCache sixelCache
= null;
221 * Number of colors in the sixel palette. Xterm 335 defines the max as
222 * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
225 private int sixelPaletteSize
= 1024;
228 * If true, emit image data via iTerm2 image protocol.
230 private boolean iterm2Images
= false;
233 * The iTerm2 post-rendered string cache.
235 private ImageCache iterm2Cache
= null;
238 * If not DISABLED, emit image data via Jexer image protocol if the
239 * terminal supports it.
241 private JexerImageOption jexerImageOption
= JexerImageOption
.JPG
;
244 * The Jexer post-rendered string cache.
246 private ImageCache jexerCache
= null;
249 * If true, then we changed System.in and need to change it back.
251 private boolean setRawMode
= false;
254 * If true, '?' was seen in terminal response.
256 private boolean decPrivateModeFlag
= false;
259 * The terminal's input. If an InputStream is not specified in the
260 * constructor, then this InputStreamReader will be bound to System.in
261 * with UTF-8 encoding.
263 private Reader input
;
266 * The terminal's raw InputStream. If an InputStream is not specified in
267 * the constructor, then this InputReader will be bound to System.in.
268 * This is used by run() to see if bytes are available() before calling
269 * (Reader)input.read().
271 private InputStream inputStream
;
274 * The terminal's output. If an OutputStream is not specified in the
275 * constructor, then this PrintWriter will be bound to System.out with
278 private PrintWriter output
;
281 * The listening object that run() wakes up on new input.
283 private Object listener
;
285 // Colors to map DOS colors to AWT colors.
286 private static java
.awt
.Color MYBLACK
;
287 private static java
.awt
.Color MYRED
;
288 private static java
.awt
.Color MYGREEN
;
289 private static java
.awt
.Color MYYELLOW
;
290 private static java
.awt
.Color MYBLUE
;
291 private static java
.awt
.Color MYMAGENTA
;
292 private static java
.awt
.Color MYCYAN
;
293 private static java
.awt
.Color MYWHITE
;
294 private static java
.awt
.Color MYBOLD_BLACK
;
295 private static java
.awt
.Color MYBOLD_RED
;
296 private static java
.awt
.Color MYBOLD_GREEN
;
297 private static java
.awt
.Color MYBOLD_YELLOW
;
298 private static java
.awt
.Color MYBOLD_BLUE
;
299 private static java
.awt
.Color MYBOLD_MAGENTA
;
300 private static java
.awt
.Color MYBOLD_CYAN
;
301 private static java
.awt
.Color MYBOLD_WHITE
;
304 * SixelPalette is used to manage the conversion of images between 24-bit
305 * RGB color and a palette of sixelPaletteSize colors.
307 private class SixelPalette
{
310 * Color palette for sixel output, sorted low to high.
312 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
315 * Map of color palette index for sixel output, from the order it was
316 * generated by makePalette() to rgbColors.
318 private int [] rgbSortedIndex
= new int[sixelPaletteSize
];
321 * The color palette, organized by hue, saturation, and luminance.
322 * This is used for a fast color match.
324 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
327 * Number of bits for hue.
329 private int hueBits
= -1;
332 * Number of bits for saturation.
334 private int satBits
= -1;
337 * Number of bits for luminance.
339 private int lumBits
= -1;
342 * Step size for hue bins.
344 private int hueStep
= -1;
347 * Step size for saturation bins.
349 private int satStep
= -1;
352 * Cached RGB to HSL result.
354 private int hsl
[] = new int[3];
357 * ColorIdx records a RGB color and its palette index.
359 private class ColorIdx
{
361 * The 24-bit RGB color.
366 * The palette index for this color.
371 * Public constructor.
373 * @param color the 24-bit RGB color
374 * @param index the palette index for this color
376 public ColorIdx(final int color
, final int index
) {
383 * Public constructor.
385 public SixelPalette() {
390 * Find the nearest match for a color in the palette.
392 * @param color the RGB color
393 * @return the index in rgbColors that is closest to color
395 public int matchColor(final int color
) {
400 * matchColor() is a critical performance bottleneck. To make it
401 * decent, we do the following:
403 * 1. Find the nearest two hues that bracket this color.
405 * 2. Find the nearest two saturations that bracket this color.
407 * 3. Iterate within these four bands of luminance values,
408 * returning the closest color by Euclidean distance.
410 * This strategy reduces the search space by about 97%.
412 int red
= (color
>>> 16) & 0xFF;
413 int green
= (color
>>> 8) & 0xFF;
414 int blue
= color
& 0xFF;
416 if (sixelPaletteSize
== 2) {
417 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
426 rgbToHsl(red
, green
, blue
, hsl
);
430 // System.err.printf("%d %d %d\n", hue, sat, lum);
432 double diff
= Double
.MAX_VALUE
;
435 int hue1
= hue
/ (360/hueStep
);
437 if (hue1
>= hslColors
.size() - 1) {
438 // Bracket pure red from above.
439 hue1
= hslColors
.size() - 1;
441 } else if (hue1
== 0) {
442 // Bracket pure red from below.
443 hue2
= hslColors
.size() - 1;
446 for (int hI
= hue1
; hI
!= -1;) {
447 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
450 } else if (hI
== hue2
) {
454 int sMin
= (sat
/ satStep
) - 1;
459 } else if (sMin
== sats
.size() - 1) {
464 assert (sMax
- sMin
== 1);
467 // int sMax = sats.size() - 1;
469 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
470 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
472 // True 3D colorspace match for the remaining values
473 for (ColorIdx c
: lums
) {
474 int rgbColor
= c
.color
;
476 int red2
= (rgbColor
>>> 16) & 0xFF;
477 int green2
= (rgbColor
>>> 8) & 0xFF;
478 int blue2
= rgbColor
& 0xFF;
479 newDiff
+= Math
.pow(red2
- red
, 2);
480 newDiff
+= Math
.pow(green2
- green
, 2);
481 newDiff
+= Math
.pow(blue2
- blue
, 2);
482 if (newDiff
< diff
) {
483 idx
= rgbSortedIndex
[c
.index
];
490 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
491 // Black is a closer match.
493 } else if ((((255 - red
) * (255 - red
)) +
494 ((255 - green
) * (255 - green
)) +
495 ((255 - blue
) * (255 - blue
))) < diff
) {
497 // White is a closer match.
498 idx
= sixelPaletteSize
- 1;
505 * Clamp an int value to [0, 255].
507 * @param x the int value
508 * @return an int between 0 and 255.
510 private int clamp(final int x
) {
521 * Dither an image to a sixelPaletteSize palette. The dithered
522 * image cells will contain indexes into the palette.
524 * @param image the image to dither
525 * @return the dithered image. Every pixel is an index into the
528 public BufferedImage
ditherImage(final BufferedImage image
) {
530 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
531 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
533 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
534 image
.getHeight(), null, 0, image
.getWidth());
535 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
536 rgbArray
, 0, image
.getWidth());
538 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
539 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
540 int oldPixel
= ditheredImage
.getRGB(imageX
,
542 int colorIdx
= matchColor(oldPixel
);
543 assert (colorIdx
>= 0);
544 assert (colorIdx
< sixelPaletteSize
);
545 int newPixel
= rgbColors
.get(colorIdx
);
546 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
548 int oldRed
= (oldPixel
>>> 16) & 0xFF;
549 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
550 int oldBlue
= oldPixel
& 0xFF;
552 int newRed
= (newPixel
>>> 16) & 0xFF;
553 int newGreen
= (newPixel
>>> 8) & 0xFF;
554 int newBlue
= newPixel
& 0xFF;
556 int redError
= (oldRed
- newRed
) / 16;
557 int greenError
= (oldGreen
- newGreen
) / 16;
558 int blueError
= (oldBlue
- newBlue
) / 16;
560 int red
, green
, blue
;
561 if (imageX
< image
.getWidth() - 1) {
562 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
563 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
564 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
565 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
567 green
= clamp(green
);
569 pXpY
= ((red
& 0xFF) << 16);
570 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
571 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
573 if (imageY
< image
.getHeight() - 1) {
574 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
576 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
577 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
578 blue
= ( pXpYp
& 0xFF) + blueError
;
580 green
= clamp(green
);
582 pXpYp
= ((red
& 0xFF) << 16);
583 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
584 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
586 } else if (imageY
< image
.getHeight() - 1) {
587 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
589 int pXYp
= ditheredImage
.getRGB(imageX
,
592 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
593 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
594 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
596 green
= clamp(green
);
598 pXmYp
= ((red
& 0xFF) << 16);
599 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
600 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
602 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
603 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
604 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
606 green
= clamp(green
);
608 pXYp
= ((red
& 0xFF) << 16);
609 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
610 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
612 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
613 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
615 return ditheredImage
;
619 * Convert an RGB color to HSL.
621 * @param red red color, between 0 and 255
622 * @param green green color, between 0 and 255
623 * @param blue blue color, between 0 and 255
624 * @param hsl the hsl color as [hue, saturation, luminance]
626 private void rgbToHsl(final int red
, final int green
,
627 final int blue
, final int [] hsl
) {
629 assert ((red
>= 0) && (red
<= 255));
630 assert ((green
>= 0) && (green
<= 255));
631 assert ((blue
>= 0) && (blue
<= 255));
633 double R
= red
/ 255.0;
634 double G
= green
/ 255.0;
635 double B
= blue
/ 255.0;
636 boolean Rmax
= false;
637 boolean Gmax
= false;
638 boolean Bmax
= false;
639 double min
= (R
< G ? R
: G
);
640 min
= (min
< B ? min
: B
);
642 if ((R
>= G
) && (R
>= B
)) {
645 } else if ((G
>= R
) && (G
>= B
)) {
648 } else if ((B
>= G
) && (B
>= R
)) {
653 double L
= (min
+ max
) / 2.0;
658 S
= (max
- min
) / (max
+ min
);
660 S
= (max
- min
) / (2.0 - max
- min
);
664 assert (Gmax
== false);
665 assert (Bmax
== false);
666 H
= (G
- B
) / (max
- min
);
668 assert (Rmax
== false);
669 assert (Bmax
== false);
670 H
= 2.0 + (B
- R
) / (max
- min
);
672 assert (Rmax
== false);
673 assert (Gmax
== false);
674 H
= 4.0 + (R
- G
) / (max
- min
);
679 hsl
[0] = (int) (H
* 60.0);
680 hsl
[1] = (int) (S
* 100.0);
681 hsl
[2] = (int) (L
* 100.0);
683 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
684 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
685 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
689 * Convert a HSL color to RGB.
691 * @param hue hue, between 0 and 359
692 * @param sat saturation, between 0 and 100
693 * @param lum luminance, between 0 and 100
694 * @return the rgb color as 0x00RRGGBB
696 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
697 assert ((hue
>= 0) && (hue
<= 360));
698 assert ((sat
>= 0) && (sat
<= 100));
699 assert ((lum
>= 0) && (lum
<= 100));
701 double S
= sat
/ 100.0;
702 double L
= lum
/ 100.0;
703 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
704 double Hp
= hue
/ 60.0;
705 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
712 } else if (Hp
<= 2.0) {
715 } else if (Hp
<= 3.0) {
718 } else if (Hp
<= 4.0) {
721 } else if (Hp
<= 5.0) {
724 } else if (Hp
<= 6.0) {
728 double m
= L
- (C
/ 2.0);
729 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
730 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
731 int blue
= (int) ((Bp
+ m
) * 255.0);
733 return (red
| green
| blue
);
737 * Create the sixel palette.
739 private void makePalette() {
740 // Generate the sixel palette. Because we have no idea at this
741 // layer which image(s) will be shown, we have to use a common
742 // palette with sixelPaletteSize colors for everything, and
743 // map the BufferedImage colors to their nearest neighbor in RGB
746 if (sixelPaletteSize
== 2) {
748 rgbColors
.add(0xFFFFFF);
749 rgbSortedIndex
[0] = 0;
750 rgbSortedIndex
[1] = 1;
754 // We build a palette using the Hue-Saturation-Luminence model,
755 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
756 // Luminance. We convert these colors to 24-bit RGB, sort them
757 // ascending, and steal the first index for pure black and the
758 // last for pure white. The 8-bit final palette favors bright
759 // colors, somewhere between pastel and classic television
760 // technicolor. 9- and 10-bit palettes are more uniform.
762 // Default at 256 colors.
767 assert (sixelPaletteSize
>= 256);
768 assert ((sixelPaletteSize
== 256)
769 || (sixelPaletteSize
== 512)
770 || (sixelPaletteSize
== 1024)
771 || (sixelPaletteSize
== 2048));
773 switch (sixelPaletteSize
) {
790 hueStep
= (int) (Math
.pow(2, hueBits
));
791 satStep
= (int) (100 / Math
.pow(2, satBits
));
792 // 1 bit for luminance: 40 and 70.
797 // 2 bits: 20, 40, 60, 80
802 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
808 // System.err.printf("<html><body>\n");
809 // Hue is evenly spaced around the wheel.
810 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
812 final boolean DEBUG
= false;
813 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
815 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
816 hue
+= (360/hueStep
)) {
818 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
819 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
820 hslColors
.add(satList
);
822 // Saturation is linearly spaced between pastel and pure.
823 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
825 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
826 satList
.add(lumList
);
828 // Luminance brackets the pure color, but leaning toward
830 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
832 System.err.printf("<font style = \"color:");
833 System.err.printf("hsl(%d, %d%%, %d%%)",
835 System.err.printf(";\">=</font>\n");
837 int rgbColor
= hslToRgb(hue
, sat
, lum
);
838 rgbColors
.add(rgbColor
);
839 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
840 rgbColors
.size() - 1);
841 lumList
.add(colorIdx
);
843 rawRgbList
.add(rgbColor
);
845 int red
= (rgbColor
>>> 16) & 0xFF;
846 int green
= (rgbColor
>>> 8) & 0xFF;
847 int blue
= rgbColor
& 0xFF;
848 int [] backToHsl
= new int[3];
849 rgbToHsl(red
, green
, blue
, backToHsl
);
850 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
851 hue
, backToHsl
[0], sat
, backToHsl
[1],
857 // System.err.printf("\n</body></html>\n");
859 assert (rgbColors
.size() == sixelPaletteSize
);
862 * We need to sort rgbColors, so that toSixel() can know where
863 * BLACK and WHITE are in it. But we also need to be able to
864 * find the sorted values using the old unsorted indexes. So we
865 * will sort it, put all the indexes into a HashMap, and then
866 * build rgbSortedIndex[].
868 Collections
.sort(rgbColors
);
869 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
870 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
871 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
872 rgbColorIndices
.put(rgbColors
.get(i
), i
);
874 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
875 int rawColor
= rawRgbList
.get(i
);
876 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
879 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
880 assert (rawRgbList
!= null);
881 int idx
= rgbSortedIndex
[i
];
882 int rgbColor
= rgbColors
.get(idx
);
883 if ((idx
!= 0) && (idx
!= sixelPaletteSize
- 1)) {
885 System.err.printf("%d %06x --> %d %06x\n",
886 i, rawRgbList.get(i), idx, rgbColors.get(idx));
888 assert (rgbColor
== rawRgbList
.get(i
));
893 // Set the dimmest color as true black, and the brightest as true
896 rgbColors
.set(sixelPaletteSize
- 1, 0xFFFFFF);
899 System.err.printf("<html><body>\n");
900 for (Integer rgb: rgbColors) {
901 System.err.printf("<font style = \"color:");
902 System.err.printf("#%06x", rgb);
903 System.err.printf(";\">=</font>\n");
905 System.err.printf("\n</body></html>\n");
911 * Emit the sixel palette.
913 * @param sb the StringBuilder to append to
914 * @param used array of booleans set to true for each color actually
915 * used in this cell, or null to emit the entire palette
916 * @return the string to emit to an ANSI / ECMA-style terminal
918 public String
emitPalette(final StringBuilder sb
,
919 final boolean [] used
) {
921 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
922 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
923 int rgbColor
= rgbColors
.get(i
);
924 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
925 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
926 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
927 ( rgbColor
& 0xFF) * 100 / 255));
930 return sb
.toString();
935 * ImageCache is a least-recently-used cache that hangs on to the
936 * post-rendered sixel or iTerm2 string for a particular set of cells.
938 private class ImageCache
{
941 * Maximum size of the cache.
943 private int maxSize
= 100;
946 * The entries stored in the cache.
948 private HashMap
<String
, CacheEntry
> cache
= null;
951 * CacheEntry is one entry in the cache.
953 private class CacheEntry
{
965 * The last time this entry was used.
967 public long millis
= 0;
970 * Public constructor.
972 * @param key the cache entry key
973 * @param data the cache entry data
975 public CacheEntry(final String key
, final String data
) {
978 this.millis
= System
.currentTimeMillis();
983 * Public constructor.
985 * @param maxSize the maximum size of the cache
987 public ImageCache(final int maxSize
) {
988 this.maxSize
= maxSize
;
989 cache
= new HashMap
<String
, CacheEntry
>();
993 * Make a unique key for a list of cells.
995 * @param cells the cells
998 private String
makeKey(final ArrayList
<Cell
> cells
) {
999 StringBuilder sb
= new StringBuilder();
1000 for (Cell cell
: cells
) {
1001 sb
.append(cell
.hashCode());
1003 return sb
.toString();
1007 * Get an entry from the cache.
1009 * @param cells the list of cells that are the cache key
1010 * @return the sixel string representing these cells, or null if this
1011 * list of cells is not in the cache
1013 public String
get(final ArrayList
<Cell
> cells
) {
1014 CacheEntry entry
= cache
.get(makeKey(cells
));
1015 if (entry
== null) {
1018 entry
.millis
= System
.currentTimeMillis();
1023 * Put an entry into the cache.
1025 * @param cells the list of cells that are the cache key
1026 * @param data the sixel string representing these cells
1028 public void put(final ArrayList
<Cell
> cells
, final String data
) {
1029 String key
= makeKey(cells
);
1031 // System.err.println("put() " + key + " size " + cache.size());
1033 assert (!cache
.containsKey(key
));
1035 assert (cache
.size() <= maxSize
);
1036 if (cache
.size() == maxSize
) {
1037 // Cache is at limit, evict oldest entry.
1038 long oldestTime
= Long
.MAX_VALUE
;
1039 String keyToRemove
= null;
1040 for (CacheEntry entry
: cache
.values()) {
1041 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
1042 keyToRemove
= entry
.key
;
1043 oldestTime
= entry
.millis
;
1047 System.err.println("put() remove key = " + keyToRemove +
1048 " size " + cache.size());
1050 assert (keyToRemove
!= null);
1051 cache
.remove(keyToRemove
);
1053 System.err.println("put() removed, size " + cache.size());
1056 assert (cache
.size() <= maxSize
);
1057 CacheEntry entry
= new CacheEntry(key
, data
);
1058 assert (key
.equals(entry
.key
));
1059 cache
.put(key
, entry
);
1061 System.err.println("put() added key " + key + " " +
1062 " size " + cache.size());
1068 // ------------------------------------------------------------------------
1069 // Constructors -----------------------------------------------------------
1070 // ------------------------------------------------------------------------
1073 * Constructor sets up state for getEvent(). If either windowWidth or
1074 * windowHeight are less than 1, the terminal is not resized.
1076 * @param listener the object this backend needs to wake up when new
1078 * @param input an InputStream connected to the remote user, or null for
1079 * System.in. If System.in is used, then on non-Windows systems it will
1080 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1081 * cooked mode. input is always converted to a Reader with UTF-8
1083 * @param output an OutputStream connected to the remote user, or null
1084 * for System.out. output is always converted to a Writer with UTF-8
1086 * @param windowWidth the number of text columns to start with
1087 * @param windowHeight the number of text rows to start with
1088 * @throws UnsupportedEncodingException if an exception is thrown when
1089 * creating the InputStreamReader
1091 public ECMA48Terminal(final Object listener
, final InputStream input
,
1092 final OutputStream output
, final int windowWidth
,
1093 final int windowHeight
) throws UnsupportedEncodingException
{
1095 this(listener
, input
, output
);
1097 // Send dtterm/xterm sequences, which will probably not work because
1098 // allowWindowOps is defaulted to false.
1099 if ((windowWidth
> 0) && (windowHeight
> 0)) {
1100 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1102 this.output
.write(resizeString
);
1103 this.output
.flush();
1108 * Constructor sets up state for getEvent().
1110 * @param listener the object this backend needs to wake up when new
1112 * @param input an InputStream connected to the remote user, or null for
1113 * System.in. If System.in is used, then on non-Windows systems it will
1114 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1115 * cooked mode. input is always converted to a Reader with UTF-8
1117 * @param output an OutputStream connected to the remote user, or null
1118 * for System.out. output is always converted to a Writer with UTF-8
1120 * @throws UnsupportedEncodingException if an exception is thrown when
1121 * creating the InputStreamReader
1123 public ECMA48Terminal(final Object listener
, final InputStream input
,
1124 final OutputStream output
) throws UnsupportedEncodingException
{
1130 stopReaderThread
= false;
1131 this.listener
= listener
;
1133 if (input
== null) {
1134 // inputStream = System.in;
1135 inputStream
= new FileInputStream(FileDescriptor
.in
);
1139 inputStream
= input
;
1141 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1143 if (input
instanceof SessionInfo
) {
1144 // This is a TelnetInputStream that exposes window size and
1145 // environment variables from the telnet layer.
1146 sessionInfo
= (SessionInfo
) input
;
1148 if (sessionInfo
== null) {
1149 if (input
== null) {
1150 // Reading right off the tty
1151 sessionInfo
= new TTYSessionInfo();
1153 sessionInfo
= new TSessionInfo();
1157 if (output
== null) {
1158 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1161 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1165 // Request Device Attributes
1166 this.output
.printf("\033[c");
1168 // Request xterm report window/cell dimensions in pixels
1169 this.output
.printf("%s", xtermReportPixelDimensions());
1171 // Enable mouse reporting and metaSendsEscape
1172 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1174 // Request xterm use the sixel settings we want
1175 this.output
.printf("%s", xtermSetSixelSettings());
1177 this.output
.flush();
1179 // Query the screen size
1180 sessionInfo
.queryWindowSize();
1181 setDimensions(sessionInfo
.getWindowWidth(),
1182 sessionInfo
.getWindowHeight());
1184 // Hang onto the window size
1185 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1186 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1190 // Spin up the input reader
1191 eventQueue
= new ArrayList
<TInputEvent
>();
1192 readerThread
= new Thread(this);
1193 readerThread
.start();
1196 this.output
.write(clearAll());
1197 this.output
.flush();
1201 * Constructor sets up state for getEvent().
1203 * @param listener the object this backend needs to wake up when new
1205 * @param input the InputStream underlying 'reader'. Its available()
1206 * method is used to determine if reader.read() will block or not.
1207 * @param reader a Reader connected to the remote user.
1208 * @param writer a PrintWriter connected to the remote user.
1209 * @param setRawMode if true, set System.in into raw mode with stty.
1210 * This should in general not be used. It is here solely for Demo3,
1211 * which uses System.in.
1212 * @throws IllegalArgumentException if input, reader, or writer are null.
1214 public ECMA48Terminal(final Object listener
, final InputStream input
,
1215 final Reader reader
, final PrintWriter writer
,
1216 final boolean setRawMode
) {
1218 if (input
== null) {
1219 throw new IllegalArgumentException("InputStream must be specified");
1221 if (reader
== null) {
1222 throw new IllegalArgumentException("Reader must be specified");
1224 if (writer
== null) {
1225 throw new IllegalArgumentException("Writer must be specified");
1231 stopReaderThread
= false;
1232 this.listener
= listener
;
1234 inputStream
= input
;
1235 this.input
= reader
;
1237 if (setRawMode
== true) {
1240 this.setRawMode
= setRawMode
;
1242 if (input
instanceof SessionInfo
) {
1243 // This is a TelnetInputStream that exposes window size and
1244 // environment variables from the telnet layer.
1245 sessionInfo
= (SessionInfo
) input
;
1247 if (sessionInfo
== null) {
1248 if (setRawMode
== true) {
1249 // Reading right off the tty
1250 sessionInfo
= new TTYSessionInfo();
1252 sessionInfo
= new TSessionInfo();
1256 this.output
= writer
;
1258 // Request Device Attributes
1259 this.output
.printf("\033[c");
1261 // Request xterm report window/cell dimensions in pixels
1262 this.output
.printf("%s", xtermReportPixelDimensions());
1264 // Enable mouse reporting and metaSendsEscape
1265 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1267 // Request xterm use the sixel settings we want
1268 this.output
.printf("%s", xtermSetSixelSettings());
1270 this.output
.flush();
1272 // Query the screen size
1273 sessionInfo
.queryWindowSize();
1274 setDimensions(sessionInfo
.getWindowWidth(),
1275 sessionInfo
.getWindowHeight());
1277 // Hang onto the window size
1278 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1279 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1283 // Spin up the input reader
1284 eventQueue
= new ArrayList
<TInputEvent
>();
1285 readerThread
= new Thread(this);
1286 readerThread
.start();
1289 this.output
.write(clearAll());
1290 this.output
.flush();
1294 * Constructor sets up state for getEvent().
1296 * @param listener the object this backend needs to wake up when new
1298 * @param input the InputStream underlying 'reader'. Its available()
1299 * method is used to determine if reader.read() will block or not.
1300 * @param reader a Reader connected to the remote user.
1301 * @param writer a PrintWriter connected to the remote user.
1302 * @throws IllegalArgumentException if input, reader, or writer are null.
1304 public ECMA48Terminal(final Object listener
, final InputStream input
,
1305 final Reader reader
, final PrintWriter writer
) {
1307 this(listener
, input
, reader
, writer
, false);
1310 // ------------------------------------------------------------------------
1311 // LogicalScreen ----------------------------------------------------------
1312 // ------------------------------------------------------------------------
1315 * Set the window title.
1317 * @param title the new title
1320 public void setTitle(final String title
) {
1321 output
.write(getSetTitleString(title
));
1326 * Push the logical screen to the physical device.
1329 public void flushPhysical() {
1330 StringBuilder sb
= new StringBuilder();
1334 && (cursorY
<= height
- 1)
1335 && (cursorX
<= width
- 1)
1338 sb
.append(cursor(true));
1339 sb
.append(gotoXY(cursorX
, cursorY
));
1341 sb
.append(cursor(false));
1344 output
.write(sb
.toString());
1349 * Resize the physical screen to match the logical screen dimensions.
1352 public void resizeToScreen() {
1353 // Send dtterm/xterm sequences, which will probably not work because
1354 // allowWindowOps is defaulted to false.
1355 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1357 this.output
.write(resizeString
);
1358 this.output
.flush();
1361 // ------------------------------------------------------------------------
1362 // TerminalReader ---------------------------------------------------------
1363 // ------------------------------------------------------------------------
1366 * Check if there are events in the queue.
1368 * @return if true, getEvents() has something to return to the backend
1370 public boolean hasEvents() {
1371 synchronized (eventQueue
) {
1372 return (eventQueue
.size() > 0);
1377 * Return any events in the IO queue.
1379 * @param queue list to append new events to
1381 public void getEvents(final List
<TInputEvent
> queue
) {
1382 synchronized (eventQueue
) {
1383 if (eventQueue
.size() > 0) {
1384 synchronized (queue
) {
1385 queue
.addAll(eventQueue
);
1393 * Restore terminal to normal state.
1395 public void closeTerminal() {
1397 // System.err.println("=== closeTerminal() ==="); System.err.flush();
1399 // Tell the reader thread to stop looking at input
1400 stopReaderThread
= true;
1402 readerThread
.join();
1403 } catch (InterruptedException e
) {
1404 if (debugToStderr
) {
1405 e
.printStackTrace();
1409 // Disable mouse reporting and show cursor. Defensive null check
1410 // here in case closeTerminal() is called twice.
1411 if (output
!= null) {
1412 output
.printf("%s%s%s%s", mouse(false), cursor(true),
1413 defaultColor(), xtermResetSixelSettings());
1420 // We don't close System.in/out
1422 // Shut down the streams, this should wake up the reader thread
1423 // and make it exit.
1424 if (input
!= null) {
1427 } catch (IOException e
) {
1432 if (output
!= null) {
1440 * Set listener to a different Object.
1442 * @param listener the new listening object that run() wakes up on new
1445 public void setListener(final Object listener
) {
1446 this.listener
= listener
;
1450 * Reload options from System properties.
1452 public void reloadOptions() {
1453 // Permit RGB colors only if externally requested.
1454 if (System
.getProperty("jexer.ECMA48.rgbColor",
1455 "false").equals("true")
1462 // Default to using images for full-width characters.
1463 if (System
.getProperty("jexer.ECMA48.wideCharImages",
1464 "true").equals("true")) {
1465 wideCharImages
= true;
1467 wideCharImages
= false;
1470 // Pull the system properties for sixel output.
1471 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1478 int paletteSize
= 1024;
1480 paletteSize
= Integer
.parseInt(System
.getProperty(
1481 "jexer.ECMA48.sixelPaletteSize", "1024"));
1482 switch (paletteSize
) {
1488 sixelPaletteSize
= paletteSize
;
1494 } catch (NumberFormatException e
) {
1499 if (System
.getProperty("jexer.ECMA48.sixelSharedPalette",
1500 "true").equals("false")) {
1501 sixelSharedPalette
= false;
1503 sixelSharedPalette
= true;
1506 // Default to not supporting iTerm2 images.
1507 if (System
.getProperty("jexer.ECMA48.iTerm2Images",
1508 "false").equals("true")) {
1509 iterm2Images
= true;
1511 iterm2Images
= false;
1514 // Default to using JPG Jexer images if terminal supports it.
1515 String jexerImageStr
= System
.getProperty("jexer.ECMA48.jexerImages",
1516 "jpg").toLowerCase();
1517 if (jexerImageStr
.equals("false")) {
1518 jexerImageOption
= JexerImageOption
.DISABLED
;
1519 } else if (jexerImageStr
.equals("jpg")) {
1520 jexerImageOption
= JexerImageOption
.JPG
;
1521 } else if (jexerImageStr
.equals("png")) {
1522 jexerImageOption
= JexerImageOption
.PNG
;
1523 } else if (jexerImageStr
.equals("rgb")) {
1524 jexerImageOption
= JexerImageOption
.RGB
;
1527 // Set custom colors
1528 setCustomSystemColors();
1531 // ------------------------------------------------------------------------
1532 // Runnable ---------------------------------------------------------------
1533 // ------------------------------------------------------------------------
1536 * Read function runs on a separate thread.
1539 boolean done
= false;
1540 // available() will often return > 1, so we need to read in chunks to
1542 char [] readBuffer
= new char[128];
1543 List
<TInputEvent
> events
= new ArrayList
<TInputEvent
>();
1545 while (!done
&& !stopReaderThread
) {
1547 // We assume that if inputStream has bytes available, then
1548 // input won't block on read().
1549 int n
= inputStream
.available();
1552 System.err.printf("inputStream.available(): %d\n", n);
1557 if (readBuffer
.length
< n
) {
1558 // The buffer wasn't big enough, make it huger
1559 readBuffer
= new char[readBuffer
.length
* 2];
1562 // System.err.printf("BEFORE read()\n"); System.err.flush();
1564 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1567 System.err.printf("AFTER read() %d\n", rc);
1575 for (int i
= 0; i
< rc
; i
++) {
1576 int ch
= readBuffer
[i
];
1577 processChar(events
, (char)ch
);
1579 getIdleEvents(events
);
1580 if (events
.size() > 0) {
1581 // Add to the queue for the backend thread to
1582 // be able to obtain.
1583 synchronized (eventQueue
) {
1584 eventQueue
.addAll(events
);
1586 if (listener
!= null) {
1587 synchronized (listener
) {
1588 listener
.notifyAll();
1595 getIdleEvents(events
);
1596 if (events
.size() > 0) {
1597 synchronized (eventQueue
) {
1598 eventQueue
.addAll(events
);
1600 if (listener
!= null) {
1601 synchronized (listener
) {
1602 listener
.notifyAll();
1608 if (output
.checkError()) {
1613 // Wait 20 millis for more data
1616 // System.err.println("end while loop"); System.err.flush();
1617 } catch (InterruptedException e
) {
1619 } catch (IOException e
) {
1620 e
.printStackTrace();
1623 } // while ((done == false) && (stopReaderThread == false))
1625 // Pass an event up to TApplication to tell it this Backend is done.
1626 synchronized (eventQueue
) {
1627 eventQueue
.add(new TCommandEvent(cmBackendDisconnect
));
1629 if (listener
!= null) {
1630 synchronized (listener
) {
1631 listener
.notifyAll();
1635 // System.err.println("*** run() exiting..."); System.err.flush();
1638 // ------------------------------------------------------------------------
1639 // ECMA48Terminal ---------------------------------------------------------
1640 // ------------------------------------------------------------------------
1643 * Get the width of a character cell in pixels.
1645 * @return the width in pixels of a character cell
1647 public int getTextWidth() {
1648 if (sessionInfo
.getWindowWidth() > 0) {
1649 return (widthPixels
/ sessionInfo
.getWindowWidth());
1655 * Get the height of a character cell in pixels.
1657 * @return the height in pixels of a character cell
1659 public int getTextHeight() {
1660 if (sessionInfo
.getWindowHeight() > 0) {
1661 return (heightPixels
/ sessionInfo
.getWindowHeight());
1667 * Getter for sessionInfo.
1669 * @return the SessionInfo
1671 public SessionInfo
getSessionInfo() {
1676 * Get the output writer.
1678 * @return the Writer
1680 public PrintWriter
getOutput() {
1685 * Call 'stty' to set cooked mode.
1687 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1689 private void sttyCooked() {
1694 * Call 'stty' to set raw mode.
1696 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1697 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1698 * -parenb cs8 min 1 < /dev/tty'
1700 private void sttyRaw() {
1705 * Call 'stty' to set raw or cooked mode.
1707 * @param mode if true, set raw mode, otherwise set cooked mode
1709 private void doStty(final boolean mode
) {
1710 String
[] cmdRaw
= {
1711 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1713 String
[] cmdCooked
= {
1714 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1719 process
= Runtime
.getRuntime().exec(cmdRaw
);
1721 process
= Runtime
.getRuntime().exec(cmdCooked
);
1723 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1724 String line
= in
.readLine();
1725 if ((line
!= null) && (line
.length() > 0)) {
1726 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1729 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1730 line
= err
.readLine();
1731 if ((line
!= null) && (line
.length() > 0)) {
1732 System
.err
.println("Error output from stty: " + line
);
1737 } catch (InterruptedException e
) {
1738 if (debugToStderr
) {
1739 e
.printStackTrace();
1743 int rc
= process
.exitValue();
1745 System
.err
.println("stty returned error code: " + rc
);
1747 } catch (IOException e
) {
1748 e
.printStackTrace();
1755 public void flush() {
1760 * Perform a somewhat-optimal rendering of a line.
1762 * @param y row coordinate. 0 is the top-most row.
1763 * @param sb StringBuilder to write escape sequences to
1764 * @param lastAttr cell attributes from the last call to flushLine
1766 private void flushLine(final int y
, final StringBuilder sb
,
1767 CellAttributes lastAttr
) {
1771 for (int x
= 0; x
< width
; x
++) {
1772 Cell lCell
= logical
[x
][y
];
1773 if (!lCell
.isBlank()) {
1777 // Push textEnd to first column beyond the text area
1781 // reallyCleared = true;
1783 boolean hasImage
= false;
1785 for (int x
= 0; x
< width
; x
++) {
1786 Cell lCell
= logical
[x
][y
];
1787 Cell pCell
= physical
[x
][y
];
1789 if (!lCell
.equals(pCell
) || reallyCleared
) {
1791 if (debugToStderr
) {
1792 System
.err
.printf("\n--\n");
1793 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1794 System
.err
.printf(" lCell: %s\n", lCell
);
1795 System
.err
.printf(" pCell: %s\n", pCell
);
1796 System
.err
.printf(" ==== \n");
1799 if (lastAttr
== null) {
1800 lastAttr
= new CellAttributes();
1801 sb
.append(normal());
1805 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1806 // Advancing at least one cell, or the first gotoXY
1807 sb
.append(gotoXY(x
, y
));
1810 assert (lastAttr
!= null);
1812 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1813 assert (lCell
.isBlank());
1815 for (int i
= x
; i
< width
; i
++) {
1816 assert (logical
[i
][y
].isBlank());
1817 // Physical is always updated
1818 physical
[i
][y
].reset();
1821 // Clear remaining line
1822 sb
.append(clearRemainingLine());
1827 // Image cell: bypass the rest of the loop, it is not
1829 if ((wideCharImages
&& lCell
.isImage())
1832 && (lCell
.getWidth() == Cell
.Width
.SINGLE
))
1836 // Save the last rendered cell
1839 // Physical is always updated
1840 physical
[x
][y
].setTo(lCell
);
1844 assert ((wideCharImages
&& !lCell
.isImage())
1846 && (!lCell
.isImage()
1848 && (lCell
.getWidth() != Cell
.Width
.SINGLE
)))));
1850 if (!wideCharImages
&& (lCell
.getWidth() == Cell
.Width
.RIGHT
)) {
1856 sb
.append(gotoXY(x
, y
));
1859 // Now emit only the modified attributes
1860 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1861 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1863 && (lCell
.isBold() == lastAttr
.isBold())
1864 && (lCell
.isReverse() == lastAttr
.isReverse())
1865 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1866 && (lCell
.isBlink() == lastAttr
.isBlink())
1868 // Both colors changed, attributes the same
1869 sb
.append(color(lCell
.isBold(),
1870 lCell
.getForeColor(), lCell
.getBackColor()));
1872 if (debugToStderr
) {
1873 System
.err
.printf("1 Change only fore/back colors\n");
1876 } else if (lCell
.isRGB()
1877 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1878 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1879 && (lCell
.isBold() == lastAttr
.isBold())
1880 && (lCell
.isReverse() == lastAttr
.isReverse())
1881 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1882 && (lCell
.isBlink() == lastAttr
.isBlink())
1884 // Both colors changed, attributes the same
1885 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1886 lCell
.getBackColorRGB()));
1888 if (debugToStderr
) {
1889 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1891 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1892 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1894 && (lCell
.isBold() != lastAttr
.isBold())
1895 && (lCell
.isReverse() != lastAttr
.isReverse())
1896 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1897 && (lCell
.isBlink() != lastAttr
.isBlink())
1899 // Everything is different
1900 sb
.append(color(lCell
.getForeColor(),
1901 lCell
.getBackColor(),
1902 lCell
.isBold(), lCell
.isReverse(),
1904 lCell
.isUnderline()));
1906 if (debugToStderr
) {
1907 System
.err
.printf("2 Set all attributes\n");
1909 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1910 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1912 && (lCell
.isBold() == lastAttr
.isBold())
1913 && (lCell
.isReverse() == lastAttr
.isReverse())
1914 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1915 && (lCell
.isBlink() == lastAttr
.isBlink())
1918 // Attributes same, foreColor different
1919 sb
.append(color(lCell
.isBold(),
1920 lCell
.getForeColor(), true));
1922 if (debugToStderr
) {
1923 System
.err
.printf("3 Change foreColor\n");
1925 } else if (lCell
.isRGB()
1926 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1927 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1928 && (lCell
.getForeColorRGB() >= 0)
1929 && (lCell
.getBackColorRGB() >= 0)
1930 && (lCell
.isBold() == lastAttr
.isBold())
1931 && (lCell
.isReverse() == lastAttr
.isReverse())
1932 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1933 && (lCell
.isBlink() == lastAttr
.isBlink())
1935 // Attributes same, foreColor different
1936 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1938 if (debugToStderr
) {
1939 System
.err
.printf("3 Change foreColor (RGB)\n");
1941 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1942 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1944 && (lCell
.isBold() == lastAttr
.isBold())
1945 && (lCell
.isReverse() == lastAttr
.isReverse())
1946 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1947 && (lCell
.isBlink() == lastAttr
.isBlink())
1949 // Attributes same, backColor different
1950 sb
.append(color(lCell
.isBold(),
1951 lCell
.getBackColor(), false));
1953 if (debugToStderr
) {
1954 System
.err
.printf("4 Change backColor\n");
1956 } else if (lCell
.isRGB()
1957 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1958 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1959 && (lCell
.isBold() == lastAttr
.isBold())
1960 && (lCell
.isReverse() == lastAttr
.isReverse())
1961 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1962 && (lCell
.isBlink() == lastAttr
.isBlink())
1964 // Attributes same, foreColor different
1965 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1967 if (debugToStderr
) {
1968 System
.err
.printf("4 Change backColor (RGB)\n");
1970 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1971 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1972 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1973 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1974 && (lCell
.isBold() == lastAttr
.isBold())
1975 && (lCell
.isReverse() == lastAttr
.isReverse())
1976 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1977 && (lCell
.isBlink() == lastAttr
.isBlink())
1980 // All attributes the same, just print the char
1983 if (debugToStderr
) {
1984 System
.err
.printf("5 Only emit character\n");
1987 // Just reset everything again
1988 if (!lCell
.isRGB()) {
1989 sb
.append(color(lCell
.getForeColor(),
1990 lCell
.getBackColor(),
1994 lCell
.isUnderline()));
1996 if (debugToStderr
) {
1997 System
.err
.printf("6 Change all attributes\n");
2000 sb
.append(colorRGB(lCell
.getForeColorRGB(),
2001 lCell
.getBackColorRGB(),
2005 lCell
.isUnderline()));
2006 if (debugToStderr
) {
2007 System
.err
.printf("6 Change all attributes (RGB)\n");
2012 // Emit the character
2014 // Don't emit the right-half of full-width chars.
2016 && (lCell
.getWidth() != Cell
.Width
.RIGHT
))
2018 sb
.append(Character
.toChars(lCell
.getChar()));
2021 // Save the last rendered cell
2023 lastAttr
.setTo(lCell
);
2025 // Physical is always updated
2026 physical
[x
][y
].setTo(lCell
);
2028 } // if (!lCell.equals(pCell) || (reallyCleared == true))
2030 } // for (int x = 0; x < width; x++)
2034 * Render the screen to a string that can be emitted to something that
2035 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
2037 * @param sb StringBuilder to write escape sequences to
2038 * @return escape sequences string that provides the updates to the
2041 private String
flushString(final StringBuilder sb
) {
2042 CellAttributes attr
= null;
2044 if (reallyCleared
) {
2045 attr
= new CellAttributes();
2046 sb
.append(clearAll());
2050 * For images support, draw all of the image output first, and then
2051 * draw everything else afterwards. This works OK, but performance
2052 * is still a drag on larger pictures.
2054 for (int y
= 0; y
< height
; y
++) {
2055 for (int x
= 0; x
< width
; x
++) {
2056 // If physical had non-image data that is now image data, the
2057 // entire row must be redrawn.
2058 Cell lCell
= logical
[x
][y
];
2059 Cell pCell
= physical
[x
][y
];
2060 if (lCell
.isImage() && !pCell
.isImage()) {
2066 for (int y
= 0; y
< height
; y
++) {
2067 for (int x
= 0; x
< width
; x
++) {
2068 Cell lCell
= logical
[x
][y
];
2069 Cell pCell
= physical
[x
][y
];
2071 if (!lCell
.isImage()
2073 && (lCell
.getWidth() != Cell
.Width
.SINGLE
))
2080 while ((right
< width
)
2081 && (logical
[right
][y
].isImage())
2082 && (!logical
[right
][y
].equals(physical
[right
][y
])
2087 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
2088 for (int i
= 0; i
< (right
- x
); i
++) {
2089 assert (logical
[x
+ i
][y
].isImage());
2090 cellsToDraw
.add(logical
[x
+ i
][y
]);
2092 // Physical is always updated.
2093 physical
[x
+ i
][y
].setTo(lCell
);
2095 if (cellsToDraw
.size() > 0) {
2097 sb
.append(toIterm2Image(x
, y
, cellsToDraw
));
2098 } else if (jexerImageOption
!= JexerImageOption
.DISABLED
) {
2099 sb
.append(toJexerImage(x
, y
, cellsToDraw
));
2101 sb
.append(toSixel(x
, y
, cellsToDraw
));
2109 // Draw the text part now.
2110 for (int y
= 0; y
< height
; y
++) {
2111 flushLine(y
, sb
, attr
);
2114 reallyCleared
= false;
2116 String result
= sb
.toString();
2117 if (debugToStderr
) {
2118 System
.err
.printf("flushString(): %s\n", result
);
2124 * Reset keyboard/mouse input parser.
2126 private void resetParser() {
2127 state
= ParseState
.GROUND
;
2128 params
= new ArrayList
<String
>();
2131 decPrivateModeFlag
= false;
2135 * Produce a control character or one of the special ones (ENTER, TAB,
2138 * @param ch Unicode code point
2139 * @param alt if true, set alt on the TKeypress
2140 * @return one TKeypress event, either a control character (e.g. isKey ==
2141 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
2144 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
2145 // System.err.printf("controlChar: %02x\n", ch);
2149 // Carriage return --> ENTER
2150 return new TKeypressEvent(kbEnter
, alt
, false, false);
2152 // Linefeed --> ENTER
2153 return new TKeypressEvent(kbEnter
, alt
, false, false);
2156 return new TKeypressEvent(kbEsc
, alt
, false, false);
2159 return new TKeypressEvent(kbTab
, alt
, false, false);
2161 // Make all other control characters come back as the alphabetic
2162 // character with the ctrl field set. So SOH would be 'A' +
2164 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
2170 * Produce special key from CSI Pn ; Pm ; ... ~
2172 * @return one KEYPRESS event representing a special key
2174 private TInputEvent
csiFnKey() {
2176 if (params
.size() > 0) {
2177 key
= Integer
.parseInt(params
.get(0));
2179 boolean alt
= false;
2180 boolean ctrl
= false;
2181 boolean shift
= false;
2182 if (params
.size() > 1) {
2183 shift
= csiIsShift(params
.get(1));
2184 alt
= csiIsAlt(params
.get(1));
2185 ctrl
= csiIsCtrl(params
.get(1));
2190 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2192 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2194 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2196 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2198 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2200 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2202 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2204 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2206 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2208 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2210 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2212 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2214 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2216 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2224 * Produce mouse events based on "Any event tracking" and UTF-8
2226 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2228 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2230 private TInputEvent
parseMouse() {
2231 int buttons
= params
.get(0).charAt(0) - 32;
2232 int x
= params
.get(0).charAt(1) - 32 - 1;
2233 int y
= params
.get(0).charAt(2) - 32 - 1;
2235 // Clamp X and Y to the physical screen coordinates.
2236 if (x
>= windowResize
.getWidth()) {
2237 x
= windowResize
.getWidth() - 1;
2239 if (y
>= windowResize
.getHeight()) {
2240 y
= windowResize
.getHeight() - 1;
2243 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2244 boolean eventMouse1
= false;
2245 boolean eventMouse2
= false;
2246 boolean eventMouse3
= false;
2247 boolean eventMouseWheelUp
= false;
2248 boolean eventMouseWheelDown
= false;
2249 boolean eventAlt
= false;
2250 boolean eventCtrl
= false;
2251 boolean eventShift
= false;
2253 // System.err.printf("buttons: %04x\r\n", buttons);
2255 switch (buttons
& 0xE3) {
2270 if (!mouse1
&& !mouse2
&& !mouse3
) {
2271 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2273 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2290 // Dragging with mouse1 down
2293 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2297 // Dragging with mouse2 down
2300 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2304 // Dragging with mouse3 down
2307 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2311 // Dragging with mouse2 down after wheelUp
2314 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2318 // Dragging with mouse2 down after wheelDown
2321 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2325 eventMouseWheelUp
= true;
2329 eventMouseWheelDown
= true;
2333 // Unknown, just make it motion
2334 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2338 if ((buttons
& 0x04) != 0) {
2341 if ((buttons
& 0x08) != 0) {
2344 if ((buttons
& 0x10) != 0) {
2348 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2349 eventMouse1
, eventMouse2
, eventMouse3
,
2350 eventMouseWheelUp
, eventMouseWheelDown
,
2351 eventAlt
, eventCtrl
, eventShift
);
2355 * Produce mouse events based on "Any event tracking" and SGR
2357 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2359 * @param release if true, this was a release ('m')
2360 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2362 private TInputEvent
parseMouseSGR(final boolean release
) {
2363 // SGR extended coordinates - mode 1006
2364 if (params
.size() < 3) {
2365 // Invalid position, bail out.
2368 int buttons
= Integer
.parseInt(params
.get(0));
2369 int x
= Integer
.parseInt(params
.get(1)) - 1;
2370 int y
= Integer
.parseInt(params
.get(2)) - 1;
2372 // Clamp X and Y to the physical screen coordinates.
2373 if (x
>= windowResize
.getWidth()) {
2374 x
= windowResize
.getWidth() - 1;
2376 if (y
>= windowResize
.getHeight()) {
2377 y
= windowResize
.getHeight() - 1;
2380 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2381 boolean eventMouse1
= false;
2382 boolean eventMouse2
= false;
2383 boolean eventMouse3
= false;
2384 boolean eventMouseWheelUp
= false;
2385 boolean eventMouseWheelDown
= false;
2386 boolean eventAlt
= false;
2387 boolean eventCtrl
= false;
2388 boolean eventShift
= false;
2391 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2394 switch (buttons
& 0xE3) {
2405 // Motion only, no buttons down
2406 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2410 // Dragging with mouse1 down
2412 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2416 // Dragging with mouse2 down
2418 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2422 // Dragging with mouse3 down
2424 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2428 // Dragging with mouse2 down after wheelUp
2430 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2434 // Dragging with mouse2 down after wheelDown
2436 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2440 eventMouseWheelUp
= true;
2444 eventMouseWheelDown
= true;
2448 // Unknown, bail out
2452 if ((buttons
& 0x04) != 0) {
2455 if ((buttons
& 0x08) != 0) {
2458 if ((buttons
& 0x10) != 0) {
2462 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2463 eventMouse1
, eventMouse2
, eventMouse3
,
2464 eventMouseWheelUp
, eventMouseWheelDown
,
2465 eventAlt
, eventCtrl
, eventShift
);
2469 * Return any events in the IO queue due to timeout.
2471 * @param queue list to append new events to
2473 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2474 long nowTime
= System
.currentTimeMillis();
2476 // Check for new window size
2477 long windowSizeDelay
= nowTime
- windowSizeTime
;
2478 if (windowSizeDelay
> 1000) {
2479 int oldTextWidth
= getTextWidth();
2480 int oldTextHeight
= getTextHeight();
2482 sessionInfo
.queryWindowSize();
2483 int newWidth
= sessionInfo
.getWindowWidth();
2484 int newHeight
= sessionInfo
.getWindowHeight();
2486 if ((newWidth
!= windowResize
.getWidth())
2487 || (newHeight
!= windowResize
.getHeight())
2490 // Request xterm report window dimensions in pixels again.
2491 // Between now and then, ensure that the reported text cell
2492 // size is the same by setting widthPixels and heightPixels
2493 // to match the new dimensions.
2494 widthPixels
= oldTextWidth
* newWidth
;
2495 heightPixels
= oldTextHeight
* newHeight
;
2497 if (debugToStderr
) {
2498 System
.err
.println("Screen size changed, old size " +
2500 System
.err
.println(" new size " +
2501 newWidth
+ " x " + newHeight
);
2502 System
.err
.println(" old pixels " +
2503 oldTextWidth
+ " x " + oldTextHeight
);
2504 System
.err
.println(" new pixels " +
2505 getTextWidth() + " x " + getTextHeight());
2508 this.output
.printf("%s", xtermReportPixelDimensions());
2509 this.output
.flush();
2511 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2512 newWidth
, newHeight
);
2513 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2514 newWidth
, newHeight
);
2517 windowSizeTime
= nowTime
;
2520 // ESCDELAY type timeout
2521 if (state
== ParseState
.ESCAPE
) {
2522 long escDelay
= nowTime
- escapeTime
;
2523 if (escDelay
> 100) {
2524 // After 0.1 seconds, assume a true escape character
2525 queue
.add(controlChar((char)0x1B, false));
2532 * Returns true if the CSI parameter for a keyboard command means that
2535 private boolean csiIsShift(final String x
) {
2547 * Returns true if the CSI parameter for a keyboard command means that
2550 private boolean csiIsAlt(final String x
) {
2562 * Returns true if the CSI parameter for a keyboard command means that
2565 private boolean csiIsCtrl(final String x
) {
2577 * Parses the next character of input to see if an InputEvent is
2580 * @param events list to append new events to
2581 * @param ch Unicode code point
2583 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2585 // ESCDELAY type timeout
2586 long nowTime
= System
.currentTimeMillis();
2587 if (state
== ParseState
.ESCAPE
) {
2588 long escDelay
= nowTime
- escapeTime
;
2589 if (escDelay
> 250) {
2590 // After 0.25 seconds, assume a true escape character
2591 events
.add(controlChar((char)0x1B, false));
2597 boolean ctrl
= false;
2598 boolean alt
= false;
2599 boolean shift
= false;
2601 // System.err.printf("state: %s ch %c\r\n", state, ch);
2607 state
= ParseState
.ESCAPE
;
2608 escapeTime
= nowTime
;
2613 // Control character
2614 events
.add(controlChar(ch
, false));
2621 events
.add(new TKeypressEvent(false, 0, ch
,
2622 false, false, false));
2631 // ALT-Control character
2632 events
.add(controlChar(ch
, true));
2638 // This will be one of the function keys
2639 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2643 // '[' goes to CSI_ENTRY
2645 state
= ParseState
.CSI_ENTRY
;
2649 // Everything else is assumed to be Alt-keystroke
2650 if ((ch
>= 'A') && (ch
<= 'Z')) {
2654 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2658 case ESCAPE_INTERMEDIATE
:
2659 if ((ch
>= 'P') && (ch
<= 'S')) {
2663 events
.add(new TKeypressEvent(kbF1
));
2666 events
.add(new TKeypressEvent(kbF2
));
2669 events
.add(new TKeypressEvent(kbF3
));
2672 events
.add(new TKeypressEvent(kbF4
));
2681 // Unknown keystroke, ignore
2686 // Numbers - parameter values
2687 if ((ch
>= '0') && (ch
<= '9')) {
2688 params
.set(params
.size() - 1,
2689 params
.get(params
.size() - 1) + ch
);
2690 state
= ParseState
.CSI_PARAM
;
2693 // Parameter separator
2699 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2703 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2708 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2713 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2718 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2723 events
.add(new TKeypressEvent(kbHome
));
2728 events
.add(new TKeypressEvent(kbEnd
));
2732 // CBT - Cursor backward X tab stops (default 1)
2733 events
.add(new TKeypressEvent(kbBackTab
));
2738 state
= ParseState
.MOUSE
;
2741 // Mouse position, SGR (1006) coordinates
2742 state
= ParseState
.MOUSE_SGR
;
2745 // DEC private mode flag
2746 decPrivateModeFlag
= true;
2753 // Unknown keystroke, ignore
2758 // Numbers - parameter values
2759 if ((ch
>= '0') && (ch
<= '9')) {
2760 params
.set(params
.size() - 1,
2761 params
.get(params
.size() - 1) + ch
);
2764 // Parameter separator
2772 // Generate a mouse press event
2773 TInputEvent event
= parseMouseSGR(false);
2774 if (event
!= null) {
2780 // Generate a mouse release event
2781 event
= parseMouseSGR(true);
2782 if (event
!= null) {
2791 // Unknown keystroke, ignore
2796 // Numbers - parameter values
2797 if ((ch
>= '0') && (ch
<= '9')) {
2798 params
.set(params
.size() - 1,
2799 params
.get(params
.size() - 1) + ch
);
2800 state
= ParseState
.CSI_PARAM
;
2803 // Parameter separator
2810 events
.add(csiFnKey());
2815 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2819 if (params
.size() > 1) {
2820 shift
= csiIsShift(params
.get(1));
2821 alt
= csiIsAlt(params
.get(1));
2822 ctrl
= csiIsCtrl(params
.get(1));
2824 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2829 if (params
.size() > 1) {
2830 shift
= csiIsShift(params
.get(1));
2831 alt
= csiIsAlt(params
.get(1));
2832 ctrl
= csiIsCtrl(params
.get(1));
2834 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2839 if (params
.size() > 1) {
2840 shift
= csiIsShift(params
.get(1));
2841 alt
= csiIsAlt(params
.get(1));
2842 ctrl
= csiIsCtrl(params
.get(1));
2844 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2849 if (params
.size() > 1) {
2850 shift
= csiIsShift(params
.get(1));
2851 alt
= csiIsAlt(params
.get(1));
2852 ctrl
= csiIsCtrl(params
.get(1));
2854 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2859 if (params
.size() > 1) {
2860 shift
= csiIsShift(params
.get(1));
2861 alt
= csiIsAlt(params
.get(1));
2862 ctrl
= csiIsCtrl(params
.get(1));
2864 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2869 if (params
.size() > 1) {
2870 shift
= csiIsShift(params
.get(1));
2871 alt
= csiIsAlt(params
.get(1));
2872 ctrl
= csiIsCtrl(params
.get(1));
2874 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2878 // Device Attributes
2879 if (decPrivateModeFlag
== false) {
2882 boolean reportsJexerImages
= false;
2883 boolean reportsIterm2Images
= false;
2884 for (String x
: params
) {
2885 if (x
.equals("4")) {
2886 // Terminal reports sixel support
2887 if (debugToStderr
) {
2888 System
.err
.println("Device Attributes: sixel");
2891 if (x
.equals("444")) {
2892 // Terminal reports Jexer images support
2893 if (debugToStderr
) {
2894 System
.err
.println("Device Attributes: Jexer images");
2896 reportsJexerImages
= true;
2898 if (x
.equals("1337")) {
2899 // Terminal reports iTerm2 images support
2900 if (debugToStderr
) {
2901 System
.err
.println("Device Attributes: iTerm2 images");
2903 reportsIterm2Images
= true;
2906 if (reportsJexerImages
== false) {
2907 // Terminal does not support Jexer images, disable
2909 jexerImageOption
= JexerImageOption
.DISABLED
;
2911 if (reportsIterm2Images
== false) {
2912 // Terminal does not support iTerm2 images, disable
2914 iterm2Images
= false;
2920 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2921 if (debugToStderr
) {
2922 System
.err
.printf("windowOp pixels: " +
2923 "height %s width %s\n",
2924 params
.get(1), params
.get(2));
2927 widthPixels
= Integer
.parseInt(params
.get(2));
2928 heightPixels
= Integer
.parseInt(params
.get(1));
2929 } catch (NumberFormatException e
) {
2930 if (debugToStderr
) {
2931 e
.printStackTrace();
2934 if (widthPixels
<= 0) {
2937 if (heightPixels
<= 0) {
2941 if ((params
.size() > 2) && (params
.get(0).equals("6"))) {
2942 if (debugToStderr
) {
2943 System
.err
.printf("windowOp text cell pixels: " +
2944 "height %s width %s\n",
2945 params
.get(1), params
.get(2));
2948 widthPixels
= width
* Integer
.parseInt(params
.get(2));
2949 heightPixels
= height
* Integer
.parseInt(params
.get(1));
2950 } catch (NumberFormatException e
) {
2951 if (debugToStderr
) {
2952 e
.printStackTrace();
2955 if (widthPixels
<= 0) {
2958 if (heightPixels
<= 0) {
2969 // Unknown keystroke, ignore
2974 params
.set(0, params
.get(params
.size() - 1) + ch
);
2975 if (params
.get(0).length() == 3) {
2976 // We have enough to generate a mouse event
2977 events
.add(parseMouse());
2986 // This "should" be impossible to reach
2991 * Request (u)xterm to use the sixel settings we need:
2993 * - enable sixel scrolling
2995 * - disable private color registers (so that we can use one common
2996 * palette) if sixelSharedPalette is set
2998 * @return the string to emit to xterm
3000 private String
xtermSetSixelSettings() {
3001 if (sixelSharedPalette
== true) {
3002 return "\033[?80h\033[?1070l";
3004 return "\033[?80h\033[?1070h";
3009 * Restore (u)xterm its default sixel settings:
3011 * - enable sixel scrolling
3013 * - enable private color registers
3015 * @return the string to emit to xterm
3017 private String
xtermResetSixelSettings() {
3018 return "\033[?80h\033[?1070h";
3022 * Request (u)xterm to report the current window and cell size dimensions
3025 * @return the string to emit to xterm
3027 private String
xtermReportPixelDimensions() {
3028 // We will ask for both window and text cell dimensions, and
3029 // hopefully one of them will work.
3030 return "\033[14t\033[16t";
3034 * Tell (u)xterm that we want alt- keystrokes to send escape + character
3035 * rather than set the 8th bit. Anyone who wants UTF8 should want this
3038 * @param on if true, enable metaSendsEscape
3039 * @return the string to emit to xterm
3041 private String
xtermMetaSendsEscape(final boolean on
) {
3043 return "\033[?1036h\033[?1034l";
3045 return "\033[?1036l";
3049 * Create an xterm OSC sequence to change the window title.
3051 * @param title the new title
3052 * @return the string to emit to xterm
3054 private String
getSetTitleString(final String title
) {
3055 return "\033]2;" + title
+ "\007";
3058 // ------------------------------------------------------------------------
3059 // Sixel output support ---------------------------------------------------
3060 // ------------------------------------------------------------------------
3063 * Get the number of colors in the sixel palette.
3065 * @return the palette size
3067 public int getSixelPaletteSize() {
3068 return sixelPaletteSize
;
3072 * Set the number of colors in the sixel palette.
3074 * @param paletteSize the new palette size
3076 public void setSixelPaletteSize(final int paletteSize
) {
3077 if (paletteSize
== sixelPaletteSize
) {
3081 switch (paletteSize
) {
3089 throw new IllegalArgumentException("Unsupported sixel palette " +
3090 " size: " + paletteSize
);
3093 // Don't step on the screen refresh thread.
3094 synchronized (this) {
3095 sixelPaletteSize
= paletteSize
;
3103 * Start a sixel string for display one row's worth of bitmap data.
3105 * @param x column coordinate. 0 is the left-most column.
3106 * @param y row coordinate. 0 is the top-most row.
3107 * @return the string to emit to an ANSI / ECMA-style terminal
3109 private String
startSixel(final int x
, final int y
) {
3110 StringBuilder sb
= new StringBuilder();
3112 assert (sixel
== true);
3115 sb
.append(gotoXY(x
, y
));
3118 sb
.append("\033Pq");
3120 if (palette
== null) {
3121 palette
= new SixelPalette();
3122 if (sixelSharedPalette
== true) {
3123 palette
.emitPalette(sb
, null);
3127 return sb
.toString();
3131 * End a sixel string for display one row's worth of bitmap data.
3133 * @return the string to emit to an ANSI / ECMA-style terminal
3135 private String
endSixel() {
3136 assert (sixel
== true);
3143 * Create a sixel string representing a row of several cells containing
3146 * @param x column coordinate. 0 is the left-most column.
3147 * @param y row coordinate. 0 is the top-most row.
3148 * @param cells the cells containing the bitmap data
3149 * @return the string to emit to an ANSI / ECMA-style terminal
3151 private String
toSixel(final int x
, final int y
,
3152 final ArrayList
<Cell
> cells
) {
3154 StringBuilder sb
= new StringBuilder();
3156 assert (cells
!= null);
3157 assert (cells
.size() > 0);
3158 assert (cells
.get(0).getImage() != null);
3160 if (sixel
== false) {
3161 sb
.append(normal());
3162 sb
.append(gotoXY(x
, y
));
3163 for (int i
= 0; i
< cells
.size(); i
++) {
3166 return sb
.toString();
3169 if (y
== height
- 1) {
3170 // We are on the bottom row. If scrolling mode is enabled
3171 // (default), then VT320/xterm will scroll the entire screen if
3172 // we draw any pixels here. Do not draw the image, bail out
3174 sb
.append(normal());
3175 sb
.append(gotoXY(x
, y
));
3176 for (int j
= 0; j
< cells
.size(); j
++) {
3179 return sb
.toString();
3182 if (sixelCache
== null) {
3183 sixelCache
= new ImageCache(height
* 10);
3186 // Save and get rows to/from the cache that do NOT have inverted
3188 boolean saveInCache
= true;
3189 for (Cell cell
: cells
) {
3190 if (cell
.isInvertedImage()) {
3191 saveInCache
= false;
3195 String cachedResult
= sixelCache
.get(cells
);
3196 if (cachedResult
!= null) {
3197 // System.err.println("CACHE HIT");
3198 sb
.append(startSixel(x
, y
));
3199 sb
.append(cachedResult
);
3200 sb
.append(endSixel());
3201 return sb
.toString();
3203 // System.err.println("CACHE MISS");
3206 BufferedImage image
= cellsToImage(cells
);
3207 int fullHeight
= image
.getHeight();
3209 // Dither the image. It is ok to lose the original here.
3210 if (palette
== null) {
3211 palette
= new SixelPalette();
3212 if (sixelSharedPalette
== true) {
3213 palette
.emitPalette(sb
, null);
3216 image
= palette
.ditherImage(image
);
3218 // Collect the raster information
3219 int rasterHeight
= 0;
3220 int rasterWidth
= image
.getWidth();
3222 if (sixelSharedPalette
== false) {
3223 // Emit the palette, but only for the colors actually used by
3225 boolean [] usedColors
= new boolean[sixelPaletteSize
];
3226 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3227 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
3228 usedColors
[image
.getRGB(imageX
, imageY
)] = true;
3231 palette
.emitPalette(sb
, usedColors
);
3234 // Render the entire row of cells.
3235 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
3236 int [][] sixels
= new int[image
.getWidth()][6];
3238 // See which colors are actually used in this band of sixels.
3239 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3240 for (int imageY
= 0;
3241 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
3244 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
3245 assert (colorIdx
>= 0);
3246 assert (colorIdx
< sixelPaletteSize
);
3248 sixels
[imageX
][imageY
] = colorIdx
;
3252 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
3253 boolean isUsed
= false;
3254 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3255 for (int j
= 0; j
< 6; j
++) {
3256 if (sixels
[imageX
][j
] == i
) {
3261 if (isUsed
== false) {
3265 // Set to the beginning of scan line for the next set of
3266 // colored pixels, and select the color.
3267 sb
.append(String
.format("$#%d", i
));
3270 int oldDataCount
= 0;
3271 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3273 // Add up all the pixels that match this color.
3276 (j
< 6) && (currentRow
+ j
< fullHeight
);
3279 if (sixels
[imageX
][j
] == i
) {
3300 if ((currentRow
+ j
+ 1) > rasterHeight
) {
3301 rasterHeight
= currentRow
+ j
+ 1;
3309 if (data
== oldData
) {
3312 if (oldDataCount
== 1) {
3313 sb
.append((char) oldData
);
3314 } else if (oldDataCount
> 1) {
3315 sb
.append(String
.format("!%d", oldDataCount
));
3316 sb
.append((char) oldData
);
3322 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
3324 // Emit the last sequence.
3325 if (oldDataCount
== 1) {
3326 sb
.append((char) oldData
);
3327 } else if (oldDataCount
> 1) {
3328 sb
.append(String
.format("!%d", oldDataCount
));
3329 sb
.append((char) oldData
);
3332 } // for (int i = 0; i < sixelPaletteSize; i++)
3334 // Advance to the next scan line.
3337 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3339 // Kill the very last "-", because it is unnecessary.
3340 sb
.deleteCharAt(sb
.length() - 1);
3342 // Add the raster information
3343 sb
.insert(0, String
.format("\"1;1;%d;%d", rasterWidth
, rasterHeight
));
3346 // This row is OK to save into the cache.
3347 sixelCache
.put(cells
, sb
.toString());
3350 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3354 * Get the sixel support flag.
3356 * @return true if this terminal is emitting sixel
3358 public boolean hasSixel() {
3363 * Convert a horizontal range of cell's image data into a single
3364 * contigous image, rescaled and anti-aliased to match the current text
3367 * @param cells the cells containing image data
3368 * @return the image resized to the current text cell size
3370 private BufferedImage
cellsToImage(final List
<Cell
> cells
) {
3371 int imageWidth
= cells
.get(0).getImage().getWidth();
3372 int imageHeight
= cells
.get(0).getImage().getHeight();
3374 // Piece cells.get(x).getImage() pieces together into one larger
3375 // image for final rendering.
3377 int fullWidth
= cells
.size() * imageWidth
;
3378 int fullHeight
= imageHeight
;
3379 for (int i
= 0; i
< cells
.size(); i
++) {
3380 totalWidth
+= cells
.get(i
).getImage().getWidth();
3383 BufferedImage image
= new BufferedImage(fullWidth
,
3384 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
3387 for (int i
= 0; i
< cells
.size() - 1; i
++) {
3388 int tileWidth
= imageWidth
;
3389 int tileHeight
= imageHeight
;
3391 if (false && cells
.get(i
).isInvertedImage()) {
3392 // I used to put an all-white cell over the cursor, don't do
3394 rgbArray
= new int[imageWidth
* imageHeight
];
3395 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3396 rgbArray
[j
] = 0xFFFFFF;
3400 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
3401 tileWidth
, tileHeight
, null, 0, tileWidth
);
3402 } catch (Exception e
) {
3403 throw new RuntimeException("image " + imageWidth
+ "x" +
3405 "tile " + tileWidth
+ "x" +
3407 " cells.get(i).getImage() " +
3408 cells
.get(i
).getImage() +
3410 " fullWidth " + fullWidth
+
3411 " fullHeight " + fullHeight
, e
);
3416 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3417 i * imageWidth, 0, imageWidth, imageHeight,
3419 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3420 fullWidth, fullHeight, cells.size(), getTextWidth());
3423 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
3424 rgbArray
, 0, tileWidth
);
3425 if (tileHeight
< fullHeight
) {
3426 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
3427 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3428 for (int imageY
= imageHeight
; imageY
< fullHeight
;
3431 image
.setRGB(imageX
, imageY
, backgroundColor
);
3436 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3437 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3438 // I used to put an all-white cell over the cursor, don't do that
3440 rgbArray
= new int[totalWidth
* imageHeight
];
3441 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3442 rgbArray
[j
] = 0xFFFFFF;
3446 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3447 totalWidth
, imageHeight
, null, 0, totalWidth
);
3448 } catch (Exception e
) {
3449 throw new RuntimeException("image " + imageWidth
+ "x" +
3450 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3451 cells
.get(cells
.size() - 1).getImage(), e
);
3454 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3455 imageHeight
, rgbArray
, 0, totalWidth
);
3457 if (totalWidth
< imageWidth
) {
3458 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3460 for (int imageX
= image
.getWidth() - totalWidth
;
3461 imageX
< image
.getWidth(); imageX
++) {
3463 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3464 image
.setRGB(imageX
, imageY
, backgroundColor
);
3469 if ((image
.getWidth() != cells
.size() * getTextWidth())
3470 || (image
.getHeight() != getTextHeight())
3472 // Rescale the image to fit the text cells it is going into.
3473 BufferedImage newImage
;
3474 newImage
= new BufferedImage(cells
.size() * getTextWidth(),
3475 getTextHeight(), BufferedImage
.TYPE_INT_ARGB
);
3477 Graphics gr
= newImage
.getGraphics();
3478 if (gr
instanceof Graphics2D
) {
3479 ((Graphics2D
) gr
).setRenderingHint(RenderingHints
.KEY_ANTIALIASING
,
3480 RenderingHints
.VALUE_ANTIALIAS_ON
);
3481 ((Graphics2D
) gr
).setRenderingHint(RenderingHints
.KEY_RENDERING
,
3482 RenderingHints
.VALUE_RENDER_QUALITY
);
3484 gr
.drawImage(image
, 0, 0, newImage
.getWidth(),
3485 newImage
.getHeight(), null, null);
3493 // ------------------------------------------------------------------------
3494 // End sixel output support -----------------------------------------------
3495 // ------------------------------------------------------------------------
3497 // ------------------------------------------------------------------------
3498 // iTerm2 image output support --------------------------------------------
3499 // ------------------------------------------------------------------------
3502 * Create an iTerm2 images string representing a row of several cells
3503 * containing bitmap data.
3505 * @param x column coordinate. 0 is the left-most column.
3506 * @param y row coordinate. 0 is the top-most row.
3507 * @param cells the cells containing the bitmap data
3508 * @return the string to emit to an ANSI / ECMA-style terminal
3510 private String
toIterm2Image(final int x
, final int y
,
3511 final ArrayList
<Cell
> cells
) {
3513 StringBuilder sb
= new StringBuilder();
3515 assert (cells
!= null);
3516 assert (cells
.size() > 0);
3517 assert (cells
.get(0).getImage() != null);
3519 if (iterm2Images
== false) {
3520 sb
.append(normal());
3521 sb
.append(gotoXY(x
, y
));
3522 for (int i
= 0; i
< cells
.size(); i
++) {
3525 return sb
.toString();
3528 if (iterm2Cache
== null) {
3529 iterm2Cache
= new ImageCache(height
* 10);
3532 // Save and get rows to/from the cache that do NOT have inverted
3534 boolean saveInCache
= true;
3535 for (Cell cell
: cells
) {
3536 if (cell
.isInvertedImage()) {
3537 saveInCache
= false;
3541 String cachedResult
= iterm2Cache
.get(cells
);
3542 if (cachedResult
!= null) {
3543 // System.err.println("CACHE HIT");
3544 sb
.append(gotoXY(x
, y
));
3545 sb
.append(cachedResult
);
3546 return sb
.toString();
3548 // System.err.println("CACHE MISS");
3551 BufferedImage image
= cellsToImage(cells
);
3552 int fullHeight
= image
.getHeight();
3555 * From https://iterm2.com/documentation-images.html:
3559 * iTerm2 extends the xterm protocol with a set of proprietary escape
3560 * sequences. In general, the pattern is:
3562 * ESC ] 1337 ; key = value ^G
3564 * Whitespace is shown here for ease of reading: in practice, no
3565 * spaces should be used.
3567 * For file transfer and inline images, the code is:
3569 * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
3571 * The optional arguments are formatted as key=value with a semicolon
3572 * between each key-value pair. They are described below:
3574 * Key Description of value
3575 * name base-64 encoded filename. Defaults to "Unnamed file".
3576 * size File size in bytes. Optional; this is only used by the
3577 * progress indicator.
3578 * width Width to render. See notes below.
3579 * height Height to render. See notes below.
3580 * preserveAspectRatio If set to 0, then the image's inherent aspect
3581 * ratio will not be respected; otherwise, it
3582 * will fill the specified width and height as
3583 * much as possible without stretching. Defaults
3585 * inline If set to 1, the file will be displayed inline. Otherwise,
3586 * it will be downloaded with no visual representation in the
3587 * terminal session. Defaults to 0.
3589 * The width and height are given as a number followed by a unit, or
3592 * N: N character cells.
3594 * N%: N percent of the session's width or height.
3595 * auto: The image's inherent size will be used to determine an
3596 * appropriate dimension.
3600 // File contents can be several image formats. We will use PNG.
3601 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3603 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3604 Math
.min(image
.getHeight(), fullHeight
)),
3605 "PNG", pngOutputStream
)
3607 // We failed to render image, bail out.
3610 } catch (IOException e
) {
3611 // We failed to render image, bail out.
3615 sb
.append("\033]1337;File=");
3617 sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
3621 sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
3622 image.getWidth(), Math.min(image.getHeight(),
3625 sb
.append("inline=1:");
3626 sb
.append(StringUtils
.toBase64(pngOutputStream
.toByteArray()));
3630 // This row is OK to save into the cache.
3631 iterm2Cache
.put(cells
, sb
.toString());
3634 return (gotoXY(x
, y
) + sb
.toString());
3638 * Get the iTerm2 images support flag.
3640 * @return true if this terminal is emitting iTerm2 images
3642 public boolean hasIterm2Images() {
3643 return iterm2Images
;
3646 // ------------------------------------------------------------------------
3647 // End iTerm2 image output support ----------------------------------------
3648 // ------------------------------------------------------------------------
3650 // ------------------------------------------------------------------------
3651 // Jexer image output support ---------------------------------------------
3652 // ------------------------------------------------------------------------
3655 * Create a Jexer images string representing a row of several cells
3656 * containing bitmap data.
3658 * @param x column coordinate. 0 is the left-most column.
3659 * @param y row coordinate. 0 is the top-most row.
3660 * @param cells the cells containing the bitmap data
3661 * @return the string to emit to an ANSI / ECMA-style terminal
3663 private String
toJexerImage(final int x
, final int y
,
3664 final ArrayList
<Cell
> cells
) {
3666 StringBuilder sb
= new StringBuilder();
3668 assert (cells
!= null);
3669 assert (cells
.size() > 0);
3670 assert (cells
.get(0).getImage() != null);
3672 if (jexerImageOption
== JexerImageOption
.DISABLED
) {
3673 sb
.append(normal());
3674 sb
.append(gotoXY(x
, y
));
3675 for (int i
= 0; i
< cells
.size(); i
++) {
3678 return sb
.toString();
3681 if (jexerCache
== null) {
3682 jexerCache
= new ImageCache(height
* 10);
3685 // Save and get rows to/from the cache that do NOT have inverted
3687 boolean saveInCache
= true;
3688 for (Cell cell
: cells
) {
3689 if (cell
.isInvertedImage()) {
3690 saveInCache
= false;
3694 String cachedResult
= jexerCache
.get(cells
);
3695 if (cachedResult
!= null) {
3696 // System.err.println("CACHE HIT");
3697 sb
.append(gotoXY(x
, y
));
3698 sb
.append(cachedResult
);
3699 return sb
.toString();
3701 // System.err.println("CACHE MISS");
3704 BufferedImage image
= cellsToImage(cells
);
3705 int fullHeight
= image
.getHeight();
3707 if (jexerImageOption
== JexerImageOption
.PNG
) {
3709 ByteArrayOutputStream pngOutputStream
= new ByteArrayOutputStream(1024);
3711 if (!ImageIO
.write(image
.getSubimage(0, 0, image
.getWidth(),
3712 Math
.min(image
.getHeight(), fullHeight
)),
3713 "PNG", pngOutputStream
)
3715 // We failed to render image, bail out.
3718 } catch (IOException e
) {
3719 // We failed to render image, bail out.
3723 sb
.append("\033]444;1;0;");
3724 sb
.append(StringUtils
.toBase64(pngOutputStream
.toByteArray()));
3727 } else if (jexerImageOption
== JexerImageOption
.JPG
) {
3730 ByteArrayOutputStream jpgOutputStream
= new ByteArrayOutputStream(1024);
3732 // Convert from ARGB to RGB, otherwise the JPG encode will fail.
3733 BufferedImage jpgImage
= new BufferedImage(image
.getWidth(),
3734 image
.getHeight(), BufferedImage
.TYPE_INT_RGB
);
3735 int [] pixels
= new int[image
.getWidth() * image
.getHeight()];
3736 image
.getRGB(0, 0, image
.getWidth(), image
.getHeight(), pixels
,
3737 0, image
.getWidth());
3738 jpgImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(), pixels
,
3739 0, image
.getWidth());
3742 if (!ImageIO
.write(jpgImage
.getSubimage(0, 0,
3743 jpgImage
.getWidth(),
3744 Math
.min(jpgImage
.getHeight(), fullHeight
)),
3745 "JPG", jpgOutputStream
)
3747 // We failed to render image, bail out.
3750 } catch (IOException e
) {
3751 // We failed to render image, bail out.
3755 sb
.append("\033]444;2;0;");
3756 sb
.append(StringUtils
.toBase64(jpgOutputStream
.toByteArray()));
3759 } else if (jexerImageOption
== JexerImageOption
.RGB
) {
3762 sb
.append(String
.format("\033]444;0;%d;%d;0;", image
.getWidth(),
3763 Math
.min(image
.getHeight(), fullHeight
)));
3765 byte [] bytes
= new byte[image
.getWidth() * image
.getHeight() * 3];
3766 int stride
= image
.getWidth();
3767 for (int px
= 0; px
< stride
; px
++) {
3768 for (int py
= 0; py
< image
.getHeight(); py
++) {
3769 int rgb
= image
.getRGB(px
, py
);
3770 bytes
[(py
* stride
* 3) + (px
* 3)] = (byte) ((rgb
>>> 16) & 0xFF);
3771 bytes
[(py
* stride
* 3) + (px
* 3) + 1] = (byte) ((rgb
>>> 8) & 0xFF);
3772 bytes
[(py
* stride
* 3) + (px
* 3) + 2] = (byte) ( rgb
& 0xFF);
3775 sb
.append(StringUtils
.toBase64(bytes
));
3780 // This row is OK to save into the cache.
3781 jexerCache
.put(cells
, sb
.toString());
3784 return (gotoXY(x
, y
) + sb
.toString());
3788 * Get the Jexer images support flag.
3790 * @return true if this terminal is emitting Jexer images
3792 public boolean hasJexerImages() {
3793 return (jexerImageOption
!= JexerImageOption
.DISABLED
);
3796 // ------------------------------------------------------------------------
3797 // End Jexer image output support -----------------------------------------
3798 // ------------------------------------------------------------------------
3801 * Setup system colors to match DOS color palette.
3803 private void setDOSColors() {
3804 MYBLACK
= new java
.awt
.Color(0x00, 0x00, 0x00);
3805 MYRED
= new java
.awt
.Color(0xa8, 0x00, 0x00);
3806 MYGREEN
= new java
.awt
.Color(0x00, 0xa8, 0x00);
3807 MYYELLOW
= new java
.awt
.Color(0xa8, 0x54, 0x00);
3808 MYBLUE
= new java
.awt
.Color(0x00, 0x00, 0xa8);
3809 MYMAGENTA
= new java
.awt
.Color(0xa8, 0x00, 0xa8);
3810 MYCYAN
= new java
.awt
.Color(0x00, 0xa8, 0xa8);
3811 MYWHITE
= new java
.awt
.Color(0xa8, 0xa8, 0xa8);
3812 MYBOLD_BLACK
= new java
.awt
.Color(0x54, 0x54, 0x54);
3813 MYBOLD_RED
= new java
.awt
.Color(0xfc, 0x54, 0x54);
3814 MYBOLD_GREEN
= new java
.awt
.Color(0x54, 0xfc, 0x54);
3815 MYBOLD_YELLOW
= new java
.awt
.Color(0xfc, 0xfc, 0x54);
3816 MYBOLD_BLUE
= new java
.awt
.Color(0x54, 0x54, 0xfc);
3817 MYBOLD_MAGENTA
= new java
.awt
.Color(0xfc, 0x54, 0xfc);
3818 MYBOLD_CYAN
= new java
.awt
.Color(0x54, 0xfc, 0xfc);
3819 MYBOLD_WHITE
= new java
.awt
.Color(0xfc, 0xfc, 0xfc);
3823 * Setup ECMA48 colors to match those provided in system properties.
3825 private void setCustomSystemColors() {
3828 MYBLACK
= getCustomColor("jexer.ECMA48.color0", MYBLACK
);
3829 MYRED
= getCustomColor("jexer.ECMA48.color1", MYRED
);
3830 MYGREEN
= getCustomColor("jexer.ECMA48.color2", MYGREEN
);
3831 MYYELLOW
= getCustomColor("jexer.ECMA48.color3", MYYELLOW
);
3832 MYBLUE
= getCustomColor("jexer.ECMA48.color4", MYBLUE
);
3833 MYMAGENTA
= getCustomColor("jexer.ECMA48.color5", MYMAGENTA
);
3834 MYCYAN
= getCustomColor("jexer.ECMA48.color6", MYCYAN
);
3835 MYWHITE
= getCustomColor("jexer.ECMA48.color7", MYWHITE
);
3836 MYBOLD_BLACK
= getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK
);
3837 MYBOLD_RED
= getCustomColor("jexer.ECMA48.color9", MYBOLD_RED
);
3838 MYBOLD_GREEN
= getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN
);
3839 MYBOLD_YELLOW
= getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW
);
3840 MYBOLD_BLUE
= getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE
);
3841 MYBOLD_MAGENTA
= getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA
);
3842 MYBOLD_CYAN
= getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN
);
3843 MYBOLD_WHITE
= getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE
);
3847 * Setup one system color to match the RGB value provided in system
3850 * @param key the system property key
3851 * @param defaultColor the default color to return if key is not set, or
3853 * @return a color from the RGB string, or defaultColor
3855 private java
.awt
.Color
getCustomColor(final String key
,
3856 final java
.awt
.Color defaultColor
) {
3858 String rgb
= System
.getProperty(key
);
3860 return defaultColor
;
3862 if (rgb
.startsWith("#")) {
3863 rgb
= rgb
.substring(1);
3867 rgbInt
= Integer
.parseInt(rgb
, 16);
3868 } catch (NumberFormatException e
) {
3869 return defaultColor
;
3871 java
.awt
.Color color
= new java
.awt
.Color((rgbInt
& 0xFF0000) >>> 16,
3872 (rgbInt
& 0x00FF00) >>> 8,
3873 (rgbInt
& 0x0000FF));
3879 * Create a T.416 RGB parameter sequence for a custom system color.
3881 * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
3882 * @return the color portion of the string to emit to an ANSI /
3883 * ECMA-style terminal
3885 private String
systemColorRGB(final java
.awt
.Color color
) {
3886 return String
.format("%d;%d;%d", color
.getRed(), color
.getGreen(),
3891 * Create a SGR parameter sequence for a single color change.
3893 * @param bold if true, set bold
3894 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3895 * @param foreground if true, this is a foreground color
3896 * @return the string to emit to an ANSI / ECMA-style terminal,
3899 private String
color(final boolean bold
, final Color color
,
3900 final boolean foreground
) {
3901 return color(color
, foreground
, true) +
3902 rgbColor(bold
, color
, foreground
);
3906 * Create a T.416 RGB parameter sequence for a single color change.
3908 * @param colorRGB a 24-bit RGB value for foreground color
3909 * @param foreground if true, this is a foreground color
3910 * @return the string to emit to an ANSI / ECMA-style terminal,
3913 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
3915 int colorRed
= (colorRGB
>>> 16) & 0xFF;
3916 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
3917 int colorBlue
= colorRGB
& 0xFF;
3919 StringBuilder sb
= new StringBuilder();
3921 sb
.append("\033[38;2;");
3923 sb
.append("\033[48;2;");
3925 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
3926 return sb
.toString();
3930 * Create a T.416 RGB parameter sequence for both foreground and
3931 * background color change.
3933 * @param foreColorRGB a 24-bit RGB value for foreground color
3934 * @param backColorRGB a 24-bit RGB value for foreground color
3935 * @return the string to emit to an ANSI / ECMA-style terminal,
3938 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
3939 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3940 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3941 int foreColorBlue
= foreColorRGB
& 0xFF;
3942 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3943 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3944 int backColorBlue
= backColorRGB
& 0xFF;
3946 StringBuilder sb
= new StringBuilder();
3947 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
3948 foreColorRed
, foreColorGreen
, foreColorBlue
));
3949 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
3950 backColorRed
, backColorGreen
, backColorBlue
));
3951 return sb
.toString();
3955 * Create a T.416 RGB parameter sequence for a single color change.
3957 * @param bold if true, set bold
3958 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3959 * @param foreground if true, this is a foreground color
3960 * @return the string to emit to an xterm terminal with RGB support,
3961 * e.g. "\033[38;2;RR;GG;BBm"
3963 private String
rgbColor(final boolean bold
, final Color color
,
3964 final boolean foreground
) {
3965 if (doRgbColor
== false) {
3968 StringBuilder sb
= new StringBuilder("\033[");
3970 // Bold implies foreground only
3972 if (color
.equals(Color
.BLACK
)) {
3973 sb
.append(systemColorRGB(MYBOLD_BLACK
));
3974 } else if (color
.equals(Color
.RED
)) {
3975 sb
.append(systemColorRGB(MYBOLD_RED
));
3976 } else if (color
.equals(Color
.GREEN
)) {
3977 sb
.append(systemColorRGB(MYBOLD_GREEN
));
3978 } else if (color
.equals(Color
.YELLOW
)) {
3979 sb
.append(systemColorRGB(MYBOLD_YELLOW
));
3980 } else if (color
.equals(Color
.BLUE
)) {
3981 sb
.append(systemColorRGB(MYBOLD_BLUE
));
3982 } else if (color
.equals(Color
.MAGENTA
)) {
3983 sb
.append(systemColorRGB(MYBOLD_MAGENTA
));
3984 } else if (color
.equals(Color
.CYAN
)) {
3985 sb
.append(systemColorRGB(MYBOLD_CYAN
));
3986 } else if (color
.equals(Color
.WHITE
)) {
3987 sb
.append(systemColorRGB(MYBOLD_WHITE
));
3995 if (color
.equals(Color
.BLACK
)) {
3996 sb
.append(systemColorRGB(MYBLACK
));
3997 } else if (color
.equals(Color
.RED
)) {
3998 sb
.append(systemColorRGB(MYRED
));
3999 } else if (color
.equals(Color
.GREEN
)) {
4000 sb
.append(systemColorRGB(MYGREEN
));
4001 } else if (color
.equals(Color
.YELLOW
)) {
4002 sb
.append(systemColorRGB(MYYELLOW
));
4003 } else if (color
.equals(Color
.BLUE
)) {
4004 sb
.append(systemColorRGB(MYBLUE
));
4005 } else if (color
.equals(Color
.MAGENTA
)) {
4006 sb
.append(systemColorRGB(MYMAGENTA
));
4007 } else if (color
.equals(Color
.CYAN
)) {
4008 sb
.append(systemColorRGB(MYCYAN
));
4009 } else if (color
.equals(Color
.WHITE
)) {
4010 sb
.append(systemColorRGB(MYWHITE
));
4014 return sb
.toString();
4018 * Create a T.416 RGB parameter sequence for both foreground and
4019 * background color change.
4021 * @param bold if true, set bold
4022 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4023 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4024 * @return the string to emit to an xterm terminal with RGB support,
4025 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
4027 private String
rgbColor(final boolean bold
, final Color foreColor
,
4028 final Color backColor
) {
4029 if (doRgbColor
== false) {
4033 return rgbColor(bold
, foreColor
, true) +
4034 rgbColor(false, backColor
, false);
4038 * Create a SGR parameter sequence for a single color change.
4040 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4041 * @param foreground if true, this is a foreground color
4042 * @param header if true, make the full header, otherwise just emit the
4043 * color parameter e.g. "42;"
4044 * @return the string to emit to an ANSI / ECMA-style terminal,
4047 private String
color(final Color color
, final boolean foreground
,
4048 final boolean header
) {
4050 int ecmaColor
= color
.getValue();
4052 // Convert Color.* values to SGR numerics
4060 return String
.format("\033[%dm", ecmaColor
);
4062 return String
.format("%d;", ecmaColor
);
4067 * Create a SGR parameter sequence for both foreground and background
4070 * @param bold if true, set bold
4071 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4072 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4073 * @return the string to emit to an ANSI / ECMA-style terminal,
4074 * e.g. "\033[31;42m"
4076 private String
color(final boolean bold
, final Color foreColor
,
4077 final Color backColor
) {
4078 return color(foreColor
, backColor
, true) +
4079 rgbColor(bold
, foreColor
, backColor
);
4083 * Create a SGR parameter sequence for both foreground and
4084 * background color change.
4086 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4087 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4088 * @param header if true, make the full header, otherwise just emit the
4089 * color parameter e.g. "31;42;"
4090 * @return the string to emit to an ANSI / ECMA-style terminal,
4091 * e.g. "\033[31;42m"
4093 private String
color(final Color foreColor
, final Color backColor
,
4094 final boolean header
) {
4096 int ecmaForeColor
= foreColor
.getValue();
4097 int ecmaBackColor
= backColor
.getValue();
4099 // Convert Color.* values to SGR numerics
4100 ecmaBackColor
+= 40;
4101 ecmaForeColor
+= 30;
4104 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
4106 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
4111 * Create a SGR parameter sequence for foreground, background, and
4112 * several attributes. This sequence first resets all attributes to
4113 * default, then sets attributes as per the parameters.
4115 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4116 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4117 * @param bold if true, set bold
4118 * @param reverse if true, set reverse
4119 * @param blink if true, set blink
4120 * @param underline if true, set underline
4121 * @return the string to emit to an ANSI / ECMA-style terminal,
4122 * e.g. "\033[0;1;31;42m"
4124 private String
color(final Color foreColor
, final Color backColor
,
4125 final boolean bold
, final boolean reverse
, final boolean blink
,
4126 final boolean underline
) {
4128 int ecmaForeColor
= foreColor
.getValue();
4129 int ecmaBackColor
= backColor
.getValue();
4131 // Convert Color.* values to SGR numerics
4132 ecmaBackColor
+= 40;
4133 ecmaForeColor
+= 30;
4135 StringBuilder sb
= new StringBuilder();
4136 if ( bold
&& reverse
&& blink
&& !underline
) {
4137 sb
.append("\033[0;1;7;5;");
4138 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4139 sb
.append("\033[0;1;7;");
4140 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4141 sb
.append("\033[0;7;5;");
4142 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4143 sb
.append("\033[0;1;5;");
4144 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4145 sb
.append("\033[0;1;");
4146 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4147 sb
.append("\033[0;7;");
4148 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4149 sb
.append("\033[0;5;");
4150 } else if ( bold
&& reverse
&& blink
&& underline
) {
4151 sb
.append("\033[0;1;7;5;4;");
4152 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4153 sb
.append("\033[0;1;7;4;");
4154 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4155 sb
.append("\033[0;7;5;4;");
4156 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4157 sb
.append("\033[0;1;5;4;");
4158 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4159 sb
.append("\033[0;1;4;");
4160 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4161 sb
.append("\033[0;7;4;");
4162 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4163 sb
.append("\033[0;5;4;");
4164 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4165 sb
.append("\033[0;4;");
4167 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4168 sb
.append("\033[0;");
4170 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
4171 sb
.append(rgbColor(bold
, foreColor
, backColor
));
4172 return sb
.toString();
4176 * Create a SGR parameter sequence for foreground, background, and
4177 * several attributes. This sequence first resets all attributes to
4178 * default, then sets attributes as per the parameters.
4180 * @param foreColorRGB a 24-bit RGB value for foreground color
4181 * @param backColorRGB a 24-bit RGB value for foreground color
4182 * @param bold if true, set bold
4183 * @param reverse if true, set reverse
4184 * @param blink if true, set blink
4185 * @param underline if true, set underline
4186 * @return the string to emit to an ANSI / ECMA-style terminal,
4187 * e.g. "\033[0;1;31;42m"
4189 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
4190 final boolean bold
, final boolean reverse
, final boolean blink
,
4191 final boolean underline
) {
4193 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
4194 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
4195 int foreColorBlue
= foreColorRGB
& 0xFF;
4196 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
4197 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
4198 int backColorBlue
= backColorRGB
& 0xFF;
4200 StringBuilder sb
= new StringBuilder();
4201 if ( bold
&& reverse
&& blink
&& !underline
) {
4202 sb
.append("\033[0;1;7;5;");
4203 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
4204 sb
.append("\033[0;1;7;");
4205 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
4206 sb
.append("\033[0;7;5;");
4207 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
4208 sb
.append("\033[0;1;5;");
4209 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
4210 sb
.append("\033[0;1;");
4211 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
4212 sb
.append("\033[0;7;");
4213 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
4214 sb
.append("\033[0;5;");
4215 } else if ( bold
&& reverse
&& blink
&& underline
) {
4216 sb
.append("\033[0;1;7;5;4;");
4217 } else if ( bold
&& reverse
&& !blink
&& underline
) {
4218 sb
.append("\033[0;1;7;4;");
4219 } else if ( !bold
&& reverse
&& blink
&& underline
) {
4220 sb
.append("\033[0;7;5;4;");
4221 } else if ( bold
&& !reverse
&& blink
&& underline
) {
4222 sb
.append("\033[0;1;5;4;");
4223 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
4224 sb
.append("\033[0;1;4;");
4225 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
4226 sb
.append("\033[0;7;4;");
4227 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
4228 sb
.append("\033[0;5;4;");
4229 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
4230 sb
.append("\033[0;4;");
4232 assert (!bold
&& !reverse
&& !blink
&& !underline
);
4233 sb
.append("\033[0;");
4236 sb
.append("m\033[38;2;");
4237 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
4239 sb
.append("m\033[48;2;");
4240 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
4243 return sb
.toString();
4247 * Create a SGR parameter sequence to reset to VT100 defaults.
4249 * @return the string to emit to an ANSI / ECMA-style terminal,
4252 private String
normal() {
4253 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
4257 * Create a SGR parameter sequence to reset to ECMA-48 default
4258 * foreground/background.
4260 * @return the string to emit to an ANSI / ECMA-style terminal,
4263 private String
defaultColor() {
4266 * Normal (neither bold nor faint).
4269 * Steady (not blinking).
4270 * Positive (not inverse).
4271 * Visible (not hidden).
4273 * Default foreground color.
4274 * Default background color.
4276 return "\033[0;22;23;24;25;27;28;29;39;49m";
4280 * Create a SGR parameter sequence to reset to defaults.
4282 * @param header if true, make the full header, otherwise just emit the
4283 * bare parameter e.g. "0;"
4284 * @return the string to emit to an ANSI / ECMA-style terminal,
4287 private String
normal(final boolean header
) {
4289 return "\033[0;37;40m";
4295 * Create a SGR parameter sequence for enabling the visible cursor.
4297 * @param on if true, turn on cursor
4298 * @return the string to emit to an ANSI / ECMA-style terminal
4300 private String
cursor(final boolean on
) {
4301 if (on
&& !cursorOn
) {
4305 if (!on
&& cursorOn
) {
4313 * Clear the entire screen. Because some terminals use back-color-erase,
4314 * set the color to white-on-black beforehand.
4316 * @return the string to emit to an ANSI / ECMA-style terminal
4318 private String
clearAll() {
4319 return "\033[0;37;40m\033[2J";
4323 * Clear the line from the cursor (inclusive) to the end of the screen.
4324 * Because some terminals use back-color-erase, set the color to
4325 * white-on-black beforehand.
4327 * @return the string to emit to an ANSI / ECMA-style terminal
4329 private String
clearRemainingLine() {
4330 return "\033[0;37;40m\033[K";
4334 * Move the cursor to (x, y).
4336 * @param x column coordinate. 0 is the left-most column.
4337 * @param y row coordinate. 0 is the top-most row.
4338 * @return the string to emit to an ANSI / ECMA-style terminal
4340 private String
gotoXY(final int x
, final int y
) {
4341 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
4345 * Tell (u)xterm that we want to receive mouse events based on "Any event
4346 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
4347 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
4349 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
4351 * Note that this also sets the alternate/primary screen buffer.
4353 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
4354 * mean "hide the mouse pointer." We have to use our own sequence to do
4355 * this because there is no standard in xterm for unilaterally hiding the
4356 * pointer all the time (regardless of typing).
4358 * @param on If true, enable mouse report and use the alternate screen
4359 * buffer. If false disable mouse reporting and use the primary screen
4361 * @return the string to emit to xterm
4363 private String
mouse(final boolean on
) {
4365 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
4367 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";