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