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
;
51 import jexer
.bits
.Cell
;
52 import jexer
.bits
.CellAttributes
;
53 import jexer
.bits
.Color
;
54 import jexer
.event
.TInputEvent
;
55 import jexer
.event
.TKeypressEvent
;
56 import jexer
.event
.TMouseEvent
;
57 import jexer
.event
.TResizeEvent
;
58 import static jexer
.TKeypress
.*;
61 * This class reads keystrokes and mouse events and emits output to ANSI
62 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
64 public class ECMA48Terminal
extends LogicalScreen
65 implements TerminalReader
, Runnable
{
67 // ------------------------------------------------------------------------
68 // Constants --------------------------------------------------------------
69 // ------------------------------------------------------------------------
72 * States in the input parser.
74 private enum ParseState
{
85 * Number of colors in the sixel palette. Xterm 335 defines the max as
88 private static final int MAX_COLOR_REGISTERS
= 1024;
90 // ------------------------------------------------------------------------
91 // Variables --------------------------------------------------------------
92 // ------------------------------------------------------------------------
95 * Emit debugging to stderr.
97 private boolean debugToStderr
= false;
100 * If true, emit T.416-style RGB colors for normal system colors. This
101 * is a) expensive in bandwidth, and b) potentially terrible looking for
104 private static boolean doRgbColor
= false;
107 * The session information.
109 private SessionInfo sessionInfo
;
112 * The event queue, filled up by a thread reading on input.
114 private List
<TInputEvent
> eventQueue
;
117 * If true, we want the reader thread to exit gracefully.
119 private boolean stopReaderThread
;
124 private Thread readerThread
;
127 * Parameters being collected. E.g. if the string is \033[1;3m, then
128 * params[0] will be 1 and params[1] will be 3.
130 private List
<String
> params
;
133 * Current parsing state.
135 private ParseState state
;
138 * The time we entered ESCAPE. If we get a bare escape without a code
139 * following it, this is used to return that bare escape.
141 private long escapeTime
;
144 * The time we last checked the window size. We try not to spawn stty
145 * more than once per second.
147 private long windowSizeTime
;
150 * true if mouse1 was down. Used to report mouse1 on the release event.
152 private boolean mouse1
;
155 * true if mouse2 was down. Used to report mouse2 on the release event.
157 private boolean mouse2
;
160 * true if mouse3 was down. Used to report mouse3 on the release event.
162 private boolean mouse3
;
165 * Cache the cursor visibility value so we only emit the sequence when we
168 private boolean cursorOn
= true;
171 * Cache the last window size to figure out if a TResizeEvent needs to be
174 private TResizeEvent windowResize
= null;
177 * Window width in pixels. Used for sixel support.
179 private int widthPixels
= 640;
182 * Window height in pixels. Used for sixel support.
184 private int heightPixels
= 400;
187 * If true, emit image data via sixel.
189 private boolean sixel
= true;
192 * The sixel palette handler.
194 private SixelPalette palette
= null;
197 * The sixel post-rendered string cache.
199 private SixelCache sixelCache
= null;
202 * If true, then we changed System.in and need to change it back.
204 private boolean setRawMode
;
207 * The terminal's input. If an InputStream is not specified in the
208 * constructor, then this InputStreamReader will be bound to System.in
209 * with UTF-8 encoding.
211 private Reader input
;
214 * The terminal's raw InputStream. If an InputStream is not specified in
215 * the constructor, then this InputReader will be bound to System.in.
216 * This is used by run() to see if bytes are available() before calling
217 * (Reader)input.read().
219 private InputStream inputStream
;
222 * The terminal's output. If an OutputStream is not specified in the
223 * constructor, then this PrintWriter will be bound to System.out with
226 private PrintWriter output
;
229 * The listening object that run() wakes up on new input.
231 private Object listener
;
234 * SixelPalette is used to manage the conversion of images between 24-bit
235 * RGB color and a palette of MAX_COLOR_REGISTERS colors.
237 private class SixelPalette
{
240 * Color palette for sixel output, sorted low to high.
242 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
245 * Map of color palette index for sixel output, from the order it was
246 * generated by makePalette() to rgbColors.
248 private int [] rgbSortedIndex
= new int[MAX_COLOR_REGISTERS
];
251 * The color palette, organized by hue, saturation, and luminance.
252 * This is used for a fast color match.
254 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
257 * Number of bits for hue.
259 private int hueBits
= -1;
262 * Number of bits for saturation.
264 private int satBits
= -1;
267 * Number of bits for luminance.
269 private int lumBits
= -1;
272 * Step size for hue bins.
274 private int hueStep
= -1;
277 * Step size for saturation bins.
279 private int satStep
= -1;
282 * Cached RGB to HSL result.
284 private int hsl
[] = new int[3];
287 * ColorIdx records a RGB color and its palette index.
289 private class ColorIdx
{
291 * The 24-bit RGB color.
296 * The palette index for this color.
301 * Public constructor.
303 * @param color the 24-bit RGB color
304 * @param index the palette index for this color
306 public ColorIdx(final int color
, final int index
) {
313 * Public constructor.
315 public SixelPalette() {
320 * Find the nearest match for a color in the palette.
322 * @param color the RGB color
323 * @return the index in rgbColors that is closest to color
325 public int matchColor(final int color
) {
330 * matchColor() is a critical performance bottleneck. To make it
331 * decent, we do the following:
333 * 1. Find the nearest two hues that bracket this color.
335 * 2. Find the nearest two saturations that bracket this color.
337 * 3. Iterate within these four bands of luminance values,
338 * returning the closest color by Euclidean distance.
340 * This strategy reduces the search space by about 97%.
342 int red
= (color
>>> 16) & 0xFF;
343 int green
= (color
>>> 8) & 0xFF;
344 int blue
= color
& 0xFF;
346 rgbToHsl(red
, green
, blue
, hsl
);
350 // System.err.printf("%d %d %d\n", hue, sat, lum);
352 double diff
= Double
.MAX_VALUE
;
355 int hue1
= hue
/ (360/hueStep
);
357 if (hue1
>= hslColors
.size() - 1) {
358 // Bracket pure red from above.
359 hue1
= hslColors
.size() - 1;
361 } else if (hue1
== 0) {
362 // Bracket pure red from below.
363 hue2
= hslColors
.size() - 1;
366 for (int hI
= hue1
; hI
!= -1;) {
367 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
370 } else if (hI
== hue2
) {
374 int sMin
= (sat
/ satStep
) - 1;
379 } else if (sMin
== sats
.size() - 1) {
384 assert (sMax
- sMin
== 1);
387 // int sMax = sats.size() - 1;
389 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
390 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
392 // True 3D colorspace match for the remaining values
393 for (ColorIdx c
: lums
) {
394 int rgbColor
= c
.color
;
396 int red2
= (rgbColor
>>> 16) & 0xFF;
397 int green2
= (rgbColor
>>> 8) & 0xFF;
398 int blue2
= rgbColor
& 0xFF;
399 newDiff
+= Math
.pow(red2
- red
, 2);
400 newDiff
+= Math
.pow(green2
- green
, 2);
401 newDiff
+= Math
.pow(blue2
- blue
, 2);
402 if (newDiff
< diff
) {
403 idx
= rgbSortedIndex
[c
.index
];
410 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
411 // Black is a closer match.
413 } else if ((((255 - red
) * (255 - red
)) +
414 ((255 - green
) * (255 - green
)) +
415 ((255 - blue
) * (255 - blue
))) < diff
) {
417 // White is a closer match.
418 idx
= MAX_COLOR_REGISTERS
- 1;
425 * Clamp an int value to [0, 255].
427 * @param x the int value
428 * @return an int between 0 and 255.
430 private int clamp(final int x
) {
441 * Dither an image to a MAX_COLOR_REGISTERS palette. The dithered
442 * image cells will contain indexes into the palette.
444 * @param image the image to dither
445 * @return the dithered image. Every pixel is an index into the
448 public BufferedImage
ditherImage(final BufferedImage image
) {
450 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
451 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
453 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
454 image
.getHeight(), null, 0, image
.getWidth());
455 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
456 rgbArray
, 0, image
.getWidth());
458 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
459 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
460 int oldPixel
= ditheredImage
.getRGB(imageX
,
462 int colorIdx
= matchColor(oldPixel
);
463 assert (colorIdx
>= 0);
464 assert (colorIdx
< MAX_COLOR_REGISTERS
);
465 int newPixel
= rgbColors
.get(colorIdx
);
466 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
468 int oldRed
= (oldPixel
>>> 16) & 0xFF;
469 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
470 int oldBlue
= oldPixel
& 0xFF;
472 int newRed
= (newPixel
>>> 16) & 0xFF;
473 int newGreen
= (newPixel
>>> 8) & 0xFF;
474 int newBlue
= newPixel
& 0xFF;
476 int redError
= (oldRed
- newRed
) / 16;
477 int greenError
= (oldGreen
- newGreen
) / 16;
478 int blueError
= (oldBlue
- newBlue
) / 16;
480 int red
, green
, blue
;
481 if (imageX
< image
.getWidth() - 1) {
482 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
483 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
484 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
485 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
487 green
= clamp(green
);
489 pXpY
= ((red
& 0xFF) << 16);
490 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
491 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
493 if (imageY
< image
.getHeight() - 1) {
494 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
496 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
497 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
498 blue
= ( pXpYp
& 0xFF) + blueError
;
500 green
= clamp(green
);
502 pXpYp
= ((red
& 0xFF) << 16);
503 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
504 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
506 } else if (imageY
< image
.getHeight() - 1) {
507 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
509 int pXYp
= ditheredImage
.getRGB(imageX
,
512 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
513 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
514 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
516 green
= clamp(green
);
518 pXmYp
= ((red
& 0xFF) << 16);
519 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
520 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
522 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
523 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
524 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
526 green
= clamp(green
);
528 pXYp
= ((red
& 0xFF) << 16);
529 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
530 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
532 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
533 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
535 return ditheredImage
;
539 * Convert an RGB color to HSL.
541 * @param red red color, between 0 and 255
542 * @param green green color, between 0 and 255
543 * @param blue blue color, between 0 and 255
544 * @param hsl the hsl color as [hue, saturation, luminance]
546 private void rgbToHsl(final int red
, final int green
,
547 final int blue
, final int [] hsl
) {
549 assert ((red
>= 0) && (red
<= 255));
550 assert ((green
>= 0) && (green
<= 255));
551 assert ((blue
>= 0) && (blue
<= 255));
553 double R
= red
/ 255.0;
554 double G
= green
/ 255.0;
555 double B
= blue
/ 255.0;
556 boolean Rmax
= false;
557 boolean Gmax
= false;
558 boolean Bmax
= false;
559 double min
= (R
< G ? R
: G
);
560 min
= (min
< B ? min
: B
);
562 if ((R
>= G
) && (R
>= B
)) {
565 } else if ((G
>= R
) && (G
>= B
)) {
568 } else if ((B
>= G
) && (B
>= R
)) {
573 double L
= (min
+ max
) / 2.0;
578 S
= (max
- min
) / (max
+ min
);
580 S
= (max
- min
) / (2.0 - max
- min
);
584 assert (Gmax
== false);
585 assert (Bmax
== false);
586 H
= (G
- B
) / (max
- min
);
588 assert (Rmax
== false);
589 assert (Bmax
== false);
590 H
= 2.0 + (B
- R
) / (max
- min
);
592 assert (Rmax
== false);
593 assert (Gmax
== false);
594 H
= 4.0 + (R
- G
) / (max
- min
);
599 hsl
[0] = (int) (H
* 60.0);
600 hsl
[1] = (int) (S
* 100.0);
601 hsl
[2] = (int) (L
* 100.0);
603 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
604 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
605 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
609 * Convert a HSL color to RGB.
611 * @param hue hue, between 0 and 359
612 * @param sat saturation, between 0 and 100
613 * @param lum luminance, between 0 and 100
614 * @return the rgb color as 0x00RRGGBB
616 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
617 assert ((hue
>= 0) && (hue
<= 360));
618 assert ((sat
>= 0) && (sat
<= 100));
619 assert ((lum
>= 0) && (lum
<= 100));
621 double S
= sat
/ 100.0;
622 double L
= lum
/ 100.0;
623 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
624 double Hp
= hue
/ 60.0;
625 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
632 } else if (Hp
<= 2.0) {
635 } else if (Hp
<= 3.0) {
638 } else if (Hp
<= 4.0) {
641 } else if (Hp
<= 5.0) {
644 } else if (Hp
<= 6.0) {
648 double m
= L
- (C
/ 2.0);
649 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
650 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
651 int blue
= (int) ((Bp
+ m
) * 255.0);
653 return (red
| green
| blue
);
657 * Create the sixel palette.
659 private void makePalette() {
660 // Generate the sixel palette. Because we have no idea at this
661 // layer which image(s) will be shown, we have to use a common
662 // palette with MAX_COLOR_REGISTERS colors for everything, and
663 // map the BufferedImage colors to their nearest neighbor in RGB
666 // We build a palette using the Hue-Saturation-Luminence model,
667 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
668 // Luminance. We convert these colors to 24-bit RGB, sort them
669 // ascending, and steal the first index for pure black and the
670 // last for pure white. The 8-bit final palette favors bright
671 // colors, somewhere between pastel and classic television
672 // technicolor. 9- and 10-bit palettes are more uniform.
674 // Default at 256 colors.
679 assert (MAX_COLOR_REGISTERS
>= 256);
680 assert ((MAX_COLOR_REGISTERS
== 256)
681 || (MAX_COLOR_REGISTERS
== 512)
682 || (MAX_COLOR_REGISTERS
== 1024)
683 || (MAX_COLOR_REGISTERS
== 2048));
685 switch (MAX_COLOR_REGISTERS
) {
702 hueStep
= (int) (Math
.pow(2, hueBits
));
703 satStep
= (int) (100 / Math
.pow(2, satBits
));
704 // 1 bit for luminance: 40 and 70.
709 // 2 bits: 20, 40, 60, 80
714 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
720 // System.err.printf("<html><body>\n");
721 // Hue is evenly spaced around the wheel.
722 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
724 final boolean DEBUG
= false;
725 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
727 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
728 hue
+= (360/hueStep
)) {
730 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
731 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
732 hslColors
.add(satList
);
734 // Saturation is linearly spaced between pastel and pure.
735 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
737 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
738 satList
.add(lumList
);
740 // Luminance brackets the pure color, but leaning toward
742 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
744 System.err.printf("<font style = \"color:");
745 System.err.printf("hsl(%d, %d%%, %d%%)",
747 System.err.printf(";\">=</font>\n");
749 int rgbColor
= hslToRgb(hue
, sat
, lum
);
750 rgbColors
.add(rgbColor
);
751 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
752 rgbColors
.size() - 1);
753 lumList
.add(colorIdx
);
755 rawRgbList
.add(rgbColor
);
757 int red
= (rgbColor
>>> 16) & 0xFF;
758 int green
= (rgbColor
>>> 8) & 0xFF;
759 int blue
= rgbColor
& 0xFF;
760 int [] backToHsl
= new int[3];
761 rgbToHsl(red
, green
, blue
, backToHsl
);
762 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
763 hue
, backToHsl
[0], sat
, backToHsl
[1],
769 // System.err.printf("\n</body></html>\n");
771 assert (rgbColors
.size() == MAX_COLOR_REGISTERS
);
774 * We need to sort rgbColors, so that toSixel() can know where
775 * BLACK and WHITE are in it. But we also need to be able to
776 * find the sorted values using the old unsorted indexes. So we
777 * will sort it, put all the indexes into a HashMap, and then
778 * build rgbSortedIndex[].
780 Collections
.sort(rgbColors
);
781 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
782 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
783 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
784 rgbColorIndices
.put(rgbColors
.get(i
), i
);
786 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
787 int rawColor
= rawRgbList
.get(i
);
788 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
791 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
792 assert (rawRgbList
!= null);
793 int idx
= rgbSortedIndex
[i
];
794 int rgbColor
= rgbColors
.get(idx
);
795 if ((idx
!= 0) && (idx
!= MAX_COLOR_REGISTERS
- 1)) {
797 System.err.printf("%d %06x --> %d %06x\n",
798 i, rawRgbList.get(i), idx, rgbColors.get(idx));
800 assert (rgbColor
== rawRgbList
.get(i
));
805 // Set the dimmest color as true black, and the brightest as true
808 rgbColors
.set(MAX_COLOR_REGISTERS
- 1, 0xFFFFFF);
811 System.err.printf("<html><body>\n");
812 for (Integer rgb: rgbColors) {
813 System.err.printf("<font style = \"color:");
814 System.err.printf("#%06x", rgb);
815 System.err.printf(";\">=</font>\n");
817 System.err.printf("\n</body></html>\n");
823 * Emit the sixel palette.
825 * @param sb the StringBuilder to append to
826 * @param used array of booleans set to true for each color actually
827 * used in this cell, or null to emit the entire palette
828 * @return the string to emit to an ANSI / ECMA-style terminal
830 public String
emitPalette(final StringBuilder sb
,
831 final boolean [] used
) {
833 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
834 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
835 int rgbColor
= rgbColors
.get(i
);
836 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
837 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
838 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
839 ( rgbColor
& 0xFF) * 100 / 255));
842 return sb
.toString();
847 * SixelCache is a least-recently-used cache that hangs on to the
848 * post-rendered sixel string for a particular set of cells.
850 private class SixelCache
{
853 * Maximum size of the cache.
855 private int maxSize
= 100;
858 * The entries stored in the cache.
860 private HashMap
<String
, CacheEntry
> cache
= null;
863 * CacheEntry is one entry in the cache.
865 private class CacheEntry
{
877 * The last time this entry was used.
879 public long millis
= 0;
882 * Public constructor.
884 * @param key the cache entry key
885 * @param data the cache entry data
887 public CacheEntry(final String key
, final String data
) {
890 this.millis
= System
.currentTimeMillis();
895 * Public constructor.
897 * @param maxSize the maximum size of the cache
899 public SixelCache(final int maxSize
) {
900 this.maxSize
= maxSize
;
901 cache
= new HashMap
<String
, CacheEntry
>();
905 * Make a unique key for a list of cells.
907 * @param cells the cells
910 private String
makeKey(final ArrayList
<Cell
> cells
) {
911 StringBuilder sb
= new StringBuilder();
912 for (Cell cell
: cells
) {
913 sb
.append(cell
.hashCode());
915 return sb
.toString();
919 * Get an entry from the cache.
921 * @param cells the list of cells that are the cache key
922 * @return the sixel string representing these cells, or null if this
923 * list of cells is not in the cache
925 public String
get(final ArrayList
<Cell
> cells
) {
926 CacheEntry entry
= cache
.get(makeKey(cells
));
930 entry
.millis
= System
.currentTimeMillis();
935 * Put an entry into the cache.
937 * @param cells the list of cells that are the cache key
938 * @param data the sixel string representing these cells
940 public void put(final ArrayList
<Cell
> cells
, final String data
) {
941 String key
= makeKey(cells
);
943 // System.err.println("put() " + key + " size " + cache.size());
945 assert (!cache
.containsKey(key
));
947 assert (cache
.size() <= maxSize
);
948 if (cache
.size() == maxSize
) {
949 // Cache is at limit, evict oldest entry.
950 long oldestTime
= Long
.MAX_VALUE
;
951 String keyToRemove
= null;
952 for (CacheEntry entry
: cache
.values()) {
953 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
954 keyToRemove
= entry
.key
;
955 oldestTime
= entry
.millis
;
959 System.err.println("put() remove key = " + keyToRemove +
960 " size " + cache.size());
962 assert (keyToRemove
!= null);
963 cache
.remove(keyToRemove
);
965 System.err.println("put() removed, size " + cache.size());
968 assert (cache
.size() <= maxSize
);
969 CacheEntry entry
= new CacheEntry(key
, data
);
970 assert (key
.equals(entry
.key
));
971 cache
.put(key
, entry
);
973 System.err.println("put() added key " + key + " " +
974 " size " + cache.size());
980 // ------------------------------------------------------------------------
981 // Constructors -----------------------------------------------------------
982 // ------------------------------------------------------------------------
985 * Constructor sets up state for getEvent().
987 * @param listener the object this backend needs to wake up when new
989 * @param input an InputStream connected to the remote user, or null for
990 * System.in. If System.in is used, then on non-Windows systems it will
991 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
992 * mode. input is always converted to a Reader with UTF-8 encoding.
993 * @param output an OutputStream connected to the remote user, or null
994 * for System.out. output is always converted to a Writer with UTF-8
996 * @param windowWidth the number of text columns to start with
997 * @param windowHeight the number of text rows to start with
998 * @throws UnsupportedEncodingException if an exception is thrown when
999 * creating the InputStreamReader
1001 public ECMA48Terminal(final Object listener
, final InputStream input
,
1002 final OutputStream output
, final int windowWidth
,
1003 final int windowHeight
) throws UnsupportedEncodingException
{
1005 this(listener
, input
, output
);
1007 // Send dtterm/xterm sequences, which will probably not work because
1008 // allowWindowOps is defaulted to false.
1009 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1011 this.output
.write(resizeString
);
1012 this.output
.flush();
1016 * Constructor sets up state for getEvent().
1018 * @param listener the object this backend needs to wake up when new
1020 * @param input an InputStream connected to the remote user, or null for
1021 * System.in. If System.in is used, then on non-Windows systems it will
1022 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
1023 * mode. input is always converted to a Reader with UTF-8 encoding.
1024 * @param output an OutputStream connected to the remote user, or null
1025 * for System.out. output is always converted to a Writer with UTF-8
1027 * @throws UnsupportedEncodingException if an exception is thrown when
1028 * creating the InputStreamReader
1030 public ECMA48Terminal(final Object listener
, final InputStream input
,
1031 final OutputStream output
) throws UnsupportedEncodingException
{
1037 stopReaderThread
= false;
1038 this.listener
= listener
;
1040 if (input
== null) {
1041 // inputStream = System.in;
1042 inputStream
= new FileInputStream(FileDescriptor
.in
);
1046 inputStream
= input
;
1048 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1050 if (input
instanceof SessionInfo
) {
1051 // This is a TelnetInputStream that exposes window size and
1052 // environment variables from the telnet layer.
1053 sessionInfo
= (SessionInfo
) input
;
1055 if (sessionInfo
== null) {
1056 if (input
== null) {
1057 // Reading right off the tty
1058 sessionInfo
= new TTYSessionInfo();
1060 sessionInfo
= new TSessionInfo();
1064 if (output
== null) {
1065 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1068 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1072 // Request xterm report window dimensions in pixels
1073 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1075 // Enable mouse reporting and metaSendsEscape
1076 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1077 this.output
.flush();
1079 // Query the screen size
1080 sessionInfo
.queryWindowSize();
1081 setDimensions(sessionInfo
.getWindowWidth(),
1082 sessionInfo
.getWindowHeight());
1084 // Hang onto the window size
1085 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1086 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1090 // Spin up the input reader
1091 eventQueue
= new LinkedList
<TInputEvent
>();
1092 readerThread
= new Thread(this);
1093 readerThread
.start();
1096 this.output
.write(clearAll());
1097 this.output
.flush();
1101 * Constructor sets up state for getEvent().
1103 * @param listener the object this backend needs to wake up when new
1105 * @param input the InputStream underlying 'reader'. Its available()
1106 * method is used to determine if reader.read() will block or not.
1107 * @param reader a Reader connected to the remote user.
1108 * @param writer a PrintWriter connected to the remote user.
1109 * @param setRawMode if true, set System.in into raw mode with stty.
1110 * This should in general not be used. It is here solely for Demo3,
1111 * which uses System.in.
1112 * @throws IllegalArgumentException if input, reader, or writer are null.
1114 public ECMA48Terminal(final Object listener
, final InputStream input
,
1115 final Reader reader
, final PrintWriter writer
,
1116 final boolean setRawMode
) {
1118 if (input
== null) {
1119 throw new IllegalArgumentException("InputStream must be specified");
1121 if (reader
== null) {
1122 throw new IllegalArgumentException("Reader must be specified");
1124 if (writer
== null) {
1125 throw new IllegalArgumentException("Writer must be specified");
1131 stopReaderThread
= false;
1132 this.listener
= listener
;
1134 inputStream
= input
;
1135 this.input
= reader
;
1137 if (setRawMode
== true) {
1140 this.setRawMode
= setRawMode
;
1142 if (input
instanceof SessionInfo
) {
1143 // This is a TelnetInputStream that exposes window size and
1144 // environment variables from the telnet layer.
1145 sessionInfo
= (SessionInfo
) input
;
1147 if (sessionInfo
== null) {
1148 if (setRawMode
== true) {
1149 // Reading right off the tty
1150 sessionInfo
= new TTYSessionInfo();
1152 sessionInfo
= new TSessionInfo();
1156 this.output
= writer
;
1158 // Request xterm report window dimensions in pixels
1159 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1161 // Enable mouse reporting and metaSendsEscape
1162 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1163 this.output
.flush();
1165 // Query the screen size
1166 sessionInfo
.queryWindowSize();
1167 setDimensions(sessionInfo
.getWindowWidth(),
1168 sessionInfo
.getWindowHeight());
1170 // Hang onto the window size
1171 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1172 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1176 // Spin up the input reader
1177 eventQueue
= new LinkedList
<TInputEvent
>();
1178 readerThread
= new Thread(this);
1179 readerThread
.start();
1182 this.output
.write(clearAll());
1183 this.output
.flush();
1187 * Constructor sets up state for getEvent().
1189 * @param listener the object this backend needs to wake up when new
1191 * @param input the InputStream underlying 'reader'. Its available()
1192 * method is used to determine if reader.read() will block or not.
1193 * @param reader a Reader connected to the remote user.
1194 * @param writer a PrintWriter connected to the remote user.
1195 * @throws IllegalArgumentException if input, reader, or writer are null.
1197 public ECMA48Terminal(final Object listener
, final InputStream input
,
1198 final Reader reader
, final PrintWriter writer
) {
1200 this(listener
, input
, reader
, writer
, false);
1203 // ------------------------------------------------------------------------
1204 // LogicalScreen ----------------------------------------------------------
1205 // ------------------------------------------------------------------------
1208 * Set the window title.
1210 * @param title the new title
1213 public void setTitle(final String title
) {
1214 output
.write(getSetTitleString(title
));
1219 * Push the logical screen to the physical device.
1222 public void flushPhysical() {
1223 StringBuilder sb
= new StringBuilder();
1227 && (cursorY
<= height
- 1)
1228 && (cursorX
<= width
- 1)
1231 sb
.append(cursor(true));
1232 sb
.append(gotoXY(cursorX
, cursorY
));
1234 sb
.append(cursor(false));
1237 output
.write(sb
.toString());
1242 * Resize the physical screen to match the logical screen dimensions.
1245 public void resizeToScreen() {
1246 // Send dtterm/xterm sequences, which will probably not work because
1247 // allowWindowOps is defaulted to false.
1248 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1250 this.output
.write(resizeString
);
1251 this.output
.flush();
1254 // ------------------------------------------------------------------------
1255 // TerminalReader ---------------------------------------------------------
1256 // ------------------------------------------------------------------------
1259 * Check if there are events in the queue.
1261 * @return if true, getEvents() has something to return to the backend
1263 public boolean hasEvents() {
1264 synchronized (eventQueue
) {
1265 return (eventQueue
.size() > 0);
1270 * Return any events in the IO queue.
1272 * @param queue list to append new events to
1274 public void getEvents(final List
<TInputEvent
> queue
) {
1275 synchronized (eventQueue
) {
1276 if (eventQueue
.size() > 0) {
1277 synchronized (queue
) {
1278 queue
.addAll(eventQueue
);
1286 * Restore terminal to normal state.
1288 public void closeTerminal() {
1290 // System.err.println("=== shutdown() ==="); System.err.flush();
1292 // Tell the reader thread to stop looking at input
1293 stopReaderThread
= true;
1295 readerThread
.join();
1296 } catch (InterruptedException e
) {
1297 if (debugToStderr
) {
1298 e
.printStackTrace();
1302 // Disable mouse reporting and show cursor. Defensive null check
1303 // here in case closeTerminal() is called twice.
1304 if (output
!= null) {
1305 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
1312 // We don't close System.in/out
1314 // Shut down the streams, this should wake up the reader thread
1315 // and make it exit.
1316 if (input
!= null) {
1319 } catch (IOException e
) {
1324 if (output
!= null) {
1332 * Set listener to a different Object.
1334 * @param listener the new listening object that run() wakes up on new
1337 public void setListener(final Object listener
) {
1338 this.listener
= listener
;
1342 * Reload options from System properties.
1344 public void reloadOptions() {
1345 // Permit RGB colors only if externally requested.
1346 if (System
.getProperty("jexer.ECMA48.rgbColor",
1347 "false").equals("true")
1354 // Pull the system properties for sixel output.
1355 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1362 // ------------------------------------------------------------------------
1363 // Runnable ---------------------------------------------------------------
1364 // ------------------------------------------------------------------------
1367 * Read function runs on a separate thread.
1370 boolean done
= false;
1371 // available() will often return > 1, so we need to read in chunks to
1373 char [] readBuffer
= new char[128];
1374 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1376 while (!done
&& !stopReaderThread
) {
1378 // We assume that if inputStream has bytes available, then
1379 // input won't block on read().
1380 int n
= inputStream
.available();
1383 System.err.printf("inputStream.available(): %d\n", n);
1388 if (readBuffer
.length
< n
) {
1389 // The buffer wasn't big enough, make it huger
1390 readBuffer
= new char[readBuffer
.length
* 2];
1393 // System.err.printf("BEFORE read()\n"); System.err.flush();
1395 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1398 System.err.printf("AFTER read() %d\n", rc);
1406 for (int i
= 0; i
< rc
; i
++) {
1407 int ch
= readBuffer
[i
];
1408 processChar(events
, (char)ch
);
1410 getIdleEvents(events
);
1411 if (events
.size() > 0) {
1412 // Add to the queue for the backend thread to
1413 // be able to obtain.
1414 synchronized (eventQueue
) {
1415 eventQueue
.addAll(events
);
1417 if (listener
!= null) {
1418 synchronized (listener
) {
1419 listener
.notifyAll();
1426 getIdleEvents(events
);
1427 if (events
.size() > 0) {
1428 synchronized (eventQueue
) {
1429 eventQueue
.addAll(events
);
1431 if (listener
!= null) {
1432 synchronized (listener
) {
1433 listener
.notifyAll();
1439 // Wait 20 millis for more data
1442 // System.err.println("end while loop"); System.err.flush();
1443 } catch (InterruptedException e
) {
1445 } catch (IOException e
) {
1446 e
.printStackTrace();
1449 } // while ((done == false) && (stopReaderThread == false))
1450 // System.err.println("*** run() exiting..."); System.err.flush();
1453 // ------------------------------------------------------------------------
1454 // ECMA48Terminal ---------------------------------------------------------
1455 // ------------------------------------------------------------------------
1458 * Get the width of a character cell in pixels.
1460 * @return the width in pixels of a character cell
1462 public int getTextWidth() {
1463 return (widthPixels
/ sessionInfo
.getWindowWidth());
1467 * Get the height of a character cell in pixels.
1469 * @return the height in pixels of a character cell
1471 public int getTextHeight() {
1472 return (heightPixels
/ sessionInfo
.getWindowHeight());
1476 * Getter for sessionInfo.
1478 * @return the SessionInfo
1480 public SessionInfo
getSessionInfo() {
1485 * Get the output writer.
1487 * @return the Writer
1489 public PrintWriter
getOutput() {
1494 * Call 'stty' to set cooked mode.
1496 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1498 private void sttyCooked() {
1503 * Call 'stty' to set raw mode.
1505 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1506 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1507 * -parenb cs8 min 1 < /dev/tty'
1509 private void sttyRaw() {
1514 * Call 'stty' to set raw or cooked mode.
1516 * @param mode if true, set raw mode, otherwise set cooked mode
1518 private void doStty(final boolean mode
) {
1519 String
[] cmdRaw
= {
1520 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1522 String
[] cmdCooked
= {
1523 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1528 process
= Runtime
.getRuntime().exec(cmdRaw
);
1530 process
= Runtime
.getRuntime().exec(cmdCooked
);
1532 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1533 String line
= in
.readLine();
1534 if ((line
!= null) && (line
.length() > 0)) {
1535 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1538 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1539 line
= err
.readLine();
1540 if ((line
!= null) && (line
.length() > 0)) {
1541 System
.err
.println("Error output from stty: " + line
);
1546 } catch (InterruptedException e
) {
1547 if (debugToStderr
) {
1548 e
.printStackTrace();
1552 int rc
= process
.exitValue();
1554 System
.err
.println("stty returned error code: " + rc
);
1556 } catch (IOException e
) {
1557 e
.printStackTrace();
1564 public void flush() {
1569 * Perform a somewhat-optimal rendering of a line.
1571 * @param y row coordinate. 0 is the top-most row.
1572 * @param sb StringBuilder to write escape sequences to
1573 * @param lastAttr cell attributes from the last call to flushLine
1575 private void flushLine(final int y
, final StringBuilder sb
,
1576 CellAttributes lastAttr
) {
1580 for (int x
= 0; x
< width
; x
++) {
1581 Cell lCell
= logical
[x
][y
];
1582 if (!lCell
.isBlank()) {
1586 // Push textEnd to first column beyond the text area
1590 // reallyCleared = true;
1592 boolean hasImage
= false;
1594 for (int x
= 0; x
< width
; x
++) {
1595 Cell lCell
= logical
[x
][y
];
1596 Cell pCell
= physical
[x
][y
];
1598 if (!lCell
.equals(pCell
) || reallyCleared
) {
1600 if (debugToStderr
) {
1601 System
.err
.printf("\n--\n");
1602 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1603 System
.err
.printf(" lCell: %s\n", lCell
);
1604 System
.err
.printf(" pCell: %s\n", pCell
);
1605 System
.err
.printf(" ==== \n");
1608 if (lastAttr
== null) {
1609 lastAttr
= new CellAttributes();
1610 sb
.append(normal());
1614 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1615 // Advancing at least one cell, or the first gotoXY
1616 sb
.append(gotoXY(x
, y
));
1619 assert (lastAttr
!= null);
1621 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1622 assert (lCell
.isBlank());
1624 for (int i
= x
; i
< width
; i
++) {
1625 assert (logical
[i
][y
].isBlank());
1626 // Physical is always updated
1627 physical
[i
][y
].reset();
1630 // Clear remaining line
1631 sb
.append(clearRemainingLine());
1636 // Image cell: bypass the rest of the loop, it is not
1638 if (lCell
.isImage()) {
1641 // Save the last rendered cell
1644 // Physical is always updated
1645 physical
[x
][y
].setTo(lCell
);
1649 assert (!lCell
.isImage());
1652 sb
.append(gotoXY(x
, y
));
1655 // Now emit only the modified attributes
1656 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1657 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1659 && (lCell
.isBold() == lastAttr
.isBold())
1660 && (lCell
.isReverse() == lastAttr
.isReverse())
1661 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1662 && (lCell
.isBlink() == lastAttr
.isBlink())
1664 // Both colors changed, attributes the same
1665 sb
.append(color(lCell
.isBold(),
1666 lCell
.getForeColor(), lCell
.getBackColor()));
1668 if (debugToStderr
) {
1669 System
.err
.printf("1 Change only fore/back colors\n");
1672 } else if (lCell
.isRGB()
1673 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1674 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1675 && (lCell
.isBold() == lastAttr
.isBold())
1676 && (lCell
.isReverse() == lastAttr
.isReverse())
1677 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1678 && (lCell
.isBlink() == lastAttr
.isBlink())
1680 // Both colors changed, attributes the same
1681 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1682 lCell
.getBackColorRGB()));
1684 if (debugToStderr
) {
1685 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1687 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1688 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1690 && (lCell
.isBold() != lastAttr
.isBold())
1691 && (lCell
.isReverse() != lastAttr
.isReverse())
1692 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1693 && (lCell
.isBlink() != lastAttr
.isBlink())
1695 // Everything is different
1696 sb
.append(color(lCell
.getForeColor(),
1697 lCell
.getBackColor(),
1698 lCell
.isBold(), lCell
.isReverse(),
1700 lCell
.isUnderline()));
1702 if (debugToStderr
) {
1703 System
.err
.printf("2 Set all attributes\n");
1705 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1706 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1708 && (lCell
.isBold() == lastAttr
.isBold())
1709 && (lCell
.isReverse() == lastAttr
.isReverse())
1710 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1711 && (lCell
.isBlink() == lastAttr
.isBlink())
1714 // Attributes same, foreColor different
1715 sb
.append(color(lCell
.isBold(),
1716 lCell
.getForeColor(), true));
1718 if (debugToStderr
) {
1719 System
.err
.printf("3 Change foreColor\n");
1721 } else if (lCell
.isRGB()
1722 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1723 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1724 && (lCell
.getForeColorRGB() >= 0)
1725 && (lCell
.getBackColorRGB() >= 0)
1726 && (lCell
.isBold() == lastAttr
.isBold())
1727 && (lCell
.isReverse() == lastAttr
.isReverse())
1728 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1729 && (lCell
.isBlink() == lastAttr
.isBlink())
1731 // Attributes same, foreColor different
1732 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1734 if (debugToStderr
) {
1735 System
.err
.printf("3 Change foreColor (RGB)\n");
1737 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1738 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1740 && (lCell
.isBold() == lastAttr
.isBold())
1741 && (lCell
.isReverse() == lastAttr
.isReverse())
1742 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1743 && (lCell
.isBlink() == lastAttr
.isBlink())
1745 // Attributes same, backColor different
1746 sb
.append(color(lCell
.isBold(),
1747 lCell
.getBackColor(), false));
1749 if (debugToStderr
) {
1750 System
.err
.printf("4 Change backColor\n");
1752 } else if (lCell
.isRGB()
1753 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1754 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1755 && (lCell
.isBold() == lastAttr
.isBold())
1756 && (lCell
.isReverse() == lastAttr
.isReverse())
1757 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1758 && (lCell
.isBlink() == lastAttr
.isBlink())
1760 // Attributes same, foreColor different
1761 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1763 if (debugToStderr
) {
1764 System
.err
.printf("4 Change backColor (RGB)\n");
1766 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1767 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1768 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1769 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1770 && (lCell
.isBold() == lastAttr
.isBold())
1771 && (lCell
.isReverse() == lastAttr
.isReverse())
1772 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1773 && (lCell
.isBlink() == lastAttr
.isBlink())
1776 // All attributes the same, just print the char
1779 if (debugToStderr
) {
1780 System
.err
.printf("5 Only emit character\n");
1783 // Just reset everything again
1784 if (!lCell
.isRGB()) {
1785 sb
.append(color(lCell
.getForeColor(),
1786 lCell
.getBackColor(),
1790 lCell
.isUnderline()));
1792 if (debugToStderr
) {
1793 System
.err
.printf("6 Change all attributes\n");
1796 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1797 lCell
.getBackColorRGB(),
1801 lCell
.isUnderline()));
1802 if (debugToStderr
) {
1803 System
.err
.printf("6 Change all attributes (RGB)\n");
1808 // Emit the character
1809 sb
.append(lCell
.getChar());
1811 // Save the last rendered cell
1813 lastAttr
.setTo(lCell
);
1815 // Physical is always updated
1816 physical
[x
][y
].setTo(lCell
);
1818 } // if (!lCell.equals(pCell) || (reallyCleared == true))
1820 } // for (int x = 0; x < width; x++)
1824 * Render the screen to a string that can be emitted to something that
1825 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
1827 * @param sb StringBuilder to write escape sequences to
1828 * @return escape sequences string that provides the updates to the
1831 private String
flushString(final StringBuilder sb
) {
1832 CellAttributes attr
= null;
1834 if (reallyCleared
) {
1835 attr
= new CellAttributes();
1836 sb
.append(clearAll());
1840 * For sixel support, draw all of the sixel output first, and then
1841 * draw everything else afterwards. This works OK, but performance
1842 * is still a drag on larger pictures.
1844 for (int y
= 0; y
< height
; y
++) {
1845 for (int x
= 0; x
< width
; x
++) {
1846 // If physical had non-image data that is now image data, the
1847 // entire row must be redrawn.
1848 Cell lCell
= logical
[x
][y
];
1849 Cell pCell
= physical
[x
][y
];
1850 if (lCell
.isImage() && !pCell
.isImage()) {
1856 for (int y
= 0; y
< height
; y
++) {
1857 for (int x
= 0; x
< width
; x
++) {
1858 Cell lCell
= logical
[x
][y
];
1859 Cell pCell
= physical
[x
][y
];
1861 if (!lCell
.isImage()) {
1867 while ((right
< width
)
1868 && (logical
[right
][y
].isImage())
1869 && (!logical
[right
][y
].equals(physical
[right
][y
])
1874 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
1875 for (int i
= 0; i
< (right
- x
); i
++) {
1876 assert (logical
[x
+ i
][y
].isImage());
1877 cellsToDraw
.add(logical
[x
+ i
][y
]);
1879 // Physical is always updated.
1880 physical
[x
+ i
][y
].setTo(lCell
);
1882 if (cellsToDraw
.size() > 0) {
1883 sb
.append(toSixel(x
, y
, cellsToDraw
));
1890 // Draw the text part now.
1891 for (int y
= 0; y
< height
; y
++) {
1892 flushLine(y
, sb
, attr
);
1895 reallyCleared
= false;
1897 String result
= sb
.toString();
1898 if (debugToStderr
) {
1899 System
.err
.printf("flushString(): %s\n", result
);
1905 * Reset keyboard/mouse input parser.
1907 private void resetParser() {
1908 state
= ParseState
.GROUND
;
1909 params
= new ArrayList
<String
>();
1915 * Produce a control character or one of the special ones (ENTER, TAB,
1918 * @param ch Unicode code point
1919 * @param alt if true, set alt on the TKeypress
1920 * @return one TKeypress event, either a control character (e.g. isKey ==
1921 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
1924 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
1925 // System.err.printf("controlChar: %02x\n", ch);
1929 // Carriage return --> ENTER
1930 return new TKeypressEvent(kbEnter
, alt
, false, false);
1932 // Linefeed --> ENTER
1933 return new TKeypressEvent(kbEnter
, alt
, false, false);
1936 return new TKeypressEvent(kbEsc
, alt
, false, false);
1939 return new TKeypressEvent(kbTab
, alt
, false, false);
1941 // Make all other control characters come back as the alphabetic
1942 // character with the ctrl field set. So SOH would be 'A' +
1944 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
1950 * Produce special key from CSI Pn ; Pm ; ... ~
1952 * @return one KEYPRESS event representing a special key
1954 private TInputEvent
csiFnKey() {
1956 if (params
.size() > 0) {
1957 key
= Integer
.parseInt(params
.get(0));
1959 boolean alt
= false;
1960 boolean ctrl
= false;
1961 boolean shift
= false;
1962 if (params
.size() > 1) {
1963 shift
= csiIsShift(params
.get(1));
1964 alt
= csiIsAlt(params
.get(1));
1965 ctrl
= csiIsCtrl(params
.get(1));
1970 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
1972 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
1974 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
1976 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
1978 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
1980 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
1982 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
1984 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
1986 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
1988 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
1990 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
1992 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
1994 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
1996 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2004 * Produce mouse events based on "Any event tracking" and UTF-8
2006 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2008 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2010 private TInputEvent
parseMouse() {
2011 int buttons
= params
.get(0).charAt(0) - 32;
2012 int x
= params
.get(0).charAt(1) - 32 - 1;
2013 int y
= params
.get(0).charAt(2) - 32 - 1;
2015 // Clamp X and Y to the physical screen coordinates.
2016 if (x
>= windowResize
.getWidth()) {
2017 x
= windowResize
.getWidth() - 1;
2019 if (y
>= windowResize
.getHeight()) {
2020 y
= windowResize
.getHeight() - 1;
2023 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2024 boolean eventMouse1
= false;
2025 boolean eventMouse2
= false;
2026 boolean eventMouse3
= false;
2027 boolean eventMouseWheelUp
= false;
2028 boolean eventMouseWheelDown
= false;
2030 // System.err.printf("buttons: %04x\r\n", buttons);
2047 if (!mouse1
&& !mouse2
&& !mouse3
) {
2048 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2050 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2067 // Dragging with mouse1 down
2070 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2074 // Dragging with mouse2 down
2077 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2081 // Dragging with mouse3 down
2084 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2088 // Dragging with mouse2 down after wheelUp
2091 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2095 // Dragging with mouse2 down after wheelDown
2098 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2102 eventMouseWheelUp
= true;
2106 eventMouseWheelDown
= true;
2110 // Unknown, just make it motion
2111 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2114 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2115 eventMouse1
, eventMouse2
, eventMouse3
,
2116 eventMouseWheelUp
, eventMouseWheelDown
);
2120 * Produce mouse events based on "Any event tracking" and SGR
2122 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2124 * @param release if true, this was a release ('m')
2125 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2127 private TInputEvent
parseMouseSGR(final boolean release
) {
2128 // SGR extended coordinates - mode 1006
2129 if (params
.size() < 3) {
2130 // Invalid position, bail out.
2133 int buttons
= Integer
.parseInt(params
.get(0));
2134 int x
= Integer
.parseInt(params
.get(1)) - 1;
2135 int y
= Integer
.parseInt(params
.get(2)) - 1;
2137 // Clamp X and Y to the physical screen coordinates.
2138 if (x
>= windowResize
.getWidth()) {
2139 x
= windowResize
.getWidth() - 1;
2141 if (y
>= windowResize
.getHeight()) {
2142 y
= windowResize
.getHeight() - 1;
2145 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2146 boolean eventMouse1
= false;
2147 boolean eventMouse2
= false;
2148 boolean eventMouse3
= false;
2149 boolean eventMouseWheelUp
= false;
2150 boolean eventMouseWheelDown
= false;
2153 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2167 // Motion only, no buttons down
2168 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2172 // Dragging with mouse1 down
2174 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2178 // Dragging with mouse2 down
2180 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2184 // Dragging with mouse3 down
2186 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2190 // Dragging with mouse2 down after wheelUp
2192 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2196 // Dragging with mouse2 down after wheelDown
2198 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2202 eventMouseWheelUp
= true;
2206 eventMouseWheelDown
= true;
2210 // Unknown, bail out
2213 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2214 eventMouse1
, eventMouse2
, eventMouse3
,
2215 eventMouseWheelUp
, eventMouseWheelDown
);
2219 * Return any events in the IO queue due to timeout.
2221 * @param queue list to append new events to
2223 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2224 long nowTime
= System
.currentTimeMillis();
2226 // Check for new window size
2227 long windowSizeDelay
= nowTime
- windowSizeTime
;
2228 if (windowSizeDelay
> 1000) {
2229 int oldTextWidth
= getTextWidth();
2230 int oldTextHeight
= getTextHeight();
2232 sessionInfo
.queryWindowSize();
2233 int newWidth
= sessionInfo
.getWindowWidth();
2234 int newHeight
= sessionInfo
.getWindowHeight();
2236 if ((newWidth
!= windowResize
.getWidth())
2237 || (newHeight
!= windowResize
.getHeight())
2240 // Request xterm report window dimensions in pixels again.
2241 // Between now and then, ensure that the reported text cell
2242 // size is the same by setting widthPixels and heightPixels
2243 // to match the new dimensions.
2244 widthPixels
= oldTextWidth
* newWidth
;
2245 heightPixels
= oldTextHeight
* newHeight
;
2247 if (debugToStderr
) {
2248 System
.err
.println("Screen size changed, old size " +
2250 System
.err
.println(" new size " +
2251 newWidth
+ " x " + newHeight
);
2252 System
.err
.println(" old pixels " +
2253 oldTextWidth
+ " x " + oldTextHeight
);
2254 System
.err
.println(" new pixels " +
2255 getTextWidth() + " x " + getTextHeight());
2258 this.output
.printf("%s", xtermReportWindowPixelDimensions());
2259 this.output
.flush();
2261 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2262 newWidth
, newHeight
);
2263 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2264 newWidth
, newHeight
);
2267 windowSizeTime
= nowTime
;
2270 // ESCDELAY type timeout
2271 if (state
== ParseState
.ESCAPE
) {
2272 long escDelay
= nowTime
- escapeTime
;
2273 if (escDelay
> 100) {
2274 // After 0.1 seconds, assume a true escape character
2275 queue
.add(controlChar((char)0x1B, false));
2282 * Returns true if the CSI parameter for a keyboard command means that
2285 private boolean csiIsShift(final String x
) {
2297 * Returns true if the CSI parameter for a keyboard command means that
2300 private boolean csiIsAlt(final String x
) {
2312 * Returns true if the CSI parameter for a keyboard command means that
2315 private boolean csiIsCtrl(final String x
) {
2327 * Parses the next character of input to see if an InputEvent is
2330 * @param events list to append new events to
2331 * @param ch Unicode code point
2333 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2335 // ESCDELAY type timeout
2336 long nowTime
= System
.currentTimeMillis();
2337 if (state
== ParseState
.ESCAPE
) {
2338 long escDelay
= nowTime
- escapeTime
;
2339 if (escDelay
> 250) {
2340 // After 0.25 seconds, assume a true escape character
2341 events
.add(controlChar((char)0x1B, false));
2347 boolean ctrl
= false;
2348 boolean alt
= false;
2349 boolean shift
= false;
2351 // System.err.printf("state: %s ch %c\r\n", state, ch);
2357 state
= ParseState
.ESCAPE
;
2358 escapeTime
= nowTime
;
2363 // Control character
2364 events
.add(controlChar(ch
, false));
2371 events
.add(new TKeypressEvent(false, 0, ch
,
2372 false, false, false));
2381 // ALT-Control character
2382 events
.add(controlChar(ch
, true));
2388 // This will be one of the function keys
2389 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2393 // '[' goes to CSI_ENTRY
2395 state
= ParseState
.CSI_ENTRY
;
2399 // Everything else is assumed to be Alt-keystroke
2400 if ((ch
>= 'A') && (ch
<= 'Z')) {
2404 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2408 case ESCAPE_INTERMEDIATE
:
2409 if ((ch
>= 'P') && (ch
<= 'S')) {
2413 events
.add(new TKeypressEvent(kbF1
));
2416 events
.add(new TKeypressEvent(kbF2
));
2419 events
.add(new TKeypressEvent(kbF3
));
2422 events
.add(new TKeypressEvent(kbF4
));
2431 // Unknown keystroke, ignore
2436 // Numbers - parameter values
2437 if ((ch
>= '0') && (ch
<= '9')) {
2438 params
.set(params
.size() - 1,
2439 params
.get(params
.size() - 1) + ch
);
2440 state
= ParseState
.CSI_PARAM
;
2443 // Parameter separator
2449 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2453 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2458 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2463 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2468 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2473 events
.add(new TKeypressEvent(kbHome
));
2478 events
.add(new TKeypressEvent(kbEnd
));
2482 // CBT - Cursor backward X tab stops (default 1)
2483 events
.add(new TKeypressEvent(kbBackTab
));
2488 state
= ParseState
.MOUSE
;
2491 // Mouse position, SGR (1006) coordinates
2492 state
= ParseState
.MOUSE_SGR
;
2499 // Unknown keystroke, ignore
2504 // Numbers - parameter values
2505 if ((ch
>= '0') && (ch
<= '9')) {
2506 params
.set(params
.size() - 1,
2507 params
.get(params
.size() - 1) + ch
);
2510 // Parameter separator
2518 // Generate a mouse press event
2519 TInputEvent event
= parseMouseSGR(false);
2520 if (event
!= null) {
2526 // Generate a mouse release event
2527 event
= parseMouseSGR(true);
2528 if (event
!= null) {
2537 // Unknown keystroke, ignore
2542 // Numbers - parameter values
2543 if ((ch
>= '0') && (ch
<= '9')) {
2544 params
.set(params
.size() - 1,
2545 params
.get(params
.size() - 1) + ch
);
2546 state
= ParseState
.CSI_PARAM
;
2549 // Parameter separator
2556 events
.add(csiFnKey());
2561 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2565 if (params
.size() > 1) {
2566 shift
= csiIsShift(params
.get(1));
2567 alt
= csiIsAlt(params
.get(1));
2568 ctrl
= csiIsCtrl(params
.get(1));
2570 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2575 if (params
.size() > 1) {
2576 shift
= csiIsShift(params
.get(1));
2577 alt
= csiIsAlt(params
.get(1));
2578 ctrl
= csiIsCtrl(params
.get(1));
2580 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2585 if (params
.size() > 1) {
2586 shift
= csiIsShift(params
.get(1));
2587 alt
= csiIsAlt(params
.get(1));
2588 ctrl
= csiIsCtrl(params
.get(1));
2590 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2595 if (params
.size() > 1) {
2596 shift
= csiIsShift(params
.get(1));
2597 alt
= csiIsAlt(params
.get(1));
2598 ctrl
= csiIsCtrl(params
.get(1));
2600 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2605 if (params
.size() > 1) {
2606 shift
= csiIsShift(params
.get(1));
2607 alt
= csiIsAlt(params
.get(1));
2608 ctrl
= csiIsCtrl(params
.get(1));
2610 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2615 if (params
.size() > 1) {
2616 shift
= csiIsShift(params
.get(1));
2617 alt
= csiIsAlt(params
.get(1));
2618 ctrl
= csiIsCtrl(params
.get(1));
2620 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2625 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2626 if (debugToStderr
) {
2627 System
.err
.printf("windowOp pixels: " +
2628 "height %s width %s\n",
2629 params
.get(1), params
.get(2));
2632 widthPixels
= Integer
.parseInt(params
.get(2));
2633 heightPixels
= Integer
.parseInt(params
.get(1));
2634 } catch (NumberFormatException e
) {
2635 if (debugToStderr
) {
2636 e
.printStackTrace();
2639 if (widthPixels
<= 0) {
2642 if (heightPixels
<= 0) {
2653 // Unknown keystroke, ignore
2658 params
.set(0, params
.get(params
.size() - 1) + ch
);
2659 if (params
.get(0).length() == 3) {
2660 // We have enough to generate a mouse event
2661 events
.add(parseMouse());
2670 // This "should" be impossible to reach
2675 * Request (u)xterm to report the current window size dimensions.
2677 * @return the string to emit to xterm
2679 private String
xtermReportWindowPixelDimensions() {
2684 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2685 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2688 * @param on if true, enable metaSendsEscape
2689 * @return the string to emit to xterm
2691 private String
xtermMetaSendsEscape(final boolean on
) {
2693 return "\033[?1036h\033[?1034l";
2695 return "\033[?1036l";
2699 * Create an xterm OSC sequence to change the window title.
2701 * @param title the new title
2702 * @return the string to emit to xterm
2704 private String
getSetTitleString(final String title
) {
2705 return "\033]2;" + title
+ "\007";
2708 // ------------------------------------------------------------------------
2709 // Sixel output support ---------------------------------------------------
2710 // ------------------------------------------------------------------------
2713 * Start a sixel string for display one row's worth of bitmap data.
2715 * @param x column coordinate. 0 is the left-most column.
2716 * @param y row coordinate. 0 is the top-most row.
2717 * @return the string to emit to an ANSI / ECMA-style terminal
2719 private String
startSixel(final int x
, final int y
) {
2720 StringBuilder sb
= new StringBuilder();
2722 assert (sixel
== true);
2725 sb
.append(gotoXY(x
, y
));
2728 sb
.append("\033Pq");
2730 if (palette
== null) {
2731 palette
= new SixelPalette();
2734 return sb
.toString();
2738 * End a sixel string for display one row's worth of bitmap data.
2740 * @return the string to emit to an ANSI / ECMA-style terminal
2742 private String
endSixel() {
2743 assert (sixel
== true);
2750 * Create a sixel string representing a row of several cells containing
2753 * @param x column coordinate. 0 is the left-most column.
2754 * @param y row coordinate. 0 is the top-most row.
2755 * @param cells the cells containing the bitmap data
2756 * @return the string to emit to an ANSI / ECMA-style terminal
2758 private String
toSixel(final int x
, final int y
,
2759 final ArrayList
<Cell
> cells
) {
2761 StringBuilder sb
= new StringBuilder();
2763 assert (cells
!= null);
2764 assert (cells
.size() > 0);
2765 assert (cells
.get(0).getImage() != null);
2767 if (sixel
== false) {
2768 sb
.append(normal());
2769 sb
.append(gotoXY(x
, y
));
2770 for (int i
= 0; i
< cells
.size(); i
++) {
2773 return sb
.toString();
2776 if (sixelCache
== null) {
2777 sixelCache
= new SixelCache(height
* 10);
2780 // Save and get rows to/from the cache that do NOT have inverted
2782 boolean saveInCache
= true;
2783 for (Cell cell
: cells
) {
2784 if (cell
.isInvertedImage()) {
2785 saveInCache
= false;
2789 String cachedResult
= sixelCache
.get(cells
);
2790 if (cachedResult
!= null) {
2791 // System.err.println("CACHE HIT");
2792 sb
.append(startSixel(x
, y
));
2793 sb
.append(cachedResult
);
2794 sb
.append(endSixel());
2795 return sb
.toString();
2797 // System.err.println("CACHE MISS");
2800 int imageWidth
= cells
.get(0).getImage().getWidth();
2801 int imageHeight
= cells
.get(0).getImage().getHeight();
2803 // cells.get(x).getImage() has a dithered bitmap containing indexes
2804 // into the color palette. Piece these together into one larger
2805 // image for final rendering.
2807 int fullWidth
= cells
.size() * getTextWidth();
2808 int fullHeight
= getTextHeight();
2809 for (int i
= 0; i
< cells
.size(); i
++) {
2810 totalWidth
+= cells
.get(i
).getImage().getWidth();
2813 BufferedImage image
= new BufferedImage(fullWidth
,
2814 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
2817 for (int i
= 0; i
< cells
.size() - 1; i
++) {
2818 if (cells
.get(i
).isInvertedImage()) {
2819 rgbArray
= new int[imageWidth
* imageHeight
];
2820 for (int j
= 0; j
< rgbArray
.length
; j
++) {
2821 rgbArray
[j
] = 0xFFFFFF;
2824 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
2825 imageWidth
, imageHeight
, null, 0, imageWidth
);
2829 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
2830 i * imageWidth, 0, imageWidth, imageHeight,
2832 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
2833 fullWidth, fullHeight, cells.size(), getTextWidth());
2836 image
.setRGB(i
* imageWidth
, 0, imageWidth
, imageHeight
,
2837 rgbArray
, 0, imageWidth
);
2838 if (imageHeight
< fullHeight
) {
2839 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
2840 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2841 for (int imageY
= imageHeight
; imageY
< fullHeight
;
2844 image
.setRGB(imageX
, imageY
, backgroundColor
);
2849 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
2850 if (cells
.get(cells
.size() - 1).isInvertedImage()) {
2851 rgbArray
= new int[totalWidth
* imageHeight
];
2852 for (int j
= 0; j
< rgbArray
.length
; j
++) {
2853 rgbArray
[j
] = 0xFFFFFF;
2856 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
2857 totalWidth
, imageHeight
, null, 0, totalWidth
);
2859 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
2860 imageHeight
, rgbArray
, 0, totalWidth
);
2862 if (totalWidth
< getTextWidth()) {
2863 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
2865 for (int imageX
= image
.getWidth() - totalWidth
;
2866 imageX
< image
.getWidth(); imageX
++) {
2868 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
2869 image
.setRGB(imageX
, imageY
, backgroundColor
);
2874 // Dither the image. It is ok to lose the original here.
2875 if (palette
== null) {
2876 palette
= new SixelPalette();
2878 image
= palette
.ditherImage(image
);
2880 // Emit the palette, but only for the colors actually used by these
2882 boolean [] usedColors
= new boolean[MAX_COLOR_REGISTERS
];
2883 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2884 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
2885 usedColors
[image
.getRGB(imageX
, imageY
)] = true;
2888 palette
.emitPalette(sb
, usedColors
);
2890 // Render the entire row of cells.
2891 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
2892 int [][] sixels
= new int[image
.getWidth()][6];
2894 // See which colors are actually used in this band of sixels.
2895 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2896 for (int imageY
= 0;
2897 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
2900 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
2901 assert (colorIdx
>= 0);
2902 assert (colorIdx
< MAX_COLOR_REGISTERS
);
2904 sixels
[imageX
][imageY
] = colorIdx
;
2908 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
2909 boolean isUsed
= false;
2910 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2911 for (int j
= 0; j
< 6; j
++) {
2912 if (sixels
[imageX
][j
] == i
) {
2917 if (isUsed
== false) {
2921 // Set to the beginning of scan line for the next set of
2922 // colored pixels, and select the color.
2923 sb
.append(String
.format("$#%d", i
));
2925 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2927 // Add up all the pixels that match this color.
2930 (j
< 6) && (currentRow
+ j
< fullHeight
);
2933 if (sixels
[imageX
][j
] == i
) {
2957 assert (data
< 127);
2959 sb
.append((char) data
);
2960 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
2961 } // for (int i = 0; i < MAX_COLOR_REGISTERS; i++)
2963 // Advance to the next scan line.
2966 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
2968 // Kill the very last "-", because it is unnecessary.
2969 sb
.deleteCharAt(sb
.length() - 1);
2972 // This row is OK to save into the cache.
2973 sixelCache
.put(cells
, sb
.toString());
2976 return (startSixel(x
, y
) + sb
.toString() + endSixel());
2979 // ------------------------------------------------------------------------
2980 // End sixel output support -----------------------------------------------
2981 // ------------------------------------------------------------------------
2984 * Create a SGR parameter sequence for a single color change.
2986 * @param bold if true, set bold
2987 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
2988 * @param foreground if true, this is a foreground color
2989 * @return the string to emit to an ANSI / ECMA-style terminal,
2992 private String
color(final boolean bold
, final Color color
,
2993 final boolean foreground
) {
2994 return color(color
, foreground
, true) +
2995 rgbColor(bold
, color
, foreground
);
2999 * Create a T.416 RGB parameter sequence for a single color change.
3001 * @param colorRGB a 24-bit RGB value for foreground color
3002 * @param foreground if true, this is a foreground color
3003 * @return the string to emit to an ANSI / ECMA-style terminal,
3006 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
3008 int colorRed
= (colorRGB
>>> 16) & 0xFF;
3009 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
3010 int colorBlue
= colorRGB
& 0xFF;
3012 StringBuilder sb
= new StringBuilder();
3014 sb
.append("\033[38;2;");
3016 sb
.append("\033[48;2;");
3018 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
3019 return sb
.toString();
3023 * Create a T.416 RGB parameter sequence for both foreground and
3024 * background color change.
3026 * @param foreColorRGB a 24-bit RGB value for foreground color
3027 * @param backColorRGB a 24-bit RGB value for foreground color
3028 * @return the string to emit to an ANSI / ECMA-style terminal,
3031 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
3032 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3033 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3034 int foreColorBlue
= foreColorRGB
& 0xFF;
3035 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3036 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3037 int backColorBlue
= backColorRGB
& 0xFF;
3039 StringBuilder sb
= new StringBuilder();
3040 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
3041 foreColorRed
, foreColorGreen
, foreColorBlue
));
3042 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
3043 backColorRed
, backColorGreen
, backColorBlue
));
3044 return sb
.toString();
3048 * Create a T.416 RGB parameter sequence for a single color change.
3050 * @param bold if true, set bold
3051 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3052 * @param foreground if true, this is a foreground color
3053 * @return the string to emit to an xterm terminal with RGB support,
3054 * e.g. "\033[38;2;RR;GG;BBm"
3056 private String
rgbColor(final boolean bold
, final Color color
,
3057 final boolean foreground
) {
3058 if (doRgbColor
== false) {
3061 StringBuilder sb
= new StringBuilder("\033[");
3063 // Bold implies foreground only
3065 if (color
.equals(Color
.BLACK
)) {
3066 sb
.append("84;84;84");
3067 } else if (color
.equals(Color
.RED
)) {
3068 sb
.append("252;84;84");
3069 } else if (color
.equals(Color
.GREEN
)) {
3070 sb
.append("84;252;84");
3071 } else if (color
.equals(Color
.YELLOW
)) {
3072 sb
.append("252;252;84");
3073 } else if (color
.equals(Color
.BLUE
)) {
3074 sb
.append("84;84;252");
3075 } else if (color
.equals(Color
.MAGENTA
)) {
3076 sb
.append("252;84;252");
3077 } else if (color
.equals(Color
.CYAN
)) {
3078 sb
.append("84;252;252");
3079 } else if (color
.equals(Color
.WHITE
)) {
3080 sb
.append("252;252;252");
3088 if (color
.equals(Color
.BLACK
)) {
3090 } else if (color
.equals(Color
.RED
)) {
3091 sb
.append("168;0;0");
3092 } else if (color
.equals(Color
.GREEN
)) {
3093 sb
.append("0;168;0");
3094 } else if (color
.equals(Color
.YELLOW
)) {
3095 sb
.append("168;84;0");
3096 } else if (color
.equals(Color
.BLUE
)) {
3097 sb
.append("0;0;168");
3098 } else if (color
.equals(Color
.MAGENTA
)) {
3099 sb
.append("168;0;168");
3100 } else if (color
.equals(Color
.CYAN
)) {
3101 sb
.append("0;168;168");
3102 } else if (color
.equals(Color
.WHITE
)) {
3103 sb
.append("168;168;168");
3107 return sb
.toString();
3111 * Create a T.416 RGB parameter sequence for both foreground and
3112 * background color change.
3114 * @param bold if true, set bold
3115 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3116 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3117 * @return the string to emit to an xterm terminal with RGB support,
3118 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
3120 private String
rgbColor(final boolean bold
, final Color foreColor
,
3121 final Color backColor
) {
3122 if (doRgbColor
== false) {
3126 return rgbColor(bold
, foreColor
, true) +
3127 rgbColor(false, backColor
, false);
3131 * Create a SGR parameter sequence for a single color change.
3133 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3134 * @param foreground if true, this is a foreground color
3135 * @param header if true, make the full header, otherwise just emit the
3136 * color parameter e.g. "42;"
3137 * @return the string to emit to an ANSI / ECMA-style terminal,
3140 private String
color(final Color color
, final boolean foreground
,
3141 final boolean header
) {
3143 int ecmaColor
= color
.getValue();
3145 // Convert Color.* values to SGR numerics
3153 return String
.format("\033[%dm", ecmaColor
);
3155 return String
.format("%d;", ecmaColor
);
3160 * Create a SGR parameter sequence for both foreground and background
3163 * @param bold if true, set bold
3164 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3165 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3166 * @return the string to emit to an ANSI / ECMA-style terminal,
3167 * e.g. "\033[31;42m"
3169 private String
color(final boolean bold
, final Color foreColor
,
3170 final Color backColor
) {
3171 return color(foreColor
, backColor
, true) +
3172 rgbColor(bold
, foreColor
, backColor
);
3176 * Create a SGR parameter sequence for both foreground and
3177 * background color change.
3179 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3180 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3181 * @param header if true, make the full header, otherwise just emit the
3182 * color parameter e.g. "31;42;"
3183 * @return the string to emit to an ANSI / ECMA-style terminal,
3184 * e.g. "\033[31;42m"
3186 private String
color(final Color foreColor
, final Color backColor
,
3187 final boolean header
) {
3189 int ecmaForeColor
= foreColor
.getValue();
3190 int ecmaBackColor
= backColor
.getValue();
3192 // Convert Color.* values to SGR numerics
3193 ecmaBackColor
+= 40;
3194 ecmaForeColor
+= 30;
3197 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
3199 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
3204 * Create a SGR parameter sequence for foreground, background, and
3205 * several attributes. This sequence first resets all attributes to
3206 * default, then sets attributes as per the parameters.
3208 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3209 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3210 * @param bold if true, set bold
3211 * @param reverse if true, set reverse
3212 * @param blink if true, set blink
3213 * @param underline if true, set underline
3214 * @return the string to emit to an ANSI / ECMA-style terminal,
3215 * e.g. "\033[0;1;31;42m"
3217 private String
color(final Color foreColor
, final Color backColor
,
3218 final boolean bold
, final boolean reverse
, final boolean blink
,
3219 final boolean underline
) {
3221 int ecmaForeColor
= foreColor
.getValue();
3222 int ecmaBackColor
= backColor
.getValue();
3224 // Convert Color.* values to SGR numerics
3225 ecmaBackColor
+= 40;
3226 ecmaForeColor
+= 30;
3228 StringBuilder sb
= new StringBuilder();
3229 if ( bold
&& reverse
&& blink
&& !underline
) {
3230 sb
.append("\033[0;1;7;5;");
3231 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3232 sb
.append("\033[0;1;7;");
3233 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3234 sb
.append("\033[0;7;5;");
3235 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3236 sb
.append("\033[0;1;5;");
3237 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3238 sb
.append("\033[0;1;");
3239 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3240 sb
.append("\033[0;7;");
3241 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3242 sb
.append("\033[0;5;");
3243 } else if ( bold
&& reverse
&& blink
&& underline
) {
3244 sb
.append("\033[0;1;7;5;4;");
3245 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3246 sb
.append("\033[0;1;7;4;");
3247 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3248 sb
.append("\033[0;7;5;4;");
3249 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3250 sb
.append("\033[0;1;5;4;");
3251 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3252 sb
.append("\033[0;1;4;");
3253 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3254 sb
.append("\033[0;7;4;");
3255 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3256 sb
.append("\033[0;5;4;");
3257 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3258 sb
.append("\033[0;4;");
3260 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3261 sb
.append("\033[0;");
3263 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
3264 sb
.append(rgbColor(bold
, foreColor
, backColor
));
3265 return sb
.toString();
3269 * Create a SGR parameter sequence for foreground, background, and
3270 * several attributes. This sequence first resets all attributes to
3271 * default, then sets attributes as per the parameters.
3273 * @param foreColorRGB a 24-bit RGB value for foreground color
3274 * @param backColorRGB a 24-bit RGB value for foreground color
3275 * @param bold if true, set bold
3276 * @param reverse if true, set reverse
3277 * @param blink if true, set blink
3278 * @param underline if true, set underline
3279 * @return the string to emit to an ANSI / ECMA-style terminal,
3280 * e.g. "\033[0;1;31;42m"
3282 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
3283 final boolean bold
, final boolean reverse
, final boolean blink
,
3284 final boolean underline
) {
3286 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3287 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3288 int foreColorBlue
= foreColorRGB
& 0xFF;
3289 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3290 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3291 int backColorBlue
= backColorRGB
& 0xFF;
3293 StringBuilder sb
= new StringBuilder();
3294 if ( bold
&& reverse
&& blink
&& !underline
) {
3295 sb
.append("\033[0;1;7;5;");
3296 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3297 sb
.append("\033[0;1;7;");
3298 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3299 sb
.append("\033[0;7;5;");
3300 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3301 sb
.append("\033[0;1;5;");
3302 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3303 sb
.append("\033[0;1;");
3304 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3305 sb
.append("\033[0;7;");
3306 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3307 sb
.append("\033[0;5;");
3308 } else if ( bold
&& reverse
&& blink
&& underline
) {
3309 sb
.append("\033[0;1;7;5;4;");
3310 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3311 sb
.append("\033[0;1;7;4;");
3312 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3313 sb
.append("\033[0;7;5;4;");
3314 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3315 sb
.append("\033[0;1;5;4;");
3316 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3317 sb
.append("\033[0;1;4;");
3318 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3319 sb
.append("\033[0;7;4;");
3320 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3321 sb
.append("\033[0;5;4;");
3322 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3323 sb
.append("\033[0;4;");
3325 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3326 sb
.append("\033[0;");
3329 sb
.append("m\033[38;2;");
3330 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
3332 sb
.append("m\033[48;2;");
3333 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
3336 return sb
.toString();
3340 * Create a SGR parameter sequence to reset to defaults.
3342 * @return the string to emit to an ANSI / ECMA-style terminal,
3345 private String
normal() {
3346 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
3350 * Create a SGR parameter sequence to reset to defaults.
3352 * @param header if true, make the full header, otherwise just emit the
3353 * bare parameter e.g. "0;"
3354 * @return the string to emit to an ANSI / ECMA-style terminal,
3357 private String
normal(final boolean header
) {
3359 return "\033[0;37;40m";
3365 * Create a SGR parameter sequence for enabling the visible cursor.
3367 * @param on if true, turn on cursor
3368 * @return the string to emit to an ANSI / ECMA-style terminal
3370 private String
cursor(final boolean on
) {
3371 if (on
&& !cursorOn
) {
3375 if (!on
&& cursorOn
) {
3383 * Clear the entire screen. Because some terminals use back-color-erase,
3384 * set the color to white-on-black beforehand.
3386 * @return the string to emit to an ANSI / ECMA-style terminal
3388 private String
clearAll() {
3389 return "\033[0;37;40m\033[2J";
3393 * Clear the line from the cursor (inclusive) to the end of the screen.
3394 * Because some terminals use back-color-erase, set the color to
3395 * white-on-black beforehand.
3397 * @return the string to emit to an ANSI / ECMA-style terminal
3399 private String
clearRemainingLine() {
3400 return "\033[0;37;40m\033[K";
3404 * Move the cursor to (x, y).
3406 * @param x column coordinate. 0 is the left-most column.
3407 * @param y row coordinate. 0 is the top-most row.
3408 * @return the string to emit to an ANSI / ECMA-style terminal
3410 private String
gotoXY(final int x
, final int y
) {
3411 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
3415 * Tell (u)xterm that we want to receive mouse events based on "Any event
3416 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
3417 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
3419 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
3421 * Note that this also sets the alternate/primary screen buffer.
3423 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
3424 * mean "hide the mouse pointer." We have to use our own sequence to do
3425 * this because there is no standard in xterm for unilaterally hiding the
3426 * pointer all the time (regardless of typing).
3428 * @param on If true, enable mouse report and use the alternate screen
3429 * buffer. If false disable mouse reporting and use the primary screen
3431 * @return the string to emit to xterm
3433 private String
mouse(final boolean on
) {
3435 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
3437 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";