2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.backend
;
31 import java
.awt
.image
.BufferedImage
;
32 import java
.io
.BufferedReader
;
33 import java
.io
.FileDescriptor
;
34 import java
.io
.FileInputStream
;
35 import java
.io
.InputStream
;
36 import java
.io
.InputStreamReader
;
37 import java
.io
.IOException
;
38 import java
.io
.OutputStream
;
39 import java
.io
.OutputStreamWriter
;
40 import java
.io
.PrintWriter
;
41 import java
.io
.Reader
;
42 import java
.io
.UnsupportedEncodingException
;
43 import java
.util
.ArrayList
;
44 import java
.util
.Collections
;
45 import java
.util
.HashMap
;
46 import java
.util
.List
;
47 import java
.util
.LinkedList
;
50 import jexer
.bits
.Cell
;
51 import jexer
.bits
.CellAttributes
;
52 import jexer
.bits
.Color
;
53 import jexer
.event
.TInputEvent
;
54 import jexer
.event
.TKeypressEvent
;
55 import jexer
.event
.TMouseEvent
;
56 import jexer
.event
.TResizeEvent
;
57 import static jexer
.TKeypress
.*;
60 * This class reads keystrokes and mouse events and emits output to ANSI
61 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
63 public class ECMA48Terminal
extends LogicalScreen
64 implements TerminalReader
, Runnable
{
66 // ------------------------------------------------------------------------
67 // Constants --------------------------------------------------------------
68 // ------------------------------------------------------------------------
71 * States in the input parser.
73 private enum ParseState
{
84 * Number of colors in the sixel palette. Xterm 335 defines the max as
87 private static final int MAX_COLOR_REGISTERS
= 1024;
88 // Black-and-white is possible too.
89 // private static final int MAX_COLOR_REGISTERS = 2;
91 // ------------------------------------------------------------------------
92 // Variables --------------------------------------------------------------
93 // ------------------------------------------------------------------------
96 * Emit debugging to stderr.
98 private boolean debugToStderr
= false;
101 * If true, emit T.416-style RGB colors for normal system colors. This
102 * is a) expensive in bandwidth, and b) potentially terrible looking for
105 private static boolean doRgbColor
= false;
108 * The session information.
110 private SessionInfo sessionInfo
;
113 * The event queue, filled up by a thread reading on input.
115 private List
<TInputEvent
> eventQueue
;
118 * If true, we want the reader thread to exit gracefully.
120 private boolean stopReaderThread
;
125 private Thread readerThread
;
128 * Parameters being collected. E.g. if the string is \033[1;3m, then
129 * params[0] will be 1 and params[1] will be 3.
131 private List
<String
> params
;
134 * Current parsing state.
136 private ParseState state
;
139 * The time we entered ESCAPE. If we get a bare escape without a code
140 * following it, this is used to return that bare escape.
142 private long escapeTime
;
145 * The time we last checked the window size. We try not to spawn stty
146 * more than once per second.
148 private long windowSizeTime
;
151 * true if mouse1 was down. Used to report mouse1 on the release event.
153 private boolean mouse1
;
156 * true if mouse2 was down. Used to report mouse2 on the release event.
158 private boolean mouse2
;
161 * true if mouse3 was down. Used to report mouse3 on the release event.
163 private boolean mouse3
;
166 * Cache the cursor visibility value so we only emit the sequence when we
169 private boolean cursorOn
= true;
172 * Cache the last window size to figure out if a TResizeEvent needs to be
175 private TResizeEvent windowResize
= null;
178 * Window width in pixels. Used for sixel support.
180 private int widthPixels
= 640;
183 * Window height in pixels. Used for sixel support.
185 private int heightPixels
= 400;
188 * If true, emit image data via sixel.
190 private boolean sixel
= true;
193 * The sixel palette handler.
195 private SixelPalette palette
= null;
198 * The sixel post-rendered string cache.
200 private SixelCache sixelCache
= null;
203 * If true, then we changed System.in and need to change it back.
205 private boolean setRawMode
;
208 * The terminal's input. If an InputStream is not specified in the
209 * constructor, then this InputStreamReader will be bound to System.in
210 * with UTF-8 encoding.
212 private Reader input
;
215 * The terminal's raw InputStream. If an InputStream is not specified in
216 * the constructor, then this InputReader will be bound to System.in.
217 * This is used by run() to see if bytes are available() before calling
218 * (Reader)input.read().
220 private InputStream inputStream
;
223 * The terminal's output. If an OutputStream is not specified in the
224 * constructor, then this PrintWriter will be bound to System.out with
227 private PrintWriter output
;
230 * The listening object that run() wakes up on new input.
232 private Object listener
;
235 * SixelPalette is used to manage the conversion of images between 24-bit
236 * RGB color and a palette of MAX_COLOR_REGISTERS colors.
238 private class SixelPalette
{
241 * Color palette for sixel output, sorted low to high.
243 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
246 * Map of color palette index for sixel output, from the order it was
247 * generated by makePalette() to rgbColors.
249 private int [] rgbSortedIndex
= new int[MAX_COLOR_REGISTERS
];
252 * The color palette, organized by hue, saturation, and luminance.
253 * This is used for a fast color match.
255 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
258 * Number of bits for hue.
260 private int hueBits
= -1;
263 * Number of bits for saturation.
265 private int satBits
= -1;
268 * Number of bits for luminance.
270 private int lumBits
= -1;
273 * Step size for hue bins.
275 private int hueStep
= -1;
278 * Step size for saturation bins.
280 private int satStep
= -1;
283 * Cached RGB to HSL result.
285 private int hsl
[] = new int[3];
288 * ColorIdx records a RGB color and its palette index.
290 private class ColorIdx
{
292 * The 24-bit RGB color.
297 * The palette index for this color.
302 * Public constructor.
304 * @param color the 24-bit RGB color
305 * @param index the palette index for this color
307 public ColorIdx(final int color
, final int index
) {
314 * Public constructor.
316 public SixelPalette() {
321 * Find the nearest match for a color in the palette.
323 * @param color the RGB color
324 * @return the index in rgbColors that is closest to color
326 public int matchColor(final int color
) {
331 * matchColor() is a critical performance bottleneck. To make it
332 * decent, we do the following:
334 * 1. Find the nearest two hues that bracket this color.
336 * 2. Find the nearest two saturations that bracket this color.
338 * 3. Iterate within these four bands of luminance values,
339 * returning the closest color by Euclidean distance.
341 * This strategy reduces the search space by about 97%.
343 int red
= (color
>>> 16) & 0xFF;
344 int green
= (color
>>> 8) & 0xFF;
345 int blue
= color
& 0xFF;
347 if (MAX_COLOR_REGISTERS
== 2) {
348 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
357 rgbToHsl(red
, green
, blue
, hsl
);
361 // System.err.printf("%d %d %d\n", hue, sat, lum);
363 double diff
= Double
.MAX_VALUE
;
366 int hue1
= hue
/ (360/hueStep
);
368 if (hue1
>= hslColors
.size() - 1) {
369 // Bracket pure red from above.
370 hue1
= hslColors
.size() - 1;
372 } else if (hue1
== 0) {
373 // Bracket pure red from below.
374 hue2
= hslColors
.size() - 1;
377 for (int hI
= hue1
; hI
!= -1;) {
378 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
381 } else if (hI
== hue2
) {
385 int sMin
= (sat
/ satStep
) - 1;
390 } else if (sMin
== sats
.size() - 1) {
395 assert (sMax
- sMin
== 1);
398 // int sMax = sats.size() - 1;
400 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
401 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
403 // True 3D colorspace match for the remaining values
404 for (ColorIdx c
: lums
) {
405 int rgbColor
= c
.color
;
407 int red2
= (rgbColor
>>> 16) & 0xFF;
408 int green2
= (rgbColor
>>> 8) & 0xFF;
409 int blue2
= rgbColor
& 0xFF;
410 newDiff
+= Math
.pow(red2
- red
, 2);
411 newDiff
+= Math
.pow(green2
- green
, 2);
412 newDiff
+= Math
.pow(blue2
- blue
, 2);
413 if (newDiff
< diff
) {
414 idx
= rgbSortedIndex
[c
.index
];
421 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
422 // Black is a closer match.
424 } else if ((((255 - red
) * (255 - red
)) +
425 ((255 - green
) * (255 - green
)) +
426 ((255 - blue
) * (255 - blue
))) < diff
) {
428 // White is a closer match.
429 idx
= MAX_COLOR_REGISTERS
- 1;
436 * Clamp an int value to [0, 255].
438 * @param x the int value
439 * @return an int between 0 and 255.
441 private int clamp(final int x
) {
452 * Dither an image to a MAX_COLOR_REGISTERS palette. The dithered
453 * image cells will contain indexes into the palette.
455 * @param image the image to dither
456 * @return the dithered image. Every pixel is an index into the
459 public BufferedImage
ditherImage(final BufferedImage image
) {
461 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
462 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
464 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
465 image
.getHeight(), null, 0, image
.getWidth());
466 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
467 rgbArray
, 0, image
.getWidth());
469 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
470 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
471 int oldPixel
= ditheredImage
.getRGB(imageX
,
473 int colorIdx
= matchColor(oldPixel
);
474 assert (colorIdx
>= 0);
475 assert (colorIdx
< MAX_COLOR_REGISTERS
);
476 int newPixel
= rgbColors
.get(colorIdx
);
477 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
479 int oldRed
= (oldPixel
>>> 16) & 0xFF;
480 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
481 int oldBlue
= oldPixel
& 0xFF;
483 int newRed
= (newPixel
>>> 16) & 0xFF;
484 int newGreen
= (newPixel
>>> 8) & 0xFF;
485 int newBlue
= newPixel
& 0xFF;
487 int redError
= (oldRed
- newRed
) / 16;
488 int greenError
= (oldGreen
- newGreen
) / 16;
489 int blueError
= (oldBlue
- newBlue
) / 16;
491 int red
, green
, blue
;
492 if (imageX
< image
.getWidth() - 1) {
493 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
494 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
495 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
496 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
498 green
= clamp(green
);
500 pXpY
= ((red
& 0xFF) << 16);
501 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
502 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
504 if (imageY
< image
.getHeight() - 1) {
505 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
507 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
508 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
509 blue
= ( pXpYp
& 0xFF) + blueError
;
511 green
= clamp(green
);
513 pXpYp
= ((red
& 0xFF) << 16);
514 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
515 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
517 } else if (imageY
< image
.getHeight() - 1) {
518 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
520 int pXYp
= ditheredImage
.getRGB(imageX
,
523 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
524 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
525 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
527 green
= clamp(green
);
529 pXmYp
= ((red
& 0xFF) << 16);
530 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
531 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
533 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
534 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
535 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
537 green
= clamp(green
);
539 pXYp
= ((red
& 0xFF) << 16);
540 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
541 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
543 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
544 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
546 return ditheredImage
;
550 * Convert an RGB color to HSL.
552 * @param red red color, between 0 and 255
553 * @param green green color, between 0 and 255
554 * @param blue blue color, between 0 and 255
555 * @param hsl the hsl color as [hue, saturation, luminance]
557 private void rgbToHsl(final int red
, final int green
,
558 final int blue
, final int [] hsl
) {
560 assert ((red
>= 0) && (red
<= 255));
561 assert ((green
>= 0) && (green
<= 255));
562 assert ((blue
>= 0) && (blue
<= 255));
564 double R
= red
/ 255.0;
565 double G
= green
/ 255.0;
566 double B
= blue
/ 255.0;
567 boolean Rmax
= false;
568 boolean Gmax
= false;
569 boolean Bmax
= false;
570 double min
= (R
< G ? R
: G
);
571 min
= (min
< B ? min
: B
);
573 if ((R
>= G
) && (R
>= B
)) {
576 } else if ((G
>= R
) && (G
>= B
)) {
579 } else if ((B
>= G
) && (B
>= R
)) {
584 double L
= (min
+ max
) / 2.0;
589 S
= (max
- min
) / (max
+ min
);
591 S
= (max
- min
) / (2.0 - max
- min
);
595 assert (Gmax
== false);
596 assert (Bmax
== false);
597 H
= (G
- B
) / (max
- min
);
599 assert (Rmax
== false);
600 assert (Bmax
== false);
601 H
= 2.0 + (B
- R
) / (max
- min
);
603 assert (Rmax
== false);
604 assert (Gmax
== false);
605 H
= 4.0 + (R
- G
) / (max
- min
);
610 hsl
[0] = (int) (H
* 60.0);
611 hsl
[1] = (int) (S
* 100.0);
612 hsl
[2] = (int) (L
* 100.0);
614 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
615 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
616 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
620 * Convert a HSL color to RGB.
622 * @param hue hue, between 0 and 359
623 * @param sat saturation, between 0 and 100
624 * @param lum luminance, between 0 and 100
625 * @return the rgb color as 0x00RRGGBB
627 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
628 assert ((hue
>= 0) && (hue
<= 360));
629 assert ((sat
>= 0) && (sat
<= 100));
630 assert ((lum
>= 0) && (lum
<= 100));
632 double S
= sat
/ 100.0;
633 double L
= lum
/ 100.0;
634 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
635 double Hp
= hue
/ 60.0;
636 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
643 } else if (Hp
<= 2.0) {
646 } else if (Hp
<= 3.0) {
649 } else if (Hp
<= 4.0) {
652 } else if (Hp
<= 5.0) {
655 } else if (Hp
<= 6.0) {
659 double m
= L
- (C
/ 2.0);
660 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
661 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
662 int blue
= (int) ((Bp
+ m
) * 255.0);
664 return (red
| green
| blue
);
668 * Create the sixel palette.
670 private void makePalette() {
671 // Generate the sixel palette. Because we have no idea at this
672 // layer which image(s) will be shown, we have to use a common
673 // palette with MAX_COLOR_REGISTERS colors for everything, and
674 // map the BufferedImage colors to their nearest neighbor in RGB
677 if (MAX_COLOR_REGISTERS
== 2) {
679 rgbColors
.add(0xFFFFFF);
680 rgbSortedIndex
[0] = 0;
681 rgbSortedIndex
[1] = 1;
685 // We build a palette using the Hue-Saturation-Luminence model,
686 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
687 // Luminance. We convert these colors to 24-bit RGB, sort them
688 // ascending, and steal the first index for pure black and the
689 // last for pure white. The 8-bit final palette favors bright
690 // colors, somewhere between pastel and classic television
691 // technicolor. 9- and 10-bit palettes are more uniform.
693 // Default at 256 colors.
698 assert (MAX_COLOR_REGISTERS
>= 256);
699 assert ((MAX_COLOR_REGISTERS
== 256)
700 || (MAX_COLOR_REGISTERS
== 512)
701 || (MAX_COLOR_REGISTERS
== 1024)
702 || (MAX_COLOR_REGISTERS
== 2048));
704 switch (MAX_COLOR_REGISTERS
) {
721 hueStep
= (int) (Math
.pow(2, hueBits
));
722 satStep
= (int) (100 / Math
.pow(2, satBits
));
723 // 1 bit for luminance: 40 and 70.
728 // 2 bits: 20, 40, 60, 80
733 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
739 // System.err.printf("<html><body>\n");
740 // Hue is evenly spaced around the wheel.
741 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
743 final boolean DEBUG
= false;
744 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
746 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
747 hue
+= (360/hueStep
)) {
749 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
750 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
751 hslColors
.add(satList
);
753 // Saturation is linearly spaced between pastel and pure.
754 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
756 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
757 satList
.add(lumList
);
759 // Luminance brackets the pure color, but leaning toward
761 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
763 System.err.printf("<font style = \"color:");
764 System.err.printf("hsl(%d, %d%%, %d%%)",
766 System.err.printf(";\">=</font>\n");
768 int rgbColor
= hslToRgb(hue
, sat
, lum
);
769 rgbColors
.add(rgbColor
);
770 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
771 rgbColors
.size() - 1);
772 lumList
.add(colorIdx
);
774 rawRgbList
.add(rgbColor
);
776 int red
= (rgbColor
>>> 16) & 0xFF;
777 int green
= (rgbColor
>>> 8) & 0xFF;
778 int blue
= rgbColor
& 0xFF;
779 int [] backToHsl
= new int[3];
780 rgbToHsl(red
, green
, blue
, backToHsl
);
781 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
782 hue
, backToHsl
[0], sat
, backToHsl
[1],
788 // System.err.printf("\n</body></html>\n");
790 assert (rgbColors
.size() == MAX_COLOR_REGISTERS
);
793 * We need to sort rgbColors, so that toSixel() can know where
794 * BLACK and WHITE are in it. But we also need to be able to
795 * find the sorted values using the old unsorted indexes. So we
796 * will sort it, put all the indexes into a HashMap, and then
797 * build rgbSortedIndex[].
799 Collections
.sort(rgbColors
);
800 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
801 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
802 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
803 rgbColorIndices
.put(rgbColors
.get(i
), i
);
805 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
806 int rawColor
= rawRgbList
.get(i
);
807 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
810 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
811 assert (rawRgbList
!= null);
812 int idx
= rgbSortedIndex
[i
];
813 int rgbColor
= rgbColors
.get(idx
);
814 if ((idx
!= 0) && (idx
!= MAX_COLOR_REGISTERS
- 1)) {
816 System.err.printf("%d %06x --> %d %06x\n",
817 i, rawRgbList.get(i), idx, rgbColors.get(idx));
819 assert (rgbColor
== rawRgbList
.get(i
));
824 // Set the dimmest color as true black, and the brightest as true
827 rgbColors
.set(MAX_COLOR_REGISTERS
- 1, 0xFFFFFF);
830 System.err.printf("<html><body>\n");
831 for (Integer rgb: rgbColors) {
832 System.err.printf("<font style = \"color:");
833 System.err.printf("#%06x", rgb);
834 System.err.printf(";\">=</font>\n");
836 System.err.printf("\n</body></html>\n");
842 * Emit the sixel palette.
844 * @param sb the StringBuilder to append to
845 * @param used array of booleans set to true for each color actually
846 * used in this cell, or null to emit the entire palette
847 * @return the string to emit to an ANSI / ECMA-style terminal
849 public String
emitPalette(final StringBuilder sb
,
850 final boolean [] used
) {
852 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
853 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
854 int rgbColor
= rgbColors
.get(i
);
855 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
856 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
857 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
858 ( rgbColor
& 0xFF) * 100 / 255));
861 return sb
.toString();
866 * SixelCache is a least-recently-used cache that hangs on to the
867 * post-rendered sixel string for a particular set of cells.
869 private class SixelCache
{
872 * Maximum size of the cache.
874 private int maxSize
= 100;
877 * The entries stored in the cache.
879 private HashMap
<String
, CacheEntry
> cache
= null;
882 * CacheEntry is one entry in the cache.
884 private class CacheEntry
{
896 * The last time this entry was used.
898 public long millis
= 0;
901 * Public constructor.
903 * @param key the cache entry key
904 * @param data the cache entry data
906 public CacheEntry(final String key
, final String data
) {
909 this.millis
= System
.currentTimeMillis();
914 * Public constructor.
916 * @param maxSize the maximum size of the cache
918 public SixelCache(final int maxSize
) {
919 this.maxSize
= maxSize
;
920 cache
= new HashMap
<String
, CacheEntry
>();
924 * Make a unique key for a list of cells.
926 * @param cells the cells
929 private String
makeKey(final ArrayList
<Cell
> cells
) {
930 StringBuilder sb
= new StringBuilder();
931 for (Cell cell
: cells
) {
932 sb
.append(cell
.hashCode());
934 return sb
.toString();
938 * Get an entry from the cache.
940 * @param cells the list of cells that are the cache key
941 * @return the sixel string representing these cells, or null if this
942 * list of cells is not in the cache
944 public String
get(final ArrayList
<Cell
> cells
) {
945 CacheEntry entry
= cache
.get(makeKey(cells
));
949 entry
.millis
= System
.currentTimeMillis();
954 * Put an entry into the cache.
956 * @param cells the list of cells that are the cache key
957 * @param data the sixel string representing these cells
959 public void put(final ArrayList
<Cell
> cells
, final String data
) {
960 String key
= makeKey(cells
);
962 // System.err.println("put() " + key + " size " + cache.size());
964 assert (!cache
.containsKey(key
));
966 assert (cache
.size() <= maxSize
);
967 if (cache
.size() == maxSize
) {
968 // Cache is at limit, evict oldest entry.
969 long oldestTime
= Long
.MAX_VALUE
;
970 String keyToRemove
= null;
971 for (CacheEntry entry
: cache
.values()) {
972 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
973 keyToRemove
= entry
.key
;
974 oldestTime
= entry
.millis
;
978 System.err.println("put() remove key = " + keyToRemove +
979 " size " + cache.size());
981 assert (keyToRemove
!= null);
982 cache
.remove(keyToRemove
);
984 System.err.println("put() removed, size " + cache.size());
987 assert (cache
.size() <= maxSize
);
988 CacheEntry entry
= new CacheEntry(key
, data
);
989 assert (key
.equals(entry
.key
));
990 cache
.put(key
, entry
);
992 System.err.println("put() added key " + key + " " +
993 " size " + cache.size());
999 // ------------------------------------------------------------------------
1000 // Constructors -----------------------------------------------------------
1001 // ------------------------------------------------------------------------
1004 * Constructor sets up state for getEvent().
1006 * @param listener the object this backend needs to wake up when new
1008 * @param input an InputStream connected to the remote user, or null for
1009 * System.in. If System.in is used, then on non-Windows systems it will
1010 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
1011 * mode. input is always converted to a Reader with UTF-8 encoding.
1012 * @param output an OutputStream connected to the remote user, or null
1013 * for System.out. output is always converted to a Writer with UTF-8
1015 * @param windowWidth the number of text columns to start with
1016 * @param windowHeight the number of text rows to start with
1017 * @throws UnsupportedEncodingException if an exception is thrown when
1018 * creating the InputStreamReader
1020 public ECMA48Terminal(final Object listener
, final InputStream input
,
1021 final OutputStream output
, final int windowWidth
,
1022 final int windowHeight
) throws UnsupportedEncodingException
{
1024 this(listener
, input
, output
);
1026 // Send dtterm/xterm sequences, which will probably not work because
1027 // allowWindowOps is defaulted to false.
1028 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1030 this.output
.write(resizeString
);
1031 this.output
.flush();
1035 * Constructor sets up state for getEvent().
1037 * @param listener the object this backend needs to wake up when new
1039 * @param input an InputStream connected to the remote user, or null for
1040 * System.in. If System.in is used, then on non-Windows systems it will
1041 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
1042 * mode. input is always converted to a Reader with UTF-8 encoding.
1043 * @param output an OutputStream connected to the remote user, or null
1044 * for System.out. output is always converted to a Writer with UTF-8
1046 * @throws UnsupportedEncodingException if an exception is thrown when
1047 * creating the InputStreamReader
1049 public ECMA48Terminal(final Object listener
, final InputStream input
,
1050 final OutputStream output
) throws UnsupportedEncodingException
{
1056 stopReaderThread
= false;
1057 this.listener
= listener
;
1059 if (input
== null) {
1060 // inputStream = System.in;
1061 inputStream
= new FileInputStream(FileDescriptor
.in
);
1065 inputStream
= input
;
1067 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1069 if (input
instanceof SessionInfo
) {
1070 // This is a TelnetInputStream that exposes window size and
1071 // environment variables from the telnet layer.
1072 sessionInfo
= (SessionInfo
) input
;
1074 if (sessionInfo
== null) {
1075 if (input
== null) {
1076 // Reading right off the tty
1077 sessionInfo
= new TTYSessionInfo();
1079 sessionInfo
= new TSessionInfo();
1083 if (output
== null) {
1084 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1087 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1091 // Request xterm report window dimensions in pixels
1092 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1094 // Enable mouse reporting and metaSendsEscape
1095 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1096 this.output
.flush();
1098 // Query the screen size
1099 sessionInfo
.queryWindowSize();
1100 setDimensions(sessionInfo
.getWindowWidth(),
1101 sessionInfo
.getWindowHeight());
1103 // Hang onto the window size
1104 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1105 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1109 // Spin up the input reader
1110 eventQueue
= new LinkedList
<TInputEvent
>();
1111 readerThread
= new Thread(this);
1112 readerThread
.start();
1115 this.output
.write(clearAll());
1116 this.output
.flush();
1120 * Constructor sets up state for getEvent().
1122 * @param listener the object this backend needs to wake up when new
1124 * @param input the InputStream underlying 'reader'. Its available()
1125 * method is used to determine if reader.read() will block or not.
1126 * @param reader a Reader connected to the remote user.
1127 * @param writer a PrintWriter connected to the remote user.
1128 * @param setRawMode if true, set System.in into raw mode with stty.
1129 * This should in general not be used. It is here solely for Demo3,
1130 * which uses System.in.
1131 * @throws IllegalArgumentException if input, reader, or writer are null.
1133 public ECMA48Terminal(final Object listener
, final InputStream input
,
1134 final Reader reader
, final PrintWriter writer
,
1135 final boolean setRawMode
) {
1137 if (input
== null) {
1138 throw new IllegalArgumentException("InputStream must be specified");
1140 if (reader
== null) {
1141 throw new IllegalArgumentException("Reader must be specified");
1143 if (writer
== null) {
1144 throw new IllegalArgumentException("Writer must be specified");
1150 stopReaderThread
= false;
1151 this.listener
= listener
;
1153 inputStream
= input
;
1154 this.input
= reader
;
1156 if (setRawMode
== true) {
1159 this.setRawMode
= setRawMode
;
1161 if (input
instanceof SessionInfo
) {
1162 // This is a TelnetInputStream that exposes window size and
1163 // environment variables from the telnet layer.
1164 sessionInfo
= (SessionInfo
) input
;
1166 if (sessionInfo
== null) {
1167 if (setRawMode
== true) {
1168 // Reading right off the tty
1169 sessionInfo
= new TTYSessionInfo();
1171 sessionInfo
= new TSessionInfo();
1175 this.output
= writer
;
1177 // Request xterm report window dimensions in pixels
1178 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1180 // Enable mouse reporting and metaSendsEscape
1181 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1182 this.output
.flush();
1184 // Query the screen size
1185 sessionInfo
.queryWindowSize();
1186 setDimensions(sessionInfo
.getWindowWidth(),
1187 sessionInfo
.getWindowHeight());
1189 // Hang onto the window size
1190 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1191 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1195 // Spin up the input reader
1196 eventQueue
= new LinkedList
<TInputEvent
>();
1197 readerThread
= new Thread(this);
1198 readerThread
.start();
1201 this.output
.write(clearAll());
1202 this.output
.flush();
1206 * Constructor sets up state for getEvent().
1208 * @param listener the object this backend needs to wake up when new
1210 * @param input the InputStream underlying 'reader'. Its available()
1211 * method is used to determine if reader.read() will block or not.
1212 * @param reader a Reader connected to the remote user.
1213 * @param writer a PrintWriter connected to the remote user.
1214 * @throws IllegalArgumentException if input, reader, or writer are null.
1216 public ECMA48Terminal(final Object listener
, final InputStream input
,
1217 final Reader reader
, final PrintWriter writer
) {
1219 this(listener
, input
, reader
, writer
, false);
1222 // ------------------------------------------------------------------------
1223 // LogicalScreen ----------------------------------------------------------
1224 // ------------------------------------------------------------------------
1227 * Set the window title.
1229 * @param title the new title
1232 public void setTitle(final String title
) {
1233 output
.write(getSetTitleString(title
));
1238 * Push the logical screen to the physical device.
1241 public void flushPhysical() {
1242 StringBuilder sb
= new StringBuilder();
1246 && (cursorY
<= height
- 1)
1247 && (cursorX
<= width
- 1)
1250 sb
.append(cursor(true));
1251 sb
.append(gotoXY(cursorX
, cursorY
));
1253 sb
.append(cursor(false));
1256 output
.write(sb
.toString());
1261 * Resize the physical screen to match the logical screen dimensions.
1264 public void resizeToScreen() {
1265 // Send dtterm/xterm sequences, which will probably not work because
1266 // allowWindowOps is defaulted to false.
1267 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1269 this.output
.write(resizeString
);
1270 this.output
.flush();
1273 // ------------------------------------------------------------------------
1274 // TerminalReader ---------------------------------------------------------
1275 // ------------------------------------------------------------------------
1278 * Check if there are events in the queue.
1280 * @return if true, getEvents() has something to return to the backend
1282 public boolean hasEvents() {
1283 synchronized (eventQueue
) {
1284 return (eventQueue
.size() > 0);
1289 * Return any events in the IO queue.
1291 * @param queue list to append new events to
1293 public void getEvents(final List
<TInputEvent
> queue
) {
1294 synchronized (eventQueue
) {
1295 if (eventQueue
.size() > 0) {
1296 synchronized (queue
) {
1297 queue
.addAll(eventQueue
);
1305 * Restore terminal to normal state.
1307 public void closeTerminal() {
1309 // System.err.println("=== shutdown() ==="); System.err.flush();
1311 // Tell the reader thread to stop looking at input
1312 stopReaderThread
= true;
1314 readerThread
.join();
1315 } catch (InterruptedException e
) {
1316 if (debugToStderr
) {
1317 e
.printStackTrace();
1321 // Disable mouse reporting and show cursor. Defensive null check
1322 // here in case closeTerminal() is called twice.
1323 if (output
!= null) {
1324 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
1331 // We don't close System.in/out
1333 // Shut down the streams, this should wake up the reader thread
1334 // and make it exit.
1335 if (input
!= null) {
1338 } catch (IOException e
) {
1343 if (output
!= null) {
1351 * Set listener to a different Object.
1353 * @param listener the new listening object that run() wakes up on new
1356 public void setListener(final Object listener
) {
1357 this.listener
= listener
;
1361 * Reload options from System properties.
1363 public void reloadOptions() {
1364 // Permit RGB colors only if externally requested.
1365 if (System
.getProperty("jexer.ECMA48.rgbColor",
1366 "false").equals("true")
1373 // Pull the system properties for sixel output.
1374 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1381 // ------------------------------------------------------------------------
1382 // Runnable ---------------------------------------------------------------
1383 // ------------------------------------------------------------------------
1386 * Read function runs on a separate thread.
1389 boolean done
= false;
1390 // available() will often return > 1, so we need to read in chunks to
1392 char [] readBuffer
= new char[128];
1393 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1395 while (!done
&& !stopReaderThread
) {
1397 // We assume that if inputStream has bytes available, then
1398 // input won't block on read().
1399 int n
= inputStream
.available();
1402 System.err.printf("inputStream.available(): %d\n", n);
1407 if (readBuffer
.length
< n
) {
1408 // The buffer wasn't big enough, make it huger
1409 readBuffer
= new char[readBuffer
.length
* 2];
1412 // System.err.printf("BEFORE read()\n"); System.err.flush();
1414 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1417 System.err.printf("AFTER read() %d\n", rc);
1425 for (int i
= 0; i
< rc
; i
++) {
1426 int ch
= readBuffer
[i
];
1427 processChar(events
, (char)ch
);
1429 getIdleEvents(events
);
1430 if (events
.size() > 0) {
1431 // Add to the queue for the backend thread to
1432 // be able to obtain.
1433 synchronized (eventQueue
) {
1434 eventQueue
.addAll(events
);
1436 if (listener
!= null) {
1437 synchronized (listener
) {
1438 listener
.notifyAll();
1445 getIdleEvents(events
);
1446 if (events
.size() > 0) {
1447 synchronized (eventQueue
) {
1448 eventQueue
.addAll(events
);
1450 if (listener
!= null) {
1451 synchronized (listener
) {
1452 listener
.notifyAll();
1458 if (output
.checkError()) {
1463 // Wait 20 millis for more data
1466 // System.err.println("end while loop"); System.err.flush();
1467 } catch (InterruptedException e
) {
1469 } catch (IOException e
) {
1470 e
.printStackTrace();
1473 } // while ((done == false) && (stopReaderThread == false))
1475 // TODO: pass an event up to TApplication to tell it this Backend is
1478 System
.err
.println("*** run() exiting..."); System
.err
.flush();
1481 // ------------------------------------------------------------------------
1482 // ECMA48Terminal ---------------------------------------------------------
1483 // ------------------------------------------------------------------------
1486 * Get the width of a character cell in pixels.
1488 * @return the width in pixels of a character cell
1490 public int getTextWidth() {
1491 return (widthPixels
/ sessionInfo
.getWindowWidth());
1495 * Get the height of a character cell in pixels.
1497 * @return the height in pixels of a character cell
1499 public int getTextHeight() {
1500 return (heightPixels
/ sessionInfo
.getWindowHeight());
1504 * Getter for sessionInfo.
1506 * @return the SessionInfo
1508 public SessionInfo
getSessionInfo() {
1513 * Get the output writer.
1515 * @return the Writer
1517 public PrintWriter
getOutput() {
1522 * Call 'stty' to set cooked mode.
1524 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1526 private void sttyCooked() {
1531 * Call 'stty' to set raw mode.
1533 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1534 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1535 * -parenb cs8 min 1 < /dev/tty'
1537 private void sttyRaw() {
1542 * Call 'stty' to set raw or cooked mode.
1544 * @param mode if true, set raw mode, otherwise set cooked mode
1546 private void doStty(final boolean mode
) {
1547 String
[] cmdRaw
= {
1548 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1550 String
[] cmdCooked
= {
1551 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1556 process
= Runtime
.getRuntime().exec(cmdRaw
);
1558 process
= Runtime
.getRuntime().exec(cmdCooked
);
1560 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1561 String line
= in
.readLine();
1562 if ((line
!= null) && (line
.length() > 0)) {
1563 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1566 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1567 line
= err
.readLine();
1568 if ((line
!= null) && (line
.length() > 0)) {
1569 System
.err
.println("Error output from stty: " + line
);
1574 } catch (InterruptedException e
) {
1575 if (debugToStderr
) {
1576 e
.printStackTrace();
1580 int rc
= process
.exitValue();
1582 System
.err
.println("stty returned error code: " + rc
);
1584 } catch (IOException e
) {
1585 e
.printStackTrace();
1592 public void flush() {
1597 * Perform a somewhat-optimal rendering of a line.
1599 * @param y row coordinate. 0 is the top-most row.
1600 * @param sb StringBuilder to write escape sequences to
1601 * @param lastAttr cell attributes from the last call to flushLine
1603 private void flushLine(final int y
, final StringBuilder sb
,
1604 CellAttributes lastAttr
) {
1608 for (int x
= 0; x
< width
; x
++) {
1609 Cell lCell
= logical
[x
][y
];
1610 if (!lCell
.isBlank()) {
1614 // Push textEnd to first column beyond the text area
1618 // reallyCleared = true;
1620 boolean hasImage
= false;
1622 for (int x
= 0; x
< width
; x
++) {
1623 Cell lCell
= logical
[x
][y
];
1624 Cell pCell
= physical
[x
][y
];
1626 if (!lCell
.equals(pCell
) || reallyCleared
) {
1628 if (debugToStderr
) {
1629 System
.err
.printf("\n--\n");
1630 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1631 System
.err
.printf(" lCell: %s\n", lCell
);
1632 System
.err
.printf(" pCell: %s\n", pCell
);
1633 System
.err
.printf(" ==== \n");
1636 if (lastAttr
== null) {
1637 lastAttr
= new CellAttributes();
1638 sb
.append(normal());
1642 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1643 // Advancing at least one cell, or the first gotoXY
1644 sb
.append(gotoXY(x
, y
));
1647 assert (lastAttr
!= null);
1649 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1650 assert (lCell
.isBlank());
1652 for (int i
= x
; i
< width
; i
++) {
1653 assert (logical
[i
][y
].isBlank());
1654 // Physical is always updated
1655 physical
[i
][y
].reset();
1658 // Clear remaining line
1659 sb
.append(clearRemainingLine());
1664 // Image cell: bypass the rest of the loop, it is not
1666 if (lCell
.isImage()) {
1669 // Save the last rendered cell
1672 // Physical is always updated
1673 physical
[x
][y
].setTo(lCell
);
1677 assert (!lCell
.isImage());
1680 sb
.append(gotoXY(x
, y
));
1683 // Now emit only the modified attributes
1684 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1685 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1687 && (lCell
.isBold() == lastAttr
.isBold())
1688 && (lCell
.isReverse() == lastAttr
.isReverse())
1689 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1690 && (lCell
.isBlink() == lastAttr
.isBlink())
1692 // Both colors changed, attributes the same
1693 sb
.append(color(lCell
.isBold(),
1694 lCell
.getForeColor(), lCell
.getBackColor()));
1696 if (debugToStderr
) {
1697 System
.err
.printf("1 Change only fore/back colors\n");
1700 } else if (lCell
.isRGB()
1701 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1702 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1703 && (lCell
.isBold() == lastAttr
.isBold())
1704 && (lCell
.isReverse() == lastAttr
.isReverse())
1705 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1706 && (lCell
.isBlink() == lastAttr
.isBlink())
1708 // Both colors changed, attributes the same
1709 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1710 lCell
.getBackColorRGB()));
1712 if (debugToStderr
) {
1713 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1715 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1716 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1718 && (lCell
.isBold() != lastAttr
.isBold())
1719 && (lCell
.isReverse() != lastAttr
.isReverse())
1720 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1721 && (lCell
.isBlink() != lastAttr
.isBlink())
1723 // Everything is different
1724 sb
.append(color(lCell
.getForeColor(),
1725 lCell
.getBackColor(),
1726 lCell
.isBold(), lCell
.isReverse(),
1728 lCell
.isUnderline()));
1730 if (debugToStderr
) {
1731 System
.err
.printf("2 Set all attributes\n");
1733 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1734 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1736 && (lCell
.isBold() == lastAttr
.isBold())
1737 && (lCell
.isReverse() == lastAttr
.isReverse())
1738 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1739 && (lCell
.isBlink() == lastAttr
.isBlink())
1742 // Attributes same, foreColor different
1743 sb
.append(color(lCell
.isBold(),
1744 lCell
.getForeColor(), true));
1746 if (debugToStderr
) {
1747 System
.err
.printf("3 Change foreColor\n");
1749 } else if (lCell
.isRGB()
1750 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1751 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1752 && (lCell
.getForeColorRGB() >= 0)
1753 && (lCell
.getBackColorRGB() >= 0)
1754 && (lCell
.isBold() == lastAttr
.isBold())
1755 && (lCell
.isReverse() == lastAttr
.isReverse())
1756 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1757 && (lCell
.isBlink() == lastAttr
.isBlink())
1759 // Attributes same, foreColor different
1760 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1762 if (debugToStderr
) {
1763 System
.err
.printf("3 Change foreColor (RGB)\n");
1765 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1766 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1768 && (lCell
.isBold() == lastAttr
.isBold())
1769 && (lCell
.isReverse() == lastAttr
.isReverse())
1770 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1771 && (lCell
.isBlink() == lastAttr
.isBlink())
1773 // Attributes same, backColor different
1774 sb
.append(color(lCell
.isBold(),
1775 lCell
.getBackColor(), false));
1777 if (debugToStderr
) {
1778 System
.err
.printf("4 Change backColor\n");
1780 } else if (lCell
.isRGB()
1781 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1782 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1783 && (lCell
.isBold() == lastAttr
.isBold())
1784 && (lCell
.isReverse() == lastAttr
.isReverse())
1785 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1786 && (lCell
.isBlink() == lastAttr
.isBlink())
1788 // Attributes same, foreColor different
1789 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1791 if (debugToStderr
) {
1792 System
.err
.printf("4 Change backColor (RGB)\n");
1794 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1795 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1796 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1797 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1798 && (lCell
.isBold() == lastAttr
.isBold())
1799 && (lCell
.isReverse() == lastAttr
.isReverse())
1800 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1801 && (lCell
.isBlink() == lastAttr
.isBlink())
1804 // All attributes the same, just print the char
1807 if (debugToStderr
) {
1808 System
.err
.printf("5 Only emit character\n");
1811 // Just reset everything again
1812 if (!lCell
.isRGB()) {
1813 sb
.append(color(lCell
.getForeColor(),
1814 lCell
.getBackColor(),
1818 lCell
.isUnderline()));
1820 if (debugToStderr
) {
1821 System
.err
.printf("6 Change all attributes\n");
1824 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1825 lCell
.getBackColorRGB(),
1829 lCell
.isUnderline()));
1830 if (debugToStderr
) {
1831 System
.err
.printf("6 Change all attributes (RGB)\n");
1836 // Emit the character
1837 sb
.append(lCell
.getChar());
1839 // Save the last rendered cell
1841 lastAttr
.setTo(lCell
);
1843 // Physical is always updated
1844 physical
[x
][y
].setTo(lCell
);
1846 } // if (!lCell.equals(pCell) || (reallyCleared == true))
1848 } // for (int x = 0; x < width; x++)
1852 * Render the screen to a string that can be emitted to something that
1853 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
1855 * @param sb StringBuilder to write escape sequences to
1856 * @return escape sequences string that provides the updates to the
1859 private String
flushString(final StringBuilder sb
) {
1860 CellAttributes attr
= null;
1862 if (reallyCleared
) {
1863 attr
= new CellAttributes();
1864 sb
.append(clearAll());
1868 * For sixel support, draw all of the sixel output first, and then
1869 * draw everything else afterwards. This works OK, but performance
1870 * is still a drag on larger pictures.
1872 for (int y
= 0; y
< height
; y
++) {
1873 for (int x
= 0; x
< width
; x
++) {
1874 // If physical had non-image data that is now image data, the
1875 // entire row must be redrawn.
1876 Cell lCell
= logical
[x
][y
];
1877 Cell pCell
= physical
[x
][y
];
1878 if (lCell
.isImage() && !pCell
.isImage()) {
1884 for (int y
= 0; y
< height
; y
++) {
1885 for (int x
= 0; x
< width
; x
++) {
1886 Cell lCell
= logical
[x
][y
];
1887 Cell pCell
= physical
[x
][y
];
1889 if (!lCell
.isImage()) {
1895 while ((right
< width
)
1896 && (logical
[right
][y
].isImage())
1897 && (!logical
[right
][y
].equals(physical
[right
][y
])
1902 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
1903 for (int i
= 0; i
< (right
- x
); i
++) {
1904 assert (logical
[x
+ i
][y
].isImage());
1905 cellsToDraw
.add(logical
[x
+ i
][y
]);
1907 // Physical is always updated.
1908 physical
[x
+ i
][y
].setTo(lCell
);
1910 if (cellsToDraw
.size() > 0) {
1911 sb
.append(toSixel(x
, y
, cellsToDraw
));
1918 // Draw the text part now.
1919 for (int y
= 0; y
< height
; y
++) {
1920 flushLine(y
, sb
, attr
);
1923 reallyCleared
= false;
1925 String result
= sb
.toString();
1926 if (debugToStderr
) {
1927 System
.err
.printf("flushString(): %s\n", result
);
1933 * Reset keyboard/mouse input parser.
1935 private void resetParser() {
1936 state
= ParseState
.GROUND
;
1937 params
= new ArrayList
<String
>();
1943 * Produce a control character or one of the special ones (ENTER, TAB,
1946 * @param ch Unicode code point
1947 * @param alt if true, set alt on the TKeypress
1948 * @return one TKeypress event, either a control character (e.g. isKey ==
1949 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
1952 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
1953 // System.err.printf("controlChar: %02x\n", ch);
1957 // Carriage return --> ENTER
1958 return new TKeypressEvent(kbEnter
, alt
, false, false);
1960 // Linefeed --> ENTER
1961 return new TKeypressEvent(kbEnter
, alt
, false, false);
1964 return new TKeypressEvent(kbEsc
, alt
, false, false);
1967 return new TKeypressEvent(kbTab
, alt
, false, false);
1969 // Make all other control characters come back as the alphabetic
1970 // character with the ctrl field set. So SOH would be 'A' +
1972 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
1978 * Produce special key from CSI Pn ; Pm ; ... ~
1980 * @return one KEYPRESS event representing a special key
1982 private TInputEvent
csiFnKey() {
1984 if (params
.size() > 0) {
1985 key
= Integer
.parseInt(params
.get(0));
1987 boolean alt
= false;
1988 boolean ctrl
= false;
1989 boolean shift
= false;
1990 if (params
.size() > 1) {
1991 shift
= csiIsShift(params
.get(1));
1992 alt
= csiIsAlt(params
.get(1));
1993 ctrl
= csiIsCtrl(params
.get(1));
1998 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2000 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2002 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2004 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2006 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2008 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2010 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2012 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2014 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2016 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2018 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2020 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2022 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2024 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2032 * Produce mouse events based on "Any event tracking" and UTF-8
2034 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2036 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2038 private TInputEvent
parseMouse() {
2039 int buttons
= params
.get(0).charAt(0) - 32;
2040 int x
= params
.get(0).charAt(1) - 32 - 1;
2041 int y
= params
.get(0).charAt(2) - 32 - 1;
2043 // Clamp X and Y to the physical screen coordinates.
2044 if (x
>= windowResize
.getWidth()) {
2045 x
= windowResize
.getWidth() - 1;
2047 if (y
>= windowResize
.getHeight()) {
2048 y
= windowResize
.getHeight() - 1;
2051 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2052 boolean eventMouse1
= false;
2053 boolean eventMouse2
= false;
2054 boolean eventMouse3
= false;
2055 boolean eventMouseWheelUp
= false;
2056 boolean eventMouseWheelDown
= false;
2058 // System.err.printf("buttons: %04x\r\n", buttons);
2075 if (!mouse1
&& !mouse2
&& !mouse3
) {
2076 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2078 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2095 // Dragging with mouse1 down
2098 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2102 // Dragging with mouse2 down
2105 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2109 // Dragging with mouse3 down
2112 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2116 // Dragging with mouse2 down after wheelUp
2119 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2123 // Dragging with mouse2 down after wheelDown
2126 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2130 eventMouseWheelUp
= true;
2134 eventMouseWheelDown
= true;
2138 // Unknown, just make it motion
2139 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2142 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2143 eventMouse1
, eventMouse2
, eventMouse3
,
2144 eventMouseWheelUp
, eventMouseWheelDown
);
2148 * Produce mouse events based on "Any event tracking" and SGR
2150 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2152 * @param release if true, this was a release ('m')
2153 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2155 private TInputEvent
parseMouseSGR(final boolean release
) {
2156 // SGR extended coordinates - mode 1006
2157 if (params
.size() < 3) {
2158 // Invalid position, bail out.
2161 int buttons
= Integer
.parseInt(params
.get(0));
2162 int x
= Integer
.parseInt(params
.get(1)) - 1;
2163 int y
= Integer
.parseInt(params
.get(2)) - 1;
2165 // Clamp X and Y to the physical screen coordinates.
2166 if (x
>= windowResize
.getWidth()) {
2167 x
= windowResize
.getWidth() - 1;
2169 if (y
>= windowResize
.getHeight()) {
2170 y
= windowResize
.getHeight() - 1;
2173 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2174 boolean eventMouse1
= false;
2175 boolean eventMouse2
= false;
2176 boolean eventMouse3
= false;
2177 boolean eventMouseWheelUp
= false;
2178 boolean eventMouseWheelDown
= false;
2181 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2195 // Motion only, no buttons down
2196 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2200 // Dragging with mouse1 down
2202 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2206 // Dragging with mouse2 down
2208 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2212 // Dragging with mouse3 down
2214 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2218 // Dragging with mouse2 down after wheelUp
2220 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2224 // Dragging with mouse2 down after wheelDown
2226 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2230 eventMouseWheelUp
= true;
2234 eventMouseWheelDown
= true;
2238 // Unknown, bail out
2241 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2242 eventMouse1
, eventMouse2
, eventMouse3
,
2243 eventMouseWheelUp
, eventMouseWheelDown
);
2247 * Return any events in the IO queue due to timeout.
2249 * @param queue list to append new events to
2251 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2252 long nowTime
= System
.currentTimeMillis();
2254 // Check for new window size
2255 long windowSizeDelay
= nowTime
- windowSizeTime
;
2256 if (windowSizeDelay
> 1000) {
2257 int oldTextWidth
= getTextWidth();
2258 int oldTextHeight
= getTextHeight();
2260 sessionInfo
.queryWindowSize();
2261 int newWidth
= sessionInfo
.getWindowWidth();
2262 int newHeight
= sessionInfo
.getWindowHeight();
2264 if ((newWidth
!= windowResize
.getWidth())
2265 || (newHeight
!= windowResize
.getHeight())
2268 // Request xterm report window dimensions in pixels again.
2269 // Between now and then, ensure that the reported text cell
2270 // size is the same by setting widthPixels and heightPixels
2271 // to match the new dimensions.
2272 widthPixels
= oldTextWidth
* newWidth
;
2273 heightPixels
= oldTextHeight
* newHeight
;
2275 if (debugToStderr
) {
2276 System
.err
.println("Screen size changed, old size " +
2278 System
.err
.println(" new size " +
2279 newWidth
+ " x " + newHeight
);
2280 System
.err
.println(" old pixels " +
2281 oldTextWidth
+ " x " + oldTextHeight
);
2282 System
.err
.println(" new pixels " +
2283 getTextWidth() + " x " + getTextHeight());
2286 this.output
.printf("%s", xtermReportWindowPixelDimensions());
2287 this.output
.flush();
2289 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2290 newWidth
, newHeight
);
2291 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2292 newWidth
, newHeight
);
2295 windowSizeTime
= nowTime
;
2298 // ESCDELAY type timeout
2299 if (state
== ParseState
.ESCAPE
) {
2300 long escDelay
= nowTime
- escapeTime
;
2301 if (escDelay
> 100) {
2302 // After 0.1 seconds, assume a true escape character
2303 queue
.add(controlChar((char)0x1B, false));
2310 * Returns true if the CSI parameter for a keyboard command means that
2313 private boolean csiIsShift(final String x
) {
2325 * Returns true if the CSI parameter for a keyboard command means that
2328 private boolean csiIsAlt(final String x
) {
2340 * Returns true if the CSI parameter for a keyboard command means that
2343 private boolean csiIsCtrl(final String x
) {
2355 * Parses the next character of input to see if an InputEvent is
2358 * @param events list to append new events to
2359 * @param ch Unicode code point
2361 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2363 // ESCDELAY type timeout
2364 long nowTime
= System
.currentTimeMillis();
2365 if (state
== ParseState
.ESCAPE
) {
2366 long escDelay
= nowTime
- escapeTime
;
2367 if (escDelay
> 250) {
2368 // After 0.25 seconds, assume a true escape character
2369 events
.add(controlChar((char)0x1B, false));
2375 boolean ctrl
= false;
2376 boolean alt
= false;
2377 boolean shift
= false;
2379 // System.err.printf("state: %s ch %c\r\n", state, ch);
2385 state
= ParseState
.ESCAPE
;
2386 escapeTime
= nowTime
;
2391 // Control character
2392 events
.add(controlChar(ch
, false));
2399 events
.add(new TKeypressEvent(false, 0, ch
,
2400 false, false, false));
2409 // ALT-Control character
2410 events
.add(controlChar(ch
, true));
2416 // This will be one of the function keys
2417 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2421 // '[' goes to CSI_ENTRY
2423 state
= ParseState
.CSI_ENTRY
;
2427 // Everything else is assumed to be Alt-keystroke
2428 if ((ch
>= 'A') && (ch
<= 'Z')) {
2432 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2436 case ESCAPE_INTERMEDIATE
:
2437 if ((ch
>= 'P') && (ch
<= 'S')) {
2441 events
.add(new TKeypressEvent(kbF1
));
2444 events
.add(new TKeypressEvent(kbF2
));
2447 events
.add(new TKeypressEvent(kbF3
));
2450 events
.add(new TKeypressEvent(kbF4
));
2459 // Unknown keystroke, ignore
2464 // Numbers - parameter values
2465 if ((ch
>= '0') && (ch
<= '9')) {
2466 params
.set(params
.size() - 1,
2467 params
.get(params
.size() - 1) + ch
);
2468 state
= ParseState
.CSI_PARAM
;
2471 // Parameter separator
2477 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2481 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2486 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2491 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2496 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2501 events
.add(new TKeypressEvent(kbHome
));
2506 events
.add(new TKeypressEvent(kbEnd
));
2510 // CBT - Cursor backward X tab stops (default 1)
2511 events
.add(new TKeypressEvent(kbBackTab
));
2516 state
= ParseState
.MOUSE
;
2519 // Mouse position, SGR (1006) coordinates
2520 state
= ParseState
.MOUSE_SGR
;
2527 // Unknown keystroke, ignore
2532 // Numbers - parameter values
2533 if ((ch
>= '0') && (ch
<= '9')) {
2534 params
.set(params
.size() - 1,
2535 params
.get(params
.size() - 1) + ch
);
2538 // Parameter separator
2546 // Generate a mouse press event
2547 TInputEvent event
= parseMouseSGR(false);
2548 if (event
!= null) {
2554 // Generate a mouse release event
2555 event
= parseMouseSGR(true);
2556 if (event
!= null) {
2565 // Unknown keystroke, ignore
2570 // Numbers - parameter values
2571 if ((ch
>= '0') && (ch
<= '9')) {
2572 params
.set(params
.size() - 1,
2573 params
.get(params
.size() - 1) + ch
);
2574 state
= ParseState
.CSI_PARAM
;
2577 // Parameter separator
2584 events
.add(csiFnKey());
2589 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2593 if (params
.size() > 1) {
2594 shift
= csiIsShift(params
.get(1));
2595 alt
= csiIsAlt(params
.get(1));
2596 ctrl
= csiIsCtrl(params
.get(1));
2598 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2603 if (params
.size() > 1) {
2604 shift
= csiIsShift(params
.get(1));
2605 alt
= csiIsAlt(params
.get(1));
2606 ctrl
= csiIsCtrl(params
.get(1));
2608 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2613 if (params
.size() > 1) {
2614 shift
= csiIsShift(params
.get(1));
2615 alt
= csiIsAlt(params
.get(1));
2616 ctrl
= csiIsCtrl(params
.get(1));
2618 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2623 if (params
.size() > 1) {
2624 shift
= csiIsShift(params
.get(1));
2625 alt
= csiIsAlt(params
.get(1));
2626 ctrl
= csiIsCtrl(params
.get(1));
2628 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2633 if (params
.size() > 1) {
2634 shift
= csiIsShift(params
.get(1));
2635 alt
= csiIsAlt(params
.get(1));
2636 ctrl
= csiIsCtrl(params
.get(1));
2638 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2643 if (params
.size() > 1) {
2644 shift
= csiIsShift(params
.get(1));
2645 alt
= csiIsAlt(params
.get(1));
2646 ctrl
= csiIsCtrl(params
.get(1));
2648 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2653 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2654 if (debugToStderr
) {
2655 System
.err
.printf("windowOp pixels: " +
2656 "height %s width %s\n",
2657 params
.get(1), params
.get(2));
2660 widthPixels
= Integer
.parseInt(params
.get(2));
2661 heightPixels
= Integer
.parseInt(params
.get(1));
2662 } catch (NumberFormatException e
) {
2663 if (debugToStderr
) {
2664 e
.printStackTrace();
2667 if (widthPixels
<= 0) {
2670 if (heightPixels
<= 0) {
2681 // Unknown keystroke, ignore
2686 params
.set(0, params
.get(params
.size() - 1) + ch
);
2687 if (params
.get(0).length() == 3) {
2688 // We have enough to generate a mouse event
2689 events
.add(parseMouse());
2698 // This "should" be impossible to reach
2703 * Request (u)xterm to report the current window size dimensions.
2705 * @return the string to emit to xterm
2707 private String
xtermReportWindowPixelDimensions() {
2712 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2713 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2716 * @param on if true, enable metaSendsEscape
2717 * @return the string to emit to xterm
2719 private String
xtermMetaSendsEscape(final boolean on
) {
2721 return "\033[?1036h\033[?1034l";
2723 return "\033[?1036l";
2727 * Create an xterm OSC sequence to change the window title.
2729 * @param title the new title
2730 * @return the string to emit to xterm
2732 private String
getSetTitleString(final String title
) {
2733 return "\033]2;" + title
+ "\007";
2736 // ------------------------------------------------------------------------
2737 // Sixel output support ---------------------------------------------------
2738 // ------------------------------------------------------------------------
2741 * Start a sixel string for display one row's worth of bitmap data.
2743 * @param x column coordinate. 0 is the left-most column.
2744 * @param y row coordinate. 0 is the top-most row.
2745 * @return the string to emit to an ANSI / ECMA-style terminal
2747 private String
startSixel(final int x
, final int y
) {
2748 StringBuilder sb
= new StringBuilder();
2750 assert (sixel
== true);
2753 sb
.append(gotoXY(x
, y
));
2756 sb
.append("\033Pq");
2758 if (palette
== null) {
2759 palette
= new SixelPalette();
2762 return sb
.toString();
2766 * End a sixel string for display one row's worth of bitmap data.
2768 * @return the string to emit to an ANSI / ECMA-style terminal
2770 private String
endSixel() {
2771 assert (sixel
== true);
2778 * Create a sixel string representing a row of several cells containing
2781 * @param x column coordinate. 0 is the left-most column.
2782 * @param y row coordinate. 0 is the top-most row.
2783 * @param cells the cells containing the bitmap data
2784 * @return the string to emit to an ANSI / ECMA-style terminal
2786 private String
toSixel(final int x
, final int y
,
2787 final ArrayList
<Cell
> cells
) {
2789 StringBuilder sb
= new StringBuilder();
2791 assert (cells
!= null);
2792 assert (cells
.size() > 0);
2793 assert (cells
.get(0).getImage() != null);
2795 if (sixel
== false) {
2796 sb
.append(normal());
2797 sb
.append(gotoXY(x
, y
));
2798 for (int i
= 0; i
< cells
.size(); i
++) {
2801 return sb
.toString();
2804 if (sixelCache
== null) {
2805 sixelCache
= new SixelCache(height
* 10);
2808 // Save and get rows to/from the cache that do NOT have inverted
2810 boolean saveInCache
= true;
2811 for (Cell cell
: cells
) {
2812 if (cell
.isInvertedImage()) {
2813 saveInCache
= false;
2817 String cachedResult
= sixelCache
.get(cells
);
2818 if (cachedResult
!= null) {
2819 // System.err.println("CACHE HIT");
2820 sb
.append(startSixel(x
, y
));
2821 sb
.append(cachedResult
);
2822 sb
.append(endSixel());
2823 return sb
.toString();
2825 // System.err.println("CACHE MISS");
2828 int imageWidth
= cells
.get(0).getImage().getWidth();
2829 int imageHeight
= cells
.get(0).getImage().getHeight();
2831 // cells.get(x).getImage() has a dithered bitmap containing indexes
2832 // into the color palette. Piece these together into one larger
2833 // image for final rendering.
2835 int fullWidth
= cells
.size() * getTextWidth();
2836 int fullHeight
= getTextHeight();
2837 for (int i
= 0; i
< cells
.size(); i
++) {
2838 totalWidth
+= cells
.get(i
).getImage().getWidth();
2841 BufferedImage image
= new BufferedImage(fullWidth
,
2842 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
2845 for (int i
= 0; i
< cells
.size() - 1; i
++) {
2846 if (cells
.get(i
).isInvertedImage()) {
2847 rgbArray
= new int[imageWidth
* imageHeight
];
2848 for (int j
= 0; j
< rgbArray
.length
; j
++) {
2849 rgbArray
[j
] = 0xFFFFFF;
2852 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
2853 imageWidth
, imageHeight
, null, 0, imageWidth
);
2857 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
2858 i * imageWidth, 0, imageWidth, imageHeight,
2860 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
2861 fullWidth, fullHeight, cells.size(), getTextWidth());
2864 image
.setRGB(i
* imageWidth
, 0, imageWidth
, imageHeight
,
2865 rgbArray
, 0, imageWidth
);
2866 if (imageHeight
< fullHeight
) {
2867 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
2868 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2869 for (int imageY
= imageHeight
; imageY
< fullHeight
;
2872 image
.setRGB(imageX
, imageY
, backgroundColor
);
2877 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
2878 if (cells
.get(cells
.size() - 1).isInvertedImage()) {
2879 rgbArray
= new int[totalWidth
* imageHeight
];
2880 for (int j
= 0; j
< rgbArray
.length
; j
++) {
2881 rgbArray
[j
] = 0xFFFFFF;
2884 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
2885 totalWidth
, imageHeight
, null, 0, totalWidth
);
2887 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
2888 imageHeight
, rgbArray
, 0, totalWidth
);
2890 if (totalWidth
< getTextWidth()) {
2891 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
2893 for (int imageX
= image
.getWidth() - totalWidth
;
2894 imageX
< image
.getWidth(); imageX
++) {
2896 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
2897 image
.setRGB(imageX
, imageY
, backgroundColor
);
2902 // Dither the image. It is ok to lose the original here.
2903 if (palette
== null) {
2904 palette
= new SixelPalette();
2906 image
= palette
.ditherImage(image
);
2908 // Emit the palette, but only for the colors actually used by these
2910 boolean [] usedColors
= new boolean[MAX_COLOR_REGISTERS
];
2911 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2912 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
2913 usedColors
[image
.getRGB(imageX
, imageY
)] = true;
2916 palette
.emitPalette(sb
, usedColors
);
2918 // Render the entire row of cells.
2919 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
2920 int [][] sixels
= new int[image
.getWidth()][6];
2922 // See which colors are actually used in this band of sixels.
2923 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2924 for (int imageY
= 0;
2925 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
2928 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
2929 assert (colorIdx
>= 0);
2930 assert (colorIdx
< MAX_COLOR_REGISTERS
);
2932 sixels
[imageX
][imageY
] = colorIdx
;
2936 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
2937 boolean isUsed
= false;
2938 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2939 for (int j
= 0; j
< 6; j
++) {
2940 if (sixels
[imageX
][j
] == i
) {
2945 if (isUsed
== false) {
2949 // Set to the beginning of scan line for the next set of
2950 // colored pixels, and select the color.
2951 sb
.append(String
.format("$#%d", i
));
2953 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2955 // Add up all the pixels that match this color.
2958 (j
< 6) && (currentRow
+ j
< fullHeight
);
2961 if (sixels
[imageX
][j
] == i
) {
2985 assert (data
< 127);
2987 sb
.append((char) data
);
2988 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
2989 } // for (int i = 0; i < MAX_COLOR_REGISTERS; i++)
2991 // Advance to the next scan line.
2994 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
2996 // Kill the very last "-", because it is unnecessary.
2997 sb
.deleteCharAt(sb
.length() - 1);
3000 // This row is OK to save into the cache.
3001 sixelCache
.put(cells
, sb
.toString());
3004 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3007 // ------------------------------------------------------------------------
3008 // End sixel output support -----------------------------------------------
3009 // ------------------------------------------------------------------------
3012 * Create a SGR parameter sequence for a single color change.
3014 * @param bold if true, set bold
3015 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3016 * @param foreground if true, this is a foreground color
3017 * @return the string to emit to an ANSI / ECMA-style terminal,
3020 private String
color(final boolean bold
, final Color color
,
3021 final boolean foreground
) {
3022 return color(color
, foreground
, true) +
3023 rgbColor(bold
, color
, foreground
);
3027 * Create a T.416 RGB parameter sequence for a single color change.
3029 * @param colorRGB a 24-bit RGB value for foreground color
3030 * @param foreground if true, this is a foreground color
3031 * @return the string to emit to an ANSI / ECMA-style terminal,
3034 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
3036 int colorRed
= (colorRGB
>>> 16) & 0xFF;
3037 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
3038 int colorBlue
= colorRGB
& 0xFF;
3040 StringBuilder sb
= new StringBuilder();
3042 sb
.append("\033[38;2;");
3044 sb
.append("\033[48;2;");
3046 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
3047 return sb
.toString();
3051 * Create a T.416 RGB parameter sequence for both foreground and
3052 * background color change.
3054 * @param foreColorRGB a 24-bit RGB value for foreground color
3055 * @param backColorRGB a 24-bit RGB value for foreground color
3056 * @return the string to emit to an ANSI / ECMA-style terminal,
3059 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
3060 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3061 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3062 int foreColorBlue
= foreColorRGB
& 0xFF;
3063 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3064 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3065 int backColorBlue
= backColorRGB
& 0xFF;
3067 StringBuilder sb
= new StringBuilder();
3068 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
3069 foreColorRed
, foreColorGreen
, foreColorBlue
));
3070 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
3071 backColorRed
, backColorGreen
, backColorBlue
));
3072 return sb
.toString();
3076 * Create a T.416 RGB parameter sequence for a single color change.
3078 * @param bold if true, set bold
3079 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3080 * @param foreground if true, this is a foreground color
3081 * @return the string to emit to an xterm terminal with RGB support,
3082 * e.g. "\033[38;2;RR;GG;BBm"
3084 private String
rgbColor(final boolean bold
, final Color color
,
3085 final boolean foreground
) {
3086 if (doRgbColor
== false) {
3089 StringBuilder sb
= new StringBuilder("\033[");
3091 // Bold implies foreground only
3093 if (color
.equals(Color
.BLACK
)) {
3094 sb
.append("84;84;84");
3095 } else if (color
.equals(Color
.RED
)) {
3096 sb
.append("252;84;84");
3097 } else if (color
.equals(Color
.GREEN
)) {
3098 sb
.append("84;252;84");
3099 } else if (color
.equals(Color
.YELLOW
)) {
3100 sb
.append("252;252;84");
3101 } else if (color
.equals(Color
.BLUE
)) {
3102 sb
.append("84;84;252");
3103 } else if (color
.equals(Color
.MAGENTA
)) {
3104 sb
.append("252;84;252");
3105 } else if (color
.equals(Color
.CYAN
)) {
3106 sb
.append("84;252;252");
3107 } else if (color
.equals(Color
.WHITE
)) {
3108 sb
.append("252;252;252");
3116 if (color
.equals(Color
.BLACK
)) {
3118 } else if (color
.equals(Color
.RED
)) {
3119 sb
.append("168;0;0");
3120 } else if (color
.equals(Color
.GREEN
)) {
3121 sb
.append("0;168;0");
3122 } else if (color
.equals(Color
.YELLOW
)) {
3123 sb
.append("168;84;0");
3124 } else if (color
.equals(Color
.BLUE
)) {
3125 sb
.append("0;0;168");
3126 } else if (color
.equals(Color
.MAGENTA
)) {
3127 sb
.append("168;0;168");
3128 } else if (color
.equals(Color
.CYAN
)) {
3129 sb
.append("0;168;168");
3130 } else if (color
.equals(Color
.WHITE
)) {
3131 sb
.append("168;168;168");
3135 return sb
.toString();
3139 * Create a T.416 RGB parameter sequence for both foreground and
3140 * background color change.
3142 * @param bold if true, set bold
3143 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3144 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3145 * @return the string to emit to an xterm terminal with RGB support,
3146 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
3148 private String
rgbColor(final boolean bold
, final Color foreColor
,
3149 final Color backColor
) {
3150 if (doRgbColor
== false) {
3154 return rgbColor(bold
, foreColor
, true) +
3155 rgbColor(false, backColor
, false);
3159 * Create a SGR parameter sequence for a single color change.
3161 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3162 * @param foreground if true, this is a foreground color
3163 * @param header if true, make the full header, otherwise just emit the
3164 * color parameter e.g. "42;"
3165 * @return the string to emit to an ANSI / ECMA-style terminal,
3168 private String
color(final Color color
, final boolean foreground
,
3169 final boolean header
) {
3171 int ecmaColor
= color
.getValue();
3173 // Convert Color.* values to SGR numerics
3181 return String
.format("\033[%dm", ecmaColor
);
3183 return String
.format("%d;", ecmaColor
);
3188 * Create a SGR parameter sequence for both foreground and background
3191 * @param bold if true, set bold
3192 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3193 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3194 * @return the string to emit to an ANSI / ECMA-style terminal,
3195 * e.g. "\033[31;42m"
3197 private String
color(final boolean bold
, final Color foreColor
,
3198 final Color backColor
) {
3199 return color(foreColor
, backColor
, true) +
3200 rgbColor(bold
, foreColor
, backColor
);
3204 * Create a SGR parameter sequence for both foreground and
3205 * background color change.
3207 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3208 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3209 * @param header if true, make the full header, otherwise just emit the
3210 * color parameter e.g. "31;42;"
3211 * @return the string to emit to an ANSI / ECMA-style terminal,
3212 * e.g. "\033[31;42m"
3214 private String
color(final Color foreColor
, final Color backColor
,
3215 final boolean header
) {
3217 int ecmaForeColor
= foreColor
.getValue();
3218 int ecmaBackColor
= backColor
.getValue();
3220 // Convert Color.* values to SGR numerics
3221 ecmaBackColor
+= 40;
3222 ecmaForeColor
+= 30;
3225 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
3227 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
3232 * Create a SGR parameter sequence for foreground, background, and
3233 * several attributes. This sequence first resets all attributes to
3234 * default, then sets attributes as per the parameters.
3236 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3237 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3238 * @param bold if true, set bold
3239 * @param reverse if true, set reverse
3240 * @param blink if true, set blink
3241 * @param underline if true, set underline
3242 * @return the string to emit to an ANSI / ECMA-style terminal,
3243 * e.g. "\033[0;1;31;42m"
3245 private String
color(final Color foreColor
, final Color backColor
,
3246 final boolean bold
, final boolean reverse
, final boolean blink
,
3247 final boolean underline
) {
3249 int ecmaForeColor
= foreColor
.getValue();
3250 int ecmaBackColor
= backColor
.getValue();
3252 // Convert Color.* values to SGR numerics
3253 ecmaBackColor
+= 40;
3254 ecmaForeColor
+= 30;
3256 StringBuilder sb
= new StringBuilder();
3257 if ( bold
&& reverse
&& blink
&& !underline
) {
3258 sb
.append("\033[0;1;7;5;");
3259 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3260 sb
.append("\033[0;1;7;");
3261 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3262 sb
.append("\033[0;7;5;");
3263 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3264 sb
.append("\033[0;1;5;");
3265 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3266 sb
.append("\033[0;1;");
3267 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3268 sb
.append("\033[0;7;");
3269 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3270 sb
.append("\033[0;5;");
3271 } else if ( bold
&& reverse
&& blink
&& underline
) {
3272 sb
.append("\033[0;1;7;5;4;");
3273 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3274 sb
.append("\033[0;1;7;4;");
3275 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3276 sb
.append("\033[0;7;5;4;");
3277 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3278 sb
.append("\033[0;1;5;4;");
3279 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3280 sb
.append("\033[0;1;4;");
3281 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3282 sb
.append("\033[0;7;4;");
3283 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3284 sb
.append("\033[0;5;4;");
3285 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3286 sb
.append("\033[0;4;");
3288 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3289 sb
.append("\033[0;");
3291 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
3292 sb
.append(rgbColor(bold
, foreColor
, backColor
));
3293 return sb
.toString();
3297 * Create a SGR parameter sequence for foreground, background, and
3298 * several attributes. This sequence first resets all attributes to
3299 * default, then sets attributes as per the parameters.
3301 * @param foreColorRGB a 24-bit RGB value for foreground color
3302 * @param backColorRGB a 24-bit RGB value for foreground color
3303 * @param bold if true, set bold
3304 * @param reverse if true, set reverse
3305 * @param blink if true, set blink
3306 * @param underline if true, set underline
3307 * @return the string to emit to an ANSI / ECMA-style terminal,
3308 * e.g. "\033[0;1;31;42m"
3310 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
3311 final boolean bold
, final boolean reverse
, final boolean blink
,
3312 final boolean underline
) {
3314 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3315 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3316 int foreColorBlue
= foreColorRGB
& 0xFF;
3317 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3318 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3319 int backColorBlue
= backColorRGB
& 0xFF;
3321 StringBuilder sb
= new StringBuilder();
3322 if ( bold
&& reverse
&& blink
&& !underline
) {
3323 sb
.append("\033[0;1;7;5;");
3324 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3325 sb
.append("\033[0;1;7;");
3326 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3327 sb
.append("\033[0;7;5;");
3328 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3329 sb
.append("\033[0;1;5;");
3330 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3331 sb
.append("\033[0;1;");
3332 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3333 sb
.append("\033[0;7;");
3334 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3335 sb
.append("\033[0;5;");
3336 } else if ( bold
&& reverse
&& blink
&& underline
) {
3337 sb
.append("\033[0;1;7;5;4;");
3338 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3339 sb
.append("\033[0;1;7;4;");
3340 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3341 sb
.append("\033[0;7;5;4;");
3342 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3343 sb
.append("\033[0;1;5;4;");
3344 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3345 sb
.append("\033[0;1;4;");
3346 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3347 sb
.append("\033[0;7;4;");
3348 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3349 sb
.append("\033[0;5;4;");
3350 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3351 sb
.append("\033[0;4;");
3353 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3354 sb
.append("\033[0;");
3357 sb
.append("m\033[38;2;");
3358 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
3360 sb
.append("m\033[48;2;");
3361 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
3364 return sb
.toString();
3368 * Create a SGR parameter sequence to reset to defaults.
3370 * @return the string to emit to an ANSI / ECMA-style terminal,
3373 private String
normal() {
3374 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
3378 * Create a SGR parameter sequence to reset to defaults.
3380 * @param header if true, make the full header, otherwise just emit the
3381 * bare parameter e.g. "0;"
3382 * @return the string to emit to an ANSI / ECMA-style terminal,
3385 private String
normal(final boolean header
) {
3387 return "\033[0;37;40m";
3393 * Create a SGR parameter sequence for enabling the visible cursor.
3395 * @param on if true, turn on cursor
3396 * @return the string to emit to an ANSI / ECMA-style terminal
3398 private String
cursor(final boolean on
) {
3399 if (on
&& !cursorOn
) {
3403 if (!on
&& cursorOn
) {
3411 * Clear the entire screen. Because some terminals use back-color-erase,
3412 * set the color to white-on-black beforehand.
3414 * @return the string to emit to an ANSI / ECMA-style terminal
3416 private String
clearAll() {
3417 return "\033[0;37;40m\033[2J";
3421 * Clear the line from the cursor (inclusive) to the end of the screen.
3422 * Because some terminals use back-color-erase, set the color to
3423 * white-on-black beforehand.
3425 * @return the string to emit to an ANSI / ECMA-style terminal
3427 private String
clearRemainingLine() {
3428 return "\033[0;37;40m\033[K";
3432 * Move the cursor to (x, y).
3434 * @param x column coordinate. 0 is the left-most column.
3435 * @param y row coordinate. 0 is the top-most row.
3436 * @return the string to emit to an ANSI / ECMA-style terminal
3438 private String
gotoXY(final int x
, final int y
) {
3439 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
3443 * Tell (u)xterm that we want to receive mouse events based on "Any event
3444 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
3445 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
3447 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
3449 * Note that this also sets the alternate/primary screen buffer.
3451 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
3452 * mean "hide the mouse pointer." We have to use our own sequence to do
3453 * this because there is no standard in xterm for unilaterally hiding the
3454 * pointer all the time (regardless of typing).
3456 * @param on If true, enable mouse report and use the alternate screen
3457 * buffer. If false disable mouse reporting and use the primary screen
3459 * @return the string to emit to xterm
3461 private String
mouse(final boolean on
) {
3463 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
3465 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";