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
.TCommandEvent
;
54 import jexer
.event
.TInputEvent
;
55 import jexer
.event
.TKeypressEvent
;
56 import jexer
.event
.TMouseEvent
;
57 import jexer
.event
.TResizeEvent
;
58 import static jexer
.TCommand
.*;
59 import static jexer
.TKeypress
.*;
62 * This class reads keystrokes and mouse events and emits output to ANSI
63 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
65 public class ECMA48Terminal
extends LogicalScreen
66 implements TerminalReader
, Runnable
{
68 // ------------------------------------------------------------------------
69 // Constants --------------------------------------------------------------
70 // ------------------------------------------------------------------------
73 * States in the input parser.
75 private enum ParseState
{
86 * Number of colors in the sixel palette. Xterm 335 defines the max as
89 private static final int MAX_COLOR_REGISTERS
= 1024;
90 // Black-and-white is possible too.
91 // private static final int MAX_COLOR_REGISTERS = 2;
93 // ------------------------------------------------------------------------
94 // Variables --------------------------------------------------------------
95 // ------------------------------------------------------------------------
98 * Emit debugging to stderr.
100 private boolean debugToStderr
= false;
103 * If true, emit T.416-style RGB colors for normal system colors. This
104 * is a) expensive in bandwidth, and b) potentially terrible looking for
107 private static boolean doRgbColor
= false;
110 * The session information.
112 private SessionInfo sessionInfo
;
115 * The event queue, filled up by a thread reading on input.
117 private List
<TInputEvent
> eventQueue
;
120 * If true, we want the reader thread to exit gracefully.
122 private boolean stopReaderThread
;
127 private Thread readerThread
;
130 * Parameters being collected. E.g. if the string is \033[1;3m, then
131 * params[0] will be 1 and params[1] will be 3.
133 private List
<String
> params
;
136 * Current parsing state.
138 private ParseState state
;
141 * The time we entered ESCAPE. If we get a bare escape without a code
142 * following it, this is used to return that bare escape.
144 private long escapeTime
;
147 * The time we last checked the window size. We try not to spawn stty
148 * more than once per second.
150 private long windowSizeTime
;
153 * true if mouse1 was down. Used to report mouse1 on the release event.
155 private boolean mouse1
;
158 * true if mouse2 was down. Used to report mouse2 on the release event.
160 private boolean mouse2
;
163 * true if mouse3 was down. Used to report mouse3 on the release event.
165 private boolean mouse3
;
168 * Cache the cursor visibility value so we only emit the sequence when we
171 private boolean cursorOn
= true;
174 * Cache the last window size to figure out if a TResizeEvent needs to be
177 private TResizeEvent windowResize
= null;
180 * Window width in pixels. Used for sixel support.
182 private int widthPixels
= 640;
185 * Window height in pixels. Used for sixel support.
187 private int heightPixels
= 400;
190 * If true, emit image data via sixel.
192 private boolean sixel
= true;
195 * The sixel palette handler.
197 private SixelPalette palette
= null;
200 * The sixel post-rendered string cache.
202 private SixelCache sixelCache
= null;
205 * If true, then we changed System.in and need to change it back.
207 private boolean setRawMode
;
210 * The terminal's input. If an InputStream is not specified in the
211 * constructor, then this InputStreamReader will be bound to System.in
212 * with UTF-8 encoding.
214 private Reader input
;
217 * The terminal's raw InputStream. If an InputStream is not specified in
218 * the constructor, then this InputReader will be bound to System.in.
219 * This is used by run() to see if bytes are available() before calling
220 * (Reader)input.read().
222 private InputStream inputStream
;
225 * The terminal's output. If an OutputStream is not specified in the
226 * constructor, then this PrintWriter will be bound to System.out with
229 private PrintWriter output
;
232 * The listening object that run() wakes up on new input.
234 private Object listener
;
237 * SixelPalette is used to manage the conversion of images between 24-bit
238 * RGB color and a palette of MAX_COLOR_REGISTERS colors.
240 private class SixelPalette
{
243 * Color palette for sixel output, sorted low to high.
245 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
248 * Map of color palette index for sixel output, from the order it was
249 * generated by makePalette() to rgbColors.
251 private int [] rgbSortedIndex
= new int[MAX_COLOR_REGISTERS
];
254 * The color palette, organized by hue, saturation, and luminance.
255 * This is used for a fast color match.
257 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
260 * Number of bits for hue.
262 private int hueBits
= -1;
265 * Number of bits for saturation.
267 private int satBits
= -1;
270 * Number of bits for luminance.
272 private int lumBits
= -1;
275 * Step size for hue bins.
277 private int hueStep
= -1;
280 * Step size for saturation bins.
282 private int satStep
= -1;
285 * Cached RGB to HSL result.
287 private int hsl
[] = new int[3];
290 * ColorIdx records a RGB color and its palette index.
292 private class ColorIdx
{
294 * The 24-bit RGB color.
299 * The palette index for this color.
304 * Public constructor.
306 * @param color the 24-bit RGB color
307 * @param index the palette index for this color
309 public ColorIdx(final int color
, final int index
) {
316 * Public constructor.
318 public SixelPalette() {
323 * Find the nearest match for a color in the palette.
325 * @param color the RGB color
326 * @return the index in rgbColors that is closest to color
328 public int matchColor(final int color
) {
333 * matchColor() is a critical performance bottleneck. To make it
334 * decent, we do the following:
336 * 1. Find the nearest two hues that bracket this color.
338 * 2. Find the nearest two saturations that bracket this color.
340 * 3. Iterate within these four bands of luminance values,
341 * returning the closest color by Euclidean distance.
343 * This strategy reduces the search space by about 97%.
345 int red
= (color
>>> 16) & 0xFF;
346 int green
= (color
>>> 8) & 0xFF;
347 int blue
= color
& 0xFF;
349 if (MAX_COLOR_REGISTERS
== 2) {
350 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
359 rgbToHsl(red
, green
, blue
, hsl
);
363 // System.err.printf("%d %d %d\n", hue, sat, lum);
365 double diff
= Double
.MAX_VALUE
;
368 int hue1
= hue
/ (360/hueStep
);
370 if (hue1
>= hslColors
.size() - 1) {
371 // Bracket pure red from above.
372 hue1
= hslColors
.size() - 1;
374 } else if (hue1
== 0) {
375 // Bracket pure red from below.
376 hue2
= hslColors
.size() - 1;
379 for (int hI
= hue1
; hI
!= -1;) {
380 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
383 } else if (hI
== hue2
) {
387 int sMin
= (sat
/ satStep
) - 1;
392 } else if (sMin
== sats
.size() - 1) {
397 assert (sMax
- sMin
== 1);
400 // int sMax = sats.size() - 1;
402 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
403 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
405 // True 3D colorspace match for the remaining values
406 for (ColorIdx c
: lums
) {
407 int rgbColor
= c
.color
;
409 int red2
= (rgbColor
>>> 16) & 0xFF;
410 int green2
= (rgbColor
>>> 8) & 0xFF;
411 int blue2
= rgbColor
& 0xFF;
412 newDiff
+= Math
.pow(red2
- red
, 2);
413 newDiff
+= Math
.pow(green2
- green
, 2);
414 newDiff
+= Math
.pow(blue2
- blue
, 2);
415 if (newDiff
< diff
) {
416 idx
= rgbSortedIndex
[c
.index
];
423 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
424 // Black is a closer match.
426 } else if ((((255 - red
) * (255 - red
)) +
427 ((255 - green
) * (255 - green
)) +
428 ((255 - blue
) * (255 - blue
))) < diff
) {
430 // White is a closer match.
431 idx
= MAX_COLOR_REGISTERS
- 1;
438 * Clamp an int value to [0, 255].
440 * @param x the int value
441 * @return an int between 0 and 255.
443 private int clamp(final int x
) {
454 * Dither an image to a MAX_COLOR_REGISTERS palette. The dithered
455 * image cells will contain indexes into the palette.
457 * @param image the image to dither
458 * @return the dithered image. Every pixel is an index into the
461 public BufferedImage
ditherImage(final BufferedImage image
) {
463 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
464 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
466 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
467 image
.getHeight(), null, 0, image
.getWidth());
468 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
469 rgbArray
, 0, image
.getWidth());
471 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
472 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
473 int oldPixel
= ditheredImage
.getRGB(imageX
,
475 int colorIdx
= matchColor(oldPixel
);
476 assert (colorIdx
>= 0);
477 assert (colorIdx
< MAX_COLOR_REGISTERS
);
478 int newPixel
= rgbColors
.get(colorIdx
);
479 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
481 int oldRed
= (oldPixel
>>> 16) & 0xFF;
482 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
483 int oldBlue
= oldPixel
& 0xFF;
485 int newRed
= (newPixel
>>> 16) & 0xFF;
486 int newGreen
= (newPixel
>>> 8) & 0xFF;
487 int newBlue
= newPixel
& 0xFF;
489 int redError
= (oldRed
- newRed
) / 16;
490 int greenError
= (oldGreen
- newGreen
) / 16;
491 int blueError
= (oldBlue
- newBlue
) / 16;
493 int red
, green
, blue
;
494 if (imageX
< image
.getWidth() - 1) {
495 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
496 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
497 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
498 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
500 green
= clamp(green
);
502 pXpY
= ((red
& 0xFF) << 16);
503 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
504 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
506 if (imageY
< image
.getHeight() - 1) {
507 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
509 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
510 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
511 blue
= ( pXpYp
& 0xFF) + blueError
;
513 green
= clamp(green
);
515 pXpYp
= ((red
& 0xFF) << 16);
516 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
517 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
519 } else if (imageY
< image
.getHeight() - 1) {
520 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
522 int pXYp
= ditheredImage
.getRGB(imageX
,
525 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
526 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
527 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
529 green
= clamp(green
);
531 pXmYp
= ((red
& 0xFF) << 16);
532 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
533 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
535 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
536 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
537 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
539 green
= clamp(green
);
541 pXYp
= ((red
& 0xFF) << 16);
542 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
543 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
545 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
546 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
548 return ditheredImage
;
552 * Convert an RGB color to HSL.
554 * @param red red color, between 0 and 255
555 * @param green green color, between 0 and 255
556 * @param blue blue color, between 0 and 255
557 * @param hsl the hsl color as [hue, saturation, luminance]
559 private void rgbToHsl(final int red
, final int green
,
560 final int blue
, final int [] hsl
) {
562 assert ((red
>= 0) && (red
<= 255));
563 assert ((green
>= 0) && (green
<= 255));
564 assert ((blue
>= 0) && (blue
<= 255));
566 double R
= red
/ 255.0;
567 double G
= green
/ 255.0;
568 double B
= blue
/ 255.0;
569 boolean Rmax
= false;
570 boolean Gmax
= false;
571 boolean Bmax
= false;
572 double min
= (R
< G ? R
: G
);
573 min
= (min
< B ? min
: B
);
575 if ((R
>= G
) && (R
>= B
)) {
578 } else if ((G
>= R
) && (G
>= B
)) {
581 } else if ((B
>= G
) && (B
>= R
)) {
586 double L
= (min
+ max
) / 2.0;
591 S
= (max
- min
) / (max
+ min
);
593 S
= (max
- min
) / (2.0 - max
- min
);
597 assert (Gmax
== false);
598 assert (Bmax
== false);
599 H
= (G
- B
) / (max
- min
);
601 assert (Rmax
== false);
602 assert (Bmax
== false);
603 H
= 2.0 + (B
- R
) / (max
- min
);
605 assert (Rmax
== false);
606 assert (Gmax
== false);
607 H
= 4.0 + (R
- G
) / (max
- min
);
612 hsl
[0] = (int) (H
* 60.0);
613 hsl
[1] = (int) (S
* 100.0);
614 hsl
[2] = (int) (L
* 100.0);
616 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
617 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
618 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
622 * Convert a HSL color to RGB.
624 * @param hue hue, between 0 and 359
625 * @param sat saturation, between 0 and 100
626 * @param lum luminance, between 0 and 100
627 * @return the rgb color as 0x00RRGGBB
629 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
630 assert ((hue
>= 0) && (hue
<= 360));
631 assert ((sat
>= 0) && (sat
<= 100));
632 assert ((lum
>= 0) && (lum
<= 100));
634 double S
= sat
/ 100.0;
635 double L
= lum
/ 100.0;
636 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
637 double Hp
= hue
/ 60.0;
638 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
645 } else if (Hp
<= 2.0) {
648 } else if (Hp
<= 3.0) {
651 } else if (Hp
<= 4.0) {
654 } else if (Hp
<= 5.0) {
657 } else if (Hp
<= 6.0) {
661 double m
= L
- (C
/ 2.0);
662 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
663 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
664 int blue
= (int) ((Bp
+ m
) * 255.0);
666 return (red
| green
| blue
);
670 * Create the sixel palette.
672 private void makePalette() {
673 // Generate the sixel palette. Because we have no idea at this
674 // layer which image(s) will be shown, we have to use a common
675 // palette with MAX_COLOR_REGISTERS colors for everything, and
676 // map the BufferedImage colors to their nearest neighbor in RGB
679 if (MAX_COLOR_REGISTERS
== 2) {
681 rgbColors
.add(0xFFFFFF);
682 rgbSortedIndex
[0] = 0;
683 rgbSortedIndex
[1] = 1;
687 // We build a palette using the Hue-Saturation-Luminence model,
688 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
689 // Luminance. We convert these colors to 24-bit RGB, sort them
690 // ascending, and steal the first index for pure black and the
691 // last for pure white. The 8-bit final palette favors bright
692 // colors, somewhere between pastel and classic television
693 // technicolor. 9- and 10-bit palettes are more uniform.
695 // Default at 256 colors.
700 assert (MAX_COLOR_REGISTERS
>= 256);
701 assert ((MAX_COLOR_REGISTERS
== 256)
702 || (MAX_COLOR_REGISTERS
== 512)
703 || (MAX_COLOR_REGISTERS
== 1024)
704 || (MAX_COLOR_REGISTERS
== 2048));
706 switch (MAX_COLOR_REGISTERS
) {
723 hueStep
= (int) (Math
.pow(2, hueBits
));
724 satStep
= (int) (100 / Math
.pow(2, satBits
));
725 // 1 bit for luminance: 40 and 70.
730 // 2 bits: 20, 40, 60, 80
735 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
741 // System.err.printf("<html><body>\n");
742 // Hue is evenly spaced around the wheel.
743 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
745 final boolean DEBUG
= false;
746 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
748 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
749 hue
+= (360/hueStep
)) {
751 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
752 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
753 hslColors
.add(satList
);
755 // Saturation is linearly spaced between pastel and pure.
756 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
758 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
759 satList
.add(lumList
);
761 // Luminance brackets the pure color, but leaning toward
763 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
765 System.err.printf("<font style = \"color:");
766 System.err.printf("hsl(%d, %d%%, %d%%)",
768 System.err.printf(";\">=</font>\n");
770 int rgbColor
= hslToRgb(hue
, sat
, lum
);
771 rgbColors
.add(rgbColor
);
772 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
773 rgbColors
.size() - 1);
774 lumList
.add(colorIdx
);
776 rawRgbList
.add(rgbColor
);
778 int red
= (rgbColor
>>> 16) & 0xFF;
779 int green
= (rgbColor
>>> 8) & 0xFF;
780 int blue
= rgbColor
& 0xFF;
781 int [] backToHsl
= new int[3];
782 rgbToHsl(red
, green
, blue
, backToHsl
);
783 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
784 hue
, backToHsl
[0], sat
, backToHsl
[1],
790 // System.err.printf("\n</body></html>\n");
792 assert (rgbColors
.size() == MAX_COLOR_REGISTERS
);
795 * We need to sort rgbColors, so that toSixel() can know where
796 * BLACK and WHITE are in it. But we also need to be able to
797 * find the sorted values using the old unsorted indexes. So we
798 * will sort it, put all the indexes into a HashMap, and then
799 * build rgbSortedIndex[].
801 Collections
.sort(rgbColors
);
802 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
803 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
804 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
805 rgbColorIndices
.put(rgbColors
.get(i
), i
);
807 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
808 int rawColor
= rawRgbList
.get(i
);
809 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
812 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
813 assert (rawRgbList
!= null);
814 int idx
= rgbSortedIndex
[i
];
815 int rgbColor
= rgbColors
.get(idx
);
816 if ((idx
!= 0) && (idx
!= MAX_COLOR_REGISTERS
- 1)) {
818 System.err.printf("%d %06x --> %d %06x\n",
819 i, rawRgbList.get(i), idx, rgbColors.get(idx));
821 assert (rgbColor
== rawRgbList
.get(i
));
826 // Set the dimmest color as true black, and the brightest as true
829 rgbColors
.set(MAX_COLOR_REGISTERS
- 1, 0xFFFFFF);
832 System.err.printf("<html><body>\n");
833 for (Integer rgb: rgbColors) {
834 System.err.printf("<font style = \"color:");
835 System.err.printf("#%06x", rgb);
836 System.err.printf(";\">=</font>\n");
838 System.err.printf("\n</body></html>\n");
844 * Emit the sixel palette.
846 * @param sb the StringBuilder to append to
847 * @param used array of booleans set to true for each color actually
848 * used in this cell, or null to emit the entire palette
849 * @return the string to emit to an ANSI / ECMA-style terminal
851 public String
emitPalette(final StringBuilder sb
,
852 final boolean [] used
) {
854 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
855 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
856 int rgbColor
= rgbColors
.get(i
);
857 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
858 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
859 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
860 ( rgbColor
& 0xFF) * 100 / 255));
863 return sb
.toString();
868 * SixelCache is a least-recently-used cache that hangs on to the
869 * post-rendered sixel string for a particular set of cells.
871 private class SixelCache
{
874 * Maximum size of the cache.
876 private int maxSize
= 100;
879 * The entries stored in the cache.
881 private HashMap
<String
, CacheEntry
> cache
= null;
884 * CacheEntry is one entry in the cache.
886 private class CacheEntry
{
898 * The last time this entry was used.
900 public long millis
= 0;
903 * Public constructor.
905 * @param key the cache entry key
906 * @param data the cache entry data
908 public CacheEntry(final String key
, final String data
) {
911 this.millis
= System
.currentTimeMillis();
916 * Public constructor.
918 * @param maxSize the maximum size of the cache
920 public SixelCache(final int maxSize
) {
921 this.maxSize
= maxSize
;
922 cache
= new HashMap
<String
, CacheEntry
>();
926 * Make a unique key for a list of cells.
928 * @param cells the cells
931 private String
makeKey(final ArrayList
<Cell
> cells
) {
932 StringBuilder sb
= new StringBuilder();
933 for (Cell cell
: cells
) {
934 sb
.append(cell
.hashCode());
936 return sb
.toString();
940 * Get an entry from the cache.
942 * @param cells the list of cells that are the cache key
943 * @return the sixel string representing these cells, or null if this
944 * list of cells is not in the cache
946 public String
get(final ArrayList
<Cell
> cells
) {
947 CacheEntry entry
= cache
.get(makeKey(cells
));
951 entry
.millis
= System
.currentTimeMillis();
956 * Put an entry into the cache.
958 * @param cells the list of cells that are the cache key
959 * @param data the sixel string representing these cells
961 public void put(final ArrayList
<Cell
> cells
, final String data
) {
962 String key
= makeKey(cells
);
964 // System.err.println("put() " + key + " size " + cache.size());
966 assert (!cache
.containsKey(key
));
968 assert (cache
.size() <= maxSize
);
969 if (cache
.size() == maxSize
) {
970 // Cache is at limit, evict oldest entry.
971 long oldestTime
= Long
.MAX_VALUE
;
972 String keyToRemove
= null;
973 for (CacheEntry entry
: cache
.values()) {
974 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
975 keyToRemove
= entry
.key
;
976 oldestTime
= entry
.millis
;
980 System.err.println("put() remove key = " + keyToRemove +
981 " size " + cache.size());
983 assert (keyToRemove
!= null);
984 cache
.remove(keyToRemove
);
986 System.err.println("put() removed, size " + cache.size());
989 assert (cache
.size() <= maxSize
);
990 CacheEntry entry
= new CacheEntry(key
, data
);
991 assert (key
.equals(entry
.key
));
992 cache
.put(key
, entry
);
994 System.err.println("put() added key " + key + " " +
995 " size " + cache.size());
1001 // ------------------------------------------------------------------------
1002 // Constructors -----------------------------------------------------------
1003 // ------------------------------------------------------------------------
1006 * Constructor sets up state for getEvent().
1008 * @param listener the object this backend needs to wake up when new
1010 * @param input an InputStream connected to the remote user, or null for
1011 * System.in. If System.in is used, then on non-Windows systems it will
1012 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
1013 * mode. input is always converted to a Reader with UTF-8 encoding.
1014 * @param output an OutputStream connected to the remote user, or null
1015 * for System.out. output is always converted to a Writer with UTF-8
1017 * @param windowWidth the number of text columns to start with
1018 * @param windowHeight the number of text rows to start with
1019 * @throws UnsupportedEncodingException if an exception is thrown when
1020 * creating the InputStreamReader
1022 public ECMA48Terminal(final Object listener
, final InputStream input
,
1023 final OutputStream output
, final int windowWidth
,
1024 final int windowHeight
) throws UnsupportedEncodingException
{
1026 this(listener
, input
, output
);
1028 // Send dtterm/xterm sequences, which will probably not work because
1029 // allowWindowOps is defaulted to false.
1030 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1032 this.output
.write(resizeString
);
1033 this.output
.flush();
1037 * Constructor sets up state for getEvent().
1039 * @param listener the object this backend needs to wake up when new
1041 * @param input an InputStream connected to the remote user, or null for
1042 * System.in. If System.in is used, then on non-Windows systems it will
1043 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
1044 * mode. input is always converted to a Reader with UTF-8 encoding.
1045 * @param output an OutputStream connected to the remote user, or null
1046 * for System.out. output is always converted to a Writer with UTF-8
1048 * @throws UnsupportedEncodingException if an exception is thrown when
1049 * creating the InputStreamReader
1051 public ECMA48Terminal(final Object listener
, final InputStream input
,
1052 final OutputStream output
) throws UnsupportedEncodingException
{
1058 stopReaderThread
= false;
1059 this.listener
= listener
;
1061 if (input
== null) {
1062 // inputStream = System.in;
1063 inputStream
= new FileInputStream(FileDescriptor
.in
);
1067 inputStream
= input
;
1069 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1071 if (input
instanceof SessionInfo
) {
1072 // This is a TelnetInputStream that exposes window size and
1073 // environment variables from the telnet layer.
1074 sessionInfo
= (SessionInfo
) input
;
1076 if (sessionInfo
== null) {
1077 if (input
== null) {
1078 // Reading right off the tty
1079 sessionInfo
= new TTYSessionInfo();
1081 sessionInfo
= new TSessionInfo();
1085 if (output
== null) {
1086 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1089 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1093 // Request xterm report window dimensions in pixels
1094 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1096 // Enable mouse reporting and metaSendsEscape
1097 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1098 this.output
.flush();
1100 // Query the screen size
1101 sessionInfo
.queryWindowSize();
1102 setDimensions(sessionInfo
.getWindowWidth(),
1103 sessionInfo
.getWindowHeight());
1105 // Hang onto the window size
1106 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1107 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1111 // Spin up the input reader
1112 eventQueue
= new LinkedList
<TInputEvent
>();
1113 readerThread
= new Thread(this);
1114 readerThread
.start();
1117 this.output
.write(clearAll());
1118 this.output
.flush();
1122 * Constructor sets up state for getEvent().
1124 * @param listener the object this backend needs to wake up when new
1126 * @param input the InputStream underlying 'reader'. Its available()
1127 * method is used to determine if reader.read() will block or not.
1128 * @param reader a Reader connected to the remote user.
1129 * @param writer a PrintWriter connected to the remote user.
1130 * @param setRawMode if true, set System.in into raw mode with stty.
1131 * This should in general not be used. It is here solely for Demo3,
1132 * which uses System.in.
1133 * @throws IllegalArgumentException if input, reader, or writer are null.
1135 public ECMA48Terminal(final Object listener
, final InputStream input
,
1136 final Reader reader
, final PrintWriter writer
,
1137 final boolean setRawMode
) {
1139 if (input
== null) {
1140 throw new IllegalArgumentException("InputStream must be specified");
1142 if (reader
== null) {
1143 throw new IllegalArgumentException("Reader must be specified");
1145 if (writer
== null) {
1146 throw new IllegalArgumentException("Writer must be specified");
1152 stopReaderThread
= false;
1153 this.listener
= listener
;
1155 inputStream
= input
;
1156 this.input
= reader
;
1158 if (setRawMode
== true) {
1161 this.setRawMode
= setRawMode
;
1163 if (input
instanceof SessionInfo
) {
1164 // This is a TelnetInputStream that exposes window size and
1165 // environment variables from the telnet layer.
1166 sessionInfo
= (SessionInfo
) input
;
1168 if (sessionInfo
== null) {
1169 if (setRawMode
== true) {
1170 // Reading right off the tty
1171 sessionInfo
= new TTYSessionInfo();
1173 sessionInfo
= new TSessionInfo();
1177 this.output
= writer
;
1179 // Request xterm report window dimensions in pixels
1180 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1182 // Enable mouse reporting and metaSendsEscape
1183 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1184 this.output
.flush();
1186 // Query the screen size
1187 sessionInfo
.queryWindowSize();
1188 setDimensions(sessionInfo
.getWindowWidth(),
1189 sessionInfo
.getWindowHeight());
1191 // Hang onto the window size
1192 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1193 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1197 // Spin up the input reader
1198 eventQueue
= new LinkedList
<TInputEvent
>();
1199 readerThread
= new Thread(this);
1200 readerThread
.start();
1203 this.output
.write(clearAll());
1204 this.output
.flush();
1208 * Constructor sets up state for getEvent().
1210 * @param listener the object this backend needs to wake up when new
1212 * @param input the InputStream underlying 'reader'. Its available()
1213 * method is used to determine if reader.read() will block or not.
1214 * @param reader a Reader connected to the remote user.
1215 * @param writer a PrintWriter connected to the remote user.
1216 * @throws IllegalArgumentException if input, reader, or writer are null.
1218 public ECMA48Terminal(final Object listener
, final InputStream input
,
1219 final Reader reader
, final PrintWriter writer
) {
1221 this(listener
, input
, reader
, writer
, false);
1224 // ------------------------------------------------------------------------
1225 // LogicalScreen ----------------------------------------------------------
1226 // ------------------------------------------------------------------------
1229 * Set the window title.
1231 * @param title the new title
1234 public void setTitle(final String title
) {
1235 output
.write(getSetTitleString(title
));
1240 * Push the logical screen to the physical device.
1243 public void flushPhysical() {
1244 StringBuilder sb
= new StringBuilder();
1248 && (cursorY
<= height
- 1)
1249 && (cursorX
<= width
- 1)
1252 sb
.append(cursor(true));
1253 sb
.append(gotoXY(cursorX
, cursorY
));
1255 sb
.append(cursor(false));
1258 output
.write(sb
.toString());
1263 * Resize the physical screen to match the logical screen dimensions.
1266 public void resizeToScreen() {
1267 // Send dtterm/xterm sequences, which will probably not work because
1268 // allowWindowOps is defaulted to false.
1269 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1271 this.output
.write(resizeString
);
1272 this.output
.flush();
1275 // ------------------------------------------------------------------------
1276 // TerminalReader ---------------------------------------------------------
1277 // ------------------------------------------------------------------------
1280 * Check if there are events in the queue.
1282 * @return if true, getEvents() has something to return to the backend
1284 public boolean hasEvents() {
1285 synchronized (eventQueue
) {
1286 return (eventQueue
.size() > 0);
1291 * Return any events in the IO queue.
1293 * @param queue list to append new events to
1295 public void getEvents(final List
<TInputEvent
> queue
) {
1296 synchronized (eventQueue
) {
1297 if (eventQueue
.size() > 0) {
1298 synchronized (queue
) {
1299 queue
.addAll(eventQueue
);
1307 * Restore terminal to normal state.
1309 public void closeTerminal() {
1311 // System.err.println("=== shutdown() ==="); System.err.flush();
1313 // Tell the reader thread to stop looking at input
1314 stopReaderThread
= true;
1316 readerThread
.join();
1317 } catch (InterruptedException e
) {
1318 if (debugToStderr
) {
1319 e
.printStackTrace();
1323 // Disable mouse reporting and show cursor. Defensive null check
1324 // here in case closeTerminal() is called twice.
1325 if (output
!= null) {
1326 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
1333 // We don't close System.in/out
1335 // Shut down the streams, this should wake up the reader thread
1336 // and make it exit.
1337 if (input
!= null) {
1340 } catch (IOException e
) {
1345 if (output
!= null) {
1353 * Set listener to a different Object.
1355 * @param listener the new listening object that run() wakes up on new
1358 public void setListener(final Object listener
) {
1359 this.listener
= listener
;
1363 * Reload options from System properties.
1365 public void reloadOptions() {
1366 // Permit RGB colors only if externally requested.
1367 if (System
.getProperty("jexer.ECMA48.rgbColor",
1368 "false").equals("true")
1375 // Pull the system properties for sixel output.
1376 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1383 // ------------------------------------------------------------------------
1384 // Runnable ---------------------------------------------------------------
1385 // ------------------------------------------------------------------------
1388 * Read function runs on a separate thread.
1391 boolean done
= false;
1392 // available() will often return > 1, so we need to read in chunks to
1394 char [] readBuffer
= new char[128];
1395 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1397 while (!done
&& !stopReaderThread
) {
1399 // We assume that if inputStream has bytes available, then
1400 // input won't block on read().
1401 int n
= inputStream
.available();
1404 System.err.printf("inputStream.available(): %d\n", n);
1409 if (readBuffer
.length
< n
) {
1410 // The buffer wasn't big enough, make it huger
1411 readBuffer
= new char[readBuffer
.length
* 2];
1414 // System.err.printf("BEFORE read()\n"); System.err.flush();
1416 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1419 System.err.printf("AFTER read() %d\n", rc);
1427 for (int i
= 0; i
< rc
; i
++) {
1428 int ch
= readBuffer
[i
];
1429 processChar(events
, (char)ch
);
1431 getIdleEvents(events
);
1432 if (events
.size() > 0) {
1433 // Add to the queue for the backend thread to
1434 // be able to obtain.
1435 synchronized (eventQueue
) {
1436 eventQueue
.addAll(events
);
1438 if (listener
!= null) {
1439 synchronized (listener
) {
1440 listener
.notifyAll();
1447 getIdleEvents(events
);
1448 if (events
.size() > 0) {
1449 synchronized (eventQueue
) {
1450 eventQueue
.addAll(events
);
1452 if (listener
!= null) {
1453 synchronized (listener
) {
1454 listener
.notifyAll();
1460 if (output
.checkError()) {
1465 // Wait 20 millis for more data
1468 // System.err.println("end while loop"); System.err.flush();
1469 } catch (InterruptedException e
) {
1471 } catch (IOException e
) {
1472 e
.printStackTrace();
1475 } // while ((done == false) && (stopReaderThread == false))
1477 // Pass an event up to TApplication to tell it this Backend is done.
1478 synchronized (eventQueue
) {
1479 eventQueue
.add(new TCommandEvent(cmBackendDisconnect
));
1481 if (listener
!= null) {
1482 synchronized (listener
) {
1483 listener
.notifyAll();
1487 // System.err.println("*** run() exiting..."); System.err.flush();
1490 // ------------------------------------------------------------------------
1491 // ECMA48Terminal ---------------------------------------------------------
1492 // ------------------------------------------------------------------------
1495 * Get the width of a character cell in pixels.
1497 * @return the width in pixels of a character cell
1499 public int getTextWidth() {
1500 return (widthPixels
/ sessionInfo
.getWindowWidth());
1504 * Get the height of a character cell in pixels.
1506 * @return the height in pixels of a character cell
1508 public int getTextHeight() {
1509 return (heightPixels
/ sessionInfo
.getWindowHeight());
1513 * Getter for sessionInfo.
1515 * @return the SessionInfo
1517 public SessionInfo
getSessionInfo() {
1522 * Get the output writer.
1524 * @return the Writer
1526 public PrintWriter
getOutput() {
1531 * Call 'stty' to set cooked mode.
1533 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1535 private void sttyCooked() {
1540 * Call 'stty' to set raw mode.
1542 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1543 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1544 * -parenb cs8 min 1 < /dev/tty'
1546 private void sttyRaw() {
1551 * Call 'stty' to set raw or cooked mode.
1553 * @param mode if true, set raw mode, otherwise set cooked mode
1555 private void doStty(final boolean mode
) {
1556 String
[] cmdRaw
= {
1557 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1559 String
[] cmdCooked
= {
1560 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1565 process
= Runtime
.getRuntime().exec(cmdRaw
);
1567 process
= Runtime
.getRuntime().exec(cmdCooked
);
1569 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1570 String line
= in
.readLine();
1571 if ((line
!= null) && (line
.length() > 0)) {
1572 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1575 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1576 line
= err
.readLine();
1577 if ((line
!= null) && (line
.length() > 0)) {
1578 System
.err
.println("Error output from stty: " + line
);
1583 } catch (InterruptedException e
) {
1584 if (debugToStderr
) {
1585 e
.printStackTrace();
1589 int rc
= process
.exitValue();
1591 System
.err
.println("stty returned error code: " + rc
);
1593 } catch (IOException e
) {
1594 e
.printStackTrace();
1601 public void flush() {
1606 * Perform a somewhat-optimal rendering of a line.
1608 * @param y row coordinate. 0 is the top-most row.
1609 * @param sb StringBuilder to write escape sequences to
1610 * @param lastAttr cell attributes from the last call to flushLine
1612 private void flushLine(final int y
, final StringBuilder sb
,
1613 CellAttributes lastAttr
) {
1617 for (int x
= 0; x
< width
; x
++) {
1618 Cell lCell
= logical
[x
][y
];
1619 if (!lCell
.isBlank()) {
1623 // Push textEnd to first column beyond the text area
1627 // reallyCleared = true;
1629 boolean hasImage
= false;
1631 for (int x
= 0; x
< width
; x
++) {
1632 Cell lCell
= logical
[x
][y
];
1633 Cell pCell
= physical
[x
][y
];
1635 if (!lCell
.equals(pCell
) || reallyCleared
) {
1637 if (debugToStderr
) {
1638 System
.err
.printf("\n--\n");
1639 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1640 System
.err
.printf(" lCell: %s\n", lCell
);
1641 System
.err
.printf(" pCell: %s\n", pCell
);
1642 System
.err
.printf(" ==== \n");
1645 if (lastAttr
== null) {
1646 lastAttr
= new CellAttributes();
1647 sb
.append(normal());
1651 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1652 // Advancing at least one cell, or the first gotoXY
1653 sb
.append(gotoXY(x
, y
));
1656 assert (lastAttr
!= null);
1658 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1659 assert (lCell
.isBlank());
1661 for (int i
= x
; i
< width
; i
++) {
1662 assert (logical
[i
][y
].isBlank());
1663 // Physical is always updated
1664 physical
[i
][y
].reset();
1667 // Clear remaining line
1668 sb
.append(clearRemainingLine());
1673 // Image cell: bypass the rest of the loop, it is not
1675 if (lCell
.isImage()) {
1678 // Save the last rendered cell
1681 // Physical is always updated
1682 physical
[x
][y
].setTo(lCell
);
1686 assert (!lCell
.isImage());
1689 sb
.append(gotoXY(x
, y
));
1692 // Now emit only the modified attributes
1693 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1694 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1696 && (lCell
.isBold() == lastAttr
.isBold())
1697 && (lCell
.isReverse() == lastAttr
.isReverse())
1698 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1699 && (lCell
.isBlink() == lastAttr
.isBlink())
1701 // Both colors changed, attributes the same
1702 sb
.append(color(lCell
.isBold(),
1703 lCell
.getForeColor(), lCell
.getBackColor()));
1705 if (debugToStderr
) {
1706 System
.err
.printf("1 Change only fore/back colors\n");
1709 } else if (lCell
.isRGB()
1710 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1711 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1712 && (lCell
.isBold() == lastAttr
.isBold())
1713 && (lCell
.isReverse() == lastAttr
.isReverse())
1714 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1715 && (lCell
.isBlink() == lastAttr
.isBlink())
1717 // Both colors changed, attributes the same
1718 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1719 lCell
.getBackColorRGB()));
1721 if (debugToStderr
) {
1722 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1724 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1725 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1727 && (lCell
.isBold() != lastAttr
.isBold())
1728 && (lCell
.isReverse() != lastAttr
.isReverse())
1729 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1730 && (lCell
.isBlink() != lastAttr
.isBlink())
1732 // Everything is different
1733 sb
.append(color(lCell
.getForeColor(),
1734 lCell
.getBackColor(),
1735 lCell
.isBold(), lCell
.isReverse(),
1737 lCell
.isUnderline()));
1739 if (debugToStderr
) {
1740 System
.err
.printf("2 Set all attributes\n");
1742 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1743 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1745 && (lCell
.isBold() == lastAttr
.isBold())
1746 && (lCell
.isReverse() == lastAttr
.isReverse())
1747 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1748 && (lCell
.isBlink() == lastAttr
.isBlink())
1751 // Attributes same, foreColor different
1752 sb
.append(color(lCell
.isBold(),
1753 lCell
.getForeColor(), true));
1755 if (debugToStderr
) {
1756 System
.err
.printf("3 Change foreColor\n");
1758 } else if (lCell
.isRGB()
1759 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1760 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1761 && (lCell
.getForeColorRGB() >= 0)
1762 && (lCell
.getBackColorRGB() >= 0)
1763 && (lCell
.isBold() == lastAttr
.isBold())
1764 && (lCell
.isReverse() == lastAttr
.isReverse())
1765 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1766 && (lCell
.isBlink() == lastAttr
.isBlink())
1768 // Attributes same, foreColor different
1769 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1771 if (debugToStderr
) {
1772 System
.err
.printf("3 Change foreColor (RGB)\n");
1774 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1775 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1777 && (lCell
.isBold() == lastAttr
.isBold())
1778 && (lCell
.isReverse() == lastAttr
.isReverse())
1779 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1780 && (lCell
.isBlink() == lastAttr
.isBlink())
1782 // Attributes same, backColor different
1783 sb
.append(color(lCell
.isBold(),
1784 lCell
.getBackColor(), false));
1786 if (debugToStderr
) {
1787 System
.err
.printf("4 Change backColor\n");
1789 } else if (lCell
.isRGB()
1790 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1791 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1792 && (lCell
.isBold() == lastAttr
.isBold())
1793 && (lCell
.isReverse() == lastAttr
.isReverse())
1794 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1795 && (lCell
.isBlink() == lastAttr
.isBlink())
1797 // Attributes same, foreColor different
1798 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1800 if (debugToStderr
) {
1801 System
.err
.printf("4 Change backColor (RGB)\n");
1803 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1804 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1805 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1806 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1807 && (lCell
.isBold() == lastAttr
.isBold())
1808 && (lCell
.isReverse() == lastAttr
.isReverse())
1809 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1810 && (lCell
.isBlink() == lastAttr
.isBlink())
1813 // All attributes the same, just print the char
1816 if (debugToStderr
) {
1817 System
.err
.printf("5 Only emit character\n");
1820 // Just reset everything again
1821 if (!lCell
.isRGB()) {
1822 sb
.append(color(lCell
.getForeColor(),
1823 lCell
.getBackColor(),
1827 lCell
.isUnderline()));
1829 if (debugToStderr
) {
1830 System
.err
.printf("6 Change all attributes\n");
1833 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1834 lCell
.getBackColorRGB(),
1838 lCell
.isUnderline()));
1839 if (debugToStderr
) {
1840 System
.err
.printf("6 Change all attributes (RGB)\n");
1845 // Emit the character
1846 sb
.append(lCell
.getChar());
1848 // Save the last rendered cell
1850 lastAttr
.setTo(lCell
);
1852 // Physical is always updated
1853 physical
[x
][y
].setTo(lCell
);
1855 } // if (!lCell.equals(pCell) || (reallyCleared == true))
1857 } // for (int x = 0; x < width; x++)
1861 * Render the screen to a string that can be emitted to something that
1862 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
1864 * @param sb StringBuilder to write escape sequences to
1865 * @return escape sequences string that provides the updates to the
1868 private String
flushString(final StringBuilder sb
) {
1869 CellAttributes attr
= null;
1871 if (reallyCleared
) {
1872 attr
= new CellAttributes();
1873 sb
.append(clearAll());
1877 * For sixel support, draw all of the sixel output first, and then
1878 * draw everything else afterwards. This works OK, but performance
1879 * is still a drag on larger pictures.
1881 for (int y
= 0; y
< height
; y
++) {
1882 for (int x
= 0; x
< width
; x
++) {
1883 // If physical had non-image data that is now image data, the
1884 // entire row must be redrawn.
1885 Cell lCell
= logical
[x
][y
];
1886 Cell pCell
= physical
[x
][y
];
1887 if (lCell
.isImage() && !pCell
.isImage()) {
1893 for (int y
= 0; y
< height
; y
++) {
1894 for (int x
= 0; x
< width
; x
++) {
1895 Cell lCell
= logical
[x
][y
];
1896 Cell pCell
= physical
[x
][y
];
1898 if (!lCell
.isImage()) {
1904 while ((right
< width
)
1905 && (logical
[right
][y
].isImage())
1906 && (!logical
[right
][y
].equals(physical
[right
][y
])
1911 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
1912 for (int i
= 0; i
< (right
- x
); i
++) {
1913 assert (logical
[x
+ i
][y
].isImage());
1914 cellsToDraw
.add(logical
[x
+ i
][y
]);
1916 // Physical is always updated.
1917 physical
[x
+ i
][y
].setTo(lCell
);
1919 if (cellsToDraw
.size() > 0) {
1920 sb
.append(toSixel(x
, y
, cellsToDraw
));
1927 // Draw the text part now.
1928 for (int y
= 0; y
< height
; y
++) {
1929 flushLine(y
, sb
, attr
);
1932 reallyCleared
= false;
1934 String result
= sb
.toString();
1935 if (debugToStderr
) {
1936 System
.err
.printf("flushString(): %s\n", result
);
1942 * Reset keyboard/mouse input parser.
1944 private void resetParser() {
1945 state
= ParseState
.GROUND
;
1946 params
= new ArrayList
<String
>();
1952 * Produce a control character or one of the special ones (ENTER, TAB,
1955 * @param ch Unicode code point
1956 * @param alt if true, set alt on the TKeypress
1957 * @return one TKeypress event, either a control character (e.g. isKey ==
1958 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
1961 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
1962 // System.err.printf("controlChar: %02x\n", ch);
1966 // Carriage return --> ENTER
1967 return new TKeypressEvent(kbEnter
, alt
, false, false);
1969 // Linefeed --> ENTER
1970 return new TKeypressEvent(kbEnter
, alt
, false, false);
1973 return new TKeypressEvent(kbEsc
, alt
, false, false);
1976 return new TKeypressEvent(kbTab
, alt
, false, false);
1978 // Make all other control characters come back as the alphabetic
1979 // character with the ctrl field set. So SOH would be 'A' +
1981 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
1987 * Produce special key from CSI Pn ; Pm ; ... ~
1989 * @return one KEYPRESS event representing a special key
1991 private TInputEvent
csiFnKey() {
1993 if (params
.size() > 0) {
1994 key
= Integer
.parseInt(params
.get(0));
1996 boolean alt
= false;
1997 boolean ctrl
= false;
1998 boolean shift
= false;
1999 if (params
.size() > 1) {
2000 shift
= csiIsShift(params
.get(1));
2001 alt
= csiIsAlt(params
.get(1));
2002 ctrl
= csiIsCtrl(params
.get(1));
2007 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2009 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2011 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2013 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2015 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2017 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2019 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2021 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2023 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2025 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2027 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2029 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2031 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2033 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2041 * Produce mouse events based on "Any event tracking" and UTF-8
2043 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2045 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2047 private TInputEvent
parseMouse() {
2048 int buttons
= params
.get(0).charAt(0) - 32;
2049 int x
= params
.get(0).charAt(1) - 32 - 1;
2050 int y
= params
.get(0).charAt(2) - 32 - 1;
2052 // Clamp X and Y to the physical screen coordinates.
2053 if (x
>= windowResize
.getWidth()) {
2054 x
= windowResize
.getWidth() - 1;
2056 if (y
>= windowResize
.getHeight()) {
2057 y
= windowResize
.getHeight() - 1;
2060 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2061 boolean eventMouse1
= false;
2062 boolean eventMouse2
= false;
2063 boolean eventMouse3
= false;
2064 boolean eventMouseWheelUp
= false;
2065 boolean eventMouseWheelDown
= false;
2067 // System.err.printf("buttons: %04x\r\n", buttons);
2084 if (!mouse1
&& !mouse2
&& !mouse3
) {
2085 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2087 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2104 // Dragging with mouse1 down
2107 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2111 // Dragging with mouse2 down
2114 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2118 // Dragging with mouse3 down
2121 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2125 // Dragging with mouse2 down after wheelUp
2128 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2132 // Dragging with mouse2 down after wheelDown
2135 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2139 eventMouseWheelUp
= true;
2143 eventMouseWheelDown
= true;
2147 // Unknown, just make it motion
2148 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2151 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2152 eventMouse1
, eventMouse2
, eventMouse3
,
2153 eventMouseWheelUp
, eventMouseWheelDown
);
2157 * Produce mouse events based on "Any event tracking" and SGR
2159 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2161 * @param release if true, this was a release ('m')
2162 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2164 private TInputEvent
parseMouseSGR(final boolean release
) {
2165 // SGR extended coordinates - mode 1006
2166 if (params
.size() < 3) {
2167 // Invalid position, bail out.
2170 int buttons
= Integer
.parseInt(params
.get(0));
2171 int x
= Integer
.parseInt(params
.get(1)) - 1;
2172 int y
= Integer
.parseInt(params
.get(2)) - 1;
2174 // Clamp X and Y to the physical screen coordinates.
2175 if (x
>= windowResize
.getWidth()) {
2176 x
= windowResize
.getWidth() - 1;
2178 if (y
>= windowResize
.getHeight()) {
2179 y
= windowResize
.getHeight() - 1;
2182 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2183 boolean eventMouse1
= false;
2184 boolean eventMouse2
= false;
2185 boolean eventMouse3
= false;
2186 boolean eventMouseWheelUp
= false;
2187 boolean eventMouseWheelDown
= false;
2190 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2204 // Motion only, no buttons down
2205 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2209 // Dragging with mouse1 down
2211 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2215 // Dragging with mouse2 down
2217 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2221 // Dragging with mouse3 down
2223 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2227 // Dragging with mouse2 down after wheelUp
2229 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2233 // Dragging with mouse2 down after wheelDown
2235 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2239 eventMouseWheelUp
= true;
2243 eventMouseWheelDown
= true;
2247 // Unknown, bail out
2250 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2251 eventMouse1
, eventMouse2
, eventMouse3
,
2252 eventMouseWheelUp
, eventMouseWheelDown
);
2256 * Return any events in the IO queue due to timeout.
2258 * @param queue list to append new events to
2260 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2261 long nowTime
= System
.currentTimeMillis();
2263 // Check for new window size
2264 long windowSizeDelay
= nowTime
- windowSizeTime
;
2265 if (windowSizeDelay
> 1000) {
2266 int oldTextWidth
= getTextWidth();
2267 int oldTextHeight
= getTextHeight();
2269 sessionInfo
.queryWindowSize();
2270 int newWidth
= sessionInfo
.getWindowWidth();
2271 int newHeight
= sessionInfo
.getWindowHeight();
2273 if ((newWidth
!= windowResize
.getWidth())
2274 || (newHeight
!= windowResize
.getHeight())
2277 // Request xterm report window dimensions in pixels again.
2278 // Between now and then, ensure that the reported text cell
2279 // size is the same by setting widthPixels and heightPixels
2280 // to match the new dimensions.
2281 widthPixels
= oldTextWidth
* newWidth
;
2282 heightPixels
= oldTextHeight
* newHeight
;
2284 if (debugToStderr
) {
2285 System
.err
.println("Screen size changed, old size " +
2287 System
.err
.println(" new size " +
2288 newWidth
+ " x " + newHeight
);
2289 System
.err
.println(" old pixels " +
2290 oldTextWidth
+ " x " + oldTextHeight
);
2291 System
.err
.println(" new pixels " +
2292 getTextWidth() + " x " + getTextHeight());
2295 this.output
.printf("%s", xtermReportWindowPixelDimensions());
2296 this.output
.flush();
2298 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2299 newWidth
, newHeight
);
2300 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2301 newWidth
, newHeight
);
2304 windowSizeTime
= nowTime
;
2307 // ESCDELAY type timeout
2308 if (state
== ParseState
.ESCAPE
) {
2309 long escDelay
= nowTime
- escapeTime
;
2310 if (escDelay
> 100) {
2311 // After 0.1 seconds, assume a true escape character
2312 queue
.add(controlChar((char)0x1B, false));
2319 * Returns true if the CSI parameter for a keyboard command means that
2322 private boolean csiIsShift(final String x
) {
2334 * Returns true if the CSI parameter for a keyboard command means that
2337 private boolean csiIsAlt(final String x
) {
2349 * Returns true if the CSI parameter for a keyboard command means that
2352 private boolean csiIsCtrl(final String x
) {
2364 * Parses the next character of input to see if an InputEvent is
2367 * @param events list to append new events to
2368 * @param ch Unicode code point
2370 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2372 // ESCDELAY type timeout
2373 long nowTime
= System
.currentTimeMillis();
2374 if (state
== ParseState
.ESCAPE
) {
2375 long escDelay
= nowTime
- escapeTime
;
2376 if (escDelay
> 250) {
2377 // After 0.25 seconds, assume a true escape character
2378 events
.add(controlChar((char)0x1B, false));
2384 boolean ctrl
= false;
2385 boolean alt
= false;
2386 boolean shift
= false;
2388 // System.err.printf("state: %s ch %c\r\n", state, ch);
2394 state
= ParseState
.ESCAPE
;
2395 escapeTime
= nowTime
;
2400 // Control character
2401 events
.add(controlChar(ch
, false));
2408 events
.add(new TKeypressEvent(false, 0, ch
,
2409 false, false, false));
2418 // ALT-Control character
2419 events
.add(controlChar(ch
, true));
2425 // This will be one of the function keys
2426 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2430 // '[' goes to CSI_ENTRY
2432 state
= ParseState
.CSI_ENTRY
;
2436 // Everything else is assumed to be Alt-keystroke
2437 if ((ch
>= 'A') && (ch
<= 'Z')) {
2441 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2445 case ESCAPE_INTERMEDIATE
:
2446 if ((ch
>= 'P') && (ch
<= 'S')) {
2450 events
.add(new TKeypressEvent(kbF1
));
2453 events
.add(new TKeypressEvent(kbF2
));
2456 events
.add(new TKeypressEvent(kbF3
));
2459 events
.add(new TKeypressEvent(kbF4
));
2468 // Unknown keystroke, ignore
2473 // Numbers - parameter values
2474 if ((ch
>= '0') && (ch
<= '9')) {
2475 params
.set(params
.size() - 1,
2476 params
.get(params
.size() - 1) + ch
);
2477 state
= ParseState
.CSI_PARAM
;
2480 // Parameter separator
2486 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2490 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2495 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2500 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2505 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2510 events
.add(new TKeypressEvent(kbHome
));
2515 events
.add(new TKeypressEvent(kbEnd
));
2519 // CBT - Cursor backward X tab stops (default 1)
2520 events
.add(new TKeypressEvent(kbBackTab
));
2525 state
= ParseState
.MOUSE
;
2528 // Mouse position, SGR (1006) coordinates
2529 state
= ParseState
.MOUSE_SGR
;
2536 // Unknown keystroke, ignore
2541 // Numbers - parameter values
2542 if ((ch
>= '0') && (ch
<= '9')) {
2543 params
.set(params
.size() - 1,
2544 params
.get(params
.size() - 1) + ch
);
2547 // Parameter separator
2555 // Generate a mouse press event
2556 TInputEvent event
= parseMouseSGR(false);
2557 if (event
!= null) {
2563 // Generate a mouse release event
2564 event
= parseMouseSGR(true);
2565 if (event
!= null) {
2574 // Unknown keystroke, ignore
2579 // Numbers - parameter values
2580 if ((ch
>= '0') && (ch
<= '9')) {
2581 params
.set(params
.size() - 1,
2582 params
.get(params
.size() - 1) + ch
);
2583 state
= ParseState
.CSI_PARAM
;
2586 // Parameter separator
2593 events
.add(csiFnKey());
2598 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2602 if (params
.size() > 1) {
2603 shift
= csiIsShift(params
.get(1));
2604 alt
= csiIsAlt(params
.get(1));
2605 ctrl
= csiIsCtrl(params
.get(1));
2607 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2612 if (params
.size() > 1) {
2613 shift
= csiIsShift(params
.get(1));
2614 alt
= csiIsAlt(params
.get(1));
2615 ctrl
= csiIsCtrl(params
.get(1));
2617 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2622 if (params
.size() > 1) {
2623 shift
= csiIsShift(params
.get(1));
2624 alt
= csiIsAlt(params
.get(1));
2625 ctrl
= csiIsCtrl(params
.get(1));
2627 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2632 if (params
.size() > 1) {
2633 shift
= csiIsShift(params
.get(1));
2634 alt
= csiIsAlt(params
.get(1));
2635 ctrl
= csiIsCtrl(params
.get(1));
2637 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2642 if (params
.size() > 1) {
2643 shift
= csiIsShift(params
.get(1));
2644 alt
= csiIsAlt(params
.get(1));
2645 ctrl
= csiIsCtrl(params
.get(1));
2647 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2652 if (params
.size() > 1) {
2653 shift
= csiIsShift(params
.get(1));
2654 alt
= csiIsAlt(params
.get(1));
2655 ctrl
= csiIsCtrl(params
.get(1));
2657 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2662 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2663 if (debugToStderr
) {
2664 System
.err
.printf("windowOp pixels: " +
2665 "height %s width %s\n",
2666 params
.get(1), params
.get(2));
2669 widthPixels
= Integer
.parseInt(params
.get(2));
2670 heightPixels
= Integer
.parseInt(params
.get(1));
2671 } catch (NumberFormatException e
) {
2672 if (debugToStderr
) {
2673 e
.printStackTrace();
2676 if (widthPixels
<= 0) {
2679 if (heightPixels
<= 0) {
2690 // Unknown keystroke, ignore
2695 params
.set(0, params
.get(params
.size() - 1) + ch
);
2696 if (params
.get(0).length() == 3) {
2697 // We have enough to generate a mouse event
2698 events
.add(parseMouse());
2707 // This "should" be impossible to reach
2712 * Request (u)xterm to report the current window size dimensions.
2714 * @return the string to emit to xterm
2716 private String
xtermReportWindowPixelDimensions() {
2721 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2722 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2725 * @param on if true, enable metaSendsEscape
2726 * @return the string to emit to xterm
2728 private String
xtermMetaSendsEscape(final boolean on
) {
2730 return "\033[?1036h\033[?1034l";
2732 return "\033[?1036l";
2736 * Create an xterm OSC sequence to change the window title.
2738 * @param title the new title
2739 * @return the string to emit to xterm
2741 private String
getSetTitleString(final String title
) {
2742 return "\033]2;" + title
+ "\007";
2745 // ------------------------------------------------------------------------
2746 // Sixel output support ---------------------------------------------------
2747 // ------------------------------------------------------------------------
2750 * Start a sixel string for display one row's worth of bitmap data.
2752 * @param x column coordinate. 0 is the left-most column.
2753 * @param y row coordinate. 0 is the top-most row.
2754 * @return the string to emit to an ANSI / ECMA-style terminal
2756 private String
startSixel(final int x
, final int y
) {
2757 StringBuilder sb
= new StringBuilder();
2759 assert (sixel
== true);
2762 sb
.append(gotoXY(x
, y
));
2765 sb
.append("\033Pq");
2767 if (palette
== null) {
2768 palette
= new SixelPalette();
2771 return sb
.toString();
2775 * End a sixel string for display one row's worth of bitmap data.
2777 * @return the string to emit to an ANSI / ECMA-style terminal
2779 private String
endSixel() {
2780 assert (sixel
== true);
2787 * Create a sixel string representing a row of several cells containing
2790 * @param x column coordinate. 0 is the left-most column.
2791 * @param y row coordinate. 0 is the top-most row.
2792 * @param cells the cells containing the bitmap data
2793 * @return the string to emit to an ANSI / ECMA-style terminal
2795 private String
toSixel(final int x
, final int y
,
2796 final ArrayList
<Cell
> cells
) {
2798 StringBuilder sb
= new StringBuilder();
2800 assert (cells
!= null);
2801 assert (cells
.size() > 0);
2802 assert (cells
.get(0).getImage() != null);
2804 if (sixel
== false) {
2805 sb
.append(normal());
2806 sb
.append(gotoXY(x
, y
));
2807 for (int i
= 0; i
< cells
.size(); i
++) {
2810 return sb
.toString();
2813 if (sixelCache
== null) {
2814 sixelCache
= new SixelCache(height
* 10);
2817 // Save and get rows to/from the cache that do NOT have inverted
2819 boolean saveInCache
= true;
2820 for (Cell cell
: cells
) {
2821 if (cell
.isInvertedImage()) {
2822 saveInCache
= false;
2826 String cachedResult
= sixelCache
.get(cells
);
2827 if (cachedResult
!= null) {
2828 // System.err.println("CACHE HIT");
2829 sb
.append(startSixel(x
, y
));
2830 sb
.append(cachedResult
);
2831 sb
.append(endSixel());
2832 return sb
.toString();
2834 // System.err.println("CACHE MISS");
2837 int imageWidth
= cells
.get(0).getImage().getWidth();
2838 int imageHeight
= cells
.get(0).getImage().getHeight();
2840 // cells.get(x).getImage() has a dithered bitmap containing indexes
2841 // into the color palette. Piece these together into one larger
2842 // image for final rendering.
2844 int fullWidth
= cells
.size() * getTextWidth();
2845 int fullHeight
= getTextHeight();
2846 for (int i
= 0; i
< cells
.size(); i
++) {
2847 totalWidth
+= cells
.get(i
).getImage().getWidth();
2850 BufferedImage image
= new BufferedImage(fullWidth
,
2851 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
2854 for (int i
= 0; i
< cells
.size() - 1; i
++) {
2855 if (cells
.get(i
).isInvertedImage()) {
2856 rgbArray
= new int[imageWidth
* imageHeight
];
2857 for (int j
= 0; j
< rgbArray
.length
; j
++) {
2858 rgbArray
[j
] = 0xFFFFFF;
2861 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
2862 imageWidth
, imageHeight
, null, 0, imageWidth
);
2866 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
2867 i * imageWidth, 0, imageWidth, imageHeight,
2869 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
2870 fullWidth, fullHeight, cells.size(), getTextWidth());
2873 image
.setRGB(i
* imageWidth
, 0, imageWidth
, imageHeight
,
2874 rgbArray
, 0, imageWidth
);
2875 if (imageHeight
< fullHeight
) {
2876 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
2877 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2878 for (int imageY
= imageHeight
; imageY
< fullHeight
;
2881 image
.setRGB(imageX
, imageY
, backgroundColor
);
2886 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
2887 if (cells
.get(cells
.size() - 1).isInvertedImage()) {
2888 rgbArray
= new int[totalWidth
* imageHeight
];
2889 for (int j
= 0; j
< rgbArray
.length
; j
++) {
2890 rgbArray
[j
] = 0xFFFFFF;
2893 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
2894 totalWidth
, imageHeight
, null, 0, totalWidth
);
2896 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
2897 imageHeight
, rgbArray
, 0, totalWidth
);
2899 if (totalWidth
< getTextWidth()) {
2900 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
2902 for (int imageX
= image
.getWidth() - totalWidth
;
2903 imageX
< image
.getWidth(); imageX
++) {
2905 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
2906 image
.setRGB(imageX
, imageY
, backgroundColor
);
2911 // Dither the image. It is ok to lose the original here.
2912 if (palette
== null) {
2913 palette
= new SixelPalette();
2915 image
= palette
.ditherImage(image
);
2917 // Emit the palette, but only for the colors actually used by these
2919 boolean [] usedColors
= new boolean[MAX_COLOR_REGISTERS
];
2920 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2921 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
2922 usedColors
[image
.getRGB(imageX
, imageY
)] = true;
2925 palette
.emitPalette(sb
, usedColors
);
2927 // Render the entire row of cells.
2928 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
2929 int [][] sixels
= new int[image
.getWidth()][6];
2931 // See which colors are actually used in this band of sixels.
2932 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2933 for (int imageY
= 0;
2934 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
2937 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
2938 assert (colorIdx
>= 0);
2939 assert (colorIdx
< MAX_COLOR_REGISTERS
);
2941 sixels
[imageX
][imageY
] = colorIdx
;
2945 for (int i
= 0; i
< MAX_COLOR_REGISTERS
; i
++) {
2946 boolean isUsed
= false;
2947 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2948 for (int j
= 0; j
< 6; j
++) {
2949 if (sixels
[imageX
][j
] == i
) {
2954 if (isUsed
== false) {
2958 // Set to the beginning of scan line for the next set of
2959 // colored pixels, and select the color.
2960 sb
.append(String
.format("$#%d", i
));
2962 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2964 // Add up all the pixels that match this color.
2967 (j
< 6) && (currentRow
+ j
< fullHeight
);
2970 if (sixels
[imageX
][j
] == i
) {
2994 assert (data
< 127);
2996 sb
.append((char) data
);
2997 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
2998 } // for (int i = 0; i < MAX_COLOR_REGISTERS; i++)
3000 // Advance to the next scan line.
3003 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3005 // Kill the very last "-", because it is unnecessary.
3006 sb
.deleteCharAt(sb
.length() - 1);
3009 // This row is OK to save into the cache.
3010 sixelCache
.put(cells
, sb
.toString());
3013 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3016 // ------------------------------------------------------------------------
3017 // End sixel output support -----------------------------------------------
3018 // ------------------------------------------------------------------------
3021 * Create a SGR parameter sequence for a single color change.
3023 * @param bold if true, set bold
3024 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3025 * @param foreground if true, this is a foreground color
3026 * @return the string to emit to an ANSI / ECMA-style terminal,
3029 private String
color(final boolean bold
, final Color color
,
3030 final boolean foreground
) {
3031 return color(color
, foreground
, true) +
3032 rgbColor(bold
, color
, foreground
);
3036 * Create a T.416 RGB parameter sequence for a single color change.
3038 * @param colorRGB a 24-bit RGB value for foreground color
3039 * @param foreground if true, this is a foreground color
3040 * @return the string to emit to an ANSI / ECMA-style terminal,
3043 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
3045 int colorRed
= (colorRGB
>>> 16) & 0xFF;
3046 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
3047 int colorBlue
= colorRGB
& 0xFF;
3049 StringBuilder sb
= new StringBuilder();
3051 sb
.append("\033[38;2;");
3053 sb
.append("\033[48;2;");
3055 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
3056 return sb
.toString();
3060 * Create a T.416 RGB parameter sequence for both foreground and
3061 * background color change.
3063 * @param foreColorRGB a 24-bit RGB value for foreground color
3064 * @param backColorRGB a 24-bit RGB value for foreground color
3065 * @return the string to emit to an ANSI / ECMA-style terminal,
3068 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
3069 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3070 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3071 int foreColorBlue
= foreColorRGB
& 0xFF;
3072 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3073 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3074 int backColorBlue
= backColorRGB
& 0xFF;
3076 StringBuilder sb
= new StringBuilder();
3077 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
3078 foreColorRed
, foreColorGreen
, foreColorBlue
));
3079 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
3080 backColorRed
, backColorGreen
, backColorBlue
));
3081 return sb
.toString();
3085 * Create a T.416 RGB parameter sequence for a single color change.
3087 * @param bold if true, set bold
3088 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3089 * @param foreground if true, this is a foreground color
3090 * @return the string to emit to an xterm terminal with RGB support,
3091 * e.g. "\033[38;2;RR;GG;BBm"
3093 private String
rgbColor(final boolean bold
, final Color color
,
3094 final boolean foreground
) {
3095 if (doRgbColor
== false) {
3098 StringBuilder sb
= new StringBuilder("\033[");
3100 // Bold implies foreground only
3102 if (color
.equals(Color
.BLACK
)) {
3103 sb
.append("84;84;84");
3104 } else if (color
.equals(Color
.RED
)) {
3105 sb
.append("252;84;84");
3106 } else if (color
.equals(Color
.GREEN
)) {
3107 sb
.append("84;252;84");
3108 } else if (color
.equals(Color
.YELLOW
)) {
3109 sb
.append("252;252;84");
3110 } else if (color
.equals(Color
.BLUE
)) {
3111 sb
.append("84;84;252");
3112 } else if (color
.equals(Color
.MAGENTA
)) {
3113 sb
.append("252;84;252");
3114 } else if (color
.equals(Color
.CYAN
)) {
3115 sb
.append("84;252;252");
3116 } else if (color
.equals(Color
.WHITE
)) {
3117 sb
.append("252;252;252");
3125 if (color
.equals(Color
.BLACK
)) {
3127 } else if (color
.equals(Color
.RED
)) {
3128 sb
.append("168;0;0");
3129 } else if (color
.equals(Color
.GREEN
)) {
3130 sb
.append("0;168;0");
3131 } else if (color
.equals(Color
.YELLOW
)) {
3132 sb
.append("168;84;0");
3133 } else if (color
.equals(Color
.BLUE
)) {
3134 sb
.append("0;0;168");
3135 } else if (color
.equals(Color
.MAGENTA
)) {
3136 sb
.append("168;0;168");
3137 } else if (color
.equals(Color
.CYAN
)) {
3138 sb
.append("0;168;168");
3139 } else if (color
.equals(Color
.WHITE
)) {
3140 sb
.append("168;168;168");
3144 return sb
.toString();
3148 * Create a T.416 RGB parameter sequence for both foreground and
3149 * background color change.
3151 * @param bold if true, set bold
3152 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3153 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3154 * @return the string to emit to an xterm terminal with RGB support,
3155 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
3157 private String
rgbColor(final boolean bold
, final Color foreColor
,
3158 final Color backColor
) {
3159 if (doRgbColor
== false) {
3163 return rgbColor(bold
, foreColor
, true) +
3164 rgbColor(false, backColor
, false);
3168 * Create a SGR parameter sequence for a single color change.
3170 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3171 * @param foreground if true, this is a foreground color
3172 * @param header if true, make the full header, otherwise just emit the
3173 * color parameter e.g. "42;"
3174 * @return the string to emit to an ANSI / ECMA-style terminal,
3177 private String
color(final Color color
, final boolean foreground
,
3178 final boolean header
) {
3180 int ecmaColor
= color
.getValue();
3182 // Convert Color.* values to SGR numerics
3190 return String
.format("\033[%dm", ecmaColor
);
3192 return String
.format("%d;", ecmaColor
);
3197 * Create a SGR parameter sequence for both foreground and background
3200 * @param bold if true, set bold
3201 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3202 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3203 * @return the string to emit to an ANSI / ECMA-style terminal,
3204 * e.g. "\033[31;42m"
3206 private String
color(final boolean bold
, final Color foreColor
,
3207 final Color backColor
) {
3208 return color(foreColor
, backColor
, true) +
3209 rgbColor(bold
, foreColor
, backColor
);
3213 * Create a SGR parameter sequence for both foreground and
3214 * background color change.
3216 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3217 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3218 * @param header if true, make the full header, otherwise just emit the
3219 * color parameter e.g. "31;42;"
3220 * @return the string to emit to an ANSI / ECMA-style terminal,
3221 * e.g. "\033[31;42m"
3223 private String
color(final Color foreColor
, final Color backColor
,
3224 final boolean header
) {
3226 int ecmaForeColor
= foreColor
.getValue();
3227 int ecmaBackColor
= backColor
.getValue();
3229 // Convert Color.* values to SGR numerics
3230 ecmaBackColor
+= 40;
3231 ecmaForeColor
+= 30;
3234 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
3236 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
3241 * Create a SGR parameter sequence for foreground, background, and
3242 * several attributes. This sequence first resets all attributes to
3243 * default, then sets attributes as per the parameters.
3245 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3246 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3247 * @param bold if true, set bold
3248 * @param reverse if true, set reverse
3249 * @param blink if true, set blink
3250 * @param underline if true, set underline
3251 * @return the string to emit to an ANSI / ECMA-style terminal,
3252 * e.g. "\033[0;1;31;42m"
3254 private String
color(final Color foreColor
, final Color backColor
,
3255 final boolean bold
, final boolean reverse
, final boolean blink
,
3256 final boolean underline
) {
3258 int ecmaForeColor
= foreColor
.getValue();
3259 int ecmaBackColor
= backColor
.getValue();
3261 // Convert Color.* values to SGR numerics
3262 ecmaBackColor
+= 40;
3263 ecmaForeColor
+= 30;
3265 StringBuilder sb
= new StringBuilder();
3266 if ( bold
&& reverse
&& blink
&& !underline
) {
3267 sb
.append("\033[0;1;7;5;");
3268 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3269 sb
.append("\033[0;1;7;");
3270 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3271 sb
.append("\033[0;7;5;");
3272 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3273 sb
.append("\033[0;1;5;");
3274 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3275 sb
.append("\033[0;1;");
3276 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3277 sb
.append("\033[0;7;");
3278 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3279 sb
.append("\033[0;5;");
3280 } else if ( bold
&& reverse
&& blink
&& underline
) {
3281 sb
.append("\033[0;1;7;5;4;");
3282 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3283 sb
.append("\033[0;1;7;4;");
3284 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3285 sb
.append("\033[0;7;5;4;");
3286 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3287 sb
.append("\033[0;1;5;4;");
3288 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3289 sb
.append("\033[0;1;4;");
3290 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3291 sb
.append("\033[0;7;4;");
3292 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3293 sb
.append("\033[0;5;4;");
3294 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3295 sb
.append("\033[0;4;");
3297 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3298 sb
.append("\033[0;");
3300 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
3301 sb
.append(rgbColor(bold
, foreColor
, backColor
));
3302 return sb
.toString();
3306 * Create a SGR parameter sequence for foreground, background, and
3307 * several attributes. This sequence first resets all attributes to
3308 * default, then sets attributes as per the parameters.
3310 * @param foreColorRGB a 24-bit RGB value for foreground color
3311 * @param backColorRGB a 24-bit RGB value for foreground color
3312 * @param bold if true, set bold
3313 * @param reverse if true, set reverse
3314 * @param blink if true, set blink
3315 * @param underline if true, set underline
3316 * @return the string to emit to an ANSI / ECMA-style terminal,
3317 * e.g. "\033[0;1;31;42m"
3319 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
3320 final boolean bold
, final boolean reverse
, final boolean blink
,
3321 final boolean underline
) {
3323 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3324 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3325 int foreColorBlue
= foreColorRGB
& 0xFF;
3326 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3327 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3328 int backColorBlue
= backColorRGB
& 0xFF;
3330 StringBuilder sb
= new StringBuilder();
3331 if ( bold
&& reverse
&& blink
&& !underline
) {
3332 sb
.append("\033[0;1;7;5;");
3333 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3334 sb
.append("\033[0;1;7;");
3335 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3336 sb
.append("\033[0;7;5;");
3337 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3338 sb
.append("\033[0;1;5;");
3339 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3340 sb
.append("\033[0;1;");
3341 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3342 sb
.append("\033[0;7;");
3343 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3344 sb
.append("\033[0;5;");
3345 } else if ( bold
&& reverse
&& blink
&& underline
) {
3346 sb
.append("\033[0;1;7;5;4;");
3347 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3348 sb
.append("\033[0;1;7;4;");
3349 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3350 sb
.append("\033[0;7;5;4;");
3351 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3352 sb
.append("\033[0;1;5;4;");
3353 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3354 sb
.append("\033[0;1;4;");
3355 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3356 sb
.append("\033[0;7;4;");
3357 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3358 sb
.append("\033[0;5;4;");
3359 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3360 sb
.append("\033[0;4;");
3362 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3363 sb
.append("\033[0;");
3366 sb
.append("m\033[38;2;");
3367 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
3369 sb
.append("m\033[48;2;");
3370 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
3373 return sb
.toString();
3377 * Create a SGR parameter sequence to reset to defaults.
3379 * @return the string to emit to an ANSI / ECMA-style terminal,
3382 private String
normal() {
3383 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
3387 * Create a SGR parameter sequence to reset to defaults.
3389 * @param header if true, make the full header, otherwise just emit the
3390 * bare parameter e.g. "0;"
3391 * @return the string to emit to an ANSI / ECMA-style terminal,
3394 private String
normal(final boolean header
) {
3396 return "\033[0;37;40m";
3402 * Create a SGR parameter sequence for enabling the visible cursor.
3404 * @param on if true, turn on cursor
3405 * @return the string to emit to an ANSI / ECMA-style terminal
3407 private String
cursor(final boolean on
) {
3408 if (on
&& !cursorOn
) {
3412 if (!on
&& cursorOn
) {
3420 * Clear the entire screen. Because some terminals use back-color-erase,
3421 * set the color to white-on-black beforehand.
3423 * @return the string to emit to an ANSI / ECMA-style terminal
3425 private String
clearAll() {
3426 return "\033[0;37;40m\033[2J";
3430 * Clear the line from the cursor (inclusive) to the end of the screen.
3431 * Because some terminals use back-color-erase, set the color to
3432 * white-on-black beforehand.
3434 * @return the string to emit to an ANSI / ECMA-style terminal
3436 private String
clearRemainingLine() {
3437 return "\033[0;37;40m\033[K";
3441 * Move the cursor to (x, y).
3443 * @param x column coordinate. 0 is the left-most column.
3444 * @param y row coordinate. 0 is the top-most row.
3445 * @return the string to emit to an ANSI / ECMA-style terminal
3447 private String
gotoXY(final int x
, final int y
) {
3448 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
3452 * Tell (u)xterm that we want to receive mouse events based on "Any event
3453 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
3454 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
3456 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
3458 * Note that this also sets the alternate/primary screen buffer.
3460 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
3461 * mean "hide the mouse pointer." We have to use our own sequence to do
3462 * this because there is no standard in xterm for unilaterally hiding the
3463 * pointer all the time (regardless of typing).
3465 * @param on If true, enable mouse report and use the alternate screen
3466 * buffer. If false disable mouse reporting and use the primary screen
3468 * @return the string to emit to xterm
3470 private String
mouse(final boolean on
) {
3472 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
3474 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";