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