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