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