#58 detect iterm2
[fanfix.git] / src / jexer / backend / ECMA48Terminal.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
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:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
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.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.backend;
30
31 import java.awt.image.BufferedImage;
32 import java.io.BufferedReader;
33 import java.io.ByteArrayOutputStream;
34 import java.io.FileDescriptor;
35 import java.io.FileInputStream;
36 import java.io.InputStream;
37 import java.io.InputStreamReader;
38 import java.io.IOException;
39 import java.io.OutputStream;
40 import java.io.OutputStreamWriter;
41 import java.io.PrintWriter;
42 import java.io.Reader;
43 import java.io.UnsupportedEncodingException;
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.HashMap;
47 import java.util.List;
48 import javax.imageio.ImageIO;
49
50 import jexer.TImage;
51 import jexer.bits.Cell;
52 import jexer.bits.CellAttributes;
53 import jexer.bits.Color;
54 import jexer.event.TCommandEvent;
55 import jexer.event.TInputEvent;
56 import jexer.event.TKeypressEvent;
57 import jexer.event.TMouseEvent;
58 import jexer.event.TResizeEvent;
59 import static jexer.TCommand.*;
60 import static jexer.TKeypress.*;
61
62 /**
63 * This class reads keystrokes and mouse events and emits output to ANSI
64 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
65 */
66 public class ECMA48Terminal extends LogicalScreen
67 implements TerminalReader, Runnable {
68
69 // ------------------------------------------------------------------------
70 // Constants --------------------------------------------------------------
71 // ------------------------------------------------------------------------
72
73 /**
74 * States in the input parser.
75 */
76 private enum ParseState {
77 GROUND,
78 ESCAPE,
79 ESCAPE_INTERMEDIATE,
80 CSI_ENTRY,
81 CSI_PARAM,
82 MOUSE,
83 MOUSE_SGR,
84 }
85
86 /**
87 * Available Jexer images support.
88 */
89 private enum JexerImageOption {
90 DISABLED,
91 JPG,
92 PNG,
93 RGB,
94 }
95
96 // ------------------------------------------------------------------------
97 // Variables --------------------------------------------------------------
98 // ------------------------------------------------------------------------
99
100 /**
101 * Emit debugging to stderr.
102 */
103 private boolean debugToStderr = false;
104
105 /**
106 * If true, emit T.416-style RGB colors for normal system colors. This
107 * is a) expensive in bandwidth, and b) potentially terrible looking for
108 * non-xterms.
109 */
110 private static boolean doRgbColor = false;
111
112 /**
113 * The session information.
114 */
115 private SessionInfo sessionInfo;
116
117 /**
118 * The event queue, filled up by a thread reading on input.
119 */
120 private List<TInputEvent> eventQueue;
121
122 /**
123 * If true, we want the reader thread to exit gracefully.
124 */
125 private boolean stopReaderThread;
126
127 /**
128 * The reader thread.
129 */
130 private Thread readerThread;
131
132 /**
133 * Parameters being collected. E.g. if the string is \033[1;3m, then
134 * params[0] will be 1 and params[1] will be 3.
135 */
136 private List<String> params;
137
138 /**
139 * Current parsing state.
140 */
141 private ParseState state;
142
143 /**
144 * The time we entered ESCAPE. If we get a bare escape without a code
145 * following it, this is used to return that bare escape.
146 */
147 private long escapeTime;
148
149 /**
150 * The time we last checked the window size. We try not to spawn stty
151 * more than once per second.
152 */
153 private long windowSizeTime;
154
155 /**
156 * true if mouse1 was down. Used to report mouse1 on the release event.
157 */
158 private boolean mouse1;
159
160 /**
161 * true if mouse2 was down. Used to report mouse2 on the release event.
162 */
163 private boolean mouse2;
164
165 /**
166 * true if mouse3 was down. Used to report mouse3 on the release event.
167 */
168 private boolean mouse3;
169
170 /**
171 * Cache the cursor visibility value so we only emit the sequence when we
172 * need to.
173 */
174 private boolean cursorOn = true;
175
176 /**
177 * Cache the last window size to figure out if a TResizeEvent needs to be
178 * generated.
179 */
180 private TResizeEvent windowResize = null;
181
182 /**
183 * If true, emit wide-char (CJK/Emoji) characters as sixel images.
184 */
185 private boolean wideCharImages = true;
186
187 /**
188 * Window width in pixels. Used for sixel support.
189 */
190 private int widthPixels = 640;
191
192 /**
193 * Window height in pixels. Used for sixel support.
194 */
195 private int heightPixels = 400;
196
197 /**
198 * If true, emit image data via sixel.
199 */
200 private boolean sixel = true;
201
202 /**
203 * If true, use a single shared palette for sixel.
204 */
205 private boolean sixelSharedPalette = true;
206
207 /**
208 * The sixel palette handler.
209 */
210 private SixelPalette palette = null;
211
212 /**
213 * The sixel post-rendered string cache.
214 */
215 private ImageCache sixelCache = null;
216
217 /**
218 * Number of colors in the sixel palette. Xterm 335 defines the max as
219 * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
220 * 2048.
221 */
222 private int sixelPaletteSize = 1024;
223
224 /**
225 * If true, emit image data via iTerm2 image protocol.
226 */
227 private boolean iterm2Images = false;
228
229 /**
230 * The iTerm2 post-rendered string cache.
231 */
232 private ImageCache iterm2Cache = null;
233
234 /**
235 * If not DISABLED, emit image data via Jexer image protocol if the
236 * terminal supports it.
237 */
238 private JexerImageOption jexerImageOption = JexerImageOption.JPG;
239
240 /**
241 * The Jexer post-rendered string cache.
242 */
243 private ImageCache jexerCache = null;
244
245 /**
246 * Base64 encoder used by iTerm2 and Jexer images.
247 */
248 private java.util.Base64.Encoder base64 = null;
249
250 /**
251 * If true, then we changed System.in and need to change it back.
252 */
253 private boolean setRawMode = false;
254
255 /**
256 * If true, '?' was seen in terminal response.
257 */
258 private boolean decPrivateModeFlag = false;
259
260 /**
261 * The terminal's input. If an InputStream is not specified in the
262 * constructor, then this InputStreamReader will be bound to System.in
263 * with UTF-8 encoding.
264 */
265 private Reader input;
266
267 /**
268 * The terminal's raw InputStream. If an InputStream is not specified in
269 * the constructor, then this InputReader will be bound to System.in.
270 * This is used by run() to see if bytes are available() before calling
271 * (Reader)input.read().
272 */
273 private InputStream inputStream;
274
275 /**
276 * The terminal's output. If an OutputStream is not specified in the
277 * constructor, then this PrintWriter will be bound to System.out with
278 * UTF-8 encoding.
279 */
280 private PrintWriter output;
281
282 /**
283 * The listening object that run() wakes up on new input.
284 */
285 private Object listener;
286
287 // Colors to map DOS colors to AWT colors.
288 private static java.awt.Color MYBLACK;
289 private static java.awt.Color MYRED;
290 private static java.awt.Color MYGREEN;
291 private static java.awt.Color MYYELLOW;
292 private static java.awt.Color MYBLUE;
293 private static java.awt.Color MYMAGENTA;
294 private static java.awt.Color MYCYAN;
295 private static java.awt.Color MYWHITE;
296 private static java.awt.Color MYBOLD_BLACK;
297 private static java.awt.Color MYBOLD_RED;
298 private static java.awt.Color MYBOLD_GREEN;
299 private static java.awt.Color MYBOLD_YELLOW;
300 private static java.awt.Color MYBOLD_BLUE;
301 private static java.awt.Color MYBOLD_MAGENTA;
302 private static java.awt.Color MYBOLD_CYAN;
303 private static java.awt.Color MYBOLD_WHITE;
304
305 /**
306 * SixelPalette is used to manage the conversion of images between 24-bit
307 * RGB color and a palette of sixelPaletteSize colors.
308 */
309 private class SixelPalette {
310
311 /**
312 * Color palette for sixel output, sorted low to high.
313 */
314 private List<Integer> rgbColors = new ArrayList<Integer>();
315
316 /**
317 * Map of color palette index for sixel output, from the order it was
318 * generated by makePalette() to rgbColors.
319 */
320 private int [] rgbSortedIndex = new int[sixelPaletteSize];
321
322 /**
323 * The color palette, organized by hue, saturation, and luminance.
324 * This is used for a fast color match.
325 */
326 private ArrayList<ArrayList<ArrayList<ColorIdx>>> hslColors;
327
328 /**
329 * Number of bits for hue.
330 */
331 private int hueBits = -1;
332
333 /**
334 * Number of bits for saturation.
335 */
336 private int satBits = -1;
337
338 /**
339 * Number of bits for luminance.
340 */
341 private int lumBits = -1;
342
343 /**
344 * Step size for hue bins.
345 */
346 private int hueStep = -1;
347
348 /**
349 * Step size for saturation bins.
350 */
351 private int satStep = -1;
352
353 /**
354 * Cached RGB to HSL result.
355 */
356 private int hsl[] = new int[3];
357
358 /**
359 * ColorIdx records a RGB color and its palette index.
360 */
361 private class ColorIdx {
362 /**
363 * The 24-bit RGB color.
364 */
365 public int color;
366
367 /**
368 * The palette index for this color.
369 */
370 public int index;
371
372 /**
373 * Public constructor.
374 *
375 * @param color the 24-bit RGB color
376 * @param index the palette index for this color
377 */
378 public ColorIdx(final int color, final int index) {
379 this.color = color;
380 this.index = index;
381 }
382 }
383
384 /**
385 * Public constructor.
386 */
387 public SixelPalette() {
388 makePalette();
389 }
390
391 /**
392 * Find the nearest match for a color in the palette.
393 *
394 * @param color the RGB color
395 * @return the index in rgbColors that is closest to color
396 */
397 public int matchColor(final int color) {
398
399 assert (color >= 0);
400
401 /*
402 * matchColor() is a critical performance bottleneck. To make it
403 * decent, we do the following:
404 *
405 * 1. Find the nearest two hues that bracket this color.
406 *
407 * 2. Find the nearest two saturations that bracket this color.
408 *
409 * 3. Iterate within these four bands of luminance values,
410 * returning the closest color by Euclidean distance.
411 *
412 * This strategy reduces the search space by about 97%.
413 */
414 int red = (color >>> 16) & 0xFF;
415 int green = (color >>> 8) & 0xFF;
416 int blue = color & 0xFF;
417
418 if (sixelPaletteSize == 2) {
419 if (((red * red) + (green * green) + (blue * blue)) < 35568) {
420 // Black
421 return 0;
422 }
423 // White
424 return 1;
425 }
426
427
428 rgbToHsl(red, green, blue, hsl);
429 int hue = hsl[0];
430 int sat = hsl[1];
431 int lum = hsl[2];
432 // System.err.printf("%d %d %d\n", hue, sat, lum);
433
434 double diff = Double.MAX_VALUE;
435 int idx = -1;
436
437 int hue1 = hue / (360/hueStep);
438 int hue2 = hue1 + 1;
439 if (hue1 >= hslColors.size() - 1) {
440 // Bracket pure red from above.
441 hue1 = hslColors.size() - 1;
442 hue2 = 0;
443 } else if (hue1 == 0) {
444 // Bracket pure red from below.
445 hue2 = hslColors.size() - 1;
446 }
447
448 for (int hI = hue1; hI != -1;) {
449 ArrayList<ArrayList<ColorIdx>> sats = hslColors.get(hI);
450 if (hI == hue1) {
451 hI = hue2;
452 } else if (hI == hue2) {
453 hI = -1;
454 }
455
456 int sMin = (sat / satStep) - 1;
457 int sMax = sMin + 1;
458 if (sMin < 0) {
459 sMin = 0;
460 sMax = 1;
461 } else if (sMin == sats.size() - 1) {
462 sMax = sMin;
463 sMin--;
464 }
465 assert (sMin >= 0);
466 assert (sMax - sMin == 1);
467
468 // int sMin = 0;
469 // int sMax = sats.size() - 1;
470
471 for (int sI = sMin; sI <= sMax; sI++) {
472 ArrayList<ColorIdx> lums = sats.get(sI);
473
474 // True 3D colorspace match for the remaining values
475 for (ColorIdx c: lums) {
476 int rgbColor = c.color;
477 double newDiff = 0;
478 int red2 = (rgbColor >>> 16) & 0xFF;
479 int green2 = (rgbColor >>> 8) & 0xFF;
480 int blue2 = rgbColor & 0xFF;
481 newDiff += Math.pow(red2 - red, 2);
482 newDiff += Math.pow(green2 - green, 2);
483 newDiff += Math.pow(blue2 - blue, 2);
484 if (newDiff < diff) {
485 idx = rgbSortedIndex[c.index];
486 diff = newDiff;
487 }
488 }
489 }
490 }
491
492 if (((red * red) + (green * green) + (blue * blue)) < diff) {
493 // Black is a closer match.
494 idx = 0;
495 } else if ((((255 - red) * (255 - red)) +
496 ((255 - green) * (255 - green)) +
497 ((255 - blue) * (255 - blue))) < diff) {
498
499 // White is a closer match.
500 idx = sixelPaletteSize - 1;
501 }
502 assert (idx != -1);
503 return idx;
504 }
505
506 /**
507 * Clamp an int value to [0, 255].
508 *
509 * @param x the int value
510 * @return an int between 0 and 255.
511 */
512 private int clamp(final int x) {
513 if (x < 0) {
514 return 0;
515 }
516 if (x > 255) {
517 return 255;
518 }
519 return x;
520 }
521
522 /**
523 * Dither an image to a sixelPaletteSize palette. The dithered
524 * image cells will contain indexes into the palette.
525 *
526 * @param image the image to dither
527 * @return the dithered image. Every pixel is an index into the
528 * palette.
529 */
530 public BufferedImage ditherImage(final BufferedImage image) {
531
532 BufferedImage ditheredImage = new BufferedImage(image.getWidth(),
533 image.getHeight(), BufferedImage.TYPE_INT_ARGB);
534
535 int [] rgbArray = image.getRGB(0, 0, image.getWidth(),
536 image.getHeight(), null, 0, image.getWidth());
537 ditheredImage.setRGB(0, 0, image.getWidth(), image.getHeight(),
538 rgbArray, 0, image.getWidth());
539
540 for (int imageY = 0; imageY < image.getHeight(); imageY++) {
541 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
542 int oldPixel = ditheredImage.getRGB(imageX,
543 imageY) & 0xFFFFFF;
544 int colorIdx = matchColor(oldPixel);
545 assert (colorIdx >= 0);
546 assert (colorIdx < sixelPaletteSize);
547 int newPixel = rgbColors.get(colorIdx);
548 ditheredImage.setRGB(imageX, imageY, colorIdx);
549
550 int oldRed = (oldPixel >>> 16) & 0xFF;
551 int oldGreen = (oldPixel >>> 8) & 0xFF;
552 int oldBlue = oldPixel & 0xFF;
553
554 int newRed = (newPixel >>> 16) & 0xFF;
555 int newGreen = (newPixel >>> 8) & 0xFF;
556 int newBlue = newPixel & 0xFF;
557
558 int redError = (oldRed - newRed) / 16;
559 int greenError = (oldGreen - newGreen) / 16;
560 int blueError = (oldBlue - newBlue) / 16;
561
562 int red, green, blue;
563 if (imageX < image.getWidth() - 1) {
564 int pXpY = ditheredImage.getRGB(imageX + 1, imageY);
565 red = ((pXpY >>> 16) & 0xFF) + (7 * redError);
566 green = ((pXpY >>> 8) & 0xFF) + (7 * greenError);
567 blue = ( pXpY & 0xFF) + (7 * blueError);
568 red = clamp(red);
569 green = clamp(green);
570 blue = clamp(blue);
571 pXpY = ((red & 0xFF) << 16);
572 pXpY |= ((green & 0xFF) << 8) | (blue & 0xFF);
573 ditheredImage.setRGB(imageX + 1, imageY, pXpY);
574
575 if (imageY < image.getHeight() - 1) {
576 int pXpYp = ditheredImage.getRGB(imageX + 1,
577 imageY + 1);
578 red = ((pXpYp >>> 16) & 0xFF) + redError;
579 green = ((pXpYp >>> 8) & 0xFF) + greenError;
580 blue = ( pXpYp & 0xFF) + blueError;
581 red = clamp(red);
582 green = clamp(green);
583 blue = clamp(blue);
584 pXpYp = ((red & 0xFF) << 16);
585 pXpYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
586 ditheredImage.setRGB(imageX + 1, imageY + 1, pXpYp);
587 }
588 } else if (imageY < image.getHeight() - 1) {
589 int pXmYp = ditheredImage.getRGB(imageX - 1,
590 imageY + 1);
591 int pXYp = ditheredImage.getRGB(imageX,
592 imageY + 1);
593
594 red = ((pXmYp >>> 16) & 0xFF) + (3 * redError);
595 green = ((pXmYp >>> 8) & 0xFF) + (3 * greenError);
596 blue = ( pXmYp & 0xFF) + (3 * blueError);
597 red = clamp(red);
598 green = clamp(green);
599 blue = clamp(blue);
600 pXmYp = ((red & 0xFF) << 16);
601 pXmYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
602 ditheredImage.setRGB(imageX - 1, imageY + 1, pXmYp);
603
604 red = ((pXYp >>> 16) & 0xFF) + (5 * redError);
605 green = ((pXYp >>> 8) & 0xFF) + (5 * greenError);
606 blue = ( pXYp & 0xFF) + (5 * blueError);
607 red = clamp(red);
608 green = clamp(green);
609 blue = clamp(blue);
610 pXYp = ((red & 0xFF) << 16);
611 pXYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
612 ditheredImage.setRGB(imageX, imageY + 1, pXYp);
613 }
614 } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
615 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
616
617 return ditheredImage;
618 }
619
620 /**
621 * Convert an RGB color to HSL.
622 *
623 * @param red red color, between 0 and 255
624 * @param green green color, between 0 and 255
625 * @param blue blue color, between 0 and 255
626 * @param hsl the hsl color as [hue, saturation, luminance]
627 */
628 private void rgbToHsl(final int red, final int green,
629 final int blue, final int [] hsl) {
630
631 assert ((red >= 0) && (red <= 255));
632 assert ((green >= 0) && (green <= 255));
633 assert ((blue >= 0) && (blue <= 255));
634
635 double R = red / 255.0;
636 double G = green / 255.0;
637 double B = blue / 255.0;
638 boolean Rmax = false;
639 boolean Gmax = false;
640 boolean Bmax = false;
641 double min = (R < G ? R : G);
642 min = (min < B ? min : B);
643 double max = 0;
644 if ((R >= G) && (R >= B)) {
645 max = R;
646 Rmax = true;
647 } else if ((G >= R) && (G >= B)) {
648 max = G;
649 Gmax = true;
650 } else if ((B >= G) && (B >= R)) {
651 max = B;
652 Bmax = true;
653 }
654
655 double L = (min + max) / 2.0;
656 double H = 0.0;
657 double S = 0.0;
658 if (min != max) {
659 if (L < 0.5) {
660 S = (max - min) / (max + min);
661 } else {
662 S = (max - min) / (2.0 - max - min);
663 }
664 }
665 if (Rmax) {
666 assert (Gmax == false);
667 assert (Bmax == false);
668 H = (G - B) / (max - min);
669 } else if (Gmax) {
670 assert (Rmax == false);
671 assert (Bmax == false);
672 H = 2.0 + (B - R) / (max - min);
673 } else if (Bmax) {
674 assert (Rmax == false);
675 assert (Gmax == false);
676 H = 4.0 + (R - G) / (max - min);
677 }
678 if (H < 0.0) {
679 H += 6.0;
680 }
681 hsl[0] = (int) (H * 60.0);
682 hsl[1] = (int) (S * 100.0);
683 hsl[2] = (int) (L * 100.0);
684
685 assert ((hsl[0] >= 0) && (hsl[0] <= 360));
686 assert ((hsl[1] >= 0) && (hsl[1] <= 100));
687 assert ((hsl[2] >= 0) && (hsl[2] <= 100));
688 }
689
690 /**
691 * Convert a HSL color to RGB.
692 *
693 * @param hue hue, between 0 and 359
694 * @param sat saturation, between 0 and 100
695 * @param lum luminance, between 0 and 100
696 * @return the rgb color as 0x00RRGGBB
697 */
698 private int hslToRgb(final int hue, final int sat, final int lum) {
699 assert ((hue >= 0) && (hue <= 360));
700 assert ((sat >= 0) && (sat <= 100));
701 assert ((lum >= 0) && (lum <= 100));
702
703 double S = sat / 100.0;
704 double L = lum / 100.0;
705 double C = (1.0 - Math.abs((2.0 * L) - 1.0)) * S;
706 double Hp = hue / 60.0;
707 double X = C * (1.0 - Math.abs((Hp % 2) - 1.0));
708 double Rp = 0.0;
709 double Gp = 0.0;
710 double Bp = 0.0;
711 if (Hp <= 1.0) {
712 Rp = C;
713 Gp = X;
714 } else if (Hp <= 2.0) {
715 Rp = X;
716 Gp = C;
717 } else if (Hp <= 3.0) {
718 Gp = C;
719 Bp = X;
720 } else if (Hp <= 4.0) {
721 Gp = X;
722 Bp = C;
723 } else if (Hp <= 5.0) {
724 Rp = X;
725 Bp = C;
726 } else if (Hp <= 6.0) {
727 Rp = C;
728 Bp = X;
729 }
730 double m = L - (C / 2.0);
731 int red = ((int) ((Rp + m) * 255.0)) << 16;
732 int green = ((int) ((Gp + m) * 255.0)) << 8;
733 int blue = (int) ((Bp + m) * 255.0);
734
735 return (red | green | blue);
736 }
737
738 /**
739 * Create the sixel palette.
740 */
741 private void makePalette() {
742 // Generate the sixel palette. Because we have no idea at this
743 // layer which image(s) will be shown, we have to use a common
744 // palette with sixelPaletteSize colors for everything, and
745 // map the BufferedImage colors to their nearest neighbor in RGB
746 // space.
747
748 if (sixelPaletteSize == 2) {
749 rgbColors.add(0);
750 rgbColors.add(0xFFFFFF);
751 rgbSortedIndex[0] = 0;
752 rgbSortedIndex[1] = 1;
753 return;
754 }
755
756 // We build a palette using the Hue-Saturation-Luminence model,
757 // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
758 // Luminance. We convert these colors to 24-bit RGB, sort them
759 // ascending, and steal the first index for pure black and the
760 // last for pure white. The 8-bit final palette favors bright
761 // colors, somewhere between pastel and classic television
762 // technicolor. 9- and 10-bit palettes are more uniform.
763
764 // Default at 256 colors.
765 hueBits = 5;
766 satBits = 2;
767 lumBits = 1;
768
769 assert (sixelPaletteSize >= 256);
770 assert ((sixelPaletteSize == 256)
771 || (sixelPaletteSize == 512)
772 || (sixelPaletteSize == 1024)
773 || (sixelPaletteSize == 2048));
774
775 switch (sixelPaletteSize) {
776 case 512:
777 hueBits = 5;
778 satBits = 2;
779 lumBits = 2;
780 break;
781 case 1024:
782 hueBits = 5;
783 satBits = 2;
784 lumBits = 3;
785 break;
786 case 2048:
787 hueBits = 5;
788 satBits = 3;
789 lumBits = 3;
790 break;
791 }
792 hueStep = (int) (Math.pow(2, hueBits));
793 satStep = (int) (100 / Math.pow(2, satBits));
794 // 1 bit for luminance: 40 and 70.
795 int lumBegin = 40;
796 int lumStep = 30;
797 switch (lumBits) {
798 case 2:
799 // 2 bits: 20, 40, 60, 80
800 lumBegin = 20;
801 lumStep = 20;
802 break;
803 case 3:
804 // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
805 lumBegin = 8;
806 lumStep = 12;
807 break;
808 }
809
810 // System.err.printf("<html><body>\n");
811 // Hue is evenly spaced around the wheel.
812 hslColors = new ArrayList<ArrayList<ArrayList<ColorIdx>>>();
813
814 final boolean DEBUG = false;
815 ArrayList<Integer> rawRgbList = new ArrayList<Integer>();
816
817 for (int hue = 0; hue < (360 - (360 % hueStep));
818 hue += (360/hueStep)) {
819
820 ArrayList<ArrayList<ColorIdx>> satList = null;
821 satList = new ArrayList<ArrayList<ColorIdx>>();
822 hslColors.add(satList);
823
824 // Saturation is linearly spaced between pastel and pure.
825 for (int sat = satStep; sat <= 100; sat += satStep) {
826
827 ArrayList<ColorIdx> lumList = new ArrayList<ColorIdx>();
828 satList.add(lumList);
829
830 // Luminance brackets the pure color, but leaning toward
831 // lighter.
832 for (int lum = lumBegin; lum < 100; lum += lumStep) {
833 /*
834 System.err.printf("<font style = \"color:");
835 System.err.printf("hsl(%d, %d%%, %d%%)",
836 hue, sat, lum);
837 System.err.printf(";\">=</font>\n");
838 */
839 int rgbColor = hslToRgb(hue, sat, lum);
840 rgbColors.add(rgbColor);
841 ColorIdx colorIdx = new ColorIdx(rgbColor,
842 rgbColors.size() - 1);
843 lumList.add(colorIdx);
844
845 rawRgbList.add(rgbColor);
846 if (DEBUG) {
847 int red = (rgbColor >>> 16) & 0xFF;
848 int green = (rgbColor >>> 8) & 0xFF;
849 int blue = rgbColor & 0xFF;
850 int [] backToHsl = new int[3];
851 rgbToHsl(red, green, blue, backToHsl);
852 System.err.printf("%d [%d] %d [%d] %d [%d]\n",
853 hue, backToHsl[0], sat, backToHsl[1],
854 lum, backToHsl[2]);
855 }
856 }
857 }
858 }
859 // System.err.printf("\n</body></html>\n");
860
861 assert (rgbColors.size() == sixelPaletteSize);
862
863 /*
864 * We need to sort rgbColors, so that toSixel() can know where
865 * BLACK and WHITE are in it. But we also need to be able to
866 * find the sorted values using the old unsorted indexes. So we
867 * will sort it, put all the indexes into a HashMap, and then
868 * build rgbSortedIndex[].
869 */
870 Collections.sort(rgbColors);
871 HashMap<Integer, Integer> rgbColorIndices = null;
872 rgbColorIndices = new HashMap<Integer, Integer>();
873 for (int i = 0; i < sixelPaletteSize; i++) {
874 rgbColorIndices.put(rgbColors.get(i), i);
875 }
876 for (int i = 0; i < sixelPaletteSize; i++) {
877 int rawColor = rawRgbList.get(i);
878 rgbSortedIndex[i] = rgbColorIndices.get(rawColor);
879 }
880 if (DEBUG) {
881 for (int i = 0; i < sixelPaletteSize; i++) {
882 assert (rawRgbList != null);
883 int idx = rgbSortedIndex[i];
884 int rgbColor = rgbColors.get(idx);
885 if ((idx != 0) && (idx != sixelPaletteSize - 1)) {
886 /*
887 System.err.printf("%d %06x --> %d %06x\n",
888 i, rawRgbList.get(i), idx, rgbColors.get(idx));
889 */
890 assert (rgbColor == rawRgbList.get(i));
891 }
892 }
893 }
894
895 // Set the dimmest color as true black, and the brightest as true
896 // white.
897 rgbColors.set(0, 0);
898 rgbColors.set(sixelPaletteSize - 1, 0xFFFFFF);
899
900 /*
901 System.err.printf("<html><body>\n");
902 for (Integer rgb: rgbColors) {
903 System.err.printf("<font style = \"color:");
904 System.err.printf("#%06x", rgb);
905 System.err.printf(";\">=</font>\n");
906 }
907 System.err.printf("\n</body></html>\n");
908 */
909
910 }
911
912 /**
913 * Emit the sixel palette.
914 *
915 * @param sb the StringBuilder to append to
916 * @param used array of booleans set to true for each color actually
917 * used in this cell, or null to emit the entire palette
918 * @return the string to emit to an ANSI / ECMA-style terminal
919 */
920 public String emitPalette(final StringBuilder sb,
921 final boolean [] used) {
922
923 for (int i = 0; i < sixelPaletteSize; i++) {
924 if (((used != null) && (used[i] == true)) || (used == null)) {
925 int rgbColor = rgbColors.get(i);
926 sb.append(String.format("#%d;2;%d;%d;%d", i,
927 ((rgbColor >>> 16) & 0xFF) * 100 / 255,
928 ((rgbColor >>> 8) & 0xFF) * 100 / 255,
929 ( rgbColor & 0xFF) * 100 / 255));
930 }
931 }
932 return sb.toString();
933 }
934 }
935
936 /**
937 * ImageCache is a least-recently-used cache that hangs on to the
938 * post-rendered sixel or iTerm2 string for a particular set of cells.
939 */
940 private class ImageCache {
941
942 /**
943 * Maximum size of the cache.
944 */
945 private int maxSize = 100;
946
947 /**
948 * The entries stored in the cache.
949 */
950 private HashMap<String, CacheEntry> cache = null;
951
952 /**
953 * CacheEntry is one entry in the cache.
954 */
955 private class CacheEntry {
956 /**
957 * The cache key.
958 */
959 public String key;
960
961 /**
962 * The cache data.
963 */
964 public String data;
965
966 /**
967 * The last time this entry was used.
968 */
969 public long millis = 0;
970
971 /**
972 * Public constructor.
973 *
974 * @param key the cache entry key
975 * @param data the cache entry data
976 */
977 public CacheEntry(final String key, final String data) {
978 this.key = key;
979 this.data = data;
980 this.millis = System.currentTimeMillis();
981 }
982 }
983
984 /**
985 * Public constructor.
986 *
987 * @param maxSize the maximum size of the cache
988 */
989 public ImageCache(final int maxSize) {
990 this.maxSize = maxSize;
991 cache = new HashMap<String, CacheEntry>();
992 }
993
994 /**
995 * Make a unique key for a list of cells.
996 *
997 * @param cells the cells
998 * @return the key
999 */
1000 private String makeKey(final ArrayList<Cell> cells) {
1001 StringBuilder sb = new StringBuilder();
1002 for (Cell cell: cells) {
1003 sb.append(cell.hashCode());
1004 }
1005 return sb.toString();
1006 }
1007
1008 /**
1009 * Get an entry from the cache.
1010 *
1011 * @param cells the list of cells that are the cache key
1012 * @return the sixel string representing these cells, or null if this
1013 * list of cells is not in the cache
1014 */
1015 public String get(final ArrayList<Cell> cells) {
1016 CacheEntry entry = cache.get(makeKey(cells));
1017 if (entry == null) {
1018 return null;
1019 }
1020 entry.millis = System.currentTimeMillis();
1021 return entry.data;
1022 }
1023
1024 /**
1025 * Put an entry into the cache.
1026 *
1027 * @param cells the list of cells that are the cache key
1028 * @param data the sixel string representing these cells
1029 */
1030 public void put(final ArrayList<Cell> cells, final String data) {
1031 String key = makeKey(cells);
1032
1033 // System.err.println("put() " + key + " size " + cache.size());
1034
1035 assert (!cache.containsKey(key));
1036
1037 assert (cache.size() <= maxSize);
1038 if (cache.size() == maxSize) {
1039 // Cache is at limit, evict oldest entry.
1040 long oldestTime = Long.MAX_VALUE;
1041 String keyToRemove = null;
1042 for (CacheEntry entry: cache.values()) {
1043 if ((entry.millis < oldestTime) || (keyToRemove == null)) {
1044 keyToRemove = entry.key;
1045 oldestTime = entry.millis;
1046 }
1047 }
1048 /*
1049 System.err.println("put() remove key = " + keyToRemove +
1050 " size " + cache.size());
1051 */
1052 assert (keyToRemove != null);
1053 cache.remove(keyToRemove);
1054 /*
1055 System.err.println("put() removed, size " + cache.size());
1056 */
1057 }
1058 assert (cache.size() <= maxSize);
1059 CacheEntry entry = new CacheEntry(key, data);
1060 assert (key.equals(entry.key));
1061 cache.put(key, entry);
1062 /*
1063 System.err.println("put() added key " + key + " " +
1064 " size " + cache.size());
1065 */
1066 }
1067
1068 }
1069
1070 // ------------------------------------------------------------------------
1071 // Constructors -----------------------------------------------------------
1072 // ------------------------------------------------------------------------
1073
1074 /**
1075 * Constructor sets up state for getEvent(). If either windowWidth or
1076 * windowHeight are less than 1, the terminal is not resized.
1077 *
1078 * @param listener the object this backend needs to wake up when new
1079 * input comes in
1080 * @param input an InputStream connected to the remote user, or null for
1081 * System.in. If System.in is used, then on non-Windows systems it will
1082 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1083 * cooked mode. input is always converted to a Reader with UTF-8
1084 * encoding.
1085 * @param output an OutputStream connected to the remote user, or null
1086 * for System.out. output is always converted to a Writer with UTF-8
1087 * encoding.
1088 * @param windowWidth the number of text columns to start with
1089 * @param windowHeight the number of text rows to start with
1090 * @throws UnsupportedEncodingException if an exception is thrown when
1091 * creating the InputStreamReader
1092 */
1093 public ECMA48Terminal(final Object listener, final InputStream input,
1094 final OutputStream output, final int windowWidth,
1095 final int windowHeight) throws UnsupportedEncodingException {
1096
1097 this(listener, input, output);
1098
1099 // Send dtterm/xterm sequences, which will probably not work because
1100 // allowWindowOps is defaulted to false.
1101 if ((windowWidth > 0) && (windowHeight > 0)) {
1102 String resizeString = String.format("\033[8;%d;%dt", windowHeight,
1103 windowWidth);
1104 this.output.write(resizeString);
1105 this.output.flush();
1106 }
1107 }
1108
1109 /**
1110 * Constructor sets up state for getEvent().
1111 *
1112 * @param listener the object this backend needs to wake up when new
1113 * input comes in
1114 * @param input an InputStream connected to the remote user, or null for
1115 * System.in. If System.in is used, then on non-Windows systems it will
1116 * be put in raw mode; closeTerminal() will (blindly!) put System.in in
1117 * cooked mode. input is always converted to a Reader with UTF-8
1118 * encoding.
1119 * @param output an OutputStream connected to the remote user, or null
1120 * for System.out. output is always converted to a Writer with UTF-8
1121 * encoding.
1122 * @throws UnsupportedEncodingException if an exception is thrown when
1123 * creating the InputStreamReader
1124 */
1125 public ECMA48Terminal(final Object listener, final InputStream input,
1126 final OutputStream output) throws UnsupportedEncodingException {
1127
1128 resetParser();
1129 mouse1 = false;
1130 mouse2 = false;
1131 mouse3 = false;
1132 stopReaderThread = false;
1133 this.listener = listener;
1134
1135 if (input == null) {
1136 // inputStream = System.in;
1137 inputStream = new FileInputStream(FileDescriptor.in);
1138 sttyRaw();
1139 setRawMode = true;
1140 } else {
1141 inputStream = input;
1142 }
1143 this.input = new InputStreamReader(inputStream, "UTF-8");
1144
1145 if (input instanceof SessionInfo) {
1146 // This is a TelnetInputStream that exposes window size and
1147 // environment variables from the telnet layer.
1148 sessionInfo = (SessionInfo) input;
1149 }
1150 if (sessionInfo == null) {
1151 if (input == null) {
1152 // Reading right off the tty
1153 sessionInfo = new TTYSessionInfo();
1154 } else {
1155 sessionInfo = new TSessionInfo();
1156 }
1157 }
1158
1159 if (output == null) {
1160 this.output = new PrintWriter(new OutputStreamWriter(System.out,
1161 "UTF-8"));
1162 } else {
1163 this.output = new PrintWriter(new OutputStreamWriter(output,
1164 "UTF-8"));
1165 }
1166
1167 // Request Device Attributes
1168 this.output.printf("\033[c");
1169
1170 // Request xterm report window/cell dimensions in pixels
1171 this.output.printf("%s", xtermReportPixelDimensions());
1172
1173 // Enable mouse reporting and metaSendsEscape
1174 this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1175
1176 // Request xterm use the sixel settings we want
1177 this.output.printf("%s", xtermSetSixelSettings());
1178
1179 this.output.flush();
1180
1181 // Query the screen size
1182 sessionInfo.queryWindowSize();
1183 setDimensions(sessionInfo.getWindowWidth(),
1184 sessionInfo.getWindowHeight());
1185
1186 // Hang onto the window size
1187 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
1188 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
1189
1190 reloadOptions();
1191
1192 // Spin up the input reader
1193 eventQueue = new ArrayList<TInputEvent>();
1194 readerThread = new Thread(this);
1195 readerThread.start();
1196
1197 // Clear the screen
1198 this.output.write(clearAll());
1199 this.output.flush();
1200 }
1201
1202 /**
1203 * Constructor sets up state for getEvent().
1204 *
1205 * @param listener the object this backend needs to wake up when new
1206 * input comes in
1207 * @param input the InputStream underlying 'reader'. Its available()
1208 * method is used to determine if reader.read() will block or not.
1209 * @param reader a Reader connected to the remote user.
1210 * @param writer a PrintWriter connected to the remote user.
1211 * @param setRawMode if true, set System.in into raw mode with stty.
1212 * This should in general not be used. It is here solely for Demo3,
1213 * which uses System.in.
1214 * @throws IllegalArgumentException if input, reader, or writer are null.
1215 */
1216 public ECMA48Terminal(final Object listener, final InputStream input,
1217 final Reader reader, final PrintWriter writer,
1218 final boolean setRawMode) {
1219
1220 if (input == null) {
1221 throw new IllegalArgumentException("InputStream must be specified");
1222 }
1223 if (reader == null) {
1224 throw new IllegalArgumentException("Reader must be specified");
1225 }
1226 if (writer == null) {
1227 throw new IllegalArgumentException("Writer must be specified");
1228 }
1229 resetParser();
1230 mouse1 = false;
1231 mouse2 = false;
1232 mouse3 = false;
1233 stopReaderThread = false;
1234 this.listener = listener;
1235
1236 inputStream = input;
1237 this.input = reader;
1238
1239 if (setRawMode == true) {
1240 sttyRaw();
1241 }
1242 this.setRawMode = setRawMode;
1243
1244 if (input instanceof SessionInfo) {
1245 // This is a TelnetInputStream that exposes window size and
1246 // environment variables from the telnet layer.
1247 sessionInfo = (SessionInfo) input;
1248 }
1249 if (sessionInfo == null) {
1250 if (setRawMode == true) {
1251 // Reading right off the tty
1252 sessionInfo = new TTYSessionInfo();
1253 } else {
1254 sessionInfo = new TSessionInfo();
1255 }
1256 }
1257
1258 this.output = writer;
1259
1260 // Request Device Attributes
1261 this.output.printf("\033[c");
1262
1263 // Request xterm report window/cell dimensions in pixels
1264 this.output.printf("%s", xtermReportPixelDimensions());
1265
1266 // Enable mouse reporting and metaSendsEscape
1267 this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
1268
1269 // Request xterm use the sixel settings we want
1270 this.output.printf("%s", xtermSetSixelSettings());
1271
1272 this.output.flush();
1273
1274 // Query the screen size
1275 sessionInfo.queryWindowSize();
1276 setDimensions(sessionInfo.getWindowWidth(),
1277 sessionInfo.getWindowHeight());
1278
1279 // Hang onto the window size
1280 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
1281 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
1282
1283 reloadOptions();
1284
1285 // Spin up the input reader
1286 eventQueue = new ArrayList<TInputEvent>();
1287 readerThread = new Thread(this);
1288 readerThread.start();
1289
1290 // Clear the screen
1291 this.output.write(clearAll());
1292 this.output.flush();
1293 }
1294
1295 /**
1296 * Constructor sets up state for getEvent().
1297 *
1298 * @param listener the object this backend needs to wake up when new
1299 * input comes in
1300 * @param input the InputStream underlying 'reader'. Its available()
1301 * method is used to determine if reader.read() will block or not.
1302 * @param reader a Reader connected to the remote user.
1303 * @param writer a PrintWriter connected to the remote user.
1304 * @throws IllegalArgumentException if input, reader, or writer are null.
1305 */
1306 public ECMA48Terminal(final Object listener, final InputStream input,
1307 final Reader reader, final PrintWriter writer) {
1308
1309 this(listener, input, reader, writer, false);
1310 }
1311
1312 // ------------------------------------------------------------------------
1313 // LogicalScreen ----------------------------------------------------------
1314 // ------------------------------------------------------------------------
1315
1316 /**
1317 * Set the window title.
1318 *
1319 * @param title the new title
1320 */
1321 @Override
1322 public void setTitle(final String title) {
1323 output.write(getSetTitleString(title));
1324 flush();
1325 }
1326
1327 /**
1328 * Push the logical screen to the physical device.
1329 */
1330 @Override
1331 public void flushPhysical() {
1332 StringBuilder sb = new StringBuilder();
1333 if ((cursorVisible)
1334 && (cursorY >= 0)
1335 && (cursorX >= 0)
1336 && (cursorY <= height - 1)
1337 && (cursorX <= width - 1)
1338 ) {
1339 flushString(sb);
1340 sb.append(cursor(true));
1341 sb.append(gotoXY(cursorX, cursorY));
1342 } else {
1343 sb.append(cursor(false));
1344 flushString(sb);
1345 }
1346 output.write(sb.toString());
1347 flush();
1348 }
1349
1350 /**
1351 * Resize the physical screen to match the logical screen dimensions.
1352 */
1353 @Override
1354 public void resizeToScreen() {
1355 // Send dtterm/xterm sequences, which will probably not work because
1356 // allowWindowOps is defaulted to false.
1357 String resizeString = String.format("\033[8;%d;%dt", getHeight(),
1358 getWidth());
1359 this.output.write(resizeString);
1360 this.output.flush();
1361 }
1362
1363 // ------------------------------------------------------------------------
1364 // TerminalReader ---------------------------------------------------------
1365 // ------------------------------------------------------------------------
1366
1367 /**
1368 * Check if there are events in the queue.
1369 *
1370 * @return if true, getEvents() has something to return to the backend
1371 */
1372 public boolean hasEvents() {
1373 synchronized (eventQueue) {
1374 return (eventQueue.size() > 0);
1375 }
1376 }
1377
1378 /**
1379 * Return any events in the IO queue.
1380 *
1381 * @param queue list to append new events to
1382 */
1383 public void getEvents(final List<TInputEvent> queue) {
1384 synchronized (eventQueue) {
1385 if (eventQueue.size() > 0) {
1386 synchronized (queue) {
1387 queue.addAll(eventQueue);
1388 }
1389 eventQueue.clear();
1390 }
1391 }
1392 }
1393
1394 /**
1395 * Restore terminal to normal state.
1396 */
1397 public void closeTerminal() {
1398
1399 // System.err.println("=== closeTerminal() ==="); System.err.flush();
1400
1401 // Tell the reader thread to stop looking at input
1402 stopReaderThread = true;
1403 try {
1404 readerThread.join();
1405 } catch (InterruptedException e) {
1406 if (debugToStderr) {
1407 e.printStackTrace();
1408 }
1409 }
1410
1411 // Disable mouse reporting and show cursor. Defensive null check
1412 // here in case closeTerminal() is called twice.
1413 if (output != null) {
1414 output.printf("%s%s%s%s", mouse(false), cursor(true),
1415 defaultColor(), xtermResetSixelSettings());
1416 output.flush();
1417 }
1418
1419 if (setRawMode) {
1420 sttyCooked();
1421 setRawMode = false;
1422 // We don't close System.in/out
1423 } else {
1424 // Shut down the streams, this should wake up the reader thread
1425 // and make it exit.
1426 if (input != null) {
1427 try {
1428 input.close();
1429 } catch (IOException e) {
1430 // SQUASH
1431 }
1432 input = null;
1433 }
1434 if (output != null) {
1435 output.close();
1436 output = null;
1437 }
1438 }
1439 }
1440
1441 /**
1442 * Set listener to a different Object.
1443 *
1444 * @param listener the new listening object that run() wakes up on new
1445 * input
1446 */
1447 public void setListener(final Object listener) {
1448 this.listener = listener;
1449 }
1450
1451 /**
1452 * Reload options from System properties.
1453 */
1454 public void reloadOptions() {
1455 // Permit RGB colors only if externally requested.
1456 if (System.getProperty("jexer.ECMA48.rgbColor",
1457 "false").equals("true")
1458 ) {
1459 doRgbColor = true;
1460 } else {
1461 doRgbColor = false;
1462 }
1463
1464 // Default to using images for full-width characters.
1465 if (System.getProperty("jexer.ECMA48.wideCharImages",
1466 "true").equals("true")) {
1467 wideCharImages = true;
1468 } else {
1469 wideCharImages = false;
1470 }
1471
1472 // Pull the system properties for sixel output.
1473 if (System.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1474 sixel = true;
1475 } else {
1476 sixel = false;
1477 }
1478
1479 // Palette size
1480 int paletteSize = 1024;
1481 try {
1482 paletteSize = Integer.parseInt(System.getProperty(
1483 "jexer.ECMA48.sixelPaletteSize", "1024"));
1484 switch (paletteSize) {
1485 case 2:
1486 case 256:
1487 case 512:
1488 case 1024:
1489 case 2048:
1490 sixelPaletteSize = paletteSize;
1491 break;
1492 default:
1493 // Ignore value
1494 break;
1495 }
1496 } catch (NumberFormatException e) {
1497 // SQUASH
1498 }
1499
1500 // Shared palette
1501 if (System.getProperty("jexer.ECMA48.sixelSharedPalette",
1502 "true").equals("false")) {
1503 sixelSharedPalette = false;
1504 } else {
1505 sixelSharedPalette = true;
1506 }
1507
1508 // Default to not supporting iTerm2 images.
1509 if (System.getProperty("jexer.ECMA48.iTerm2Images",
1510 "false").equals("true")) {
1511 iterm2Images = true;
1512 } else {
1513 iterm2Images = false;
1514 }
1515
1516 // Default to using JPG Jexer images if terminal supports it.
1517 String jexerImageStr = System.getProperty("jexer.ECMA48.jexerImages",
1518 "jpg").toLowerCase();
1519 if (jexerImageStr.equals("false")) {
1520 jexerImageOption = JexerImageOption.DISABLED;
1521 } else if (jexerImageStr.equals("jpg")) {
1522 jexerImageOption = JexerImageOption.JPG;
1523 } else if (jexerImageStr.equals("png")) {
1524 jexerImageOption = JexerImageOption.PNG;
1525 } else if (jexerImageStr.equals("rgb")) {
1526 jexerImageOption = JexerImageOption.RGB;
1527 }
1528
1529 // Set custom colors
1530 setCustomSystemColors();
1531 }
1532
1533 // ------------------------------------------------------------------------
1534 // Runnable ---------------------------------------------------------------
1535 // ------------------------------------------------------------------------
1536
1537 /**
1538 * Read function runs on a separate thread.
1539 */
1540 public void run() {
1541 boolean done = false;
1542 // available() will often return > 1, so we need to read in chunks to
1543 // stay caught up.
1544 char [] readBuffer = new char[128];
1545 List<TInputEvent> events = new ArrayList<TInputEvent>();
1546
1547 while (!done && !stopReaderThread) {
1548 try {
1549 // We assume that if inputStream has bytes available, then
1550 // input won't block on read().
1551 int n = inputStream.available();
1552
1553 /*
1554 System.err.printf("inputStream.available(): %d\n", n);
1555 System.err.flush();
1556 */
1557
1558 if (n > 0) {
1559 if (readBuffer.length < n) {
1560 // The buffer wasn't big enough, make it huger
1561 readBuffer = new char[readBuffer.length * 2];
1562 }
1563
1564 // System.err.printf("BEFORE read()\n"); System.err.flush();
1565
1566 int rc = input.read(readBuffer, 0, readBuffer.length);
1567
1568 /*
1569 System.err.printf("AFTER read() %d\n", rc);
1570 System.err.flush();
1571 */
1572
1573 if (rc == -1) {
1574 // This is EOF
1575 done = true;
1576 } else {
1577 for (int i = 0; i < rc; i++) {
1578 int ch = readBuffer[i];
1579 processChar(events, (char)ch);
1580 }
1581 getIdleEvents(events);
1582 if (events.size() > 0) {
1583 // Add to the queue for the backend thread to
1584 // be able to obtain.
1585 synchronized (eventQueue) {
1586 eventQueue.addAll(events);
1587 }
1588 if (listener != null) {
1589 synchronized (listener) {
1590 listener.notifyAll();
1591 }
1592 }
1593 events.clear();
1594 }
1595 }
1596 } else {
1597 getIdleEvents(events);
1598 if (events.size() > 0) {
1599 synchronized (eventQueue) {
1600 eventQueue.addAll(events);
1601 }
1602 if (listener != null) {
1603 synchronized (listener) {
1604 listener.notifyAll();
1605 }
1606 }
1607 events.clear();
1608 }
1609
1610 if (output.checkError()) {
1611 // This is EOF.
1612 done = true;
1613 }
1614
1615 // Wait 20 millis for more data
1616 Thread.sleep(20);
1617 }
1618 // System.err.println("end while loop"); System.err.flush();
1619 } catch (InterruptedException e) {
1620 // SQUASH
1621 } catch (IOException e) {
1622 e.printStackTrace();
1623 done = true;
1624 }
1625 } // while ((done == false) && (stopReaderThread == false))
1626
1627 // Pass an event up to TApplication to tell it this Backend is done.
1628 synchronized (eventQueue) {
1629 eventQueue.add(new TCommandEvent(cmBackendDisconnect));
1630 }
1631 if (listener != null) {
1632 synchronized (listener) {
1633 listener.notifyAll();
1634 }
1635 }
1636
1637 // System.err.println("*** run() exiting..."); System.err.flush();
1638 }
1639
1640 // ------------------------------------------------------------------------
1641 // ECMA48Terminal ---------------------------------------------------------
1642 // ------------------------------------------------------------------------
1643
1644 /**
1645 * Get the width of a character cell in pixels.
1646 *
1647 * @return the width in pixels of a character cell
1648 */
1649 public int getTextWidth() {
1650 return (widthPixels / sessionInfo.getWindowWidth());
1651 }
1652
1653 /**
1654 * Get the height of a character cell in pixels.
1655 *
1656 * @return the height in pixels of a character cell
1657 */
1658 public int getTextHeight() {
1659 return (heightPixels / sessionInfo.getWindowHeight());
1660 }
1661
1662 /**
1663 * Getter for sessionInfo.
1664 *
1665 * @return the SessionInfo
1666 */
1667 public SessionInfo getSessionInfo() {
1668 return sessionInfo;
1669 }
1670
1671 /**
1672 * Get the output writer.
1673 *
1674 * @return the Writer
1675 */
1676 public PrintWriter getOutput() {
1677 return output;
1678 }
1679
1680 /**
1681 * Call 'stty' to set cooked mode.
1682 *
1683 * <p>Actually executes '/bin/sh -c stty sane cooked &lt; /dev/tty'
1684 */
1685 private void sttyCooked() {
1686 doStty(false);
1687 }
1688
1689 /**
1690 * Call 'stty' to set raw mode.
1691 *
1692 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1693 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1694 * -parenb cs8 min 1 &lt; /dev/tty'
1695 */
1696 private void sttyRaw() {
1697 doStty(true);
1698 }
1699
1700 /**
1701 * Call 'stty' to set raw or cooked mode.
1702 *
1703 * @param mode if true, set raw mode, otherwise set cooked mode
1704 */
1705 private void doStty(final boolean mode) {
1706 String [] cmdRaw = {
1707 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1708 };
1709 String [] cmdCooked = {
1710 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1711 };
1712 try {
1713 Process process;
1714 if (mode) {
1715 process = Runtime.getRuntime().exec(cmdRaw);
1716 } else {
1717 process = Runtime.getRuntime().exec(cmdCooked);
1718 }
1719 BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
1720 String line = in.readLine();
1721 if ((line != null) && (line.length() > 0)) {
1722 System.err.println("WEIRD?! Normal output from stty: " + line);
1723 }
1724 while (true) {
1725 BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
1726 line = err.readLine();
1727 if ((line != null) && (line.length() > 0)) {
1728 System.err.println("Error output from stty: " + line);
1729 }
1730 try {
1731 process.waitFor();
1732 break;
1733 } catch (InterruptedException e) {
1734 if (debugToStderr) {
1735 e.printStackTrace();
1736 }
1737 }
1738 }
1739 int rc = process.exitValue();
1740 if (rc != 0) {
1741 System.err.println("stty returned error code: " + rc);
1742 }
1743 } catch (IOException e) {
1744 e.printStackTrace();
1745 }
1746 }
1747
1748 /**
1749 * Flush output.
1750 */
1751 public void flush() {
1752 output.flush();
1753 }
1754
1755 /**
1756 * Perform a somewhat-optimal rendering of a line.
1757 *
1758 * @param y row coordinate. 0 is the top-most row.
1759 * @param sb StringBuilder to write escape sequences to
1760 * @param lastAttr cell attributes from the last call to flushLine
1761 */
1762 private void flushLine(final int y, final StringBuilder sb,
1763 CellAttributes lastAttr) {
1764
1765 int lastX = -1;
1766 int textEnd = 0;
1767 for (int x = 0; x < width; x++) {
1768 Cell lCell = logical[x][y];
1769 if (!lCell.isBlank()) {
1770 textEnd = x;
1771 }
1772 }
1773 // Push textEnd to first column beyond the text area
1774 textEnd++;
1775
1776 // DEBUG
1777 // reallyCleared = true;
1778
1779 boolean hasImage = false;
1780
1781 for (int x = 0; x < width; x++) {
1782 Cell lCell = logical[x][y];
1783 Cell pCell = physical[x][y];
1784
1785 if (!lCell.equals(pCell) || reallyCleared) {
1786
1787 if (debugToStderr) {
1788 System.err.printf("\n--\n");
1789 System.err.printf(" Y: %d X: %d\n", y, x);
1790 System.err.printf(" lCell: %s\n", lCell);
1791 System.err.printf(" pCell: %s\n", pCell);
1792 System.err.printf(" ==== \n");
1793 }
1794
1795 if (lastAttr == null) {
1796 lastAttr = new CellAttributes();
1797 sb.append(normal());
1798 }
1799
1800 // Place the cell
1801 if ((lastX != (x - 1)) || (lastX == -1)) {
1802 // Advancing at least one cell, or the first gotoXY
1803 sb.append(gotoXY(x, y));
1804 }
1805
1806 assert (lastAttr != null);
1807
1808 if ((x == textEnd) && (textEnd < width - 1)) {
1809 assert (lCell.isBlank());
1810
1811 for (int i = x; i < width; i++) {
1812 assert (logical[i][y].isBlank());
1813 // Physical is always updated
1814 physical[i][y].reset();
1815 }
1816
1817 // Clear remaining line
1818 sb.append(clearRemainingLine());
1819 lastAttr.reset();
1820 return;
1821 }
1822
1823 // Image cell: bypass the rest of the loop, it is not
1824 // rendered here.
1825 if ((wideCharImages && lCell.isImage())
1826 || (!wideCharImages
1827 && lCell.isImage()
1828 && (lCell.getWidth() == Cell.Width.SINGLE))
1829 ) {
1830 hasImage = true;
1831
1832 // Save the last rendered cell
1833 lastX = x;
1834
1835 // Physical is always updated
1836 physical[x][y].setTo(lCell);
1837 continue;
1838 }
1839
1840 assert ((wideCharImages && !lCell.isImage())
1841 || (!wideCharImages
1842 && (!lCell.isImage()
1843 || (lCell.isImage()
1844 && (lCell.getWidth() != Cell.Width.SINGLE)))));
1845
1846 if (!wideCharImages && (lCell.getWidth() == Cell.Width.RIGHT)) {
1847 continue;
1848 }
1849
1850 if (hasImage) {
1851 hasImage = false;
1852 sb.append(gotoXY(x, y));
1853 }
1854
1855 // Now emit only the modified attributes
1856 if ((lCell.getForeColor() != lastAttr.getForeColor())
1857 && (lCell.getBackColor() != lastAttr.getBackColor())
1858 && (!lCell.isRGB())
1859 && (lCell.isBold() == lastAttr.isBold())
1860 && (lCell.isReverse() == lastAttr.isReverse())
1861 && (lCell.isUnderline() == lastAttr.isUnderline())
1862 && (lCell.isBlink() == lastAttr.isBlink())
1863 ) {
1864 // Both colors changed, attributes the same
1865 sb.append(color(lCell.isBold(),
1866 lCell.getForeColor(), lCell.getBackColor()));
1867
1868 if (debugToStderr) {
1869 System.err.printf("1 Change only fore/back colors\n");
1870 }
1871
1872 } else if (lCell.isRGB()
1873 && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB())
1874 && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB())
1875 && (lCell.isBold() == lastAttr.isBold())
1876 && (lCell.isReverse() == lastAttr.isReverse())
1877 && (lCell.isUnderline() == lastAttr.isUnderline())
1878 && (lCell.isBlink() == lastAttr.isBlink())
1879 ) {
1880 // Both colors changed, attributes the same
1881 sb.append(colorRGB(lCell.getForeColorRGB(),
1882 lCell.getBackColorRGB()));
1883
1884 if (debugToStderr) {
1885 System.err.printf("1 Change only fore/back colors (RGB)\n");
1886 }
1887 } else if ((lCell.getForeColor() != lastAttr.getForeColor())
1888 && (lCell.getBackColor() != lastAttr.getBackColor())
1889 && (!lCell.isRGB())
1890 && (lCell.isBold() != lastAttr.isBold())
1891 && (lCell.isReverse() != lastAttr.isReverse())
1892 && (lCell.isUnderline() != lastAttr.isUnderline())
1893 && (lCell.isBlink() != lastAttr.isBlink())
1894 ) {
1895 // Everything is different
1896 sb.append(color(lCell.getForeColor(),
1897 lCell.getBackColor(),
1898 lCell.isBold(), lCell.isReverse(),
1899 lCell.isBlink(),
1900 lCell.isUnderline()));
1901
1902 if (debugToStderr) {
1903 System.err.printf("2 Set all attributes\n");
1904 }
1905 } else if ((lCell.getForeColor() != lastAttr.getForeColor())
1906 && (lCell.getBackColor() == lastAttr.getBackColor())
1907 && (!lCell.isRGB())
1908 && (lCell.isBold() == lastAttr.isBold())
1909 && (lCell.isReverse() == lastAttr.isReverse())
1910 && (lCell.isUnderline() == lastAttr.isUnderline())
1911 && (lCell.isBlink() == lastAttr.isBlink())
1912 ) {
1913
1914 // Attributes same, foreColor different
1915 sb.append(color(lCell.isBold(),
1916 lCell.getForeColor(), true));
1917
1918 if (debugToStderr) {
1919 System.err.printf("3 Change foreColor\n");
1920 }
1921 } else if (lCell.isRGB()
1922 && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB())
1923 && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB())
1924 && (lCell.getForeColorRGB() >= 0)
1925 && (lCell.getBackColorRGB() >= 0)
1926 && (lCell.isBold() == lastAttr.isBold())
1927 && (lCell.isReverse() == lastAttr.isReverse())
1928 && (lCell.isUnderline() == lastAttr.isUnderline())
1929 && (lCell.isBlink() == lastAttr.isBlink())
1930 ) {
1931 // Attributes same, foreColor different
1932 sb.append(colorRGB(lCell.getForeColorRGB(), true));
1933
1934 if (debugToStderr) {
1935 System.err.printf("3 Change foreColor (RGB)\n");
1936 }
1937 } else if ((lCell.getForeColor() == lastAttr.getForeColor())
1938 && (lCell.getBackColor() != lastAttr.getBackColor())
1939 && (!lCell.isRGB())
1940 && (lCell.isBold() == lastAttr.isBold())
1941 && (lCell.isReverse() == lastAttr.isReverse())
1942 && (lCell.isUnderline() == lastAttr.isUnderline())
1943 && (lCell.isBlink() == lastAttr.isBlink())
1944 ) {
1945 // Attributes same, backColor different
1946 sb.append(color(lCell.isBold(),
1947 lCell.getBackColor(), false));
1948
1949 if (debugToStderr) {
1950 System.err.printf("4 Change backColor\n");
1951 }
1952 } else if (lCell.isRGB()
1953 && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB())
1954 && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB())
1955 && (lCell.isBold() == lastAttr.isBold())
1956 && (lCell.isReverse() == lastAttr.isReverse())
1957 && (lCell.isUnderline() == lastAttr.isUnderline())
1958 && (lCell.isBlink() == lastAttr.isBlink())
1959 ) {
1960 // Attributes same, foreColor different
1961 sb.append(colorRGB(lCell.getBackColorRGB(), false));
1962
1963 if (debugToStderr) {
1964 System.err.printf("4 Change backColor (RGB)\n");
1965 }
1966 } else if ((lCell.getForeColor() == lastAttr.getForeColor())
1967 && (lCell.getBackColor() == lastAttr.getBackColor())
1968 && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB())
1969 && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB())
1970 && (lCell.isBold() == lastAttr.isBold())
1971 && (lCell.isReverse() == lastAttr.isReverse())
1972 && (lCell.isUnderline() == lastAttr.isUnderline())
1973 && (lCell.isBlink() == lastAttr.isBlink())
1974 ) {
1975
1976 // All attributes the same, just print the char
1977 // NOP
1978
1979 if (debugToStderr) {
1980 System.err.printf("5 Only emit character\n");
1981 }
1982 } else {
1983 // Just reset everything again
1984 if (!lCell.isRGB()) {
1985 sb.append(color(lCell.getForeColor(),
1986 lCell.getBackColor(),
1987 lCell.isBold(),
1988 lCell.isReverse(),
1989 lCell.isBlink(),
1990 lCell.isUnderline()));
1991
1992 if (debugToStderr) {
1993 System.err.printf("6 Change all attributes\n");
1994 }
1995 } else {
1996 sb.append(colorRGB(lCell.getForeColorRGB(),
1997 lCell.getBackColorRGB(),
1998 lCell.isBold(),
1999 lCell.isReverse(),
2000 lCell.isBlink(),
2001 lCell.isUnderline()));
2002 if (debugToStderr) {
2003 System.err.printf("6 Change all attributes (RGB)\n");
2004 }
2005 }
2006
2007 }
2008 // Emit the character
2009 if (wideCharImages
2010 // Don't emit the right-half of full-width chars.
2011 || (!wideCharImages
2012 && (lCell.getWidth() != Cell.Width.RIGHT))
2013 ) {
2014 sb.append(Character.toChars(lCell.getChar()));
2015 }
2016
2017 // Save the last rendered cell
2018 lastX = x;
2019 lastAttr.setTo(lCell);
2020
2021 // Physical is always updated
2022 physical[x][y].setTo(lCell);
2023
2024 } // if (!lCell.equals(pCell) || (reallyCleared == true))
2025
2026 } // for (int x = 0; x < width; x++)
2027 }
2028
2029 /**
2030 * Render the screen to a string that can be emitted to something that
2031 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
2032 *
2033 * @param sb StringBuilder to write escape sequences to
2034 * @return escape sequences string that provides the updates to the
2035 * physical screen
2036 */
2037 private String flushString(final StringBuilder sb) {
2038 CellAttributes attr = null;
2039
2040 if (reallyCleared) {
2041 attr = new CellAttributes();
2042 sb.append(clearAll());
2043 }
2044
2045 /*
2046 * For images support, draw all of the image output first, and then
2047 * draw everything else afterwards. This works OK, but performance
2048 * is still a drag on larger pictures.
2049 */
2050 for (int y = 0; y < height; y++) {
2051 for (int x = 0; x < width; x++) {
2052 // If physical had non-image data that is now image data, the
2053 // entire row must be redrawn.
2054 Cell lCell = logical[x][y];
2055 Cell pCell = physical[x][y];
2056 if (lCell.isImage() && !pCell.isImage()) {
2057 unsetImageRow(y);
2058 break;
2059 }
2060 }
2061 }
2062 for (int y = 0; y < height; y++) {
2063 for (int x = 0; x < width; x++) {
2064 Cell lCell = logical[x][y];
2065 Cell pCell = physical[x][y];
2066
2067 if (!lCell.isImage()
2068 || (!wideCharImages
2069 && (lCell.getWidth() != Cell.Width.SINGLE))
2070 ) {
2071 continue;
2072 }
2073
2074 int left = x;
2075 int right = x;
2076 while ((right < width)
2077 && (logical[right][y].isImage())
2078 && (!logical[right][y].equals(physical[right][y])
2079 || reallyCleared)
2080 ) {
2081 right++;
2082 }
2083 ArrayList<Cell> cellsToDraw = new ArrayList<Cell>();
2084 for (int i = 0; i < (right - x); i++) {
2085 assert (logical[x + i][y].isImage());
2086 cellsToDraw.add(logical[x + i][y]);
2087
2088 // Physical is always updated.
2089 physical[x + i][y].setTo(lCell);
2090 }
2091 if (cellsToDraw.size() > 0) {
2092 if (iterm2Images) {
2093 sb.append(toIterm2Image(x, y, cellsToDraw));
2094 } else if (jexerImageOption != JexerImageOption.DISABLED) {
2095 sb.append(toJexerImage(x, y, cellsToDraw));
2096 } else {
2097 sb.append(toSixel(x, y, cellsToDraw));
2098 }
2099 }
2100
2101 x = right;
2102 }
2103 }
2104
2105 // Draw the text part now.
2106 for (int y = 0; y < height; y++) {
2107 flushLine(y, sb, attr);
2108 }
2109
2110 reallyCleared = false;
2111
2112 String result = sb.toString();
2113 if (debugToStderr) {
2114 System.err.printf("flushString(): %s\n", result);
2115 }
2116 return result;
2117 }
2118
2119 /**
2120 * Reset keyboard/mouse input parser.
2121 */
2122 private void resetParser() {
2123 state = ParseState.GROUND;
2124 params = new ArrayList<String>();
2125 params.clear();
2126 params.add("");
2127 decPrivateModeFlag = false;
2128 }
2129
2130 /**
2131 * Produce a control character or one of the special ones (ENTER, TAB,
2132 * etc.).
2133 *
2134 * @param ch Unicode code point
2135 * @param alt if true, set alt on the TKeypress
2136 * @return one TKeypress event, either a control character (e.g. isKey ==
2137 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
2138 * fnKey == ESC)
2139 */
2140 private TKeypressEvent controlChar(final char ch, final boolean alt) {
2141 // System.err.printf("controlChar: %02x\n", ch);
2142
2143 switch (ch) {
2144 case 0x0D:
2145 // Carriage return --> ENTER
2146 return new TKeypressEvent(kbEnter, alt, false, false);
2147 case 0x0A:
2148 // Linefeed --> ENTER
2149 return new TKeypressEvent(kbEnter, alt, false, false);
2150 case 0x1B:
2151 // ESC
2152 return new TKeypressEvent(kbEsc, alt, false, false);
2153 case '\t':
2154 // TAB
2155 return new TKeypressEvent(kbTab, alt, false, false);
2156 default:
2157 // Make all other control characters come back as the alphabetic
2158 // character with the ctrl field set. So SOH would be 'A' +
2159 // ctrl.
2160 return new TKeypressEvent(false, 0, (char)(ch + 0x40),
2161 alt, true, false);
2162 }
2163 }
2164
2165 /**
2166 * Produce special key from CSI Pn ; Pm ; ... ~
2167 *
2168 * @return one KEYPRESS event representing a special key
2169 */
2170 private TInputEvent csiFnKey() {
2171 int key = 0;
2172 if (params.size() > 0) {
2173 key = Integer.parseInt(params.get(0));
2174 }
2175 boolean alt = false;
2176 boolean ctrl = false;
2177 boolean shift = false;
2178 if (params.size() > 1) {
2179 shift = csiIsShift(params.get(1));
2180 alt = csiIsAlt(params.get(1));
2181 ctrl = csiIsCtrl(params.get(1));
2182 }
2183
2184 switch (key) {
2185 case 1:
2186 return new TKeypressEvent(kbHome, alt, ctrl, shift);
2187 case 2:
2188 return new TKeypressEvent(kbIns, alt, ctrl, shift);
2189 case 3:
2190 return new TKeypressEvent(kbDel, alt, ctrl, shift);
2191 case 4:
2192 return new TKeypressEvent(kbEnd, alt, ctrl, shift);
2193 case 5:
2194 return new TKeypressEvent(kbPgUp, alt, ctrl, shift);
2195 case 6:
2196 return new TKeypressEvent(kbPgDn, alt, ctrl, shift);
2197 case 15:
2198 return new TKeypressEvent(kbF5, alt, ctrl, shift);
2199 case 17:
2200 return new TKeypressEvent(kbF6, alt, ctrl, shift);
2201 case 18:
2202 return new TKeypressEvent(kbF7, alt, ctrl, shift);
2203 case 19:
2204 return new TKeypressEvent(kbF8, alt, ctrl, shift);
2205 case 20:
2206 return new TKeypressEvent(kbF9, alt, ctrl, shift);
2207 case 21:
2208 return new TKeypressEvent(kbF10, alt, ctrl, shift);
2209 case 23:
2210 return new TKeypressEvent(kbF11, alt, ctrl, shift);
2211 case 24:
2212 return new TKeypressEvent(kbF12, alt, ctrl, shift);
2213 default:
2214 // Unknown
2215 return null;
2216 }
2217 }
2218
2219 /**
2220 * Produce mouse events based on "Any event tracking" and UTF-8
2221 * coordinates. See
2222 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2223 *
2224 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2225 */
2226 private TInputEvent parseMouse() {
2227 int buttons = params.get(0).charAt(0) - 32;
2228 int x = params.get(0).charAt(1) - 32 - 1;
2229 int y = params.get(0).charAt(2) - 32 - 1;
2230
2231 // Clamp X and Y to the physical screen coordinates.
2232 if (x >= windowResize.getWidth()) {
2233 x = windowResize.getWidth() - 1;
2234 }
2235 if (y >= windowResize.getHeight()) {
2236 y = windowResize.getHeight() - 1;
2237 }
2238
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;
2245
2246 // System.err.printf("buttons: %04x\r\n", buttons);
2247
2248 switch (buttons) {
2249 case 0:
2250 eventMouse1 = true;
2251 mouse1 = true;
2252 break;
2253 case 1:
2254 eventMouse2 = true;
2255 mouse2 = true;
2256 break;
2257 case 2:
2258 eventMouse3 = true;
2259 mouse3 = true;
2260 break;
2261 case 3:
2262 // Release or Move
2263 if (!mouse1 && !mouse2 && !mouse3) {
2264 eventType = TMouseEvent.Type.MOUSE_MOTION;
2265 } else {
2266 eventType = TMouseEvent.Type.MOUSE_UP;
2267 }
2268 if (mouse1) {
2269 mouse1 = false;
2270 eventMouse1 = true;
2271 }
2272 if (mouse2) {
2273 mouse2 = false;
2274 eventMouse2 = true;
2275 }
2276 if (mouse3) {
2277 mouse3 = false;
2278 eventMouse3 = true;
2279 }
2280 break;
2281
2282 case 32:
2283 // Dragging with mouse1 down
2284 eventMouse1 = true;
2285 mouse1 = true;
2286 eventType = TMouseEvent.Type.MOUSE_MOTION;
2287 break;
2288
2289 case 33:
2290 // Dragging with mouse2 down
2291 eventMouse2 = true;
2292 mouse2 = true;
2293 eventType = TMouseEvent.Type.MOUSE_MOTION;
2294 break;
2295
2296 case 34:
2297 // Dragging with mouse3 down
2298 eventMouse3 = true;
2299 mouse3 = true;
2300 eventType = TMouseEvent.Type.MOUSE_MOTION;
2301 break;
2302
2303 case 96:
2304 // Dragging with mouse2 down after wheelUp
2305 eventMouse2 = true;
2306 mouse2 = true;
2307 eventType = TMouseEvent.Type.MOUSE_MOTION;
2308 break;
2309
2310 case 97:
2311 // Dragging with mouse2 down after wheelDown
2312 eventMouse2 = true;
2313 mouse2 = true;
2314 eventType = TMouseEvent.Type.MOUSE_MOTION;
2315 break;
2316
2317 case 64:
2318 eventMouseWheelUp = true;
2319 break;
2320
2321 case 65:
2322 eventMouseWheelDown = true;
2323 break;
2324
2325 default:
2326 // Unknown, just make it motion
2327 eventType = TMouseEvent.Type.MOUSE_MOTION;
2328 break;
2329 }
2330 return new TMouseEvent(eventType, x, y, x, y,
2331 eventMouse1, eventMouse2, eventMouse3,
2332 eventMouseWheelUp, eventMouseWheelDown);
2333 }
2334
2335 /**
2336 * Produce mouse events based on "Any event tracking" and SGR
2337 * coordinates. See
2338 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2339 *
2340 * @param release if true, this was a release ('m')
2341 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2342 */
2343 private TInputEvent parseMouseSGR(final boolean release) {
2344 // SGR extended coordinates - mode 1006
2345 if (params.size() < 3) {
2346 // Invalid position, bail out.
2347 return null;
2348 }
2349 int buttons = Integer.parseInt(params.get(0));
2350 int x = Integer.parseInt(params.get(1)) - 1;
2351 int y = Integer.parseInt(params.get(2)) - 1;
2352
2353 // Clamp X and Y to the physical screen coordinates.
2354 if (x >= windowResize.getWidth()) {
2355 x = windowResize.getWidth() - 1;
2356 }
2357 if (y >= windowResize.getHeight()) {
2358 y = windowResize.getHeight() - 1;
2359 }
2360
2361 TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
2362 boolean eventMouse1 = false;
2363 boolean eventMouse2 = false;
2364 boolean eventMouse3 = false;
2365 boolean eventMouseWheelUp = false;
2366 boolean eventMouseWheelDown = false;
2367
2368 if (release) {
2369 eventType = TMouseEvent.Type.MOUSE_UP;
2370 }
2371
2372 switch (buttons) {
2373 case 0:
2374 eventMouse1 = true;
2375 break;
2376 case 1:
2377 eventMouse2 = true;
2378 break;
2379 case 2:
2380 eventMouse3 = true;
2381 break;
2382 case 35:
2383 // Motion only, no buttons down
2384 eventType = TMouseEvent.Type.MOUSE_MOTION;
2385 break;
2386
2387 case 32:
2388 // Dragging with mouse1 down
2389 eventMouse1 = true;
2390 eventType = TMouseEvent.Type.MOUSE_MOTION;
2391 break;
2392
2393 case 33:
2394 // Dragging with mouse2 down
2395 eventMouse2 = true;
2396 eventType = TMouseEvent.Type.MOUSE_MOTION;
2397 break;
2398
2399 case 34:
2400 // Dragging with mouse3 down
2401 eventMouse3 = true;
2402 eventType = TMouseEvent.Type.MOUSE_MOTION;
2403 break;
2404
2405 case 96:
2406 // Dragging with mouse2 down after wheelUp
2407 eventMouse2 = true;
2408 eventType = TMouseEvent.Type.MOUSE_MOTION;
2409 break;
2410
2411 case 97:
2412 // Dragging with mouse2 down after wheelDown
2413 eventMouse2 = true;
2414 eventType = TMouseEvent.Type.MOUSE_MOTION;
2415 break;
2416
2417 case 64:
2418 eventMouseWheelUp = true;
2419 break;
2420
2421 case 65:
2422 eventMouseWheelDown = true;
2423 break;
2424
2425 default:
2426 // Unknown, bail out
2427 return null;
2428 }
2429 return new TMouseEvent(eventType, x, y, x, y,
2430 eventMouse1, eventMouse2, eventMouse3,
2431 eventMouseWheelUp, eventMouseWheelDown);
2432 }
2433
2434 /**
2435 * Return any events in the IO queue due to timeout.
2436 *
2437 * @param queue list to append new events to
2438 */
2439 private void getIdleEvents(final List<TInputEvent> queue) {
2440 long nowTime = System.currentTimeMillis();
2441
2442 // Check for new window size
2443 long windowSizeDelay = nowTime - windowSizeTime;
2444 if (windowSizeDelay > 1000) {
2445 int oldTextWidth = getTextWidth();
2446 int oldTextHeight = getTextHeight();
2447
2448 sessionInfo.queryWindowSize();
2449 int newWidth = sessionInfo.getWindowWidth();
2450 int newHeight = sessionInfo.getWindowHeight();
2451
2452 if ((newWidth != windowResize.getWidth())
2453 || (newHeight != windowResize.getHeight())
2454 ) {
2455
2456 // Request xterm report window dimensions in pixels again.
2457 // Between now and then, ensure that the reported text cell
2458 // size is the same by setting widthPixels and heightPixels
2459 // to match the new dimensions.
2460 widthPixels = oldTextWidth * newWidth;
2461 heightPixels = oldTextHeight * newHeight;
2462
2463 if (debugToStderr) {
2464 System.err.println("Screen size changed, old size " +
2465 windowResize);
2466 System.err.println(" new size " +
2467 newWidth + " x " + newHeight);
2468 System.err.println(" old pixels " +
2469 oldTextWidth + " x " + oldTextHeight);
2470 System.err.println(" new pixels " +
2471 getTextWidth() + " x " + getTextHeight());
2472 }
2473
2474 this.output.printf("%s", xtermReportPixelDimensions());
2475 this.output.flush();
2476
2477 TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
2478 newWidth, newHeight);
2479 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
2480 newWidth, newHeight);
2481 queue.add(event);
2482 }
2483 windowSizeTime = nowTime;
2484 }
2485
2486 // ESCDELAY type timeout
2487 if (state == ParseState.ESCAPE) {
2488 long escDelay = nowTime - escapeTime;
2489 if (escDelay > 100) {
2490 // After 0.1 seconds, assume a true escape character
2491 queue.add(controlChar((char)0x1B, false));
2492 resetParser();
2493 }
2494 }
2495 }
2496
2497 /**
2498 * Returns true if the CSI parameter for a keyboard command means that
2499 * shift was down.
2500 */
2501 private boolean csiIsShift(final String x) {
2502 if ((x.equals("2"))
2503 || (x.equals("4"))
2504 || (x.equals("6"))
2505 || (x.equals("8"))
2506 ) {
2507 return true;
2508 }
2509 return false;
2510 }
2511
2512 /**
2513 * Returns true if the CSI parameter for a keyboard command means that
2514 * alt was down.
2515 */
2516 private boolean csiIsAlt(final String x) {
2517 if ((x.equals("3"))
2518 || (x.equals("4"))
2519 || (x.equals("7"))
2520 || (x.equals("8"))
2521 ) {
2522 return true;
2523 }
2524 return false;
2525 }
2526
2527 /**
2528 * Returns true if the CSI parameter for a keyboard command means that
2529 * ctrl was down.
2530 */
2531 private boolean csiIsCtrl(final String x) {
2532 if ((x.equals("5"))
2533 || (x.equals("6"))
2534 || (x.equals("7"))
2535 || (x.equals("8"))
2536 ) {
2537 return true;
2538 }
2539 return false;
2540 }
2541
2542 /**
2543 * Parses the next character of input to see if an InputEvent is
2544 * fully here.
2545 *
2546 * @param events list to append new events to
2547 * @param ch Unicode code point
2548 */
2549 private void processChar(final List<TInputEvent> events, final char ch) {
2550
2551 // ESCDELAY type timeout
2552 long nowTime = System.currentTimeMillis();
2553 if (state == ParseState.ESCAPE) {
2554 long escDelay = nowTime - escapeTime;
2555 if (escDelay > 250) {
2556 // After 0.25 seconds, assume a true escape character
2557 events.add(controlChar((char)0x1B, false));
2558 resetParser();
2559 }
2560 }
2561
2562 // TKeypress fields
2563 boolean ctrl = false;
2564 boolean alt = false;
2565 boolean shift = false;
2566
2567 // System.err.printf("state: %s ch %c\r\n", state, ch);
2568
2569 switch (state) {
2570 case GROUND:
2571
2572 if (ch == 0x1B) {
2573 state = ParseState.ESCAPE;
2574 escapeTime = nowTime;
2575 return;
2576 }
2577
2578 if (ch <= 0x1F) {
2579 // Control character
2580 events.add(controlChar(ch, false));
2581 resetParser();
2582 return;
2583 }
2584
2585 if (ch >= 0x20) {
2586 // Normal character
2587 events.add(new TKeypressEvent(false, 0, ch,
2588 false, false, false));
2589 resetParser();
2590 return;
2591 }
2592
2593 break;
2594
2595 case ESCAPE:
2596 if (ch <= 0x1F) {
2597 // ALT-Control character
2598 events.add(controlChar(ch, true));
2599 resetParser();
2600 return;
2601 }
2602
2603 if (ch == 'O') {
2604 // This will be one of the function keys
2605 state = ParseState.ESCAPE_INTERMEDIATE;
2606 return;
2607 }
2608
2609 // '[' goes to CSI_ENTRY
2610 if (ch == '[') {
2611 state = ParseState.CSI_ENTRY;
2612 return;
2613 }
2614
2615 // Everything else is assumed to be Alt-keystroke
2616 if ((ch >= 'A') && (ch <= 'Z')) {
2617 shift = true;
2618 }
2619 alt = true;
2620 events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift));
2621 resetParser();
2622 return;
2623
2624 case ESCAPE_INTERMEDIATE:
2625 if ((ch >= 'P') && (ch <= 'S')) {
2626 // Function key
2627 switch (ch) {
2628 case 'P':
2629 events.add(new TKeypressEvent(kbF1));
2630 break;
2631 case 'Q':
2632 events.add(new TKeypressEvent(kbF2));
2633 break;
2634 case 'R':
2635 events.add(new TKeypressEvent(kbF3));
2636 break;
2637 case 'S':
2638 events.add(new TKeypressEvent(kbF4));
2639 break;
2640 default:
2641 break;
2642 }
2643 resetParser();
2644 return;
2645 }
2646
2647 // Unknown keystroke, ignore
2648 resetParser();
2649 return;
2650
2651 case CSI_ENTRY:
2652 // Numbers - parameter values
2653 if ((ch >= '0') && (ch <= '9')) {
2654 params.set(params.size() - 1,
2655 params.get(params.size() - 1) + ch);
2656 state = ParseState.CSI_PARAM;
2657 return;
2658 }
2659 // Parameter separator
2660 if (ch == ';') {
2661 params.add("");
2662 return;
2663 }
2664
2665 if ((ch >= 0x30) && (ch <= 0x7E)) {
2666 switch (ch) {
2667 case 'A':
2668 // Up
2669 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
2670 resetParser();
2671 return;
2672 case 'B':
2673 // Down
2674 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
2675 resetParser();
2676 return;
2677 case 'C':
2678 // Right
2679 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
2680 resetParser();
2681 return;
2682 case 'D':
2683 // Left
2684 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
2685 resetParser();
2686 return;
2687 case 'H':
2688 // Home
2689 events.add(new TKeypressEvent(kbHome));
2690 resetParser();
2691 return;
2692 case 'F':
2693 // End
2694 events.add(new TKeypressEvent(kbEnd));
2695 resetParser();
2696 return;
2697 case 'Z':
2698 // CBT - Cursor backward X tab stops (default 1)
2699 events.add(new TKeypressEvent(kbBackTab));
2700 resetParser();
2701 return;
2702 case 'M':
2703 // Mouse position
2704 state = ParseState.MOUSE;
2705 return;
2706 case '<':
2707 // Mouse position, SGR (1006) coordinates
2708 state = ParseState.MOUSE_SGR;
2709 return;
2710 case '?':
2711 // DEC private mode flag
2712 decPrivateModeFlag = true;
2713 return;
2714 default:
2715 break;
2716 }
2717 }
2718
2719 // Unknown keystroke, ignore
2720 resetParser();
2721 return;
2722
2723 case MOUSE_SGR:
2724 // Numbers - parameter values
2725 if ((ch >= '0') && (ch <= '9')) {
2726 params.set(params.size() - 1,
2727 params.get(params.size() - 1) + ch);
2728 return;
2729 }
2730 // Parameter separator
2731 if (ch == ';') {
2732 params.add("");
2733 return;
2734 }
2735
2736 switch (ch) {
2737 case 'M':
2738 // Generate a mouse press event
2739 TInputEvent event = parseMouseSGR(false);
2740 if (event != null) {
2741 events.add(event);
2742 }
2743 resetParser();
2744 return;
2745 case 'm':
2746 // Generate a mouse release event
2747 event = parseMouseSGR(true);
2748 if (event != null) {
2749 events.add(event);
2750 }
2751 resetParser();
2752 return;
2753 default:
2754 break;
2755 }
2756
2757 // Unknown keystroke, ignore
2758 resetParser();
2759 return;
2760
2761 case CSI_PARAM:
2762 // Numbers - parameter values
2763 if ((ch >= '0') && (ch <= '9')) {
2764 params.set(params.size() - 1,
2765 params.get(params.size() - 1) + ch);
2766 state = ParseState.CSI_PARAM;
2767 return;
2768 }
2769 // Parameter separator
2770 if (ch == ';') {
2771 params.add("");
2772 return;
2773 }
2774
2775 if (ch == '~') {
2776 events.add(csiFnKey());
2777 resetParser();
2778 return;
2779 }
2780
2781 if ((ch >= 0x30) && (ch <= 0x7E)) {
2782 switch (ch) {
2783 case 'A':
2784 // Up
2785 if (params.size() > 1) {
2786 shift = csiIsShift(params.get(1));
2787 alt = csiIsAlt(params.get(1));
2788 ctrl = csiIsCtrl(params.get(1));
2789 }
2790 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
2791 resetParser();
2792 return;
2793 case 'B':
2794 // Down
2795 if (params.size() > 1) {
2796 shift = csiIsShift(params.get(1));
2797 alt = csiIsAlt(params.get(1));
2798 ctrl = csiIsCtrl(params.get(1));
2799 }
2800 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
2801 resetParser();
2802 return;
2803 case 'C':
2804 // Right
2805 if (params.size() > 1) {
2806 shift = csiIsShift(params.get(1));
2807 alt = csiIsAlt(params.get(1));
2808 ctrl = csiIsCtrl(params.get(1));
2809 }
2810 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
2811 resetParser();
2812 return;
2813 case 'D':
2814 // Left
2815 if (params.size() > 1) {
2816 shift = csiIsShift(params.get(1));
2817 alt = csiIsAlt(params.get(1));
2818 ctrl = csiIsCtrl(params.get(1));
2819 }
2820 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
2821 resetParser();
2822 return;
2823 case 'H':
2824 // Home
2825 if (params.size() > 1) {
2826 shift = csiIsShift(params.get(1));
2827 alt = csiIsAlt(params.get(1));
2828 ctrl = csiIsCtrl(params.get(1));
2829 }
2830 events.add(new TKeypressEvent(kbHome, alt, ctrl, shift));
2831 resetParser();
2832 return;
2833 case 'F':
2834 // End
2835 if (params.size() > 1) {
2836 shift = csiIsShift(params.get(1));
2837 alt = csiIsAlt(params.get(1));
2838 ctrl = csiIsCtrl(params.get(1));
2839 }
2840 events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift));
2841 resetParser();
2842 return;
2843 case 'c':
2844 // Device Attributes
2845 if (decPrivateModeFlag == false) {
2846 break;
2847 }
2848 boolean jexerImages = false;
2849 for (String x: params) {
2850 if (x.equals("4")) {
2851 // Terminal reports sixel support
2852 if (debugToStderr) {
2853 System.err.println("Device Attributes: sixel");
2854 }
2855 }
2856 if (x.equals("444")) {
2857 // Terminal reports Jexer images support
2858 if (debugToStderr) {
2859 System.err.println("Device Attributes: Jexer images");
2860 }
2861 jexerImages = true;
2862 }
2863 if (x.equals("1337")) {
2864 // Terminal reports iTerm2 images support
2865 if (debugToStderr) {
2866 System.err.println("Device Attributes: iTerm2 images");
2867 }
2868 iterm2Images = true;
2869 }
2870 }
2871 if (jexerImages == false) {
2872 // Terminal does not support Jexer images, disable
2873 // them.
2874 jexerImageOption = JexerImageOption.DISABLED;
2875 }
2876 return;
2877 case 't':
2878 // windowOps
2879 if ((params.size() > 2) && (params.get(0).equals("4"))) {
2880 if (debugToStderr) {
2881 System.err.printf("windowOp pixels: " +
2882 "height %s width %s\n",
2883 params.get(1), params.get(2));
2884 }
2885 try {
2886 widthPixels = Integer.parseInt(params.get(2));
2887 heightPixels = Integer.parseInt(params.get(1));
2888 } catch (NumberFormatException e) {
2889 if (debugToStderr) {
2890 e.printStackTrace();
2891 }
2892 }
2893 if (widthPixels <= 0) {
2894 widthPixels = 640;
2895 }
2896 if (heightPixels <= 0) {
2897 heightPixels = 400;
2898 }
2899 }
2900 if ((params.size() > 2) && (params.get(0).equals("6"))) {
2901 if (debugToStderr) {
2902 System.err.printf("windowOp text cell pixels: " +
2903 "height %s width %s\n",
2904 params.get(1), params.get(2));
2905 }
2906 try {
2907 widthPixels = width * Integer.parseInt(params.get(2));
2908 heightPixels = height * Integer.parseInt(params.get(1));
2909 } catch (NumberFormatException e) {
2910 if (debugToStderr) {
2911 e.printStackTrace();
2912 }
2913 }
2914 if (widthPixels <= 0) {
2915 widthPixels = 640;
2916 }
2917 if (heightPixels <= 0) {
2918 heightPixels = 400;
2919 }
2920 }
2921 resetParser();
2922 return;
2923 default:
2924 break;
2925 }
2926 }
2927
2928 // Unknown keystroke, ignore
2929 resetParser();
2930 return;
2931
2932 case MOUSE:
2933 params.set(0, params.get(params.size() - 1) + ch);
2934 if (params.get(0).length() == 3) {
2935 // We have enough to generate a mouse event
2936 events.add(parseMouse());
2937 resetParser();
2938 }
2939 return;
2940
2941 default:
2942 break;
2943 }
2944
2945 // This "should" be impossible to reach
2946 return;
2947 }
2948
2949 /**
2950 * Request (u)xterm to use the sixel settings we need:
2951 *
2952 * - enable sixel scrolling
2953 *
2954 * - disable private color registers (so that we can use one common
2955 * palette) if sixelSharedPalette is set
2956 *
2957 * @return the string to emit to xterm
2958 */
2959 private String xtermSetSixelSettings() {
2960 if (sixelSharedPalette == true) {
2961 return "\033[?80h\033[?1070l";
2962 } else {
2963 return "\033[?80h\033[?1070h";
2964 }
2965 }
2966
2967 /**
2968 * Restore (u)xterm its default sixel settings:
2969 *
2970 * - enable sixel scrolling
2971 *
2972 * - enable private color registers
2973 *
2974 * @return the string to emit to xterm
2975 */
2976 private String xtermResetSixelSettings() {
2977 return "\033[?80h\033[?1070h";
2978 }
2979
2980 /**
2981 * Request (u)xterm to report the current window and cell size dimensions
2982 * in pixels.
2983 *
2984 * @return the string to emit to xterm
2985 */
2986 private String xtermReportPixelDimensions() {
2987 // We will ask for both window and text cell dimensions, and
2988 // hopefully one of them will work.
2989 return "\033[14t\033[16t";
2990 }
2991
2992 /**
2993 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2994 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2995 * enabled.
2996 *
2997 * @param on if true, enable metaSendsEscape
2998 * @return the string to emit to xterm
2999 */
3000 private String xtermMetaSendsEscape(final boolean on) {
3001 if (on) {
3002 return "\033[?1036h\033[?1034l";
3003 }
3004 return "\033[?1036l";
3005 }
3006
3007 /**
3008 * Create an xterm OSC sequence to change the window title.
3009 *
3010 * @param title the new title
3011 * @return the string to emit to xterm
3012 */
3013 private String getSetTitleString(final String title) {
3014 return "\033]2;" + title + "\007";
3015 }
3016
3017 // ------------------------------------------------------------------------
3018 // Sixel output support ---------------------------------------------------
3019 // ------------------------------------------------------------------------
3020
3021 /**
3022 * Get the number of colors in the sixel palette.
3023 *
3024 * @return the palette size
3025 */
3026 public int getSixelPaletteSize() {
3027 return sixelPaletteSize;
3028 }
3029
3030 /**
3031 * Set the number of colors in the sixel palette.
3032 *
3033 * @param paletteSize the new palette size
3034 */
3035 public void setSixelPaletteSize(final int paletteSize) {
3036 if (paletteSize == sixelPaletteSize) {
3037 return;
3038 }
3039
3040 switch (paletteSize) {
3041 case 2:
3042 case 256:
3043 case 512:
3044 case 1024:
3045 case 2048:
3046 break;
3047 default:
3048 throw new IllegalArgumentException("Unsupported sixel palette " +
3049 " size: " + paletteSize);
3050 }
3051
3052 // Don't step on the screen refresh thread.
3053 synchronized (this) {
3054 sixelPaletteSize = paletteSize;
3055 palette = null;
3056 sixelCache = null;
3057 clearPhysical();
3058 }
3059 }
3060
3061 /**
3062 * Start a sixel string for display one row's worth of bitmap data.
3063 *
3064 * @param x column coordinate. 0 is the left-most column.
3065 * @param y row coordinate. 0 is the top-most row.
3066 * @return the string to emit to an ANSI / ECMA-style terminal
3067 */
3068 private String startSixel(final int x, final int y) {
3069 StringBuilder sb = new StringBuilder();
3070
3071 assert (sixel == true);
3072
3073 // Place the cursor
3074 sb.append(gotoXY(x, y));
3075
3076 // DCS
3077 sb.append("\033Pq");
3078
3079 if (palette == null) {
3080 palette = new SixelPalette();
3081 if (sixelSharedPalette == true) {
3082 palette.emitPalette(sb, null);
3083 }
3084 }
3085
3086 return sb.toString();
3087 }
3088
3089 /**
3090 * End a sixel string for display one row's worth of bitmap data.
3091 *
3092 * @return the string to emit to an ANSI / ECMA-style terminal
3093 */
3094 private String endSixel() {
3095 assert (sixel == true);
3096
3097 // ST
3098 return ("\033\\");
3099 }
3100
3101 /**
3102 * Create a sixel string representing a row of several cells containing
3103 * bitmap data.
3104 *
3105 * @param x column coordinate. 0 is the left-most column.
3106 * @param y row coordinate. 0 is the top-most row.
3107 * @param cells the cells containing the bitmap data
3108 * @return the string to emit to an ANSI / ECMA-style terminal
3109 */
3110 private String toSixel(final int x, final int y,
3111 final ArrayList<Cell> cells) {
3112
3113 StringBuilder sb = new StringBuilder();
3114
3115 assert (cells != null);
3116 assert (cells.size() > 0);
3117 assert (cells.get(0).getImage() != null);
3118
3119 if (sixel == false) {
3120 sb.append(normal());
3121 sb.append(gotoXY(x, y));
3122 for (int i = 0; i < cells.size(); i++) {
3123 sb.append(' ');
3124 }
3125 return sb.toString();
3126 }
3127
3128 if (y == height - 1) {
3129 // We are on the bottom row. If scrolling mode is enabled
3130 // (default), then VT320/xterm will scroll the entire screen if
3131 // we draw any pixels here. Do not draw the image, bail out
3132 // instead.
3133 sb.append(normal());
3134 sb.append(gotoXY(x, y));
3135 for (int j = 0; j < cells.size(); j++) {
3136 sb.append(' ');
3137 }
3138 return sb.toString();
3139 }
3140
3141 if (sixelCache == null) {
3142 sixelCache = new ImageCache(height * 10);
3143 }
3144
3145 // Save and get rows to/from the cache that do NOT have inverted
3146 // cells.
3147 boolean saveInCache = true;
3148 for (Cell cell: cells) {
3149 if (cell.isInvertedImage()) {
3150 saveInCache = false;
3151 }
3152 }
3153 if (saveInCache) {
3154 String cachedResult = sixelCache.get(cells);
3155 if (cachedResult != null) {
3156 // System.err.println("CACHE HIT");
3157 sb.append(startSixel(x, y));
3158 sb.append(cachedResult);
3159 sb.append(endSixel());
3160 return sb.toString();
3161 }
3162 // System.err.println("CACHE MISS");
3163 }
3164
3165 int imageWidth = cells.get(0).getImage().getWidth();
3166 int imageHeight = cells.get(0).getImage().getHeight();
3167
3168 // cells.get(x).getImage() has a dithered bitmap containing indexes
3169 // into the color palette. Piece these together into one larger
3170 // image for final rendering.
3171 int totalWidth = 0;
3172 int fullWidth = cells.size() * getTextWidth();
3173 int fullHeight = getTextHeight();
3174 for (int i = 0; i < cells.size(); i++) {
3175 totalWidth += cells.get(i).getImage().getWidth();
3176 }
3177
3178 BufferedImage image = new BufferedImage(fullWidth,
3179 fullHeight, BufferedImage.TYPE_INT_ARGB);
3180
3181 int [] rgbArray;
3182 for (int i = 0; i < cells.size() - 1; i++) {
3183 int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
3184 imageWidth);
3185 int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
3186 imageHeight);
3187
3188 if (false && cells.get(i).isInvertedImage()) {
3189 // I used to put an all-white cell over the cursor, don't do
3190 // that anymore.
3191 rgbArray = new int[imageWidth * imageHeight];
3192 for (int j = 0; j < rgbArray.length; j++) {
3193 rgbArray[j] = 0xFFFFFF;
3194 }
3195 } else {
3196 try {
3197 rgbArray = cells.get(i).getImage().getRGB(0, 0,
3198 tileWidth, tileHeight, null, 0, tileWidth);
3199 } catch (Exception e) {
3200 throw new RuntimeException("image " + imageWidth + "x" +
3201 imageHeight +
3202 "tile " + tileWidth + "x" +
3203 tileHeight +
3204 " cells.get(i).getImage() " +
3205 cells.get(i).getImage() +
3206 " i " + i +
3207 " fullWidth " + fullWidth +
3208 " fullHeight " + fullHeight, e);
3209 }
3210 }
3211
3212 /*
3213 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3214 i * imageWidth, 0, imageWidth, imageHeight,
3215 0, imageWidth);
3216 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3217 fullWidth, fullHeight, cells.size(), getTextWidth());
3218 */
3219
3220 image.setRGB(i * imageWidth, 0, tileWidth, tileHeight,
3221 rgbArray, 0, tileWidth);
3222 if (tileHeight < fullHeight) {
3223 int backgroundColor = cells.get(i).getBackground().getRGB();
3224 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3225 for (int imageY = imageHeight; imageY < fullHeight;
3226 imageY++) {
3227
3228 image.setRGB(imageX, imageY, backgroundColor);
3229 }
3230 }
3231 }
3232 }
3233 totalWidth -= ((cells.size() - 1) * imageWidth);
3234 if (false && cells.get(cells.size() - 1).isInvertedImage()) {
3235 // I used to put an all-white cell over the cursor, don't do that
3236 // anymore.
3237 rgbArray = new int[totalWidth * imageHeight];
3238 for (int j = 0; j < rgbArray.length; j++) {
3239 rgbArray[j] = 0xFFFFFF;
3240 }
3241 } else {
3242 try {
3243 rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
3244 totalWidth, imageHeight, null, 0, totalWidth);
3245 } catch (Exception e) {
3246 throw new RuntimeException("image " + imageWidth + "x" +
3247 imageHeight + " cells.get(cells.size() - 1).getImage() " +
3248 cells.get(cells.size() - 1).getImage(), e);
3249 }
3250 }
3251 image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
3252 imageHeight, rgbArray, 0, totalWidth);
3253
3254 if (totalWidth < getTextWidth()) {
3255 int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
3256
3257 for (int imageX = image.getWidth() - totalWidth;
3258 imageX < image.getWidth(); imageX++) {
3259
3260 for (int imageY = 0; imageY < fullHeight; imageY++) {
3261 image.setRGB(imageX, imageY, backgroundColor);
3262 }
3263 }
3264 }
3265
3266 // Dither the image. It is ok to lose the original here.
3267 if (palette == null) {
3268 palette = new SixelPalette();
3269 if (sixelSharedPalette == true) {
3270 palette.emitPalette(sb, null);
3271 }
3272 }
3273 image = palette.ditherImage(image);
3274
3275 // Collect the raster information
3276 int rasterHeight = 0;
3277 int rasterWidth = image.getWidth();
3278
3279 if (sixelSharedPalette == false) {
3280 // Emit the palette, but only for the colors actually used by
3281 // these cells.
3282 boolean [] usedColors = new boolean[sixelPaletteSize];
3283 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3284 for (int imageY = 0; imageY < image.getHeight(); imageY++) {
3285 usedColors[image.getRGB(imageX, imageY)] = true;
3286 }
3287 }
3288 palette.emitPalette(sb, usedColors);
3289 }
3290
3291 // Render the entire row of cells.
3292 for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
3293 int [][] sixels = new int[image.getWidth()][6];
3294
3295 // See which colors are actually used in this band of sixels.
3296 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3297 for (int imageY = 0;
3298 (imageY < 6) && (imageY + currentRow < fullHeight);
3299 imageY++) {
3300
3301 int colorIdx = image.getRGB(imageX, imageY + currentRow);
3302 assert (colorIdx >= 0);
3303 assert (colorIdx < sixelPaletteSize);
3304
3305 sixels[imageX][imageY] = colorIdx;
3306 }
3307 }
3308
3309 for (int i = 0; i < sixelPaletteSize; i++) {
3310 boolean isUsed = false;
3311 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3312 for (int j = 0; j < 6; j++) {
3313 if (sixels[imageX][j] == i) {
3314 isUsed = true;
3315 }
3316 }
3317 }
3318 if (isUsed == false) {
3319 continue;
3320 }
3321
3322 // Set to the beginning of scan line for the next set of
3323 // colored pixels, and select the color.
3324 sb.append(String.format("$#%d", i));
3325
3326 int oldData = -1;
3327 int oldDataCount = 0;
3328 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3329
3330 // Add up all the pixels that match this color.
3331 int data = 0;
3332 for (int j = 0;
3333 (j < 6) && (currentRow + j < fullHeight);
3334 j++) {
3335
3336 if (sixels[imageX][j] == i) {
3337 switch (j) {
3338 case 0:
3339 data += 1;
3340 break;
3341 case 1:
3342 data += 2;
3343 break;
3344 case 2:
3345 data += 4;
3346 break;
3347 case 3:
3348 data += 8;
3349 break;
3350 case 4:
3351 data += 16;
3352 break;
3353 case 5:
3354 data += 32;
3355 break;
3356 }
3357 if ((currentRow + j + 1) > rasterHeight) {
3358 rasterHeight = currentRow + j + 1;
3359 }
3360 }
3361 }
3362 assert (data >= 0);
3363 assert (data < 64);
3364 data += 63;
3365
3366 if (data == oldData) {
3367 oldDataCount++;
3368 } else {
3369 if (oldDataCount == 1) {
3370 sb.append((char) oldData);
3371 } else if (oldDataCount > 1) {
3372 sb.append(String.format("!%d", oldDataCount));
3373 sb.append((char) oldData);
3374 }
3375 oldDataCount = 1;
3376 oldData = data;
3377 }
3378
3379 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
3380
3381 // Emit the last sequence.
3382 if (oldDataCount == 1) {
3383 sb.append((char) oldData);
3384 } else if (oldDataCount > 1) {
3385 sb.append(String.format("!%d", oldDataCount));
3386 sb.append((char) oldData);
3387 }
3388
3389 } // for (int i = 0; i < sixelPaletteSize; i++)
3390
3391 // Advance to the next scan line.
3392 sb.append("-");
3393
3394 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
3395
3396 // Kill the very last "-", because it is unnecessary.
3397 sb.deleteCharAt(sb.length() - 1);
3398
3399 // Add the raster information
3400 sb.insert(0, String.format("\"1;1;%d;%d", rasterWidth, rasterHeight));
3401
3402 if (saveInCache) {
3403 // This row is OK to save into the cache.
3404 sixelCache.put(cells, sb.toString());
3405 }
3406
3407 return (startSixel(x, y) + sb.toString() + endSixel());
3408 }
3409
3410 /**
3411 * Get the sixel support flag.
3412 *
3413 * @return true if this terminal is emitting sixel
3414 */
3415 public boolean hasSixel() {
3416 return sixel;
3417 }
3418
3419 // ------------------------------------------------------------------------
3420 // End sixel output support -----------------------------------------------
3421 // ------------------------------------------------------------------------
3422
3423 // ------------------------------------------------------------------------
3424 // iTerm2 image output support --------------------------------------------
3425 // ------------------------------------------------------------------------
3426
3427 /**
3428 * Create an iTerm2 images string representing a row of several cells
3429 * containing bitmap data.
3430 *
3431 * @param x column coordinate. 0 is the left-most column.
3432 * @param y row coordinate. 0 is the top-most row.
3433 * @param cells the cells containing the bitmap data
3434 * @return the string to emit to an ANSI / ECMA-style terminal
3435 */
3436 private String toIterm2Image(final int x, final int y,
3437 final ArrayList<Cell> cells) {
3438
3439 StringBuilder sb = new StringBuilder();
3440
3441 assert (cells != null);
3442 assert (cells.size() > 0);
3443 assert (cells.get(0).getImage() != null);
3444
3445 if (iterm2Images == false) {
3446 sb.append(normal());
3447 sb.append(gotoXY(x, y));
3448 for (int i = 0; i < cells.size(); i++) {
3449 sb.append(' ');
3450 }
3451 return sb.toString();
3452 }
3453
3454 if (iterm2Cache == null) {
3455 iterm2Cache = new ImageCache(height * 10);
3456 base64 = java.util.Base64.getEncoder();
3457 }
3458
3459 // Save and get rows to/from the cache that do NOT have inverted
3460 // cells.
3461 boolean saveInCache = true;
3462 for (Cell cell: cells) {
3463 if (cell.isInvertedImage()) {
3464 saveInCache = false;
3465 }
3466 }
3467 if (saveInCache) {
3468 String cachedResult = iterm2Cache.get(cells);
3469 if (cachedResult != null) {
3470 // System.err.println("CACHE HIT");
3471 sb.append(gotoXY(x, y));
3472 sb.append(cachedResult);
3473 return sb.toString();
3474 }
3475 // System.err.println("CACHE MISS");
3476 }
3477
3478 int imageWidth = cells.get(0).getImage().getWidth();
3479 int imageHeight = cells.get(0).getImage().getHeight();
3480
3481 // Piece cells.get(x).getImage() pieces together into one larger
3482 // image for final rendering.
3483 int totalWidth = 0;
3484 int fullWidth = cells.size() * getTextWidth();
3485 int fullHeight = getTextHeight();
3486 for (int i = 0; i < cells.size(); i++) {
3487 totalWidth += cells.get(i).getImage().getWidth();
3488 }
3489
3490 BufferedImage image = new BufferedImage(fullWidth,
3491 fullHeight, BufferedImage.TYPE_INT_ARGB);
3492
3493 int [] rgbArray;
3494 for (int i = 0; i < cells.size() - 1; i++) {
3495 int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
3496 imageWidth);
3497 int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
3498 imageHeight);
3499 if (false && cells.get(i).isInvertedImage()) {
3500 // I used to put an all-white cell over the cursor, don't do
3501 // that anymore.
3502 rgbArray = new int[imageWidth * imageHeight];
3503 for (int j = 0; j < rgbArray.length; j++) {
3504 rgbArray[j] = 0xFFFFFF;
3505 }
3506 } else {
3507 try {
3508 rgbArray = cells.get(i).getImage().getRGB(0, 0,
3509 tileWidth, tileHeight, null, 0, tileWidth);
3510 } catch (Exception e) {
3511 throw new RuntimeException("image " + imageWidth + "x" +
3512 imageHeight +
3513 "tile " + tileWidth + "x" +
3514 tileHeight +
3515 " cells.get(i).getImage() " +
3516 cells.get(i).getImage() +
3517 " i " + i +
3518 " fullWidth " + fullWidth +
3519 " fullHeight " + fullHeight, e);
3520 }
3521 }
3522
3523 /*
3524 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3525 i * imageWidth, 0, imageWidth, imageHeight,
3526 0, imageWidth);
3527 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3528 fullWidth, fullHeight, cells.size(), getTextWidth());
3529 */
3530
3531 image.setRGB(i * imageWidth, 0, tileWidth, tileHeight,
3532 rgbArray, 0, tileWidth);
3533 if (tileHeight < fullHeight) {
3534 int backgroundColor = cells.get(i).getBackground().getRGB();
3535 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3536 for (int imageY = imageHeight; imageY < fullHeight;
3537 imageY++) {
3538
3539 image.setRGB(imageX, imageY, backgroundColor);
3540 }
3541 }
3542 }
3543 }
3544 totalWidth -= ((cells.size() - 1) * imageWidth);
3545 if (false && cells.get(cells.size() - 1).isInvertedImage()) {
3546 // I used to put an all-white cell over the cursor, don't do that
3547 // anymore.
3548 rgbArray = new int[totalWidth * imageHeight];
3549 for (int j = 0; j < rgbArray.length; j++) {
3550 rgbArray[j] = 0xFFFFFF;
3551 }
3552 } else {
3553 try {
3554 rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
3555 totalWidth, imageHeight, null, 0, totalWidth);
3556 } catch (Exception e) {
3557 throw new RuntimeException("image " + imageWidth + "x" +
3558 imageHeight + " cells.get(cells.size() - 1).getImage() " +
3559 cells.get(cells.size() - 1).getImage(), e);
3560 }
3561 }
3562 image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
3563 imageHeight, rgbArray, 0, totalWidth);
3564
3565 if (totalWidth < getTextWidth()) {
3566 int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
3567
3568 for (int imageX = image.getWidth() - totalWidth;
3569 imageX < image.getWidth(); imageX++) {
3570
3571 for (int imageY = 0; imageY < fullHeight; imageY++) {
3572 image.setRGB(imageX, imageY, backgroundColor);
3573 }
3574 }
3575 }
3576
3577 /*
3578 * From https://iterm2.com/documentation-images.html:
3579 *
3580 * Protocol
3581 *
3582 * iTerm2 extends the xterm protocol with a set of proprietary escape
3583 * sequences. In general, the pattern is:
3584 *
3585 * ESC ] 1337 ; key = value ^G
3586 *
3587 * Whitespace is shown here for ease of reading: in practice, no
3588 * spaces should be used.
3589 *
3590 * For file transfer and inline images, the code is:
3591 *
3592 * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
3593 *
3594 * The optional arguments are formatted as key=value with a semicolon
3595 * between each key-value pair. They are described below:
3596 *
3597 * Key Description of value
3598 * name base-64 encoded filename. Defaults to "Unnamed file".
3599 * size File size in bytes. Optional; this is only used by the
3600 * progress indicator.
3601 * width Width to render. See notes below.
3602 * height Height to render. See notes below.
3603 * preserveAspectRatio If set to 0, then the image's inherent aspect
3604 * ratio will not be respected; otherwise, it
3605 * will fill the specified width and height as
3606 * much as possible without stretching. Defaults
3607 * to 1.
3608 * inline If set to 1, the file will be displayed inline. Otherwise,
3609 * it will be downloaded with no visual representation in the
3610 * terminal session. Defaults to 0.
3611 *
3612 * The width and height are given as a number followed by a unit, or
3613 * the word "auto".
3614 *
3615 * N: N character cells.
3616 * Npx: N pixels.
3617 * N%: N percent of the session's width or height.
3618 * auto: The image's inherent size will be used to determine an
3619 * appropriate dimension.
3620 *
3621 */
3622
3623 // File contents can be several image formats. We will use PNG.
3624 ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(1024);
3625 try {
3626 if (!ImageIO.write(image.getSubimage(0, 0, image.getWidth(),
3627 Math.min(image.getHeight(), fullHeight)),
3628 "PNG", pngOutputStream)
3629 ) {
3630 // We failed to render image, bail out.
3631 return "";
3632 }
3633 } catch (IOException e) {
3634 // We failed to render image, bail out.
3635 return "";
3636 }
3637
3638 sb.append("\033]1337;File=");
3639 /*
3640 sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
3641 cells.size()));
3642 */
3643 /*
3644 sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
3645 image.getWidth(), Math.min(image.getHeight(),
3646 getTextHeight())));
3647 */
3648 sb.append("inline=1:");
3649 sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
3650 sb.append("\007");
3651
3652 if (saveInCache) {
3653 // This row is OK to save into the cache.
3654 iterm2Cache.put(cells, sb.toString());
3655 }
3656
3657 return (gotoXY(x, y) + sb.toString());
3658 }
3659
3660 /**
3661 * Get the iTerm2 images support flag.
3662 *
3663 * @return true if this terminal is emitting iTerm2 images
3664 */
3665 public boolean hasIterm2Images() {
3666 return iterm2Images;
3667 }
3668
3669 // ------------------------------------------------------------------------
3670 // End iTerm2 image output support ----------------------------------------
3671 // ------------------------------------------------------------------------
3672
3673 // ------------------------------------------------------------------------
3674 // Jexer image output support ---------------------------------------------
3675 // ------------------------------------------------------------------------
3676
3677 /**
3678 * Create a Jexer images string representing a row of several cells
3679 * containing bitmap data.
3680 *
3681 * @param x column coordinate. 0 is the left-most column.
3682 * @param y row coordinate. 0 is the top-most row.
3683 * @param cells the cells containing the bitmap data
3684 * @return the string to emit to an ANSI / ECMA-style terminal
3685 */
3686 private String toJexerImage(final int x, final int y,
3687 final ArrayList<Cell> cells) {
3688
3689 StringBuilder sb = new StringBuilder();
3690
3691 assert (cells != null);
3692 assert (cells.size() > 0);
3693 assert (cells.get(0).getImage() != null);
3694
3695 if (jexerImageOption == JexerImageOption.DISABLED) {
3696 sb.append(normal());
3697 sb.append(gotoXY(x, y));
3698 for (int i = 0; i < cells.size(); i++) {
3699 sb.append(' ');
3700 }
3701 return sb.toString();
3702 }
3703
3704 if (jexerCache == null) {
3705 jexerCache = new ImageCache(height * 10);
3706 base64 = java.util.Base64.getEncoder();
3707 }
3708
3709 // Save and get rows to/from the cache that do NOT have inverted
3710 // cells.
3711 boolean saveInCache = true;
3712 for (Cell cell: cells) {
3713 if (cell.isInvertedImage()) {
3714 saveInCache = false;
3715 }
3716 }
3717 if (saveInCache) {
3718 String cachedResult = jexerCache.get(cells);
3719 if (cachedResult != null) {
3720 // System.err.println("CACHE HIT");
3721 sb.append(gotoXY(x, y));
3722 sb.append(cachedResult);
3723 return sb.toString();
3724 }
3725 // System.err.println("CACHE MISS");
3726 }
3727
3728 int imageWidth = cells.get(0).getImage().getWidth();
3729 int imageHeight = cells.get(0).getImage().getHeight();
3730
3731 // Piece cells.get(x).getImage() pieces together into one larger
3732 // image for final rendering.
3733 int totalWidth = 0;
3734 int fullWidth = cells.size() * getTextWidth();
3735 int fullHeight = getTextHeight();
3736 for (int i = 0; i < cells.size(); i++) {
3737 totalWidth += cells.get(i).getImage().getWidth();
3738 }
3739
3740 BufferedImage image = new BufferedImage(fullWidth,
3741 fullHeight, BufferedImage.TYPE_INT_ARGB);
3742
3743 int [] rgbArray;
3744 for (int i = 0; i < cells.size() - 1; i++) {
3745 int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
3746 imageWidth);
3747 int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
3748 imageHeight);
3749 if (false && cells.get(i).isInvertedImage()) {
3750 // I used to put an all-white cell over the cursor, don't do
3751 // that anymore.
3752 rgbArray = new int[imageWidth * imageHeight];
3753 for (int j = 0; j < rgbArray.length; j++) {
3754 rgbArray[j] = 0xFFFFFF;
3755 }
3756 } else {
3757 try {
3758 rgbArray = cells.get(i).getImage().getRGB(0, 0,
3759 tileWidth, tileHeight, null, 0, tileWidth);
3760 } catch (Exception e) {
3761 throw new RuntimeException("image " + imageWidth + "x" +
3762 imageHeight +
3763 "tile " + tileWidth + "x" +
3764 tileHeight +
3765 " cells.get(i).getImage() " +
3766 cells.get(i).getImage() +
3767 " i " + i +
3768 " fullWidth " + fullWidth +
3769 " fullHeight " + fullHeight, e);
3770 }
3771 }
3772
3773 /*
3774 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
3775 i * imageWidth, 0, imageWidth, imageHeight,
3776 0, imageWidth);
3777 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
3778 fullWidth, fullHeight, cells.size(), getTextWidth());
3779 */
3780
3781 image.setRGB(i * imageWidth, 0, tileWidth, tileHeight,
3782 rgbArray, 0, tileWidth);
3783 if (tileHeight < fullHeight) {
3784 int backgroundColor = cells.get(i).getBackground().getRGB();
3785 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
3786 for (int imageY = imageHeight; imageY < fullHeight;
3787 imageY++) {
3788
3789 image.setRGB(imageX, imageY, backgroundColor);
3790 }
3791 }
3792 }
3793 }
3794 totalWidth -= ((cells.size() - 1) * imageWidth);
3795 if (false && cells.get(cells.size() - 1).isInvertedImage()) {
3796 // I used to put an all-white cell over the cursor, don't do that
3797 // anymore.
3798 rgbArray = new int[totalWidth * imageHeight];
3799 for (int j = 0; j < rgbArray.length; j++) {
3800 rgbArray[j] = 0xFFFFFF;
3801 }
3802 } else {
3803 try {
3804 rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
3805 totalWidth, imageHeight, null, 0, totalWidth);
3806 } catch (Exception e) {
3807 throw new RuntimeException("image " + imageWidth + "x" +
3808 imageHeight + " cells.get(cells.size() - 1).getImage() " +
3809 cells.get(cells.size() - 1).getImage(), e);
3810 }
3811 }
3812 image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
3813 imageHeight, rgbArray, 0, totalWidth);
3814
3815 if (totalWidth < getTextWidth()) {
3816 int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
3817
3818 for (int imageX = image.getWidth() - totalWidth;
3819 imageX < image.getWidth(); imageX++) {
3820
3821 for (int imageY = 0; imageY < fullHeight; imageY++) {
3822 image.setRGB(imageX, imageY, backgroundColor);
3823 }
3824 }
3825 }
3826
3827 if (jexerImageOption == JexerImageOption.PNG) {
3828 // Encode as PNG
3829 ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(1024);
3830 try {
3831 if (!ImageIO.write(image.getSubimage(0, 0, image.getWidth(),
3832 Math.min(image.getHeight(), fullHeight)),
3833 "PNG", pngOutputStream)
3834 ) {
3835 // We failed to render image, bail out.
3836 return "";
3837 }
3838 } catch (IOException e) {
3839 // We failed to render image, bail out.
3840 return "";
3841 }
3842
3843 sb.append("\033]444;1;0;");
3844 sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
3845 sb.append("\007");
3846
3847 } else if (jexerImageOption == JexerImageOption.JPG) {
3848
3849 // Encode as JPG
3850 ByteArrayOutputStream jpgOutputStream = new ByteArrayOutputStream(1024);
3851
3852 // Convert from ARGB to RGB, otherwise the JPG encode will fail.
3853 BufferedImage jpgImage = new BufferedImage(image.getWidth(),
3854 image.getHeight(), BufferedImage.TYPE_INT_RGB);
3855 int [] pixels = new int[image.getWidth() * image.getHeight()];
3856 image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels,
3857 0, image.getWidth());
3858 jpgImage.setRGB(0, 0, image.getWidth(), image.getHeight(), pixels,
3859 0, image.getWidth());
3860
3861 try {
3862 if (!ImageIO.write(jpgImage.getSubimage(0, 0,
3863 jpgImage.getWidth(),
3864 Math.min(jpgImage.getHeight(), fullHeight)),
3865 "JPG", jpgOutputStream)
3866 ) {
3867 // We failed to render image, bail out.
3868 return "";
3869 }
3870 } catch (IOException e) {
3871 // We failed to render image, bail out.
3872 return "";
3873 }
3874
3875 sb.append("\033]444;2;0;");
3876 sb.append(base64.encodeToString(jpgOutputStream.toByteArray()));
3877 sb.append("\007");
3878
3879 } else if (jexerImageOption == JexerImageOption.RGB) {
3880
3881 // RGB
3882 sb.append(String.format("\033]444;0;%d;%d;0;", image.getWidth(),
3883 Math.min(image.getHeight(), fullHeight)));
3884
3885 byte [] bytes = new byte[image.getWidth() * image.getHeight() * 3];
3886 int stride = image.getWidth();
3887 for (int px = 0; px < stride; px++) {
3888 for (int py = 0; py < image.getHeight(); py++) {
3889 int rgb = image.getRGB(px, py);
3890 bytes[(py * stride * 3) + (px * 3)] = (byte) ((rgb >>> 16) & 0xFF);
3891 bytes[(py * stride * 3) + (px * 3) + 1] = (byte) ((rgb >>> 8) & 0xFF);
3892 bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb & 0xFF);
3893 }
3894 }
3895 sb.append(base64.encodeToString(bytes));
3896 sb.append("\007");
3897 }
3898
3899 if (saveInCache) {
3900 // This row is OK to save into the cache.
3901 jexerCache.put(cells, sb.toString());
3902 }
3903
3904 return (gotoXY(x, y) + sb.toString());
3905 }
3906
3907 /**
3908 * Get the Jexer images support flag.
3909 *
3910 * @return true if this terminal is emitting Jexer images
3911 */
3912 public boolean hasJexerImages() {
3913 return (jexerImageOption != JexerImageOption.DISABLED);
3914 }
3915
3916 // ------------------------------------------------------------------------
3917 // End Jexer image output support -----------------------------------------
3918 // ------------------------------------------------------------------------
3919
3920 /**
3921 * Setup system colors to match DOS color palette.
3922 */
3923 private void setDOSColors() {
3924 MYBLACK = new java.awt.Color(0x00, 0x00, 0x00);
3925 MYRED = new java.awt.Color(0xa8, 0x00, 0x00);
3926 MYGREEN = new java.awt.Color(0x00, 0xa8, 0x00);
3927 MYYELLOW = new java.awt.Color(0xa8, 0x54, 0x00);
3928 MYBLUE = new java.awt.Color(0x00, 0x00, 0xa8);
3929 MYMAGENTA = new java.awt.Color(0xa8, 0x00, 0xa8);
3930 MYCYAN = new java.awt.Color(0x00, 0xa8, 0xa8);
3931 MYWHITE = new java.awt.Color(0xa8, 0xa8, 0xa8);
3932 MYBOLD_BLACK = new java.awt.Color(0x54, 0x54, 0x54);
3933 MYBOLD_RED = new java.awt.Color(0xfc, 0x54, 0x54);
3934 MYBOLD_GREEN = new java.awt.Color(0x54, 0xfc, 0x54);
3935 MYBOLD_YELLOW = new java.awt.Color(0xfc, 0xfc, 0x54);
3936 MYBOLD_BLUE = new java.awt.Color(0x54, 0x54, 0xfc);
3937 MYBOLD_MAGENTA = new java.awt.Color(0xfc, 0x54, 0xfc);
3938 MYBOLD_CYAN = new java.awt.Color(0x54, 0xfc, 0xfc);
3939 MYBOLD_WHITE = new java.awt.Color(0xfc, 0xfc, 0xfc);
3940 }
3941
3942 /**
3943 * Setup ECMA48 colors to match those provided in system properties.
3944 */
3945 private void setCustomSystemColors() {
3946 setDOSColors();
3947
3948 MYBLACK = getCustomColor("jexer.ECMA48.color0", MYBLACK);
3949 MYRED = getCustomColor("jexer.ECMA48.color1", MYRED);
3950 MYGREEN = getCustomColor("jexer.ECMA48.color2", MYGREEN);
3951 MYYELLOW = getCustomColor("jexer.ECMA48.color3", MYYELLOW);
3952 MYBLUE = getCustomColor("jexer.ECMA48.color4", MYBLUE);
3953 MYMAGENTA = getCustomColor("jexer.ECMA48.color5", MYMAGENTA);
3954 MYCYAN = getCustomColor("jexer.ECMA48.color6", MYCYAN);
3955 MYWHITE = getCustomColor("jexer.ECMA48.color7", MYWHITE);
3956 MYBOLD_BLACK = getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK);
3957 MYBOLD_RED = getCustomColor("jexer.ECMA48.color9", MYBOLD_RED);
3958 MYBOLD_GREEN = getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN);
3959 MYBOLD_YELLOW = getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW);
3960 MYBOLD_BLUE = getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE);
3961 MYBOLD_MAGENTA = getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA);
3962 MYBOLD_CYAN = getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN);
3963 MYBOLD_WHITE = getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE);
3964 }
3965
3966 /**
3967 * Setup one system color to match the RGB value provided in system
3968 * properties.
3969 *
3970 * @param key the system property key
3971 * @param defaultColor the default color to return if key is not set, or
3972 * incorrect
3973 * @return a color from the RGB string, or defaultColor
3974 */
3975 private java.awt.Color getCustomColor(final String key,
3976 final java.awt.Color defaultColor) {
3977
3978 String rgb = System.getProperty(key);
3979 if (rgb == null) {
3980 return defaultColor;
3981 }
3982 if (rgb.startsWith("#")) {
3983 rgb = rgb.substring(1);
3984 }
3985 int rgbInt = 0;
3986 try {
3987 rgbInt = Integer.parseInt(rgb, 16);
3988 } catch (NumberFormatException e) {
3989 return defaultColor;
3990 }
3991 java.awt.Color color = new java.awt.Color((rgbInt & 0xFF0000) >>> 16,
3992 (rgbInt & 0x00FF00) >>> 8,
3993 (rgbInt & 0x0000FF));
3994
3995 return color;
3996 }
3997
3998 /**
3999 * Create a T.416 RGB parameter sequence for a custom system color.
4000 *
4001 * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
4002 * @return the color portion of the string to emit to an ANSI /
4003 * ECMA-style terminal
4004 */
4005 private String systemColorRGB(final java.awt.Color color) {
4006 return String.format("%d;%d;%d", color.getRed(), color.getGreen(),
4007 color.getBlue());
4008 }
4009
4010 /**
4011 * Create a SGR parameter sequence for a single color change.
4012 *
4013 * @param bold if true, set bold
4014 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4015 * @param foreground if true, this is a foreground color
4016 * @return the string to emit to an ANSI / ECMA-style terminal,
4017 * e.g. "\033[42m"
4018 */
4019 private String color(final boolean bold, final Color color,
4020 final boolean foreground) {
4021 return color(color, foreground, true) +
4022 rgbColor(bold, color, foreground);
4023 }
4024
4025 /**
4026 * Create a T.416 RGB parameter sequence for a single color change.
4027 *
4028 * @param colorRGB a 24-bit RGB value for foreground color
4029 * @param foreground if true, this is a foreground color
4030 * @return the string to emit to an ANSI / ECMA-style terminal,
4031 * e.g. "\033[42m"
4032 */
4033 private String colorRGB(final int colorRGB, final boolean foreground) {
4034
4035 int colorRed = (colorRGB >>> 16) & 0xFF;
4036 int colorGreen = (colorRGB >>> 8) & 0xFF;
4037 int colorBlue = colorRGB & 0xFF;
4038
4039 StringBuilder sb = new StringBuilder();
4040 if (foreground) {
4041 sb.append("\033[38;2;");
4042 } else {
4043 sb.append("\033[48;2;");
4044 }
4045 sb.append(String.format("%d;%d;%dm", colorRed, colorGreen, colorBlue));
4046 return sb.toString();
4047 }
4048
4049 /**
4050 * Create a T.416 RGB parameter sequence for both foreground and
4051 * background color change.
4052 *
4053 * @param foreColorRGB a 24-bit RGB value for foreground color
4054 * @param backColorRGB a 24-bit RGB value for foreground color
4055 * @return the string to emit to an ANSI / ECMA-style terminal,
4056 * e.g. "\033[42m"
4057 */
4058 private String colorRGB(final int foreColorRGB, final int backColorRGB) {
4059 int foreColorRed = (foreColorRGB >>> 16) & 0xFF;
4060 int foreColorGreen = (foreColorRGB >>> 8) & 0xFF;
4061 int foreColorBlue = foreColorRGB & 0xFF;
4062 int backColorRed = (backColorRGB >>> 16) & 0xFF;
4063 int backColorGreen = (backColorRGB >>> 8) & 0xFF;
4064 int backColorBlue = backColorRGB & 0xFF;
4065
4066 StringBuilder sb = new StringBuilder();
4067 sb.append(String.format("\033[38;2;%d;%d;%dm",
4068 foreColorRed, foreColorGreen, foreColorBlue));
4069 sb.append(String.format("\033[48;2;%d;%d;%dm",
4070 backColorRed, backColorGreen, backColorBlue));
4071 return sb.toString();
4072 }
4073
4074 /**
4075 * Create a T.416 RGB parameter sequence for a single color change.
4076 *
4077 * @param bold if true, set bold
4078 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4079 * @param foreground if true, this is a foreground color
4080 * @return the string to emit to an xterm terminal with RGB support,
4081 * e.g. "\033[38;2;RR;GG;BBm"
4082 */
4083 private String rgbColor(final boolean bold, final Color color,
4084 final boolean foreground) {
4085 if (doRgbColor == false) {
4086 return "";
4087 }
4088 StringBuilder sb = new StringBuilder("\033[");
4089 if (bold) {
4090 // Bold implies foreground only
4091 sb.append("38;2;");
4092 if (color.equals(Color.BLACK)) {
4093 sb.append(systemColorRGB(MYBOLD_BLACK));
4094 } else if (color.equals(Color.RED)) {
4095 sb.append(systemColorRGB(MYBOLD_RED));
4096 } else if (color.equals(Color.GREEN)) {
4097 sb.append(systemColorRGB(MYBOLD_GREEN));
4098 } else if (color.equals(Color.YELLOW)) {
4099 sb.append(systemColorRGB(MYBOLD_YELLOW));
4100 } else if (color.equals(Color.BLUE)) {
4101 sb.append(systemColorRGB(MYBOLD_BLUE));
4102 } else if (color.equals(Color.MAGENTA)) {
4103 sb.append(systemColorRGB(MYBOLD_MAGENTA));
4104 } else if (color.equals(Color.CYAN)) {
4105 sb.append(systemColorRGB(MYBOLD_CYAN));
4106 } else if (color.equals(Color.WHITE)) {
4107 sb.append(systemColorRGB(MYBOLD_WHITE));
4108 }
4109 } else {
4110 if (foreground) {
4111 sb.append("38;2;");
4112 } else {
4113 sb.append("48;2;");
4114 }
4115 if (color.equals(Color.BLACK)) {
4116 sb.append(systemColorRGB(MYBLACK));
4117 } else if (color.equals(Color.RED)) {
4118 sb.append(systemColorRGB(MYRED));
4119 } else if (color.equals(Color.GREEN)) {
4120 sb.append(systemColorRGB(MYGREEN));
4121 } else if (color.equals(Color.YELLOW)) {
4122 sb.append(systemColorRGB(MYYELLOW));
4123 } else if (color.equals(Color.BLUE)) {
4124 sb.append(systemColorRGB(MYBLUE));
4125 } else if (color.equals(Color.MAGENTA)) {
4126 sb.append(systemColorRGB(MYMAGENTA));
4127 } else if (color.equals(Color.CYAN)) {
4128 sb.append(systemColorRGB(MYCYAN));
4129 } else if (color.equals(Color.WHITE)) {
4130 sb.append(systemColorRGB(MYWHITE));
4131 }
4132 }
4133 sb.append("m");
4134 return sb.toString();
4135 }
4136
4137 /**
4138 * Create a T.416 RGB parameter sequence for both foreground and
4139 * background color change.
4140 *
4141 * @param bold if true, set bold
4142 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4143 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4144 * @return the string to emit to an xterm terminal with RGB support,
4145 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
4146 */
4147 private String rgbColor(final boolean bold, final Color foreColor,
4148 final Color backColor) {
4149 if (doRgbColor == false) {
4150 return "";
4151 }
4152
4153 return rgbColor(bold, foreColor, true) +
4154 rgbColor(false, backColor, false);
4155 }
4156
4157 /**
4158 * Create a SGR parameter sequence for a single color change.
4159 *
4160 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
4161 * @param foreground if true, this is a foreground color
4162 * @param header if true, make the full header, otherwise just emit the
4163 * color parameter e.g. "42;"
4164 * @return the string to emit to an ANSI / ECMA-style terminal,
4165 * e.g. "\033[42m"
4166 */
4167 private String color(final Color color, final boolean foreground,
4168 final boolean header) {
4169
4170 int ecmaColor = color.getValue();
4171
4172 // Convert Color.* values to SGR numerics
4173 if (foreground) {
4174 ecmaColor += 30;
4175 } else {
4176 ecmaColor += 40;
4177 }
4178
4179 if (header) {
4180 return String.format("\033[%dm", ecmaColor);
4181 } else {
4182 return String.format("%d;", ecmaColor);
4183 }
4184 }
4185
4186 /**
4187 * Create a SGR parameter sequence for both foreground and background
4188 * color change.
4189 *
4190 * @param bold if true, set bold
4191 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4192 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4193 * @return the string to emit to an ANSI / ECMA-style terminal,
4194 * e.g. "\033[31;42m"
4195 */
4196 private String color(final boolean bold, final Color foreColor,
4197 final Color backColor) {
4198 return color(foreColor, backColor, true) +
4199 rgbColor(bold, foreColor, backColor);
4200 }
4201
4202 /**
4203 * Create a SGR parameter sequence for both foreground and
4204 * background color change.
4205 *
4206 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4207 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4208 * @param header if true, make the full header, otherwise just emit the
4209 * color parameter e.g. "31;42;"
4210 * @return the string to emit to an ANSI / ECMA-style terminal,
4211 * e.g. "\033[31;42m"
4212 */
4213 private String color(final Color foreColor, final Color backColor,
4214 final boolean header) {
4215
4216 int ecmaForeColor = foreColor.getValue();
4217 int ecmaBackColor = backColor.getValue();
4218
4219 // Convert Color.* values to SGR numerics
4220 ecmaBackColor += 40;
4221 ecmaForeColor += 30;
4222
4223 if (header) {
4224 return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
4225 } else {
4226 return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
4227 }
4228 }
4229
4230 /**
4231 * Create a SGR parameter sequence for foreground, background, and
4232 * several attributes. This sequence first resets all attributes to
4233 * default, then sets attributes as per the parameters.
4234 *
4235 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
4236 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
4237 * @param bold if true, set bold
4238 * @param reverse if true, set reverse
4239 * @param blink if true, set blink
4240 * @param underline if true, set underline
4241 * @return the string to emit to an ANSI / ECMA-style terminal,
4242 * e.g. "\033[0;1;31;42m"
4243 */
4244 private String color(final Color foreColor, final Color backColor,
4245 final boolean bold, final boolean reverse, final boolean blink,
4246 final boolean underline) {
4247
4248 int ecmaForeColor = foreColor.getValue();
4249 int ecmaBackColor = backColor.getValue();
4250
4251 // Convert Color.* values to SGR numerics
4252 ecmaBackColor += 40;
4253 ecmaForeColor += 30;
4254
4255 StringBuilder sb = new StringBuilder();
4256 if ( bold && reverse && blink && !underline ) {
4257 sb.append("\033[0;1;7;5;");
4258 } else if ( bold && reverse && !blink && !underline ) {
4259 sb.append("\033[0;1;7;");
4260 } else if ( !bold && reverse && blink && !underline ) {
4261 sb.append("\033[0;7;5;");
4262 } else if ( bold && !reverse && blink && !underline ) {
4263 sb.append("\033[0;1;5;");
4264 } else if ( bold && !reverse && !blink && !underline ) {
4265 sb.append("\033[0;1;");
4266 } else if ( !bold && reverse && !blink && !underline ) {
4267 sb.append("\033[0;7;");
4268 } else if ( !bold && !reverse && blink && !underline) {
4269 sb.append("\033[0;5;");
4270 } else if ( bold && reverse && blink && underline ) {
4271 sb.append("\033[0;1;7;5;4;");
4272 } else if ( bold && reverse && !blink && underline ) {
4273 sb.append("\033[0;1;7;4;");
4274 } else if ( !bold && reverse && blink && underline ) {
4275 sb.append("\033[0;7;5;4;");
4276 } else if ( bold && !reverse && blink && underline ) {
4277 sb.append("\033[0;1;5;4;");
4278 } else if ( bold && !reverse && !blink && underline ) {
4279 sb.append("\033[0;1;4;");
4280 } else if ( !bold && reverse && !blink && underline ) {
4281 sb.append("\033[0;7;4;");
4282 } else if ( !bold && !reverse && blink && underline) {
4283 sb.append("\033[0;5;4;");
4284 } else if ( !bold && !reverse && !blink && underline) {
4285 sb.append("\033[0;4;");
4286 } else {
4287 assert (!bold && !reverse && !blink && !underline);
4288 sb.append("\033[0;");
4289 }
4290 sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
4291 sb.append(rgbColor(bold, foreColor, backColor));
4292 return sb.toString();
4293 }
4294
4295 /**
4296 * Create a SGR parameter sequence for foreground, background, and
4297 * several attributes. This sequence first resets all attributes to
4298 * default, then sets attributes as per the parameters.
4299 *
4300 * @param foreColorRGB a 24-bit RGB value for foreground color
4301 * @param backColorRGB a 24-bit RGB value for foreground color
4302 * @param bold if true, set bold
4303 * @param reverse if true, set reverse
4304 * @param blink if true, set blink
4305 * @param underline if true, set underline
4306 * @return the string to emit to an ANSI / ECMA-style terminal,
4307 * e.g. "\033[0;1;31;42m"
4308 */
4309 private String colorRGB(final int foreColorRGB, final int backColorRGB,
4310 final boolean bold, final boolean reverse, final boolean blink,
4311 final boolean underline) {
4312
4313 int foreColorRed = (foreColorRGB >>> 16) & 0xFF;
4314 int foreColorGreen = (foreColorRGB >>> 8) & 0xFF;
4315 int foreColorBlue = foreColorRGB & 0xFF;
4316 int backColorRed = (backColorRGB >>> 16) & 0xFF;
4317 int backColorGreen = (backColorRGB >>> 8) & 0xFF;
4318 int backColorBlue = backColorRGB & 0xFF;
4319
4320 StringBuilder sb = new StringBuilder();
4321 if ( bold && reverse && blink && !underline ) {
4322 sb.append("\033[0;1;7;5;");
4323 } else if ( bold && reverse && !blink && !underline ) {
4324 sb.append("\033[0;1;7;");
4325 } else if ( !bold && reverse && blink && !underline ) {
4326 sb.append("\033[0;7;5;");
4327 } else if ( bold && !reverse && blink && !underline ) {
4328 sb.append("\033[0;1;5;");
4329 } else if ( bold && !reverse && !blink && !underline ) {
4330 sb.append("\033[0;1;");
4331 } else if ( !bold && reverse && !blink && !underline ) {
4332 sb.append("\033[0;7;");
4333 } else if ( !bold && !reverse && blink && !underline) {
4334 sb.append("\033[0;5;");
4335 } else if ( bold && reverse && blink && underline ) {
4336 sb.append("\033[0;1;7;5;4;");
4337 } else if ( bold && reverse && !blink && underline ) {
4338 sb.append("\033[0;1;7;4;");
4339 } else if ( !bold && reverse && blink && underline ) {
4340 sb.append("\033[0;7;5;4;");
4341 } else if ( bold && !reverse && blink && underline ) {
4342 sb.append("\033[0;1;5;4;");
4343 } else if ( bold && !reverse && !blink && underline ) {
4344 sb.append("\033[0;1;4;");
4345 } else if ( !bold && reverse && !blink && underline ) {
4346 sb.append("\033[0;7;4;");
4347 } else if ( !bold && !reverse && blink && underline) {
4348 sb.append("\033[0;5;4;");
4349 } else if ( !bold && !reverse && !blink && underline) {
4350 sb.append("\033[0;4;");
4351 } else {
4352 assert (!bold && !reverse && !blink && !underline);
4353 sb.append("\033[0;");
4354 }
4355
4356 sb.append("m\033[38;2;");
4357 sb.append(String.format("%d;%d;%d", foreColorRed, foreColorGreen,
4358 foreColorBlue));
4359 sb.append("m\033[48;2;");
4360 sb.append(String.format("%d;%d;%d", backColorRed, backColorGreen,
4361 backColorBlue));
4362 sb.append("m");
4363 return sb.toString();
4364 }
4365
4366 /**
4367 * Create a SGR parameter sequence to reset to VT100 defaults.
4368 *
4369 * @return the string to emit to an ANSI / ECMA-style terminal,
4370 * e.g. "\033[0m"
4371 */
4372 private String normal() {
4373 return normal(true) + rgbColor(false, Color.WHITE, Color.BLACK);
4374 }
4375
4376 /**
4377 * Create a SGR parameter sequence to reset to ECMA-48 default
4378 * foreground/background.
4379 *
4380 * @return the string to emit to an ANSI / ECMA-style terminal,
4381 * e.g. "\033[0m"
4382 */
4383 private String defaultColor() {
4384 /*
4385 * VT100 normal.
4386 * Normal (neither bold nor faint).
4387 * Not italicized.
4388 * Not underlined.
4389 * Steady (not blinking).
4390 * Positive (not inverse).
4391 * Visible (not hidden).
4392 * Not crossed-out.
4393 * Default foreground color.
4394 * Default background color.
4395 */
4396 return "\033[0;22;23;24;25;27;28;29;39;49m";
4397 }
4398
4399 /**
4400 * Create a SGR parameter sequence to reset to defaults.
4401 *
4402 * @param header if true, make the full header, otherwise just emit the
4403 * bare parameter e.g. "0;"
4404 * @return the string to emit to an ANSI / ECMA-style terminal,
4405 * e.g. "\033[0m"
4406 */
4407 private String normal(final boolean header) {
4408 if (header) {
4409 return "\033[0;37;40m";
4410 }
4411 return "0;37;40";
4412 }
4413
4414 /**
4415 * Create a SGR parameter sequence for enabling the visible cursor.
4416 *
4417 * @param on if true, turn on cursor
4418 * @return the string to emit to an ANSI / ECMA-style terminal
4419 */
4420 private String cursor(final boolean on) {
4421 if (on && !cursorOn) {
4422 cursorOn = true;
4423 return "\033[?25h";
4424 }
4425 if (!on && cursorOn) {
4426 cursorOn = false;
4427 return "\033[?25l";
4428 }
4429 return "";
4430 }
4431
4432 /**
4433 * Clear the entire screen. Because some terminals use back-color-erase,
4434 * set the color to white-on-black beforehand.
4435 *
4436 * @return the string to emit to an ANSI / ECMA-style terminal
4437 */
4438 private String clearAll() {
4439 return "\033[0;37;40m\033[2J";
4440 }
4441
4442 /**
4443 * Clear the line from the cursor (inclusive) to the end of the screen.
4444 * Because some terminals use back-color-erase, set the color to
4445 * white-on-black beforehand.
4446 *
4447 * @return the string to emit to an ANSI / ECMA-style terminal
4448 */
4449 private String clearRemainingLine() {
4450 return "\033[0;37;40m\033[K";
4451 }
4452
4453 /**
4454 * Move the cursor to (x, y).
4455 *
4456 * @param x column coordinate. 0 is the left-most column.
4457 * @param y row coordinate. 0 is the top-most row.
4458 * @return the string to emit to an ANSI / ECMA-style terminal
4459 */
4460 private String gotoXY(final int x, final int y) {
4461 return String.format("\033[%d;%dH", y + 1, x + 1);
4462 }
4463
4464 /**
4465 * Tell (u)xterm that we want to receive mouse events based on "Any event
4466 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
4467 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
4468 * See
4469 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
4470 *
4471 * Note that this also sets the alternate/primary screen buffer.
4472 *
4473 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
4474 * mean "hide the mouse pointer." We have to use our own sequence to do
4475 * this because there is no standard in xterm for unilaterally hiding the
4476 * pointer all the time (regardless of typing).
4477 *
4478 * @param on If true, enable mouse report and use the alternate screen
4479 * buffer. If false disable mouse reporting and use the primary screen
4480 * buffer.
4481 * @return the string to emit to xterm
4482 */
4483 private String mouse(final boolean on) {
4484 if (on) {
4485 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
4486 }
4487 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";
4488 }
4489
4490 }