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