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
;
49 import jexer
.bits
.Cell
;
50 import jexer
.bits
.CellAttributes
;
51 import jexer
.bits
.Color
;
52 import jexer
.event
.TCommandEvent
;
53 import jexer
.event
.TInputEvent
;
54 import jexer
.event
.TKeypressEvent
;
55 import jexer
.event
.TMouseEvent
;
56 import jexer
.event
.TResizeEvent
;
57 import static jexer
.TCommand
.*;
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
{
84 // ------------------------------------------------------------------------
85 // Variables --------------------------------------------------------------
86 // ------------------------------------------------------------------------
89 * Emit debugging to stderr.
91 private boolean debugToStderr
= false;
94 * If true, emit T.416-style RGB colors for normal system colors. This
95 * is a) expensive in bandwidth, and b) potentially terrible looking for
98 private static boolean doRgbColor
= false;
101 * The session information.
103 private SessionInfo sessionInfo
;
106 * The event queue, filled up by a thread reading on input.
108 private List
<TInputEvent
> eventQueue
;
111 * If true, we want the reader thread to exit gracefully.
113 private boolean stopReaderThread
;
118 private Thread readerThread
;
121 * Parameters being collected. E.g. if the string is \033[1;3m, then
122 * params[0] will be 1 and params[1] will be 3.
124 private List
<String
> params
;
127 * Current parsing state.
129 private ParseState state
;
132 * The time we entered ESCAPE. If we get a bare escape without a code
133 * following it, this is used to return that bare escape.
135 private long escapeTime
;
138 * The time we last checked the window size. We try not to spawn stty
139 * more than once per second.
141 private long windowSizeTime
;
144 * true if mouse1 was down. Used to report mouse1 on the release event.
146 private boolean mouse1
;
149 * true if mouse2 was down. Used to report mouse2 on the release event.
151 private boolean mouse2
;
154 * true if mouse3 was down. Used to report mouse3 on the release event.
156 private boolean mouse3
;
159 * Cache the cursor visibility value so we only emit the sequence when we
162 private boolean cursorOn
= true;
165 * Cache the last window size to figure out if a TResizeEvent needs to be
168 private TResizeEvent windowResize
= null;
171 * If true, emit wide-char (CJK/Emoji) characters as sixel images.
173 private boolean wideCharImages
= true;
176 * Window width in pixels. Used for sixel support.
178 private int widthPixels
= 640;
181 * Window height in pixels. Used for sixel support.
183 private int heightPixels
= 400;
186 * If true, emit image data via sixel.
188 private boolean sixel
= true;
191 * The sixel palette handler.
193 private SixelPalette palette
= null;
196 * The sixel post-rendered string cache.
198 private SixelCache sixelCache
= null;
201 * Number of colors in the sixel palette. Xterm 335 defines the max as
202 * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
205 private int sixelPaletteSize
= 1024;
208 * If true, then we changed System.in and need to change it back.
210 private boolean setRawMode
;
213 * The terminal's input. If an InputStream is not specified in the
214 * constructor, then this InputStreamReader will be bound to System.in
215 * with UTF-8 encoding.
217 private Reader input
;
220 * The terminal's raw InputStream. If an InputStream is not specified in
221 * the constructor, then this InputReader will be bound to System.in.
222 * This is used by run() to see if bytes are available() before calling
223 * (Reader)input.read().
225 private InputStream inputStream
;
228 * The terminal's output. If an OutputStream is not specified in the
229 * constructor, then this PrintWriter will be bound to System.out with
232 private PrintWriter output
;
235 * The listening object that run() wakes up on new input.
237 private Object listener
;
240 * SixelPalette is used to manage the conversion of images between 24-bit
241 * RGB color and a palette of sixelPaletteSize colors.
243 private class SixelPalette
{
246 * Color palette for sixel output, sorted low to high.
248 private List
<Integer
> rgbColors
= new ArrayList
<Integer
>();
251 * Map of color palette index for sixel output, from the order it was
252 * generated by makePalette() to rgbColors.
254 private int [] rgbSortedIndex
= new int[sixelPaletteSize
];
257 * The color palette, organized by hue, saturation, and luminance.
258 * This is used for a fast color match.
260 private ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>> hslColors
;
263 * Number of bits for hue.
265 private int hueBits
= -1;
268 * Number of bits for saturation.
270 private int satBits
= -1;
273 * Number of bits for luminance.
275 private int lumBits
= -1;
278 * Step size for hue bins.
280 private int hueStep
= -1;
283 * Step size for saturation bins.
285 private int satStep
= -1;
288 * Cached RGB to HSL result.
290 private int hsl
[] = new int[3];
293 * ColorIdx records a RGB color and its palette index.
295 private class ColorIdx
{
297 * The 24-bit RGB color.
302 * The palette index for this color.
307 * Public constructor.
309 * @param color the 24-bit RGB color
310 * @param index the palette index for this color
312 public ColorIdx(final int color
, final int index
) {
319 * Public constructor.
321 public SixelPalette() {
326 * Find the nearest match for a color in the palette.
328 * @param color the RGB color
329 * @return the index in rgbColors that is closest to color
331 public int matchColor(final int color
) {
336 * matchColor() is a critical performance bottleneck. To make it
337 * decent, we do the following:
339 * 1. Find the nearest two hues that bracket this color.
341 * 2. Find the nearest two saturations that bracket this color.
343 * 3. Iterate within these four bands of luminance values,
344 * returning the closest color by Euclidean distance.
346 * This strategy reduces the search space by about 97%.
348 int red
= (color
>>> 16) & 0xFF;
349 int green
= (color
>>> 8) & 0xFF;
350 int blue
= color
& 0xFF;
352 if (sixelPaletteSize
== 2) {
353 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < 35568) {
362 rgbToHsl(red
, green
, blue
, hsl
);
366 // System.err.printf("%d %d %d\n", hue, sat, lum);
368 double diff
= Double
.MAX_VALUE
;
371 int hue1
= hue
/ (360/hueStep
);
373 if (hue1
>= hslColors
.size() - 1) {
374 // Bracket pure red from above.
375 hue1
= hslColors
.size() - 1;
377 } else if (hue1
== 0) {
378 // Bracket pure red from below.
379 hue2
= hslColors
.size() - 1;
382 for (int hI
= hue1
; hI
!= -1;) {
383 ArrayList
<ArrayList
<ColorIdx
>> sats
= hslColors
.get(hI
);
386 } else if (hI
== hue2
) {
390 int sMin
= (sat
/ satStep
) - 1;
395 } else if (sMin
== sats
.size() - 1) {
400 assert (sMax
- sMin
== 1);
403 // int sMax = sats.size() - 1;
405 for (int sI
= sMin
; sI
<= sMax
; sI
++) {
406 ArrayList
<ColorIdx
> lums
= sats
.get(sI
);
408 // True 3D colorspace match for the remaining values
409 for (ColorIdx c
: lums
) {
410 int rgbColor
= c
.color
;
412 int red2
= (rgbColor
>>> 16) & 0xFF;
413 int green2
= (rgbColor
>>> 8) & 0xFF;
414 int blue2
= rgbColor
& 0xFF;
415 newDiff
+= Math
.pow(red2
- red
, 2);
416 newDiff
+= Math
.pow(green2
- green
, 2);
417 newDiff
+= Math
.pow(blue2
- blue
, 2);
418 if (newDiff
< diff
) {
419 idx
= rgbSortedIndex
[c
.index
];
426 if (((red
* red
) + (green
* green
) + (blue
* blue
)) < diff
) {
427 // Black is a closer match.
429 } else if ((((255 - red
) * (255 - red
)) +
430 ((255 - green
) * (255 - green
)) +
431 ((255 - blue
) * (255 - blue
))) < diff
) {
433 // White is a closer match.
434 idx
= sixelPaletteSize
- 1;
441 * Clamp an int value to [0, 255].
443 * @param x the int value
444 * @return an int between 0 and 255.
446 private int clamp(final int x
) {
457 * Dither an image to a sixelPaletteSize palette. The dithered
458 * image cells will contain indexes into the palette.
460 * @param image the image to dither
461 * @return the dithered image. Every pixel is an index into the
464 public BufferedImage
ditherImage(final BufferedImage image
) {
466 BufferedImage ditheredImage
= new BufferedImage(image
.getWidth(),
467 image
.getHeight(), BufferedImage
.TYPE_INT_ARGB
);
469 int [] rgbArray
= image
.getRGB(0, 0, image
.getWidth(),
470 image
.getHeight(), null, 0, image
.getWidth());
471 ditheredImage
.setRGB(0, 0, image
.getWidth(), image
.getHeight(),
472 rgbArray
, 0, image
.getWidth());
474 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
475 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
476 int oldPixel
= ditheredImage
.getRGB(imageX
,
478 int colorIdx
= matchColor(oldPixel
);
479 assert (colorIdx
>= 0);
480 assert (colorIdx
< sixelPaletteSize
);
481 int newPixel
= rgbColors
.get(colorIdx
);
482 ditheredImage
.setRGB(imageX
, imageY
, colorIdx
);
484 int oldRed
= (oldPixel
>>> 16) & 0xFF;
485 int oldGreen
= (oldPixel
>>> 8) & 0xFF;
486 int oldBlue
= oldPixel
& 0xFF;
488 int newRed
= (newPixel
>>> 16) & 0xFF;
489 int newGreen
= (newPixel
>>> 8) & 0xFF;
490 int newBlue
= newPixel
& 0xFF;
492 int redError
= (oldRed
- newRed
) / 16;
493 int greenError
= (oldGreen
- newGreen
) / 16;
494 int blueError
= (oldBlue
- newBlue
) / 16;
496 int red
, green
, blue
;
497 if (imageX
< image
.getWidth() - 1) {
498 int pXpY
= ditheredImage
.getRGB(imageX
+ 1, imageY
);
499 red
= ((pXpY
>>> 16) & 0xFF) + (7 * redError
);
500 green
= ((pXpY
>>> 8) & 0xFF) + (7 * greenError
);
501 blue
= ( pXpY
& 0xFF) + (7 * blueError
);
503 green
= clamp(green
);
505 pXpY
= ((red
& 0xFF) << 16);
506 pXpY
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
507 ditheredImage
.setRGB(imageX
+ 1, imageY
, pXpY
);
509 if (imageY
< image
.getHeight() - 1) {
510 int pXpYp
= ditheredImage
.getRGB(imageX
+ 1,
512 red
= ((pXpYp
>>> 16) & 0xFF) + redError
;
513 green
= ((pXpYp
>>> 8) & 0xFF) + greenError
;
514 blue
= ( pXpYp
& 0xFF) + blueError
;
516 green
= clamp(green
);
518 pXpYp
= ((red
& 0xFF) << 16);
519 pXpYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
520 ditheredImage
.setRGB(imageX
+ 1, imageY
+ 1, pXpYp
);
522 } else if (imageY
< image
.getHeight() - 1) {
523 int pXmYp
= ditheredImage
.getRGB(imageX
- 1,
525 int pXYp
= ditheredImage
.getRGB(imageX
,
528 red
= ((pXmYp
>>> 16) & 0xFF) + (3 * redError
);
529 green
= ((pXmYp
>>> 8) & 0xFF) + (3 * greenError
);
530 blue
= ( pXmYp
& 0xFF) + (3 * blueError
);
532 green
= clamp(green
);
534 pXmYp
= ((red
& 0xFF) << 16);
535 pXmYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
536 ditheredImage
.setRGB(imageX
- 1, imageY
+ 1, pXmYp
);
538 red
= ((pXYp
>>> 16) & 0xFF) + (5 * redError
);
539 green
= ((pXYp
>>> 8) & 0xFF) + (5 * greenError
);
540 blue
= ( pXYp
& 0xFF) + (5 * blueError
);
542 green
= clamp(green
);
544 pXYp
= ((red
& 0xFF) << 16);
545 pXYp
|= ((green
& 0xFF) << 8) | (blue
& 0xFF);
546 ditheredImage
.setRGB(imageX
, imageY
+ 1, pXYp
);
548 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
549 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
551 return ditheredImage
;
555 * Convert an RGB color to HSL.
557 * @param red red color, between 0 and 255
558 * @param green green color, between 0 and 255
559 * @param blue blue color, between 0 and 255
560 * @param hsl the hsl color as [hue, saturation, luminance]
562 private void rgbToHsl(final int red
, final int green
,
563 final int blue
, final int [] hsl
) {
565 assert ((red
>= 0) && (red
<= 255));
566 assert ((green
>= 0) && (green
<= 255));
567 assert ((blue
>= 0) && (blue
<= 255));
569 double R
= red
/ 255.0;
570 double G
= green
/ 255.0;
571 double B
= blue
/ 255.0;
572 boolean Rmax
= false;
573 boolean Gmax
= false;
574 boolean Bmax
= false;
575 double min
= (R
< G ? R
: G
);
576 min
= (min
< B ? min
: B
);
578 if ((R
>= G
) && (R
>= B
)) {
581 } else if ((G
>= R
) && (G
>= B
)) {
584 } else if ((B
>= G
) && (B
>= R
)) {
589 double L
= (min
+ max
) / 2.0;
594 S
= (max
- min
) / (max
+ min
);
596 S
= (max
- min
) / (2.0 - max
- min
);
600 assert (Gmax
== false);
601 assert (Bmax
== false);
602 H
= (G
- B
) / (max
- min
);
604 assert (Rmax
== false);
605 assert (Bmax
== false);
606 H
= 2.0 + (B
- R
) / (max
- min
);
608 assert (Rmax
== false);
609 assert (Gmax
== false);
610 H
= 4.0 + (R
- G
) / (max
- min
);
615 hsl
[0] = (int) (H
* 60.0);
616 hsl
[1] = (int) (S
* 100.0);
617 hsl
[2] = (int) (L
* 100.0);
619 assert ((hsl
[0] >= 0) && (hsl
[0] <= 360));
620 assert ((hsl
[1] >= 0) && (hsl
[1] <= 100));
621 assert ((hsl
[2] >= 0) && (hsl
[2] <= 100));
625 * Convert a HSL color to RGB.
627 * @param hue hue, between 0 and 359
628 * @param sat saturation, between 0 and 100
629 * @param lum luminance, between 0 and 100
630 * @return the rgb color as 0x00RRGGBB
632 private int hslToRgb(final int hue
, final int sat
, final int lum
) {
633 assert ((hue
>= 0) && (hue
<= 360));
634 assert ((sat
>= 0) && (sat
<= 100));
635 assert ((lum
>= 0) && (lum
<= 100));
637 double S
= sat
/ 100.0;
638 double L
= lum
/ 100.0;
639 double C
= (1.0 - Math
.abs((2.0 * L
) - 1.0)) * S
;
640 double Hp
= hue
/ 60.0;
641 double X
= C
* (1.0 - Math
.abs((Hp
% 2) - 1.0));
648 } else if (Hp
<= 2.0) {
651 } else if (Hp
<= 3.0) {
654 } else if (Hp
<= 4.0) {
657 } else if (Hp
<= 5.0) {
660 } else if (Hp
<= 6.0) {
664 double m
= L
- (C
/ 2.0);
665 int red
= ((int) ((Rp
+ m
) * 255.0)) << 16;
666 int green
= ((int) ((Gp
+ m
) * 255.0)) << 8;
667 int blue
= (int) ((Bp
+ m
) * 255.0);
669 return (red
| green
| blue
);
673 * Create the sixel palette.
675 private void makePalette() {
676 // Generate the sixel palette. Because we have no idea at this
677 // layer which image(s) will be shown, we have to use a common
678 // palette with sixelPaletteSize colors for everything, and
679 // map the BufferedImage colors to their nearest neighbor in RGB
682 if (sixelPaletteSize
== 2) {
684 rgbColors
.add(0xFFFFFF);
685 rgbSortedIndex
[0] = 0;
686 rgbSortedIndex
[1] = 1;
690 // We build a palette using the Hue-Saturation-Luminence model,
691 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
692 // Luminance. We convert these colors to 24-bit RGB, sort them
693 // ascending, and steal the first index for pure black and the
694 // last for pure white. The 8-bit final palette favors bright
695 // colors, somewhere between pastel and classic television
696 // technicolor. 9- and 10-bit palettes are more uniform.
698 // Default at 256 colors.
703 assert (sixelPaletteSize
>= 256);
704 assert ((sixelPaletteSize
== 256)
705 || (sixelPaletteSize
== 512)
706 || (sixelPaletteSize
== 1024)
707 || (sixelPaletteSize
== 2048));
709 switch (sixelPaletteSize
) {
726 hueStep
= (int) (Math
.pow(2, hueBits
));
727 satStep
= (int) (100 / Math
.pow(2, satBits
));
728 // 1 bit for luminance: 40 and 70.
733 // 2 bits: 20, 40, 60, 80
738 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
744 // System.err.printf("<html><body>\n");
745 // Hue is evenly spaced around the wheel.
746 hslColors
= new ArrayList
<ArrayList
<ArrayList
<ColorIdx
>>>();
748 final boolean DEBUG
= false;
749 ArrayList
<Integer
> rawRgbList
= new ArrayList
<Integer
>();
751 for (int hue
= 0; hue
< (360 - (360 % hueStep
));
752 hue
+= (360/hueStep
)) {
754 ArrayList
<ArrayList
<ColorIdx
>> satList
= null;
755 satList
= new ArrayList
<ArrayList
<ColorIdx
>>();
756 hslColors
.add(satList
);
758 // Saturation is linearly spaced between pastel and pure.
759 for (int sat
= satStep
; sat
<= 100; sat
+= satStep
) {
761 ArrayList
<ColorIdx
> lumList
= new ArrayList
<ColorIdx
>();
762 satList
.add(lumList
);
764 // Luminance brackets the pure color, but leaning toward
766 for (int lum
= lumBegin
; lum
< 100; lum
+= lumStep
) {
768 System.err.printf("<font style = \"color:");
769 System.err.printf("hsl(%d, %d%%, %d%%)",
771 System.err.printf(";\">=</font>\n");
773 int rgbColor
= hslToRgb(hue
, sat
, lum
);
774 rgbColors
.add(rgbColor
);
775 ColorIdx colorIdx
= new ColorIdx(rgbColor
,
776 rgbColors
.size() - 1);
777 lumList
.add(colorIdx
);
779 rawRgbList
.add(rgbColor
);
781 int red
= (rgbColor
>>> 16) & 0xFF;
782 int green
= (rgbColor
>>> 8) & 0xFF;
783 int blue
= rgbColor
& 0xFF;
784 int [] backToHsl
= new int[3];
785 rgbToHsl(red
, green
, blue
, backToHsl
);
786 System
.err
.printf("%d [%d] %d [%d] %d [%d]\n",
787 hue
, backToHsl
[0], sat
, backToHsl
[1],
793 // System.err.printf("\n</body></html>\n");
795 assert (rgbColors
.size() == sixelPaletteSize
);
798 * We need to sort rgbColors, so that toSixel() can know where
799 * BLACK and WHITE are in it. But we also need to be able to
800 * find the sorted values using the old unsorted indexes. So we
801 * will sort it, put all the indexes into a HashMap, and then
802 * build rgbSortedIndex[].
804 Collections
.sort(rgbColors
);
805 HashMap
<Integer
, Integer
> rgbColorIndices
= null;
806 rgbColorIndices
= new HashMap
<Integer
, Integer
>();
807 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
808 rgbColorIndices
.put(rgbColors
.get(i
), i
);
810 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
811 int rawColor
= rawRgbList
.get(i
);
812 rgbSortedIndex
[i
] = rgbColorIndices
.get(rawColor
);
815 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
816 assert (rawRgbList
!= null);
817 int idx
= rgbSortedIndex
[i
];
818 int rgbColor
= rgbColors
.get(idx
);
819 if ((idx
!= 0) && (idx
!= sixelPaletteSize
- 1)) {
821 System.err.printf("%d %06x --> %d %06x\n",
822 i, rawRgbList.get(i), idx, rgbColors.get(idx));
824 assert (rgbColor
== rawRgbList
.get(i
));
829 // Set the dimmest color as true black, and the brightest as true
832 rgbColors
.set(sixelPaletteSize
- 1, 0xFFFFFF);
835 System.err.printf("<html><body>\n");
836 for (Integer rgb: rgbColors) {
837 System.err.printf("<font style = \"color:");
838 System.err.printf("#%06x", rgb);
839 System.err.printf(";\">=</font>\n");
841 System.err.printf("\n</body></html>\n");
847 * Emit the sixel palette.
849 * @param sb the StringBuilder to append to
850 * @param used array of booleans set to true for each color actually
851 * used in this cell, or null to emit the entire palette
852 * @return the string to emit to an ANSI / ECMA-style terminal
854 public String
emitPalette(final StringBuilder sb
,
855 final boolean [] used
) {
857 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
858 if (((used
!= null) && (used
[i
] == true)) || (used
== null)) {
859 int rgbColor
= rgbColors
.get(i
);
860 sb
.append(String
.format("#%d;2;%d;%d;%d", i
,
861 ((rgbColor
>>> 16) & 0xFF) * 100 / 255,
862 ((rgbColor
>>> 8) & 0xFF) * 100 / 255,
863 ( rgbColor
& 0xFF) * 100 / 255));
866 return sb
.toString();
871 * SixelCache is a least-recently-used cache that hangs on to the
872 * post-rendered sixel string for a particular set of cells.
874 private class SixelCache
{
877 * Maximum size of the cache.
879 private int maxSize
= 100;
882 * The entries stored in the cache.
884 private HashMap
<String
, CacheEntry
> cache
= null;
887 * CacheEntry is one entry in the cache.
889 private class CacheEntry
{
901 * The last time this entry was used.
903 public long millis
= 0;
906 * Public constructor.
908 * @param key the cache entry key
909 * @param data the cache entry data
911 public CacheEntry(final String key
, final String data
) {
914 this.millis
= System
.currentTimeMillis();
919 * Public constructor.
921 * @param maxSize the maximum size of the cache
923 public SixelCache(final int maxSize
) {
924 this.maxSize
= maxSize
;
925 cache
= new HashMap
<String
, CacheEntry
>();
929 * Make a unique key for a list of cells.
931 * @param cells the cells
934 private String
makeKey(final ArrayList
<Cell
> cells
) {
935 StringBuilder sb
= new StringBuilder();
936 for (Cell cell
: cells
) {
937 sb
.append(cell
.hashCode());
939 return sb
.toString();
943 * Get an entry from the cache.
945 * @param cells the list of cells that are the cache key
946 * @return the sixel string representing these cells, or null if this
947 * list of cells is not in the cache
949 public String
get(final ArrayList
<Cell
> cells
) {
950 CacheEntry entry
= cache
.get(makeKey(cells
));
954 entry
.millis
= System
.currentTimeMillis();
959 * Put an entry into the cache.
961 * @param cells the list of cells that are the cache key
962 * @param data the sixel string representing these cells
964 public void put(final ArrayList
<Cell
> cells
, final String data
) {
965 String key
= makeKey(cells
);
967 // System.err.println("put() " + key + " size " + cache.size());
969 assert (!cache
.containsKey(key
));
971 assert (cache
.size() <= maxSize
);
972 if (cache
.size() == maxSize
) {
973 // Cache is at limit, evict oldest entry.
974 long oldestTime
= Long
.MAX_VALUE
;
975 String keyToRemove
= null;
976 for (CacheEntry entry
: cache
.values()) {
977 if ((entry
.millis
< oldestTime
) || (keyToRemove
== null)) {
978 keyToRemove
= entry
.key
;
979 oldestTime
= entry
.millis
;
983 System.err.println("put() remove key = " + keyToRemove +
984 " size " + cache.size());
986 assert (keyToRemove
!= null);
987 cache
.remove(keyToRemove
);
989 System.err.println("put() removed, size " + cache.size());
992 assert (cache
.size() <= maxSize
);
993 CacheEntry entry
= new CacheEntry(key
, data
);
994 assert (key
.equals(entry
.key
));
995 cache
.put(key
, entry
);
997 System.err.println("put() added key " + key + " " +
998 " size " + cache.size());
1004 // ------------------------------------------------------------------------
1005 // Constructors -----------------------------------------------------------
1006 // ------------------------------------------------------------------------
1009 * Constructor sets up state for getEvent(). If either windowWidth or
1010 * windowHeight are less than 1, the terminal is not resized.
1012 * @param listener the object this backend needs to wake up when new
1014 * @param input an InputStream connected to the remote user, or null for
1015 * System.in. If System.in is used, then on non-Windows systems it will
1016 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
1017 * mode. input is always converted to a Reader with UTF-8 encoding.
1018 * @param output an OutputStream connected to the remote user, or null
1019 * for System.out. output is always converted to a Writer with UTF-8
1021 * @param windowWidth the number of text columns to start with
1022 * @param windowHeight the number of text rows to start with
1023 * @throws UnsupportedEncodingException if an exception is thrown when
1024 * creating the InputStreamReader
1026 public ECMA48Terminal(final Object listener
, final InputStream input
,
1027 final OutputStream output
, final int windowWidth
,
1028 final int windowHeight
) throws UnsupportedEncodingException
{
1030 this(listener
, input
, output
);
1032 // Send dtterm/xterm sequences, which will probably not work because
1033 // allowWindowOps is defaulted to false.
1034 if ((windowWidth
> 0) && (windowHeight
> 0)) {
1035 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
1037 this.output
.write(resizeString
);
1038 this.output
.flush();
1043 * Constructor sets up state for getEvent().
1045 * @param listener the object this backend needs to wake up when new
1047 * @param input an InputStream connected to the remote user, or null for
1048 * System.in. If System.in is used, then on non-Windows systems it will
1049 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
1050 * mode. input is always converted to a Reader with UTF-8 encoding.
1051 * @param output an OutputStream connected to the remote user, or null
1052 * for System.out. output is always converted to a Writer with UTF-8
1054 * @throws UnsupportedEncodingException if an exception is thrown when
1055 * creating the InputStreamReader
1057 public ECMA48Terminal(final Object listener
, final InputStream input
,
1058 final OutputStream output
) throws UnsupportedEncodingException
{
1064 stopReaderThread
= false;
1065 this.listener
= listener
;
1067 if (input
== null) {
1068 // inputStream = System.in;
1069 inputStream
= new FileInputStream(FileDescriptor
.in
);
1073 inputStream
= input
;
1075 this.input
= new InputStreamReader(inputStream
, "UTF-8");
1077 if (input
instanceof SessionInfo
) {
1078 // This is a TelnetInputStream that exposes window size and
1079 // environment variables from the telnet layer.
1080 sessionInfo
= (SessionInfo
) input
;
1082 if (sessionInfo
== null) {
1083 if (input
== null) {
1084 // Reading right off the tty
1085 sessionInfo
= new TTYSessionInfo();
1087 sessionInfo
= new TSessionInfo();
1091 if (output
== null) {
1092 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
1095 this.output
= new PrintWriter(new OutputStreamWriter(output
,
1099 // Request xterm report window dimensions in pixels
1100 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1102 // Enable mouse reporting and metaSendsEscape
1103 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1104 this.output
.flush();
1106 // Query the screen size
1107 sessionInfo
.queryWindowSize();
1108 setDimensions(sessionInfo
.getWindowWidth(),
1109 sessionInfo
.getWindowHeight());
1111 // Hang onto the window size
1112 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1113 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1117 // Spin up the input reader
1118 eventQueue
= new ArrayList
<TInputEvent
>();
1119 readerThread
= new Thread(this);
1120 readerThread
.start();
1123 this.output
.write(clearAll());
1124 this.output
.flush();
1128 * Constructor sets up state for getEvent().
1130 * @param listener the object this backend needs to wake up when new
1132 * @param input the InputStream underlying 'reader'. Its available()
1133 * method is used to determine if reader.read() will block or not.
1134 * @param reader a Reader connected to the remote user.
1135 * @param writer a PrintWriter connected to the remote user.
1136 * @param setRawMode if true, set System.in into raw mode with stty.
1137 * This should in general not be used. It is here solely for Demo3,
1138 * which uses System.in.
1139 * @throws IllegalArgumentException if input, reader, or writer are null.
1141 public ECMA48Terminal(final Object listener
, final InputStream input
,
1142 final Reader reader
, final PrintWriter writer
,
1143 final boolean setRawMode
) {
1145 if (input
== null) {
1146 throw new IllegalArgumentException("InputStream must be specified");
1148 if (reader
== null) {
1149 throw new IllegalArgumentException("Reader must be specified");
1151 if (writer
== null) {
1152 throw new IllegalArgumentException("Writer must be specified");
1158 stopReaderThread
= false;
1159 this.listener
= listener
;
1161 inputStream
= input
;
1162 this.input
= reader
;
1164 if (setRawMode
== true) {
1167 this.setRawMode
= setRawMode
;
1169 if (input
instanceof SessionInfo
) {
1170 // This is a TelnetInputStream that exposes window size and
1171 // environment variables from the telnet layer.
1172 sessionInfo
= (SessionInfo
) input
;
1174 if (sessionInfo
== null) {
1175 if (setRawMode
== true) {
1176 // Reading right off the tty
1177 sessionInfo
= new TTYSessionInfo();
1179 sessionInfo
= new TSessionInfo();
1183 this.output
= writer
;
1185 // Request xterm report window dimensions in pixels
1186 this.output
.printf("%s", xtermReportWindowPixelDimensions());
1188 // Enable mouse reporting and metaSendsEscape
1189 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1190 this.output
.flush();
1192 // Query the screen size
1193 sessionInfo
.queryWindowSize();
1194 setDimensions(sessionInfo
.getWindowWidth(),
1195 sessionInfo
.getWindowHeight());
1197 // Hang onto the window size
1198 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1199 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
1203 // Spin up the input reader
1204 eventQueue
= new ArrayList
<TInputEvent
>();
1205 readerThread
= new Thread(this);
1206 readerThread
.start();
1209 this.output
.write(clearAll());
1210 this.output
.flush();
1214 * Constructor sets up state for getEvent().
1216 * @param listener the object this backend needs to wake up when new
1218 * @param input the InputStream underlying 'reader'. Its available()
1219 * method is used to determine if reader.read() will block or not.
1220 * @param reader a Reader connected to the remote user.
1221 * @param writer a PrintWriter connected to the remote user.
1222 * @throws IllegalArgumentException if input, reader, or writer are null.
1224 public ECMA48Terminal(final Object listener
, final InputStream input
,
1225 final Reader reader
, final PrintWriter writer
) {
1227 this(listener
, input
, reader
, writer
, false);
1230 // ------------------------------------------------------------------------
1231 // LogicalScreen ----------------------------------------------------------
1232 // ------------------------------------------------------------------------
1235 * Set the window title.
1237 * @param title the new title
1240 public void setTitle(final String title
) {
1241 output
.write(getSetTitleString(title
));
1246 * Push the logical screen to the physical device.
1249 public void flushPhysical() {
1250 StringBuilder sb
= new StringBuilder();
1254 && (cursorY
<= height
- 1)
1255 && (cursorX
<= width
- 1)
1258 sb
.append(cursor(true));
1259 sb
.append(gotoXY(cursorX
, cursorY
));
1261 sb
.append(cursor(false));
1264 output
.write(sb
.toString());
1269 * Resize the physical screen to match the logical screen dimensions.
1272 public void resizeToScreen() {
1273 // Send dtterm/xterm sequences, which will probably not work because
1274 // allowWindowOps is defaulted to false.
1275 String resizeString
= String
.format("\033[8;%d;%dt", getHeight(),
1277 this.output
.write(resizeString
);
1278 this.output
.flush();
1281 // ------------------------------------------------------------------------
1282 // TerminalReader ---------------------------------------------------------
1283 // ------------------------------------------------------------------------
1286 * Check if there are events in the queue.
1288 * @return if true, getEvents() has something to return to the backend
1290 public boolean hasEvents() {
1291 synchronized (eventQueue
) {
1292 return (eventQueue
.size() > 0);
1297 * Return any events in the IO queue.
1299 * @param queue list to append new events to
1301 public void getEvents(final List
<TInputEvent
> queue
) {
1302 synchronized (eventQueue
) {
1303 if (eventQueue
.size() > 0) {
1304 synchronized (queue
) {
1305 queue
.addAll(eventQueue
);
1313 * Restore terminal to normal state.
1315 public void closeTerminal() {
1317 // System.err.println("=== shutdown() ==="); System.err.flush();
1319 // Tell the reader thread to stop looking at input
1320 stopReaderThread
= true;
1322 readerThread
.join();
1323 } catch (InterruptedException e
) {
1324 if (debugToStderr
) {
1325 e
.printStackTrace();
1329 // Disable mouse reporting and show cursor. Defensive null check
1330 // here in case closeTerminal() is called twice.
1331 if (output
!= null) {
1332 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
1339 // We don't close System.in/out
1341 // Shut down the streams, this should wake up the reader thread
1342 // and make it exit.
1343 if (input
!= null) {
1346 } catch (IOException e
) {
1351 if (output
!= null) {
1359 * Set listener to a different Object.
1361 * @param listener the new listening object that run() wakes up on new
1364 public void setListener(final Object listener
) {
1365 this.listener
= listener
;
1369 * Reload options from System properties.
1371 public void reloadOptions() {
1372 // Permit RGB colors only if externally requested.
1373 if (System
.getProperty("jexer.ECMA48.rgbColor",
1374 "false").equals("true")
1381 // Default to using sixel for full-width characters.
1382 if (System
.getProperty("jexer.ECMA48.wideCharImages",
1383 "true").equals("true")) {
1384 wideCharImages
= true;
1386 wideCharImages
= false;
1389 // Pull the system properties for sixel output.
1390 if (System
.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1397 int paletteSize
= 1024;
1399 paletteSize
= Integer
.parseInt(System
.getProperty(
1400 "jexer.ECMA48.sixelPaletteSize", "1024"));
1401 switch (paletteSize
) {
1407 sixelPaletteSize
= paletteSize
;
1413 } catch (NumberFormatException e
) {
1418 // ------------------------------------------------------------------------
1419 // Runnable ---------------------------------------------------------------
1420 // ------------------------------------------------------------------------
1423 * Read function runs on a separate thread.
1426 boolean done
= false;
1427 // available() will often return > 1, so we need to read in chunks to
1429 char [] readBuffer
= new char[128];
1430 List
<TInputEvent
> events
= new ArrayList
<TInputEvent
>();
1432 while (!done
&& !stopReaderThread
) {
1434 // We assume that if inputStream has bytes available, then
1435 // input won't block on read().
1436 int n
= inputStream
.available();
1439 System.err.printf("inputStream.available(): %d\n", n);
1444 if (readBuffer
.length
< n
) {
1445 // The buffer wasn't big enough, make it huger
1446 readBuffer
= new char[readBuffer
.length
* 2];
1449 // System.err.printf("BEFORE read()\n"); System.err.flush();
1451 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1454 System.err.printf("AFTER read() %d\n", rc);
1462 for (int i
= 0; i
< rc
; i
++) {
1463 int ch
= readBuffer
[i
];
1464 processChar(events
, (char)ch
);
1466 getIdleEvents(events
);
1467 if (events
.size() > 0) {
1468 // Add to the queue for the backend thread to
1469 // be able to obtain.
1470 synchronized (eventQueue
) {
1471 eventQueue
.addAll(events
);
1473 if (listener
!= null) {
1474 synchronized (listener
) {
1475 listener
.notifyAll();
1482 getIdleEvents(events
);
1483 if (events
.size() > 0) {
1484 synchronized (eventQueue
) {
1485 eventQueue
.addAll(events
);
1487 if (listener
!= null) {
1488 synchronized (listener
) {
1489 listener
.notifyAll();
1495 if (output
.checkError()) {
1500 // Wait 20 millis for more data
1503 // System.err.println("end while loop"); System.err.flush();
1504 } catch (InterruptedException e
) {
1506 } catch (IOException e
) {
1507 e
.printStackTrace();
1510 } // while ((done == false) && (stopReaderThread == false))
1512 // Pass an event up to TApplication to tell it this Backend is done.
1513 synchronized (eventQueue
) {
1514 eventQueue
.add(new TCommandEvent(cmBackendDisconnect
));
1516 if (listener
!= null) {
1517 synchronized (listener
) {
1518 listener
.notifyAll();
1522 // System.err.println("*** run() exiting..."); System.err.flush();
1525 // ------------------------------------------------------------------------
1526 // ECMA48Terminal ---------------------------------------------------------
1527 // ------------------------------------------------------------------------
1530 * Get the width of a character cell in pixels.
1532 * @return the width in pixels of a character cell
1534 public int getTextWidth() {
1535 return (widthPixels
/ sessionInfo
.getWindowWidth());
1539 * Get the height of a character cell in pixels.
1541 * @return the height in pixels of a character cell
1543 public int getTextHeight() {
1544 return (heightPixels
/ sessionInfo
.getWindowHeight());
1548 * Getter for sessionInfo.
1550 * @return the SessionInfo
1552 public SessionInfo
getSessionInfo() {
1557 * Get the output writer.
1559 * @return the Writer
1561 public PrintWriter
getOutput() {
1566 * Call 'stty' to set cooked mode.
1568 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
1570 private void sttyCooked() {
1575 * Call 'stty' to set raw mode.
1577 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1578 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1579 * -parenb cs8 min 1 < /dev/tty'
1581 private void sttyRaw() {
1586 * Call 'stty' to set raw or cooked mode.
1588 * @param mode if true, set raw mode, otherwise set cooked mode
1590 private void doStty(final boolean mode
) {
1591 String
[] cmdRaw
= {
1592 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1594 String
[] cmdCooked
= {
1595 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1600 process
= Runtime
.getRuntime().exec(cmdRaw
);
1602 process
= Runtime
.getRuntime().exec(cmdCooked
);
1604 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
1605 String line
= in
.readLine();
1606 if ((line
!= null) && (line
.length() > 0)) {
1607 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
1610 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
1611 line
= err
.readLine();
1612 if ((line
!= null) && (line
.length() > 0)) {
1613 System
.err
.println("Error output from stty: " + line
);
1618 } catch (InterruptedException e
) {
1619 if (debugToStderr
) {
1620 e
.printStackTrace();
1624 int rc
= process
.exitValue();
1626 System
.err
.println("stty returned error code: " + rc
);
1628 } catch (IOException e
) {
1629 e
.printStackTrace();
1636 public void flush() {
1641 * Perform a somewhat-optimal rendering of a line.
1643 * @param y row coordinate. 0 is the top-most row.
1644 * @param sb StringBuilder to write escape sequences to
1645 * @param lastAttr cell attributes from the last call to flushLine
1647 private void flushLine(final int y
, final StringBuilder sb
,
1648 CellAttributes lastAttr
) {
1652 for (int x
= 0; x
< width
; x
++) {
1653 Cell lCell
= logical
[x
][y
];
1654 if (!lCell
.isBlank()) {
1658 // Push textEnd to first column beyond the text area
1662 // reallyCleared = true;
1664 boolean hasImage
= false;
1666 for (int x
= 0; x
< width
; x
++) {
1667 Cell lCell
= logical
[x
][y
];
1668 Cell pCell
= physical
[x
][y
];
1670 if (!lCell
.equals(pCell
) || reallyCleared
) {
1672 if (debugToStderr
) {
1673 System
.err
.printf("\n--\n");
1674 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
1675 System
.err
.printf(" lCell: %s\n", lCell
);
1676 System
.err
.printf(" pCell: %s\n", pCell
);
1677 System
.err
.printf(" ==== \n");
1680 if (lastAttr
== null) {
1681 lastAttr
= new CellAttributes();
1682 sb
.append(normal());
1686 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
1687 // Advancing at least one cell, or the first gotoXY
1688 sb
.append(gotoXY(x
, y
));
1691 assert (lastAttr
!= null);
1693 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
1694 assert (lCell
.isBlank());
1696 for (int i
= x
; i
< width
; i
++) {
1697 assert (logical
[i
][y
].isBlank());
1698 // Physical is always updated
1699 physical
[i
][y
].reset();
1702 // Clear remaining line
1703 sb
.append(clearRemainingLine());
1708 // Image cell: bypass the rest of the loop, it is not
1710 if ((wideCharImages
&& lCell
.isImage())
1713 && (lCell
.getWidth() == Cell
.Width
.SINGLE
))
1717 // Save the last rendered cell
1720 // Physical is always updated
1721 physical
[x
][y
].setTo(lCell
);
1725 assert ((wideCharImages
&& !lCell
.isImage())
1727 && (!lCell
.isImage()
1729 && (lCell
.getWidth() != Cell
.Width
.SINGLE
)))));
1731 if (!wideCharImages
&& (lCell
.getWidth() == Cell
.Width
.RIGHT
)) {
1737 sb
.append(gotoXY(x
, y
));
1740 // Now emit only the modified attributes
1741 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1742 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1744 && (lCell
.isBold() == lastAttr
.isBold())
1745 && (lCell
.isReverse() == lastAttr
.isReverse())
1746 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1747 && (lCell
.isBlink() == lastAttr
.isBlink())
1749 // Both colors changed, attributes the same
1750 sb
.append(color(lCell
.isBold(),
1751 lCell
.getForeColor(), lCell
.getBackColor()));
1753 if (debugToStderr
) {
1754 System
.err
.printf("1 Change only fore/back colors\n");
1757 } else if (lCell
.isRGB()
1758 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1759 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1760 && (lCell
.isBold() == lastAttr
.isBold())
1761 && (lCell
.isReverse() == lastAttr
.isReverse())
1762 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1763 && (lCell
.isBlink() == lastAttr
.isBlink())
1765 // Both colors changed, attributes the same
1766 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1767 lCell
.getBackColorRGB()));
1769 if (debugToStderr
) {
1770 System
.err
.printf("1 Change only fore/back colors (RGB)\n");
1772 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1773 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1775 && (lCell
.isBold() != lastAttr
.isBold())
1776 && (lCell
.isReverse() != lastAttr
.isReverse())
1777 && (lCell
.isUnderline() != lastAttr
.isUnderline())
1778 && (lCell
.isBlink() != lastAttr
.isBlink())
1780 // Everything is different
1781 sb
.append(color(lCell
.getForeColor(),
1782 lCell
.getBackColor(),
1783 lCell
.isBold(), lCell
.isReverse(),
1785 lCell
.isUnderline()));
1787 if (debugToStderr
) {
1788 System
.err
.printf("2 Set all attributes\n");
1790 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
1791 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1793 && (lCell
.isBold() == lastAttr
.isBold())
1794 && (lCell
.isReverse() == lastAttr
.isReverse())
1795 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1796 && (lCell
.isBlink() == lastAttr
.isBlink())
1799 // Attributes same, foreColor different
1800 sb
.append(color(lCell
.isBold(),
1801 lCell
.getForeColor(), true));
1803 if (debugToStderr
) {
1804 System
.err
.printf("3 Change foreColor\n");
1806 } else if (lCell
.isRGB()
1807 && (lCell
.getForeColorRGB() != lastAttr
.getForeColorRGB())
1808 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1809 && (lCell
.getForeColorRGB() >= 0)
1810 && (lCell
.getBackColorRGB() >= 0)
1811 && (lCell
.isBold() == lastAttr
.isBold())
1812 && (lCell
.isReverse() == lastAttr
.isReverse())
1813 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1814 && (lCell
.isBlink() == lastAttr
.isBlink())
1816 // Attributes same, foreColor different
1817 sb
.append(colorRGB(lCell
.getForeColorRGB(), true));
1819 if (debugToStderr
) {
1820 System
.err
.printf("3 Change foreColor (RGB)\n");
1822 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1823 && (lCell
.getBackColor() != lastAttr
.getBackColor())
1825 && (lCell
.isBold() == lastAttr
.isBold())
1826 && (lCell
.isReverse() == lastAttr
.isReverse())
1827 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1828 && (lCell
.isBlink() == lastAttr
.isBlink())
1830 // Attributes same, backColor different
1831 sb
.append(color(lCell
.isBold(),
1832 lCell
.getBackColor(), false));
1834 if (debugToStderr
) {
1835 System
.err
.printf("4 Change backColor\n");
1837 } else if (lCell
.isRGB()
1838 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1839 && (lCell
.getBackColorRGB() != lastAttr
.getBackColorRGB())
1840 && (lCell
.isBold() == lastAttr
.isBold())
1841 && (lCell
.isReverse() == lastAttr
.isReverse())
1842 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1843 && (lCell
.isBlink() == lastAttr
.isBlink())
1845 // Attributes same, foreColor different
1846 sb
.append(colorRGB(lCell
.getBackColorRGB(), false));
1848 if (debugToStderr
) {
1849 System
.err
.printf("4 Change backColor (RGB)\n");
1851 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
1852 && (lCell
.getBackColor() == lastAttr
.getBackColor())
1853 && (lCell
.getForeColorRGB() == lastAttr
.getForeColorRGB())
1854 && (lCell
.getBackColorRGB() == lastAttr
.getBackColorRGB())
1855 && (lCell
.isBold() == lastAttr
.isBold())
1856 && (lCell
.isReverse() == lastAttr
.isReverse())
1857 && (lCell
.isUnderline() == lastAttr
.isUnderline())
1858 && (lCell
.isBlink() == lastAttr
.isBlink())
1861 // All attributes the same, just print the char
1864 if (debugToStderr
) {
1865 System
.err
.printf("5 Only emit character\n");
1868 // Just reset everything again
1869 if (!lCell
.isRGB()) {
1870 sb
.append(color(lCell
.getForeColor(),
1871 lCell
.getBackColor(),
1875 lCell
.isUnderline()));
1877 if (debugToStderr
) {
1878 System
.err
.printf("6 Change all attributes\n");
1881 sb
.append(colorRGB(lCell
.getForeColorRGB(),
1882 lCell
.getBackColorRGB(),
1886 lCell
.isUnderline()));
1887 if (debugToStderr
) {
1888 System
.err
.printf("6 Change all attributes (RGB)\n");
1893 // Emit the character
1895 // Don't emit the right-half of full-width chars.
1897 && (lCell
.getWidth() != Cell
.Width
.RIGHT
))
1899 sb
.append(Character
.toChars(lCell
.getChar()));
1902 // Save the last rendered cell
1904 lastAttr
.setTo(lCell
);
1906 // Physical is always updated
1907 physical
[x
][y
].setTo(lCell
);
1909 } // if (!lCell.equals(pCell) || (reallyCleared == true))
1911 } // for (int x = 0; x < width; x++)
1915 * Render the screen to a string that can be emitted to something that
1916 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
1918 * @param sb StringBuilder to write escape sequences to
1919 * @return escape sequences string that provides the updates to the
1922 private String
flushString(final StringBuilder sb
) {
1923 CellAttributes attr
= null;
1925 if (reallyCleared
) {
1926 attr
= new CellAttributes();
1927 sb
.append(clearAll());
1931 * For sixel support, draw all of the sixel output first, and then
1932 * draw everything else afterwards. This works OK, but performance
1933 * is still a drag on larger pictures.
1935 for (int y
= 0; y
< height
; y
++) {
1936 for (int x
= 0; x
< width
; x
++) {
1937 // If physical had non-image data that is now image data, the
1938 // entire row must be redrawn.
1939 Cell lCell
= logical
[x
][y
];
1940 Cell pCell
= physical
[x
][y
];
1941 if (lCell
.isImage() && !pCell
.isImage()) {
1947 for (int y
= 0; y
< height
; y
++) {
1948 for (int x
= 0; x
< width
; x
++) {
1949 Cell lCell
= logical
[x
][y
];
1950 Cell pCell
= physical
[x
][y
];
1952 if (!lCell
.isImage()
1954 && (lCell
.getWidth() != Cell
.Width
.SINGLE
))
1961 while ((right
< width
)
1962 && (logical
[right
][y
].isImage())
1963 && (!logical
[right
][y
].equals(physical
[right
][y
])
1968 ArrayList
<Cell
> cellsToDraw
= new ArrayList
<Cell
>();
1969 for (int i
= 0; i
< (right
- x
); i
++) {
1970 assert (logical
[x
+ i
][y
].isImage());
1971 cellsToDraw
.add(logical
[x
+ i
][y
]);
1973 // Physical is always updated.
1974 physical
[x
+ i
][y
].setTo(lCell
);
1976 if (cellsToDraw
.size() > 0) {
1977 sb
.append(toSixel(x
, y
, cellsToDraw
));
1984 // Draw the text part now.
1985 for (int y
= 0; y
< height
; y
++) {
1986 flushLine(y
, sb
, attr
);
1989 reallyCleared
= false;
1991 String result
= sb
.toString();
1992 if (debugToStderr
) {
1993 System
.err
.printf("flushString(): %s\n", result
);
1999 * Reset keyboard/mouse input parser.
2001 private void resetParser() {
2002 state
= ParseState
.GROUND
;
2003 params
= new ArrayList
<String
>();
2009 * Produce a control character or one of the special ones (ENTER, TAB,
2012 * @param ch Unicode code point
2013 * @param alt if true, set alt on the TKeypress
2014 * @return one TKeypress event, either a control character (e.g. isKey ==
2015 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
2018 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
2019 // System.err.printf("controlChar: %02x\n", ch);
2023 // Carriage return --> ENTER
2024 return new TKeypressEvent(kbEnter
, alt
, false, false);
2026 // Linefeed --> ENTER
2027 return new TKeypressEvent(kbEnter
, alt
, false, false);
2030 return new TKeypressEvent(kbEsc
, alt
, false, false);
2033 return new TKeypressEvent(kbTab
, alt
, false, false);
2035 // Make all other control characters come back as the alphabetic
2036 // character with the ctrl field set. So SOH would be 'A' +
2038 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
2044 * Produce special key from CSI Pn ; Pm ; ... ~
2046 * @return one KEYPRESS event representing a special key
2048 private TInputEvent
csiFnKey() {
2050 if (params
.size() > 0) {
2051 key
= Integer
.parseInt(params
.get(0));
2053 boolean alt
= false;
2054 boolean ctrl
= false;
2055 boolean shift
= false;
2056 if (params
.size() > 1) {
2057 shift
= csiIsShift(params
.get(1));
2058 alt
= csiIsAlt(params
.get(1));
2059 ctrl
= csiIsCtrl(params
.get(1));
2064 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
2066 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
2068 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
2070 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
2072 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
2074 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
2076 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
2078 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
2080 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
2082 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
2084 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
2086 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
2088 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
2090 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
2098 * Produce mouse events based on "Any event tracking" and UTF-8
2100 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2102 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2104 private TInputEvent
parseMouse() {
2105 int buttons
= params
.get(0).charAt(0) - 32;
2106 int x
= params
.get(0).charAt(1) - 32 - 1;
2107 int y
= params
.get(0).charAt(2) - 32 - 1;
2109 // Clamp X and Y to the physical screen coordinates.
2110 if (x
>= windowResize
.getWidth()) {
2111 x
= windowResize
.getWidth() - 1;
2113 if (y
>= windowResize
.getHeight()) {
2114 y
= windowResize
.getHeight() - 1;
2117 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2118 boolean eventMouse1
= false;
2119 boolean eventMouse2
= false;
2120 boolean eventMouse3
= false;
2121 boolean eventMouseWheelUp
= false;
2122 boolean eventMouseWheelDown
= false;
2124 // System.err.printf("buttons: %04x\r\n", buttons);
2141 if (!mouse1
&& !mouse2
&& !mouse3
) {
2142 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2144 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2161 // Dragging with mouse1 down
2164 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2168 // Dragging with mouse2 down
2171 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2175 // Dragging with mouse3 down
2178 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2182 // Dragging with mouse2 down after wheelUp
2185 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2189 // Dragging with mouse2 down after wheelDown
2192 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2196 eventMouseWheelUp
= true;
2200 eventMouseWheelDown
= true;
2204 // Unknown, just make it motion
2205 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2208 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2209 eventMouse1
, eventMouse2
, eventMouse3
,
2210 eventMouseWheelUp
, eventMouseWheelDown
);
2214 * Produce mouse events based on "Any event tracking" and SGR
2216 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2218 * @param release if true, this was a release ('m')
2219 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2221 private TInputEvent
parseMouseSGR(final boolean release
) {
2222 // SGR extended coordinates - mode 1006
2223 if (params
.size() < 3) {
2224 // Invalid position, bail out.
2227 int buttons
= Integer
.parseInt(params
.get(0));
2228 int x
= Integer
.parseInt(params
.get(1)) - 1;
2229 int y
= Integer
.parseInt(params
.get(2)) - 1;
2231 // Clamp X and Y to the physical screen coordinates.
2232 if (x
>= windowResize
.getWidth()) {
2233 x
= windowResize
.getWidth() - 1;
2235 if (y
>= windowResize
.getHeight()) {
2236 y
= windowResize
.getHeight() - 1;
2239 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
2240 boolean eventMouse1
= false;
2241 boolean eventMouse2
= false;
2242 boolean eventMouse3
= false;
2243 boolean eventMouseWheelUp
= false;
2244 boolean eventMouseWheelDown
= false;
2247 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
2261 // Motion only, no buttons down
2262 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2266 // Dragging with mouse1 down
2268 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2272 // Dragging with mouse2 down
2274 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2278 // Dragging with mouse3 down
2280 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2284 // Dragging with mouse2 down after wheelUp
2286 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2290 // Dragging with mouse2 down after wheelDown
2292 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
2296 eventMouseWheelUp
= true;
2300 eventMouseWheelDown
= true;
2304 // Unknown, bail out
2307 return new TMouseEvent(eventType
, x
, y
, x
, y
,
2308 eventMouse1
, eventMouse2
, eventMouse3
,
2309 eventMouseWheelUp
, eventMouseWheelDown
);
2313 * Return any events in the IO queue due to timeout.
2315 * @param queue list to append new events to
2317 private void getIdleEvents(final List
<TInputEvent
> queue
) {
2318 long nowTime
= System
.currentTimeMillis();
2320 // Check for new window size
2321 long windowSizeDelay
= nowTime
- windowSizeTime
;
2322 if (windowSizeDelay
> 1000) {
2323 int oldTextWidth
= getTextWidth();
2324 int oldTextHeight
= getTextHeight();
2326 sessionInfo
.queryWindowSize();
2327 int newWidth
= sessionInfo
.getWindowWidth();
2328 int newHeight
= sessionInfo
.getWindowHeight();
2330 if ((newWidth
!= windowResize
.getWidth())
2331 || (newHeight
!= windowResize
.getHeight())
2334 // Request xterm report window dimensions in pixels again.
2335 // Between now and then, ensure that the reported text cell
2336 // size is the same by setting widthPixels and heightPixels
2337 // to match the new dimensions.
2338 widthPixels
= oldTextWidth
* newWidth
;
2339 heightPixels
= oldTextHeight
* newHeight
;
2341 if (debugToStderr
) {
2342 System
.err
.println("Screen size changed, old size " +
2344 System
.err
.println(" new size " +
2345 newWidth
+ " x " + newHeight
);
2346 System
.err
.println(" old pixels " +
2347 oldTextWidth
+ " x " + oldTextHeight
);
2348 System
.err
.println(" new pixels " +
2349 getTextWidth() + " x " + getTextHeight());
2352 this.output
.printf("%s", xtermReportWindowPixelDimensions());
2353 this.output
.flush();
2355 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2356 newWidth
, newHeight
);
2357 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
2358 newWidth
, newHeight
);
2361 windowSizeTime
= nowTime
;
2364 // ESCDELAY type timeout
2365 if (state
== ParseState
.ESCAPE
) {
2366 long escDelay
= nowTime
- escapeTime
;
2367 if (escDelay
> 100) {
2368 // After 0.1 seconds, assume a true escape character
2369 queue
.add(controlChar((char)0x1B, false));
2376 * Returns true if the CSI parameter for a keyboard command means that
2379 private boolean csiIsShift(final String x
) {
2391 * Returns true if the CSI parameter for a keyboard command means that
2394 private boolean csiIsAlt(final String x
) {
2406 * Returns true if the CSI parameter for a keyboard command means that
2409 private boolean csiIsCtrl(final String x
) {
2421 * Parses the next character of input to see if an InputEvent is
2424 * @param events list to append new events to
2425 * @param ch Unicode code point
2427 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
2429 // ESCDELAY type timeout
2430 long nowTime
= System
.currentTimeMillis();
2431 if (state
== ParseState
.ESCAPE
) {
2432 long escDelay
= nowTime
- escapeTime
;
2433 if (escDelay
> 250) {
2434 // After 0.25 seconds, assume a true escape character
2435 events
.add(controlChar((char)0x1B, false));
2441 boolean ctrl
= false;
2442 boolean alt
= false;
2443 boolean shift
= false;
2445 // System.err.printf("state: %s ch %c\r\n", state, ch);
2451 state
= ParseState
.ESCAPE
;
2452 escapeTime
= nowTime
;
2457 // Control character
2458 events
.add(controlChar(ch
, false));
2465 events
.add(new TKeypressEvent(false, 0, ch
,
2466 false, false, false));
2475 // ALT-Control character
2476 events
.add(controlChar(ch
, true));
2482 // This will be one of the function keys
2483 state
= ParseState
.ESCAPE_INTERMEDIATE
;
2487 // '[' goes to CSI_ENTRY
2489 state
= ParseState
.CSI_ENTRY
;
2493 // Everything else is assumed to be Alt-keystroke
2494 if ((ch
>= 'A') && (ch
<= 'Z')) {
2498 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
2502 case ESCAPE_INTERMEDIATE
:
2503 if ((ch
>= 'P') && (ch
<= 'S')) {
2507 events
.add(new TKeypressEvent(kbF1
));
2510 events
.add(new TKeypressEvent(kbF2
));
2513 events
.add(new TKeypressEvent(kbF3
));
2516 events
.add(new TKeypressEvent(kbF4
));
2525 // Unknown keystroke, ignore
2530 // Numbers - parameter values
2531 if ((ch
>= '0') && (ch
<= '9')) {
2532 params
.set(params
.size() - 1,
2533 params
.get(params
.size() - 1) + ch
);
2534 state
= ParseState
.CSI_PARAM
;
2537 // Parameter separator
2543 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2547 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2552 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2557 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2562 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2567 events
.add(new TKeypressEvent(kbHome
));
2572 events
.add(new TKeypressEvent(kbEnd
));
2576 // CBT - Cursor backward X tab stops (default 1)
2577 events
.add(new TKeypressEvent(kbBackTab
));
2582 state
= ParseState
.MOUSE
;
2585 // Mouse position, SGR (1006) coordinates
2586 state
= ParseState
.MOUSE_SGR
;
2593 // Unknown keystroke, ignore
2598 // Numbers - parameter values
2599 if ((ch
>= '0') && (ch
<= '9')) {
2600 params
.set(params
.size() - 1,
2601 params
.get(params
.size() - 1) + ch
);
2604 // Parameter separator
2612 // Generate a mouse press event
2613 TInputEvent event
= parseMouseSGR(false);
2614 if (event
!= null) {
2620 // Generate a mouse release event
2621 event
= parseMouseSGR(true);
2622 if (event
!= null) {
2631 // Unknown keystroke, ignore
2636 // Numbers - parameter values
2637 if ((ch
>= '0') && (ch
<= '9')) {
2638 params
.set(params
.size() - 1,
2639 params
.get(params
.size() - 1) + ch
);
2640 state
= ParseState
.CSI_PARAM
;
2643 // Parameter separator
2650 events
.add(csiFnKey());
2655 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
2659 if (params
.size() > 1) {
2660 shift
= csiIsShift(params
.get(1));
2661 alt
= csiIsAlt(params
.get(1));
2662 ctrl
= csiIsCtrl(params
.get(1));
2664 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
2669 if (params
.size() > 1) {
2670 shift
= csiIsShift(params
.get(1));
2671 alt
= csiIsAlt(params
.get(1));
2672 ctrl
= csiIsCtrl(params
.get(1));
2674 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
2679 if (params
.size() > 1) {
2680 shift
= csiIsShift(params
.get(1));
2681 alt
= csiIsAlt(params
.get(1));
2682 ctrl
= csiIsCtrl(params
.get(1));
2684 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
2689 if (params
.size() > 1) {
2690 shift
= csiIsShift(params
.get(1));
2691 alt
= csiIsAlt(params
.get(1));
2692 ctrl
= csiIsCtrl(params
.get(1));
2694 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
2699 if (params
.size() > 1) {
2700 shift
= csiIsShift(params
.get(1));
2701 alt
= csiIsAlt(params
.get(1));
2702 ctrl
= csiIsCtrl(params
.get(1));
2704 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
2709 if (params
.size() > 1) {
2710 shift
= csiIsShift(params
.get(1));
2711 alt
= csiIsAlt(params
.get(1));
2712 ctrl
= csiIsCtrl(params
.get(1));
2714 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
2719 if ((params
.size() > 2) && (params
.get(0).equals("4"))) {
2720 if (debugToStderr
) {
2721 System
.err
.printf("windowOp pixels: " +
2722 "height %s width %s\n",
2723 params
.get(1), params
.get(2));
2726 widthPixels
= Integer
.parseInt(params
.get(2));
2727 heightPixels
= Integer
.parseInt(params
.get(1));
2728 } catch (NumberFormatException e
) {
2729 if (debugToStderr
) {
2730 e
.printStackTrace();
2733 if (widthPixels
<= 0) {
2736 if (heightPixels
<= 0) {
2747 // Unknown keystroke, ignore
2752 params
.set(0, params
.get(params
.size() - 1) + ch
);
2753 if (params
.get(0).length() == 3) {
2754 // We have enough to generate a mouse event
2755 events
.add(parseMouse());
2764 // This "should" be impossible to reach
2769 * Request (u)xterm to report the current window size dimensions.
2771 * @return the string to emit to xterm
2773 private String
xtermReportWindowPixelDimensions() {
2778 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2779 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2782 * @param on if true, enable metaSendsEscape
2783 * @return the string to emit to xterm
2785 private String
xtermMetaSendsEscape(final boolean on
) {
2787 return "\033[?1036h\033[?1034l";
2789 return "\033[?1036l";
2793 * Create an xterm OSC sequence to change the window title.
2795 * @param title the new title
2796 * @return the string to emit to xterm
2798 private String
getSetTitleString(final String title
) {
2799 return "\033]2;" + title
+ "\007";
2802 // ------------------------------------------------------------------------
2803 // Sixel output support ---------------------------------------------------
2804 // ------------------------------------------------------------------------
2807 * Get the number of colors in the sixel palette.
2809 * @return the palette size
2811 public int getSixelPaletteSize() {
2812 return sixelPaletteSize
;
2816 * Set the number of colors in the sixel palette.
2818 * @param paletteSize the new palette size
2820 public void setSixelPaletteSize(final int paletteSize
) {
2821 if (paletteSize
== sixelPaletteSize
) {
2825 switch (paletteSize
) {
2833 throw new IllegalArgumentException("Unsupported sixel palette " +
2834 " size: " + paletteSize
);
2837 // Don't step on the screen refresh thread.
2838 synchronized (this) {
2839 sixelPaletteSize
= paletteSize
;
2847 * Start a sixel string for display one row's worth of bitmap data.
2849 * @param x column coordinate. 0 is the left-most column.
2850 * @param y row coordinate. 0 is the top-most row.
2851 * @return the string to emit to an ANSI / ECMA-style terminal
2853 private String
startSixel(final int x
, final int y
) {
2854 StringBuilder sb
= new StringBuilder();
2856 assert (sixel
== true);
2859 sb
.append(gotoXY(x
, y
));
2862 sb
.append("\033Pq");
2864 if (palette
== null) {
2865 palette
= new SixelPalette();
2868 return sb
.toString();
2872 * End a sixel string for display one row's worth of bitmap data.
2874 * @return the string to emit to an ANSI / ECMA-style terminal
2876 private String
endSixel() {
2877 assert (sixel
== true);
2884 * Create a sixel string representing a row of several cells containing
2887 * @param x column coordinate. 0 is the left-most column.
2888 * @param y row coordinate. 0 is the top-most row.
2889 * @param cells the cells containing the bitmap data
2890 * @return the string to emit to an ANSI / ECMA-style terminal
2892 private String
toSixel(final int x
, final int y
,
2893 final ArrayList
<Cell
> cells
) {
2895 StringBuilder sb
= new StringBuilder();
2897 assert (cells
!= null);
2898 assert (cells
.size() > 0);
2899 assert (cells
.get(0).getImage() != null);
2901 if (sixel
== false) {
2902 sb
.append(normal());
2903 sb
.append(gotoXY(x
, y
));
2904 for (int i
= 0; i
< cells
.size(); i
++) {
2907 return sb
.toString();
2910 if (sixelCache
== null) {
2911 sixelCache
= new SixelCache(height
* 10);
2914 // Save and get rows to/from the cache that do NOT have inverted
2916 boolean saveInCache
= true;
2917 for (Cell cell
: cells
) {
2918 if (cell
.isInvertedImage()) {
2919 saveInCache
= false;
2923 String cachedResult
= sixelCache
.get(cells
);
2924 if (cachedResult
!= null) {
2925 // System.err.println("CACHE HIT");
2926 sb
.append(startSixel(x
, y
));
2927 sb
.append(cachedResult
);
2928 sb
.append(endSixel());
2929 return sb
.toString();
2931 // System.err.println("CACHE MISS");
2934 int imageWidth
= cells
.get(0).getImage().getWidth();
2935 int imageHeight
= cells
.get(0).getImage().getHeight();
2937 // cells.get(x).getImage() has a dithered bitmap containing indexes
2938 // into the color palette. Piece these together into one larger
2939 // image for final rendering.
2941 int fullWidth
= cells
.size() * getTextWidth();
2942 int fullHeight
= getTextHeight();
2943 for (int i
= 0; i
< cells
.size(); i
++) {
2944 totalWidth
+= cells
.get(i
).getImage().getWidth();
2947 BufferedImage image
= new BufferedImage(fullWidth
,
2948 fullHeight
, BufferedImage
.TYPE_INT_ARGB
);
2951 for (int i
= 0; i
< cells
.size() - 1; i
++) {
2952 int tileWidth
= Math
.min(cells
.get(i
).getImage().getWidth(),
2954 int tileHeight
= Math
.min(cells
.get(i
).getImage().getHeight(),
2956 if (false && cells
.get(i
).isInvertedImage()) {
2957 // I used to put an all-white cell over the cursor, don't do
2959 rgbArray
= new int[imageWidth
* imageHeight
];
2960 for (int j
= 0; j
< rgbArray
.length
; j
++) {
2961 rgbArray
[j
] = 0xFFFFFF;
2965 rgbArray
= cells
.get(i
).getImage().getRGB(0, 0,
2966 tileWidth
, tileHeight
, null, 0, tileWidth
);
2967 } catch (Exception e
) {
2968 throw new RuntimeException("image " + imageWidth
+ "x" +
2970 "tile " + tileWidth
+ "x" +
2972 " cells.get(i).getImage() " +
2973 cells
.get(i
).getImage() +
2975 " fullWidth " + fullWidth
+
2976 " fullHeight " + fullHeight
, e
);
2981 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
2982 i * imageWidth, 0, imageWidth, imageHeight,
2984 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
2985 fullWidth, fullHeight, cells.size(), getTextWidth());
2988 image
.setRGB(i
* imageWidth
, 0, tileWidth
, tileHeight
,
2989 rgbArray
, 0, tileWidth
);
2990 if (tileHeight
< fullHeight
) {
2991 int backgroundColor
= cells
.get(i
).getBackground().getRGB();
2992 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
2993 for (int imageY
= imageHeight
; imageY
< fullHeight
;
2996 image
.setRGB(imageX
, imageY
, backgroundColor
);
3001 totalWidth
-= ((cells
.size() - 1) * imageWidth
);
3002 if (false && cells
.get(cells
.size() - 1).isInvertedImage()) {
3003 // I used to put an all-white cell over the cursor, don't do that
3005 rgbArray
= new int[totalWidth
* imageHeight
];
3006 for (int j
= 0; j
< rgbArray
.length
; j
++) {
3007 rgbArray
[j
] = 0xFFFFFF;
3011 rgbArray
= cells
.get(cells
.size() - 1).getImage().getRGB(0, 0,
3012 totalWidth
, imageHeight
, null, 0, totalWidth
);
3013 } catch (Exception e
) {
3014 throw new RuntimeException("image " + imageWidth
+ "x" +
3015 imageHeight
+ " cells.get(cells.size() - 1).getImage() " +
3016 cells
.get(cells
.size() - 1).getImage(), e
);
3019 image
.setRGB((cells
.size() - 1) * imageWidth
, 0, totalWidth
,
3020 imageHeight
, rgbArray
, 0, totalWidth
);
3022 if (totalWidth
< getTextWidth()) {
3023 int backgroundColor
= cells
.get(cells
.size() - 1).getBackground().getRGB();
3025 for (int imageX
= image
.getWidth() - totalWidth
;
3026 imageX
< image
.getWidth(); imageX
++) {
3028 for (int imageY
= 0; imageY
< fullHeight
; imageY
++) {
3029 image
.setRGB(imageX
, imageY
, backgroundColor
);
3034 // Dither the image. It is ok to lose the original here.
3035 if (palette
== null) {
3036 palette
= new SixelPalette();
3038 image
= palette
.ditherImage(image
);
3040 // Emit the palette, but only for the colors actually used by these
3042 boolean [] usedColors
= new boolean[sixelPaletteSize
];
3043 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3044 for (int imageY
= 0; imageY
< image
.getHeight(); imageY
++) {
3045 usedColors
[image
.getRGB(imageX
, imageY
)] = true;
3048 palette
.emitPalette(sb
, usedColors
);
3050 // Render the entire row of cells.
3051 for (int currentRow
= 0; currentRow
< fullHeight
; currentRow
+= 6) {
3052 int [][] sixels
= new int[image
.getWidth()][6];
3054 // See which colors are actually used in this band of sixels.
3055 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3056 for (int imageY
= 0;
3057 (imageY
< 6) && (imageY
+ currentRow
< fullHeight
);
3060 int colorIdx
= image
.getRGB(imageX
, imageY
+ currentRow
);
3061 assert (colorIdx
>= 0);
3062 assert (colorIdx
< sixelPaletteSize
);
3064 sixels
[imageX
][imageY
] = colorIdx
;
3068 for (int i
= 0; i
< sixelPaletteSize
; i
++) {
3069 boolean isUsed
= false;
3070 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3071 for (int j
= 0; j
< 6; j
++) {
3072 if (sixels
[imageX
][j
] == i
) {
3077 if (isUsed
== false) {
3081 // Set to the beginning of scan line for the next set of
3082 // colored pixels, and select the color.
3083 sb
.append(String
.format("$#%d", i
));
3086 int oldDataCount
= 0;
3087 for (int imageX
= 0; imageX
< image
.getWidth(); imageX
++) {
3089 // Add up all the pixels that match this color.
3092 (j
< 6) && (currentRow
+ j
< fullHeight
);
3095 if (sixels
[imageX
][j
] == i
) {
3122 if (data
== oldData
) {
3125 if (oldDataCount
== 1) {
3126 sb
.append((char) oldData
);
3127 } else if (oldDataCount
> 1) {
3128 sb
.append(String
.format("!%d", oldDataCount
));
3129 sb
.append((char) oldData
);
3135 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
3137 // Emit the last sequence.
3138 if (oldDataCount
== 1) {
3139 sb
.append((char) oldData
);
3140 } else if (oldDataCount
> 1) {
3141 sb
.append(String
.format("!%d", oldDataCount
));
3142 sb
.append((char) oldData
);
3145 } // for (int i = 0; i < sixelPaletteSize; i++)
3147 // Advance to the next scan line.
3150 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3152 // Kill the very last "-", because it is unnecessary.
3153 sb
.deleteCharAt(sb
.length() - 1);
3156 // This row is OK to save into the cache.
3157 sixelCache
.put(cells
, sb
.toString());
3160 return (startSixel(x
, y
) + sb
.toString() + endSixel());
3164 * Get the sixel support flag.
3166 * @return true if this terminal is emitting sixel
3168 public boolean hasSixel() {
3172 // ------------------------------------------------------------------------
3173 // End sixel output support -----------------------------------------------
3174 // ------------------------------------------------------------------------
3177 * Create a SGR parameter sequence for a single color change.
3179 * @param bold if true, set bold
3180 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3181 * @param foreground if true, this is a foreground color
3182 * @return the string to emit to an ANSI / ECMA-style terminal,
3185 private String
color(final boolean bold
, final Color color
,
3186 final boolean foreground
) {
3187 return color(color
, foreground
, true) +
3188 rgbColor(bold
, color
, foreground
);
3192 * Create a T.416 RGB parameter sequence for a single color change.
3194 * @param colorRGB a 24-bit RGB value for foreground color
3195 * @param foreground if true, this is a foreground color
3196 * @return the string to emit to an ANSI / ECMA-style terminal,
3199 private String
colorRGB(final int colorRGB
, final boolean foreground
) {
3201 int colorRed
= (colorRGB
>>> 16) & 0xFF;
3202 int colorGreen
= (colorRGB
>>> 8) & 0xFF;
3203 int colorBlue
= colorRGB
& 0xFF;
3205 StringBuilder sb
= new StringBuilder();
3207 sb
.append("\033[38;2;");
3209 sb
.append("\033[48;2;");
3211 sb
.append(String
.format("%d;%d;%dm", colorRed
, colorGreen
, colorBlue
));
3212 return sb
.toString();
3216 * Create a T.416 RGB parameter sequence for both foreground and
3217 * background color change.
3219 * @param foreColorRGB a 24-bit RGB value for foreground color
3220 * @param backColorRGB a 24-bit RGB value for foreground color
3221 * @return the string to emit to an ANSI / ECMA-style terminal,
3224 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
) {
3225 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3226 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3227 int foreColorBlue
= foreColorRGB
& 0xFF;
3228 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3229 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3230 int backColorBlue
= backColorRGB
& 0xFF;
3232 StringBuilder sb
= new StringBuilder();
3233 sb
.append(String
.format("\033[38;2;%d;%d;%dm",
3234 foreColorRed
, foreColorGreen
, foreColorBlue
));
3235 sb
.append(String
.format("\033[48;2;%d;%d;%dm",
3236 backColorRed
, backColorGreen
, backColorBlue
));
3237 return sb
.toString();
3241 * Create a T.416 RGB parameter sequence for a single color change.
3243 * @param bold if true, set bold
3244 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3245 * @param foreground if true, this is a foreground color
3246 * @return the string to emit to an xterm terminal with RGB support,
3247 * e.g. "\033[38;2;RR;GG;BBm"
3249 private String
rgbColor(final boolean bold
, final Color color
,
3250 final boolean foreground
) {
3251 if (doRgbColor
== false) {
3254 StringBuilder sb
= new StringBuilder("\033[");
3256 // Bold implies foreground only
3258 if (color
.equals(Color
.BLACK
)) {
3259 sb
.append("84;84;84");
3260 } else if (color
.equals(Color
.RED
)) {
3261 sb
.append("252;84;84");
3262 } else if (color
.equals(Color
.GREEN
)) {
3263 sb
.append("84;252;84");
3264 } else if (color
.equals(Color
.YELLOW
)) {
3265 sb
.append("252;252;84");
3266 } else if (color
.equals(Color
.BLUE
)) {
3267 sb
.append("84;84;252");
3268 } else if (color
.equals(Color
.MAGENTA
)) {
3269 sb
.append("252;84;252");
3270 } else if (color
.equals(Color
.CYAN
)) {
3271 sb
.append("84;252;252");
3272 } else if (color
.equals(Color
.WHITE
)) {
3273 sb
.append("252;252;252");
3281 if (color
.equals(Color
.BLACK
)) {
3283 } else if (color
.equals(Color
.RED
)) {
3284 sb
.append("168;0;0");
3285 } else if (color
.equals(Color
.GREEN
)) {
3286 sb
.append("0;168;0");
3287 } else if (color
.equals(Color
.YELLOW
)) {
3288 sb
.append("168;84;0");
3289 } else if (color
.equals(Color
.BLUE
)) {
3290 sb
.append("0;0;168");
3291 } else if (color
.equals(Color
.MAGENTA
)) {
3292 sb
.append("168;0;168");
3293 } else if (color
.equals(Color
.CYAN
)) {
3294 sb
.append("0;168;168");
3295 } else if (color
.equals(Color
.WHITE
)) {
3296 sb
.append("168;168;168");
3300 return sb
.toString();
3304 * Create a T.416 RGB parameter sequence for both foreground and
3305 * background color change.
3307 * @param bold if true, set bold
3308 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3309 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3310 * @return the string to emit to an xterm terminal with RGB support,
3311 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
3313 private String
rgbColor(final boolean bold
, final Color foreColor
,
3314 final Color backColor
) {
3315 if (doRgbColor
== false) {
3319 return rgbColor(bold
, foreColor
, true) +
3320 rgbColor(false, backColor
, false);
3324 * Create a SGR parameter sequence for a single color change.
3326 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3327 * @param foreground if true, this is a foreground color
3328 * @param header if true, make the full header, otherwise just emit the
3329 * color parameter e.g. "42;"
3330 * @return the string to emit to an ANSI / ECMA-style terminal,
3333 private String
color(final Color color
, final boolean foreground
,
3334 final boolean header
) {
3336 int ecmaColor
= color
.getValue();
3338 // Convert Color.* values to SGR numerics
3346 return String
.format("\033[%dm", ecmaColor
);
3348 return String
.format("%d;", ecmaColor
);
3353 * Create a SGR parameter sequence for both foreground and background
3356 * @param bold if true, set bold
3357 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3358 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3359 * @return the string to emit to an ANSI / ECMA-style terminal,
3360 * e.g. "\033[31;42m"
3362 private String
color(final boolean bold
, final Color foreColor
,
3363 final Color backColor
) {
3364 return color(foreColor
, backColor
, true) +
3365 rgbColor(bold
, foreColor
, backColor
);
3369 * Create a SGR parameter sequence for both foreground and
3370 * background color change.
3372 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3373 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3374 * @param header if true, make the full header, otherwise just emit the
3375 * color parameter e.g. "31;42;"
3376 * @return the string to emit to an ANSI / ECMA-style terminal,
3377 * e.g. "\033[31;42m"
3379 private String
color(final Color foreColor
, final Color backColor
,
3380 final boolean header
) {
3382 int ecmaForeColor
= foreColor
.getValue();
3383 int ecmaBackColor
= backColor
.getValue();
3385 // Convert Color.* values to SGR numerics
3386 ecmaBackColor
+= 40;
3387 ecmaForeColor
+= 30;
3390 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
3392 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
3397 * Create a SGR parameter sequence for foreground, background, and
3398 * several attributes. This sequence first resets all attributes to
3399 * default, then sets attributes as per the parameters.
3401 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3402 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3403 * @param bold if true, set bold
3404 * @param reverse if true, set reverse
3405 * @param blink if true, set blink
3406 * @param underline if true, set underline
3407 * @return the string to emit to an ANSI / ECMA-style terminal,
3408 * e.g. "\033[0;1;31;42m"
3410 private String
color(final Color foreColor
, final Color backColor
,
3411 final boolean bold
, final boolean reverse
, final boolean blink
,
3412 final boolean underline
) {
3414 int ecmaForeColor
= foreColor
.getValue();
3415 int ecmaBackColor
= backColor
.getValue();
3417 // Convert Color.* values to SGR numerics
3418 ecmaBackColor
+= 40;
3419 ecmaForeColor
+= 30;
3421 StringBuilder sb
= new StringBuilder();
3422 if ( bold
&& reverse
&& blink
&& !underline
) {
3423 sb
.append("\033[0;1;7;5;");
3424 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3425 sb
.append("\033[0;1;7;");
3426 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3427 sb
.append("\033[0;7;5;");
3428 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3429 sb
.append("\033[0;1;5;");
3430 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3431 sb
.append("\033[0;1;");
3432 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3433 sb
.append("\033[0;7;");
3434 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3435 sb
.append("\033[0;5;");
3436 } else if ( bold
&& reverse
&& blink
&& underline
) {
3437 sb
.append("\033[0;1;7;5;4;");
3438 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3439 sb
.append("\033[0;1;7;4;");
3440 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3441 sb
.append("\033[0;7;5;4;");
3442 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3443 sb
.append("\033[0;1;5;4;");
3444 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3445 sb
.append("\033[0;1;4;");
3446 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3447 sb
.append("\033[0;7;4;");
3448 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3449 sb
.append("\033[0;5;4;");
3450 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3451 sb
.append("\033[0;4;");
3453 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3454 sb
.append("\033[0;");
3456 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
3457 sb
.append(rgbColor(bold
, foreColor
, backColor
));
3458 return sb
.toString();
3462 * Create a SGR parameter sequence for foreground, background, and
3463 * several attributes. This sequence first resets all attributes to
3464 * default, then sets attributes as per the parameters.
3466 * @param foreColorRGB a 24-bit RGB value for foreground color
3467 * @param backColorRGB a 24-bit RGB value for foreground color
3468 * @param bold if true, set bold
3469 * @param reverse if true, set reverse
3470 * @param blink if true, set blink
3471 * @param underline if true, set underline
3472 * @return the string to emit to an ANSI / ECMA-style terminal,
3473 * e.g. "\033[0;1;31;42m"
3475 private String
colorRGB(final int foreColorRGB
, final int backColorRGB
,
3476 final boolean bold
, final boolean reverse
, final boolean blink
,
3477 final boolean underline
) {
3479 int foreColorRed
= (foreColorRGB
>>> 16) & 0xFF;
3480 int foreColorGreen
= (foreColorRGB
>>> 8) & 0xFF;
3481 int foreColorBlue
= foreColorRGB
& 0xFF;
3482 int backColorRed
= (backColorRGB
>>> 16) & 0xFF;
3483 int backColorGreen
= (backColorRGB
>>> 8) & 0xFF;
3484 int backColorBlue
= backColorRGB
& 0xFF;
3486 StringBuilder sb
= new StringBuilder();
3487 if ( bold
&& reverse
&& blink
&& !underline
) {
3488 sb
.append("\033[0;1;7;5;");
3489 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
3490 sb
.append("\033[0;1;7;");
3491 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
3492 sb
.append("\033[0;7;5;");
3493 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
3494 sb
.append("\033[0;1;5;");
3495 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
3496 sb
.append("\033[0;1;");
3497 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
3498 sb
.append("\033[0;7;");
3499 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
3500 sb
.append("\033[0;5;");
3501 } else if ( bold
&& reverse
&& blink
&& underline
) {
3502 sb
.append("\033[0;1;7;5;4;");
3503 } else if ( bold
&& reverse
&& !blink
&& underline
) {
3504 sb
.append("\033[0;1;7;4;");
3505 } else if ( !bold
&& reverse
&& blink
&& underline
) {
3506 sb
.append("\033[0;7;5;4;");
3507 } else if ( bold
&& !reverse
&& blink
&& underline
) {
3508 sb
.append("\033[0;1;5;4;");
3509 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
3510 sb
.append("\033[0;1;4;");
3511 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
3512 sb
.append("\033[0;7;4;");
3513 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
3514 sb
.append("\033[0;5;4;");
3515 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
3516 sb
.append("\033[0;4;");
3518 assert (!bold
&& !reverse
&& !blink
&& !underline
);
3519 sb
.append("\033[0;");
3522 sb
.append("m\033[38;2;");
3523 sb
.append(String
.format("%d;%d;%d", foreColorRed
, foreColorGreen
,
3525 sb
.append("m\033[48;2;");
3526 sb
.append(String
.format("%d;%d;%d", backColorRed
, backColorGreen
,
3529 return sb
.toString();
3533 * Create a SGR parameter sequence to reset to defaults.
3535 * @return the string to emit to an ANSI / ECMA-style terminal,
3538 private String
normal() {
3539 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
3543 * Create a SGR parameter sequence to reset to defaults.
3545 * @param header if true, make the full header, otherwise just emit the
3546 * bare parameter e.g. "0;"
3547 * @return the string to emit to an ANSI / ECMA-style terminal,
3550 private String
normal(final boolean header
) {
3552 return "\033[0;37;40m";
3558 * Create a SGR parameter sequence for enabling the visible cursor.
3560 * @param on if true, turn on cursor
3561 * @return the string to emit to an ANSI / ECMA-style terminal
3563 private String
cursor(final boolean on
) {
3564 if (on
&& !cursorOn
) {
3568 if (!on
&& cursorOn
) {
3576 * Clear the entire screen. Because some terminals use back-color-erase,
3577 * set the color to white-on-black beforehand.
3579 * @return the string to emit to an ANSI / ECMA-style terminal
3581 private String
clearAll() {
3582 return "\033[0;37;40m\033[2J";
3586 * Clear the line from the cursor (inclusive) to the end of the screen.
3587 * Because some terminals use back-color-erase, set the color to
3588 * white-on-black beforehand.
3590 * @return the string to emit to an ANSI / ECMA-style terminal
3592 private String
clearRemainingLine() {
3593 return "\033[0;37;40m\033[K";
3597 * Move the cursor to (x, y).
3599 * @param x column coordinate. 0 is the left-most column.
3600 * @param y row coordinate. 0 is the top-most row.
3601 * @return the string to emit to an ANSI / ECMA-style terminal
3603 private String
gotoXY(final int x
, final int y
) {
3604 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
3608 * Tell (u)xterm that we want to receive mouse events based on "Any event
3609 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
3610 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
3612 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
3614 * Note that this also sets the alternate/primary screen buffer.
3616 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
3617 * mean "hide the mouse pointer." We have to use our own sequence to do
3618 * this because there is no standard in xterm for unilaterally hiding the
3619 * pointer all the time (regardless of typing).
3621 * @param on If true, enable mouse report and use the alternate screen
3622 * buffer. If false disable mouse reporting and use the primary screen
3624 * @return the string to emit to xterm
3626 private String
mouse(final boolean on
) {
3628 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
3630 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";