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