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