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