retrofit from gjexer
[nikiroo-utils.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 = ((pXpY >>> 16) & 0xFF) + (7 * redError);
484 green = ((pXpY >>> 8) & 0xFF) + (7 * greenError);
485 blue = ( 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 = ((pXpYp >>> 16) & 0xFF) + redError;
497 green = ((pXpYp >>> 8) & 0xFF) + greenError;
498 blue = ( 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 = ((pXmYp >>> 16) & 0xFF) + (3 * redError);
513 green = ((pXmYp >>> 8) & 0xFF) + (3 * greenError);
514 blue = ( 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 = ((pXYp >>> 16) & 0xFF) + (5 * redError);
523 green = ((pXYp >>> 8) & 0xFF) + (5 * greenError);
524 blue = ( 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 * Resize the physical screen to match the logical screen dimensions.
1243 */
1244 @Override
1245 public void resizeToScreen() {
1246 // Send dtterm/xterm sequences, which will probably not work because
1247 // allowWindowOps is defaulted to false.
1248 String resizeString = String.format("\033[8;%d;%dt", getHeight(),
1249 getWidth());
1250 this.output.write(resizeString);
1251 this.output.flush();
1252 }
1253
1254 // ------------------------------------------------------------------------
1255 // TerminalReader ---------------------------------------------------------
1256 // ------------------------------------------------------------------------
1257
1258 /**
1259 * Check if there are events in the queue.
1260 *
1261 * @return if true, getEvents() has something to return to the backend
1262 */
1263 public boolean hasEvents() {
1264 synchronized (eventQueue) {
1265 return (eventQueue.size() > 0);
1266 }
1267 }
1268
1269 /**
1270 * Return any events in the IO queue.
1271 *
1272 * @param queue list to append new events to
1273 */
1274 public void getEvents(final List<TInputEvent> queue) {
1275 synchronized (eventQueue) {
1276 if (eventQueue.size() > 0) {
1277 synchronized (queue) {
1278 queue.addAll(eventQueue);
1279 }
1280 eventQueue.clear();
1281 }
1282 }
1283 }
1284
1285 /**
1286 * Restore terminal to normal state.
1287 */
1288 public void closeTerminal() {
1289
1290 // System.err.println("=== shutdown() ==="); System.err.flush();
1291
1292 // Tell the reader thread to stop looking at input
1293 stopReaderThread = true;
1294 try {
1295 readerThread.join();
1296 } catch (InterruptedException e) {
1297 if (debugToStderr) {
1298 e.printStackTrace();
1299 }
1300 }
1301
1302 // Disable mouse reporting and show cursor. Defensive null check
1303 // here in case closeTerminal() is called twice.
1304 if (output != null) {
1305 output.printf("%s%s%s", mouse(false), cursor(true), normal());
1306 output.flush();
1307 }
1308
1309 if (setRawMode) {
1310 sttyCooked();
1311 setRawMode = false;
1312 // We don't close System.in/out
1313 } else {
1314 // Shut down the streams, this should wake up the reader thread
1315 // and make it exit.
1316 if (input != null) {
1317 try {
1318 input.close();
1319 } catch (IOException e) {
1320 // SQUASH
1321 }
1322 input = null;
1323 }
1324 if (output != null) {
1325 output.close();
1326 output = null;
1327 }
1328 }
1329 }
1330
1331 /**
1332 * Set listener to a different Object.
1333 *
1334 * @param listener the new listening object that run() wakes up on new
1335 * input
1336 */
1337 public void setListener(final Object listener) {
1338 this.listener = listener;
1339 }
1340
1341 /**
1342 * Reload options from System properties.
1343 */
1344 public void reloadOptions() {
1345 // Permit RGB colors only if externally requested.
1346 if (System.getProperty("jexer.ECMA48.rgbColor",
1347 "false").equals("true")
1348 ) {
1349 doRgbColor = true;
1350 } else {
1351 doRgbColor = false;
1352 }
1353
1354 // Pull the system properties for sixel output.
1355 if (System.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
1356 sixel = true;
1357 } else {
1358 sixel = false;
1359 }
1360 }
1361
1362 // ------------------------------------------------------------------------
1363 // Runnable ---------------------------------------------------------------
1364 // ------------------------------------------------------------------------
1365
1366 /**
1367 * Read function runs on a separate thread.
1368 */
1369 public void run() {
1370 boolean done = false;
1371 // available() will often return > 1, so we need to read in chunks to
1372 // stay caught up.
1373 char [] readBuffer = new char[128];
1374 List<TInputEvent> events = new LinkedList<TInputEvent>();
1375
1376 while (!done && !stopReaderThread) {
1377 try {
1378 // We assume that if inputStream has bytes available, then
1379 // input won't block on read().
1380 int n = inputStream.available();
1381
1382 /*
1383 System.err.printf("inputStream.available(): %d\n", n);
1384 System.err.flush();
1385 */
1386
1387 if (n > 0) {
1388 if (readBuffer.length < n) {
1389 // The buffer wasn't big enough, make it huger
1390 readBuffer = new char[readBuffer.length * 2];
1391 }
1392
1393 // System.err.printf("BEFORE read()\n"); System.err.flush();
1394
1395 int rc = input.read(readBuffer, 0, readBuffer.length);
1396
1397 /*
1398 System.err.printf("AFTER read() %d\n", rc);
1399 System.err.flush();
1400 */
1401
1402 if (rc == -1) {
1403 // This is EOF
1404 done = true;
1405 } else {
1406 for (int i = 0; i < rc; i++) {
1407 int ch = readBuffer[i];
1408 processChar(events, (char)ch);
1409 }
1410 getIdleEvents(events);
1411 if (events.size() > 0) {
1412 // Add to the queue for the backend thread to
1413 // be able to obtain.
1414 synchronized (eventQueue) {
1415 eventQueue.addAll(events);
1416 }
1417 if (listener != null) {
1418 synchronized (listener) {
1419 listener.notifyAll();
1420 }
1421 }
1422 events.clear();
1423 }
1424 }
1425 } else {
1426 getIdleEvents(events);
1427 if (events.size() > 0) {
1428 synchronized (eventQueue) {
1429 eventQueue.addAll(events);
1430 }
1431 if (listener != null) {
1432 synchronized (listener) {
1433 listener.notifyAll();
1434 }
1435 }
1436 events.clear();
1437 }
1438
1439 // Wait 20 millis for more data
1440 Thread.sleep(20);
1441 }
1442 // System.err.println("end while loop"); System.err.flush();
1443 } catch (InterruptedException e) {
1444 // SQUASH
1445 } catch (IOException e) {
1446 e.printStackTrace();
1447 done = true;
1448 }
1449 } // while ((done == false) && (stopReaderThread == false))
1450 // System.err.println("*** run() exiting..."); System.err.flush();
1451 }
1452
1453 // ------------------------------------------------------------------------
1454 // ECMA48Terminal ---------------------------------------------------------
1455 // ------------------------------------------------------------------------
1456
1457 /**
1458 * Get the width of a character cell in pixels.
1459 *
1460 * @return the width in pixels of a character cell
1461 */
1462 public int getTextWidth() {
1463 return (widthPixels / sessionInfo.getWindowWidth());
1464 }
1465
1466 /**
1467 * Get the height of a character cell in pixels.
1468 *
1469 * @return the height in pixels of a character cell
1470 */
1471 public int getTextHeight() {
1472 return (heightPixels / sessionInfo.getWindowHeight());
1473 }
1474
1475 /**
1476 * Getter for sessionInfo.
1477 *
1478 * @return the SessionInfo
1479 */
1480 public SessionInfo getSessionInfo() {
1481 return sessionInfo;
1482 }
1483
1484 /**
1485 * Get the output writer.
1486 *
1487 * @return the Writer
1488 */
1489 public PrintWriter getOutput() {
1490 return output;
1491 }
1492
1493 /**
1494 * Call 'stty' to set cooked mode.
1495 *
1496 * <p>Actually executes '/bin/sh -c stty sane cooked &lt; /dev/tty'
1497 */
1498 private void sttyCooked() {
1499 doStty(false);
1500 }
1501
1502 /**
1503 * Call 'stty' to set raw mode.
1504 *
1505 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
1506 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
1507 * -parenb cs8 min 1 &lt; /dev/tty'
1508 */
1509 private void sttyRaw() {
1510 doStty(true);
1511 }
1512
1513 /**
1514 * Call 'stty' to set raw or cooked mode.
1515 *
1516 * @param mode if true, set raw mode, otherwise set cooked mode
1517 */
1518 private void doStty(final boolean mode) {
1519 String [] cmdRaw = {
1520 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
1521 };
1522 String [] cmdCooked = {
1523 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
1524 };
1525 try {
1526 Process process;
1527 if (mode) {
1528 process = Runtime.getRuntime().exec(cmdRaw);
1529 } else {
1530 process = Runtime.getRuntime().exec(cmdCooked);
1531 }
1532 BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
1533 String line = in.readLine();
1534 if ((line != null) && (line.length() > 0)) {
1535 System.err.println("WEIRD?! Normal output from stty: " + line);
1536 }
1537 while (true) {
1538 BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
1539 line = err.readLine();
1540 if ((line != null) && (line.length() > 0)) {
1541 System.err.println("Error output from stty: " + line);
1542 }
1543 try {
1544 process.waitFor();
1545 break;
1546 } catch (InterruptedException e) {
1547 if (debugToStderr) {
1548 e.printStackTrace();
1549 }
1550 }
1551 }
1552 int rc = process.exitValue();
1553 if (rc != 0) {
1554 System.err.println("stty returned error code: " + rc);
1555 }
1556 } catch (IOException e) {
1557 e.printStackTrace();
1558 }
1559 }
1560
1561 /**
1562 * Flush output.
1563 */
1564 public void flush() {
1565 output.flush();
1566 }
1567
1568 /**
1569 * Perform a somewhat-optimal rendering of a line.
1570 *
1571 * @param y row coordinate. 0 is the top-most row.
1572 * @param sb StringBuilder to write escape sequences to
1573 * @param lastAttr cell attributes from the last call to flushLine
1574 */
1575 private void flushLine(final int y, final StringBuilder sb,
1576 CellAttributes lastAttr) {
1577
1578 int lastX = -1;
1579 int textEnd = 0;
1580 for (int x = 0; x < width; x++) {
1581 Cell lCell = logical[x][y];
1582 if (!lCell.isBlank()) {
1583 textEnd = x;
1584 }
1585 }
1586 // Push textEnd to first column beyond the text area
1587 textEnd++;
1588
1589 // DEBUG
1590 // reallyCleared = true;
1591
1592 boolean hasImage = false;
1593
1594 for (int x = 0; x < width; x++) {
1595 Cell lCell = logical[x][y];
1596 Cell pCell = physical[x][y];
1597
1598 if (!lCell.equals(pCell) || reallyCleared) {
1599
1600 if (debugToStderr) {
1601 System.err.printf("\n--\n");
1602 System.err.printf(" Y: %d X: %d\n", y, x);
1603 System.err.printf(" lCell: %s\n", lCell);
1604 System.err.printf(" pCell: %s\n", pCell);
1605 System.err.printf(" ==== \n");
1606 }
1607
1608 if (lastAttr == null) {
1609 lastAttr = new CellAttributes();
1610 sb.append(normal());
1611 }
1612
1613 // Place the cell
1614 if ((lastX != (x - 1)) || (lastX == -1)) {
1615 // Advancing at least one cell, or the first gotoXY
1616 sb.append(gotoXY(x, y));
1617 }
1618
1619 assert (lastAttr != null);
1620
1621 if ((x == textEnd) && (textEnd < width - 1)) {
1622 assert (lCell.isBlank());
1623
1624 for (int i = x; i < width; i++) {
1625 assert (logical[i][y].isBlank());
1626 // Physical is always updated
1627 physical[i][y].reset();
1628 }
1629
1630 // Clear remaining line
1631 sb.append(clearRemainingLine());
1632 lastAttr.reset();
1633 return;
1634 }
1635
1636 // Image cell: bypass the rest of the loop, it is not
1637 // rendered here.
1638 if (lCell.isImage()) {
1639 hasImage = true;
1640
1641 // Save the last rendered cell
1642 lastX = x;
1643
1644 // Physical is always updated
1645 physical[x][y].setTo(lCell);
1646 continue;
1647 }
1648
1649 assert (!lCell.isImage());
1650 if (hasImage) {
1651 hasImage = false;
1652 sb.append(gotoXY(x, y));
1653 }
1654
1655 // Now emit only the modified attributes
1656 if ((lCell.getForeColor() != lastAttr.getForeColor())
1657 && (lCell.getBackColor() != lastAttr.getBackColor())
1658 && (!lCell.isRGB())
1659 && (lCell.isBold() == lastAttr.isBold())
1660 && (lCell.isReverse() == lastAttr.isReverse())
1661 && (lCell.isUnderline() == lastAttr.isUnderline())
1662 && (lCell.isBlink() == lastAttr.isBlink())
1663 ) {
1664 // Both colors changed, attributes the same
1665 sb.append(color(lCell.isBold(),
1666 lCell.getForeColor(), lCell.getBackColor()));
1667
1668 if (debugToStderr) {
1669 System.err.printf("1 Change only fore/back colors\n");
1670 }
1671
1672 } else if (lCell.isRGB()
1673 && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB())
1674 && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB())
1675 && (lCell.isBold() == lastAttr.isBold())
1676 && (lCell.isReverse() == lastAttr.isReverse())
1677 && (lCell.isUnderline() == lastAttr.isUnderline())
1678 && (lCell.isBlink() == lastAttr.isBlink())
1679 ) {
1680 // Both colors changed, attributes the same
1681 sb.append(colorRGB(lCell.getForeColorRGB(),
1682 lCell.getBackColorRGB()));
1683
1684 if (debugToStderr) {
1685 System.err.printf("1 Change only fore/back colors (RGB)\n");
1686 }
1687 } else if ((lCell.getForeColor() != lastAttr.getForeColor())
1688 && (lCell.getBackColor() != lastAttr.getBackColor())
1689 && (!lCell.isRGB())
1690 && (lCell.isBold() != lastAttr.isBold())
1691 && (lCell.isReverse() != lastAttr.isReverse())
1692 && (lCell.isUnderline() != lastAttr.isUnderline())
1693 && (lCell.isBlink() != lastAttr.isBlink())
1694 ) {
1695 // Everything is different
1696 sb.append(color(lCell.getForeColor(),
1697 lCell.getBackColor(),
1698 lCell.isBold(), lCell.isReverse(),
1699 lCell.isBlink(),
1700 lCell.isUnderline()));
1701
1702 if (debugToStderr) {
1703 System.err.printf("2 Set all attributes\n");
1704 }
1705 } else if ((lCell.getForeColor() != lastAttr.getForeColor())
1706 && (lCell.getBackColor() == lastAttr.getBackColor())
1707 && (!lCell.isRGB())
1708 && (lCell.isBold() == lastAttr.isBold())
1709 && (lCell.isReverse() == lastAttr.isReverse())
1710 && (lCell.isUnderline() == lastAttr.isUnderline())
1711 && (lCell.isBlink() == lastAttr.isBlink())
1712 ) {
1713
1714 // Attributes same, foreColor different
1715 sb.append(color(lCell.isBold(),
1716 lCell.getForeColor(), true));
1717
1718 if (debugToStderr) {
1719 System.err.printf("3 Change foreColor\n");
1720 }
1721 } else if (lCell.isRGB()
1722 && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB())
1723 && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB())
1724 && (lCell.getForeColorRGB() >= 0)
1725 && (lCell.getBackColorRGB() >= 0)
1726 && (lCell.isBold() == lastAttr.isBold())
1727 && (lCell.isReverse() == lastAttr.isReverse())
1728 && (lCell.isUnderline() == lastAttr.isUnderline())
1729 && (lCell.isBlink() == lastAttr.isBlink())
1730 ) {
1731 // Attributes same, foreColor different
1732 sb.append(colorRGB(lCell.getForeColorRGB(), true));
1733
1734 if (debugToStderr) {
1735 System.err.printf("3 Change foreColor (RGB)\n");
1736 }
1737 } else if ((lCell.getForeColor() == lastAttr.getForeColor())
1738 && (lCell.getBackColor() != lastAttr.getBackColor())
1739 && (!lCell.isRGB())
1740 && (lCell.isBold() == lastAttr.isBold())
1741 && (lCell.isReverse() == lastAttr.isReverse())
1742 && (lCell.isUnderline() == lastAttr.isUnderline())
1743 && (lCell.isBlink() == lastAttr.isBlink())
1744 ) {
1745 // Attributes same, backColor different
1746 sb.append(color(lCell.isBold(),
1747 lCell.getBackColor(), false));
1748
1749 if (debugToStderr) {
1750 System.err.printf("4 Change backColor\n");
1751 }
1752 } else if (lCell.isRGB()
1753 && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB())
1754 && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB())
1755 && (lCell.isBold() == lastAttr.isBold())
1756 && (lCell.isReverse() == lastAttr.isReverse())
1757 && (lCell.isUnderline() == lastAttr.isUnderline())
1758 && (lCell.isBlink() == lastAttr.isBlink())
1759 ) {
1760 // Attributes same, foreColor different
1761 sb.append(colorRGB(lCell.getBackColorRGB(), false));
1762
1763 if (debugToStderr) {
1764 System.err.printf("4 Change backColor (RGB)\n");
1765 }
1766 } else if ((lCell.getForeColor() == lastAttr.getForeColor())
1767 && (lCell.getBackColor() == lastAttr.getBackColor())
1768 && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB())
1769 && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB())
1770 && (lCell.isBold() == lastAttr.isBold())
1771 && (lCell.isReverse() == lastAttr.isReverse())
1772 && (lCell.isUnderline() == lastAttr.isUnderline())
1773 && (lCell.isBlink() == lastAttr.isBlink())
1774 ) {
1775
1776 // All attributes the same, just print the char
1777 // NOP
1778
1779 if (debugToStderr) {
1780 System.err.printf("5 Only emit character\n");
1781 }
1782 } else {
1783 // Just reset everything again
1784 if (!lCell.isRGB()) {
1785 sb.append(color(lCell.getForeColor(),
1786 lCell.getBackColor(),
1787 lCell.isBold(),
1788 lCell.isReverse(),
1789 lCell.isBlink(),
1790 lCell.isUnderline()));
1791
1792 if (debugToStderr) {
1793 System.err.printf("6 Change all attributes\n");
1794 }
1795 } else {
1796 sb.append(colorRGB(lCell.getForeColorRGB(),
1797 lCell.getBackColorRGB(),
1798 lCell.isBold(),
1799 lCell.isReverse(),
1800 lCell.isBlink(),
1801 lCell.isUnderline()));
1802 if (debugToStderr) {
1803 System.err.printf("6 Change all attributes (RGB)\n");
1804 }
1805 }
1806
1807 }
1808 // Emit the character
1809 sb.append(lCell.getChar());
1810
1811 // Save the last rendered cell
1812 lastX = x;
1813 lastAttr.setTo(lCell);
1814
1815 // Physical is always updated
1816 physical[x][y].setTo(lCell);
1817
1818 } // if (!lCell.equals(pCell) || (reallyCleared == true))
1819
1820 } // for (int x = 0; x < width; x++)
1821 }
1822
1823 /**
1824 * Render the screen to a string that can be emitted to something that
1825 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
1826 *
1827 * @param sb StringBuilder to write escape sequences to
1828 * @return escape sequences string that provides the updates to the
1829 * physical screen
1830 */
1831 private String flushString(final StringBuilder sb) {
1832 CellAttributes attr = null;
1833
1834 if (reallyCleared) {
1835 attr = new CellAttributes();
1836 sb.append(clearAll());
1837 }
1838
1839 /*
1840 * For sixel support, draw all of the sixel output first, and then
1841 * draw everything else afterwards. This works OK, but performance
1842 * is still a drag on larger pictures.
1843 */
1844 for (int y = 0; y < height; y++) {
1845 for (int x = 0; x < width; x++) {
1846 // If physical had non-image data that is now image data, the
1847 // entire row must be redrawn.
1848 Cell lCell = logical[x][y];
1849 Cell pCell = physical[x][y];
1850 if (lCell.isImage() && !pCell.isImage()) {
1851 unsetImageRow(y);
1852 break;
1853 }
1854 }
1855 }
1856 for (int y = 0; y < height; y++) {
1857 for (int x = 0; x < width; x++) {
1858 Cell lCell = logical[x][y];
1859 Cell pCell = physical[x][y];
1860
1861 if (!lCell.isImage()) {
1862 continue;
1863 }
1864
1865 int left = x;
1866 int right = x;
1867 while ((right < width)
1868 && (logical[right][y].isImage())
1869 && (!logical[right][y].equals(physical[right][y])
1870 || reallyCleared)
1871 ) {
1872 right++;
1873 }
1874 ArrayList<Cell> cellsToDraw = new ArrayList<Cell>();
1875 for (int i = 0; i < (right - x); i++) {
1876 assert (logical[x + i][y].isImage());
1877 cellsToDraw.add(logical[x + i][y]);
1878
1879 // Physical is always updated.
1880 physical[x + i][y].setTo(lCell);
1881 }
1882 if (cellsToDraw.size() > 0) {
1883 sb.append(toSixel(x, y, cellsToDraw));
1884 }
1885
1886 x = right;
1887 }
1888 }
1889
1890 // Draw the text part now.
1891 for (int y = 0; y < height; y++) {
1892 flushLine(y, sb, attr);
1893 }
1894
1895 reallyCleared = false;
1896
1897 String result = sb.toString();
1898 if (debugToStderr) {
1899 System.err.printf("flushString(): %s\n", result);
1900 }
1901 return result;
1902 }
1903
1904 /**
1905 * Reset keyboard/mouse input parser.
1906 */
1907 private void resetParser() {
1908 state = ParseState.GROUND;
1909 params = new ArrayList<String>();
1910 params.clear();
1911 params.add("");
1912 }
1913
1914 /**
1915 * Produce a control character or one of the special ones (ENTER, TAB,
1916 * etc.).
1917 *
1918 * @param ch Unicode code point
1919 * @param alt if true, set alt on the TKeypress
1920 * @return one TKeypress event, either a control character (e.g. isKey ==
1921 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
1922 * fnKey == ESC)
1923 */
1924 private TKeypressEvent controlChar(final char ch, final boolean alt) {
1925 // System.err.printf("controlChar: %02x\n", ch);
1926
1927 switch (ch) {
1928 case 0x0D:
1929 // Carriage return --> ENTER
1930 return new TKeypressEvent(kbEnter, alt, false, false);
1931 case 0x0A:
1932 // Linefeed --> ENTER
1933 return new TKeypressEvent(kbEnter, alt, false, false);
1934 case 0x1B:
1935 // ESC
1936 return new TKeypressEvent(kbEsc, alt, false, false);
1937 case '\t':
1938 // TAB
1939 return new TKeypressEvent(kbTab, alt, false, false);
1940 default:
1941 // Make all other control characters come back as the alphabetic
1942 // character with the ctrl field set. So SOH would be 'A' +
1943 // ctrl.
1944 return new TKeypressEvent(false, 0, (char)(ch + 0x40),
1945 alt, true, false);
1946 }
1947 }
1948
1949 /**
1950 * Produce special key from CSI Pn ; Pm ; ... ~
1951 *
1952 * @return one KEYPRESS event representing a special key
1953 */
1954 private TInputEvent csiFnKey() {
1955 int key = 0;
1956 if (params.size() > 0) {
1957 key = Integer.parseInt(params.get(0));
1958 }
1959 boolean alt = false;
1960 boolean ctrl = false;
1961 boolean shift = false;
1962 if (params.size() > 1) {
1963 shift = csiIsShift(params.get(1));
1964 alt = csiIsAlt(params.get(1));
1965 ctrl = csiIsCtrl(params.get(1));
1966 }
1967
1968 switch (key) {
1969 case 1:
1970 return new TKeypressEvent(kbHome, alt, ctrl, shift);
1971 case 2:
1972 return new TKeypressEvent(kbIns, alt, ctrl, shift);
1973 case 3:
1974 return new TKeypressEvent(kbDel, alt, ctrl, shift);
1975 case 4:
1976 return new TKeypressEvent(kbEnd, alt, ctrl, shift);
1977 case 5:
1978 return new TKeypressEvent(kbPgUp, alt, ctrl, shift);
1979 case 6:
1980 return new TKeypressEvent(kbPgDn, alt, ctrl, shift);
1981 case 15:
1982 return new TKeypressEvent(kbF5, alt, ctrl, shift);
1983 case 17:
1984 return new TKeypressEvent(kbF6, alt, ctrl, shift);
1985 case 18:
1986 return new TKeypressEvent(kbF7, alt, ctrl, shift);
1987 case 19:
1988 return new TKeypressEvent(kbF8, alt, ctrl, shift);
1989 case 20:
1990 return new TKeypressEvent(kbF9, alt, ctrl, shift);
1991 case 21:
1992 return new TKeypressEvent(kbF10, alt, ctrl, shift);
1993 case 23:
1994 return new TKeypressEvent(kbF11, alt, ctrl, shift);
1995 case 24:
1996 return new TKeypressEvent(kbF12, alt, ctrl, shift);
1997 default:
1998 // Unknown
1999 return null;
2000 }
2001 }
2002
2003 /**
2004 * Produce mouse events based on "Any event tracking" and UTF-8
2005 * coordinates. See
2006 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2007 *
2008 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2009 */
2010 private TInputEvent parseMouse() {
2011 int buttons = params.get(0).charAt(0) - 32;
2012 int x = params.get(0).charAt(1) - 32 - 1;
2013 int y = params.get(0).charAt(2) - 32 - 1;
2014
2015 // Clamp X and Y to the physical screen coordinates.
2016 if (x >= windowResize.getWidth()) {
2017 x = windowResize.getWidth() - 1;
2018 }
2019 if (y >= windowResize.getHeight()) {
2020 y = windowResize.getHeight() - 1;
2021 }
2022
2023 TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
2024 boolean eventMouse1 = false;
2025 boolean eventMouse2 = false;
2026 boolean eventMouse3 = false;
2027 boolean eventMouseWheelUp = false;
2028 boolean eventMouseWheelDown = false;
2029
2030 // System.err.printf("buttons: %04x\r\n", buttons);
2031
2032 switch (buttons) {
2033 case 0:
2034 eventMouse1 = true;
2035 mouse1 = true;
2036 break;
2037 case 1:
2038 eventMouse2 = true;
2039 mouse2 = true;
2040 break;
2041 case 2:
2042 eventMouse3 = true;
2043 mouse3 = true;
2044 break;
2045 case 3:
2046 // Release or Move
2047 if (!mouse1 && !mouse2 && !mouse3) {
2048 eventType = TMouseEvent.Type.MOUSE_MOTION;
2049 } else {
2050 eventType = TMouseEvent.Type.MOUSE_UP;
2051 }
2052 if (mouse1) {
2053 mouse1 = false;
2054 eventMouse1 = true;
2055 }
2056 if (mouse2) {
2057 mouse2 = false;
2058 eventMouse2 = true;
2059 }
2060 if (mouse3) {
2061 mouse3 = false;
2062 eventMouse3 = true;
2063 }
2064 break;
2065
2066 case 32:
2067 // Dragging with mouse1 down
2068 eventMouse1 = true;
2069 mouse1 = true;
2070 eventType = TMouseEvent.Type.MOUSE_MOTION;
2071 break;
2072
2073 case 33:
2074 // Dragging with mouse2 down
2075 eventMouse2 = true;
2076 mouse2 = true;
2077 eventType = TMouseEvent.Type.MOUSE_MOTION;
2078 break;
2079
2080 case 34:
2081 // Dragging with mouse3 down
2082 eventMouse3 = true;
2083 mouse3 = true;
2084 eventType = TMouseEvent.Type.MOUSE_MOTION;
2085 break;
2086
2087 case 96:
2088 // Dragging with mouse2 down after wheelUp
2089 eventMouse2 = true;
2090 mouse2 = true;
2091 eventType = TMouseEvent.Type.MOUSE_MOTION;
2092 break;
2093
2094 case 97:
2095 // Dragging with mouse2 down after wheelDown
2096 eventMouse2 = true;
2097 mouse2 = true;
2098 eventType = TMouseEvent.Type.MOUSE_MOTION;
2099 break;
2100
2101 case 64:
2102 eventMouseWheelUp = true;
2103 break;
2104
2105 case 65:
2106 eventMouseWheelDown = true;
2107 break;
2108
2109 default:
2110 // Unknown, just make it motion
2111 eventType = TMouseEvent.Type.MOUSE_MOTION;
2112 break;
2113 }
2114 return new TMouseEvent(eventType, x, y, x, y,
2115 eventMouse1, eventMouse2, eventMouse3,
2116 eventMouseWheelUp, eventMouseWheelDown);
2117 }
2118
2119 /**
2120 * Produce mouse events based on "Any event tracking" and SGR
2121 * coordinates. See
2122 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
2123 *
2124 * @param release if true, this was a release ('m')
2125 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
2126 */
2127 private TInputEvent parseMouseSGR(final boolean release) {
2128 // SGR extended coordinates - mode 1006
2129 if (params.size() < 3) {
2130 // Invalid position, bail out.
2131 return null;
2132 }
2133 int buttons = Integer.parseInt(params.get(0));
2134 int x = Integer.parseInt(params.get(1)) - 1;
2135 int y = Integer.parseInt(params.get(2)) - 1;
2136
2137 // Clamp X and Y to the physical screen coordinates.
2138 if (x >= windowResize.getWidth()) {
2139 x = windowResize.getWidth() - 1;
2140 }
2141 if (y >= windowResize.getHeight()) {
2142 y = windowResize.getHeight() - 1;
2143 }
2144
2145 TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
2146 boolean eventMouse1 = false;
2147 boolean eventMouse2 = false;
2148 boolean eventMouse3 = false;
2149 boolean eventMouseWheelUp = false;
2150 boolean eventMouseWheelDown = false;
2151
2152 if (release) {
2153 eventType = TMouseEvent.Type.MOUSE_UP;
2154 }
2155
2156 switch (buttons) {
2157 case 0:
2158 eventMouse1 = true;
2159 break;
2160 case 1:
2161 eventMouse2 = true;
2162 break;
2163 case 2:
2164 eventMouse3 = true;
2165 break;
2166 case 35:
2167 // Motion only, no buttons down
2168 eventType = TMouseEvent.Type.MOUSE_MOTION;
2169 break;
2170
2171 case 32:
2172 // Dragging with mouse1 down
2173 eventMouse1 = true;
2174 eventType = TMouseEvent.Type.MOUSE_MOTION;
2175 break;
2176
2177 case 33:
2178 // Dragging with mouse2 down
2179 eventMouse2 = true;
2180 eventType = TMouseEvent.Type.MOUSE_MOTION;
2181 break;
2182
2183 case 34:
2184 // Dragging with mouse3 down
2185 eventMouse3 = true;
2186 eventType = TMouseEvent.Type.MOUSE_MOTION;
2187 break;
2188
2189 case 96:
2190 // Dragging with mouse2 down after wheelUp
2191 eventMouse2 = true;
2192 eventType = TMouseEvent.Type.MOUSE_MOTION;
2193 break;
2194
2195 case 97:
2196 // Dragging with mouse2 down after wheelDown
2197 eventMouse2 = true;
2198 eventType = TMouseEvent.Type.MOUSE_MOTION;
2199 break;
2200
2201 case 64:
2202 eventMouseWheelUp = true;
2203 break;
2204
2205 case 65:
2206 eventMouseWheelDown = true;
2207 break;
2208
2209 default:
2210 // Unknown, bail out
2211 return null;
2212 }
2213 return new TMouseEvent(eventType, x, y, x, y,
2214 eventMouse1, eventMouse2, eventMouse3,
2215 eventMouseWheelUp, eventMouseWheelDown);
2216 }
2217
2218 /**
2219 * Return any events in the IO queue due to timeout.
2220 *
2221 * @param queue list to append new events to
2222 */
2223 private void getIdleEvents(final List<TInputEvent> queue) {
2224 long nowTime = System.currentTimeMillis();
2225
2226 // Check for new window size
2227 long windowSizeDelay = nowTime - windowSizeTime;
2228 if (windowSizeDelay > 1000) {
2229 int oldTextWidth = getTextWidth();
2230 int oldTextHeight = getTextHeight();
2231
2232 sessionInfo.queryWindowSize();
2233 int newWidth = sessionInfo.getWindowWidth();
2234 int newHeight = sessionInfo.getWindowHeight();
2235
2236 if ((newWidth != windowResize.getWidth())
2237 || (newHeight != windowResize.getHeight())
2238 ) {
2239
2240 // Request xterm report window dimensions in pixels again.
2241 // Between now and then, ensure that the reported text cell
2242 // size is the same by setting widthPixels and heightPixels
2243 // to match the new dimensions.
2244 widthPixels = oldTextWidth * newWidth;
2245 heightPixels = oldTextHeight * newHeight;
2246
2247 if (debugToStderr) {
2248 System.err.println("Screen size changed, old size " +
2249 windowResize);
2250 System.err.println(" new size " +
2251 newWidth + " x " + newHeight);
2252 System.err.println(" old pixels " +
2253 oldTextWidth + " x " + oldTextHeight);
2254 System.err.println(" new pixels " +
2255 getTextWidth() + " x " + getTextHeight());
2256 }
2257
2258 this.output.printf("%s", xtermReportWindowPixelDimensions());
2259 this.output.flush();
2260
2261 TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
2262 newWidth, newHeight);
2263 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
2264 newWidth, newHeight);
2265 queue.add(event);
2266 }
2267 windowSizeTime = nowTime;
2268 }
2269
2270 // ESCDELAY type timeout
2271 if (state == ParseState.ESCAPE) {
2272 long escDelay = nowTime - escapeTime;
2273 if (escDelay > 100) {
2274 // After 0.1 seconds, assume a true escape character
2275 queue.add(controlChar((char)0x1B, false));
2276 resetParser();
2277 }
2278 }
2279 }
2280
2281 /**
2282 * Returns true if the CSI parameter for a keyboard command means that
2283 * shift was down.
2284 */
2285 private boolean csiIsShift(final String x) {
2286 if ((x.equals("2"))
2287 || (x.equals("4"))
2288 || (x.equals("6"))
2289 || (x.equals("8"))
2290 ) {
2291 return true;
2292 }
2293 return false;
2294 }
2295
2296 /**
2297 * Returns true if the CSI parameter for a keyboard command means that
2298 * alt was down.
2299 */
2300 private boolean csiIsAlt(final String x) {
2301 if ((x.equals("3"))
2302 || (x.equals("4"))
2303 || (x.equals("7"))
2304 || (x.equals("8"))
2305 ) {
2306 return true;
2307 }
2308 return false;
2309 }
2310
2311 /**
2312 * Returns true if the CSI parameter for a keyboard command means that
2313 * ctrl was down.
2314 */
2315 private boolean csiIsCtrl(final String x) {
2316 if ((x.equals("5"))
2317 || (x.equals("6"))
2318 || (x.equals("7"))
2319 || (x.equals("8"))
2320 ) {
2321 return true;
2322 }
2323 return false;
2324 }
2325
2326 /**
2327 * Parses the next character of input to see if an InputEvent is
2328 * fully here.
2329 *
2330 * @param events list to append new events to
2331 * @param ch Unicode code point
2332 */
2333 private void processChar(final List<TInputEvent> events, final char ch) {
2334
2335 // ESCDELAY type timeout
2336 long nowTime = System.currentTimeMillis();
2337 if (state == ParseState.ESCAPE) {
2338 long escDelay = nowTime - escapeTime;
2339 if (escDelay > 250) {
2340 // After 0.25 seconds, assume a true escape character
2341 events.add(controlChar((char)0x1B, false));
2342 resetParser();
2343 }
2344 }
2345
2346 // TKeypress fields
2347 boolean ctrl = false;
2348 boolean alt = false;
2349 boolean shift = false;
2350
2351 // System.err.printf("state: %s ch %c\r\n", state, ch);
2352
2353 switch (state) {
2354 case GROUND:
2355
2356 if (ch == 0x1B) {
2357 state = ParseState.ESCAPE;
2358 escapeTime = nowTime;
2359 return;
2360 }
2361
2362 if (ch <= 0x1F) {
2363 // Control character
2364 events.add(controlChar(ch, false));
2365 resetParser();
2366 return;
2367 }
2368
2369 if (ch >= 0x20) {
2370 // Normal character
2371 events.add(new TKeypressEvent(false, 0, ch,
2372 false, false, false));
2373 resetParser();
2374 return;
2375 }
2376
2377 break;
2378
2379 case ESCAPE:
2380 if (ch <= 0x1F) {
2381 // ALT-Control character
2382 events.add(controlChar(ch, true));
2383 resetParser();
2384 return;
2385 }
2386
2387 if (ch == 'O') {
2388 // This will be one of the function keys
2389 state = ParseState.ESCAPE_INTERMEDIATE;
2390 return;
2391 }
2392
2393 // '[' goes to CSI_ENTRY
2394 if (ch == '[') {
2395 state = ParseState.CSI_ENTRY;
2396 return;
2397 }
2398
2399 // Everything else is assumed to be Alt-keystroke
2400 if ((ch >= 'A') && (ch <= 'Z')) {
2401 shift = true;
2402 }
2403 alt = true;
2404 events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift));
2405 resetParser();
2406 return;
2407
2408 case ESCAPE_INTERMEDIATE:
2409 if ((ch >= 'P') && (ch <= 'S')) {
2410 // Function key
2411 switch (ch) {
2412 case 'P':
2413 events.add(new TKeypressEvent(kbF1));
2414 break;
2415 case 'Q':
2416 events.add(new TKeypressEvent(kbF2));
2417 break;
2418 case 'R':
2419 events.add(new TKeypressEvent(kbF3));
2420 break;
2421 case 'S':
2422 events.add(new TKeypressEvent(kbF4));
2423 break;
2424 default:
2425 break;
2426 }
2427 resetParser();
2428 return;
2429 }
2430
2431 // Unknown keystroke, ignore
2432 resetParser();
2433 return;
2434
2435 case CSI_ENTRY:
2436 // Numbers - parameter values
2437 if ((ch >= '0') && (ch <= '9')) {
2438 params.set(params.size() - 1,
2439 params.get(params.size() - 1) + ch);
2440 state = ParseState.CSI_PARAM;
2441 return;
2442 }
2443 // Parameter separator
2444 if (ch == ';') {
2445 params.add("");
2446 return;
2447 }
2448
2449 if ((ch >= 0x30) && (ch <= 0x7E)) {
2450 switch (ch) {
2451 case 'A':
2452 // Up
2453 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
2454 resetParser();
2455 return;
2456 case 'B':
2457 // Down
2458 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
2459 resetParser();
2460 return;
2461 case 'C':
2462 // Right
2463 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
2464 resetParser();
2465 return;
2466 case 'D':
2467 // Left
2468 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
2469 resetParser();
2470 return;
2471 case 'H':
2472 // Home
2473 events.add(new TKeypressEvent(kbHome));
2474 resetParser();
2475 return;
2476 case 'F':
2477 // End
2478 events.add(new TKeypressEvent(kbEnd));
2479 resetParser();
2480 return;
2481 case 'Z':
2482 // CBT - Cursor backward X tab stops (default 1)
2483 events.add(new TKeypressEvent(kbBackTab));
2484 resetParser();
2485 return;
2486 case 'M':
2487 // Mouse position
2488 state = ParseState.MOUSE;
2489 return;
2490 case '<':
2491 // Mouse position, SGR (1006) coordinates
2492 state = ParseState.MOUSE_SGR;
2493 return;
2494 default:
2495 break;
2496 }
2497 }
2498
2499 // Unknown keystroke, ignore
2500 resetParser();
2501 return;
2502
2503 case MOUSE_SGR:
2504 // Numbers - parameter values
2505 if ((ch >= '0') && (ch <= '9')) {
2506 params.set(params.size() - 1,
2507 params.get(params.size() - 1) + ch);
2508 return;
2509 }
2510 // Parameter separator
2511 if (ch == ';') {
2512 params.add("");
2513 return;
2514 }
2515
2516 switch (ch) {
2517 case 'M':
2518 // Generate a mouse press event
2519 TInputEvent event = parseMouseSGR(false);
2520 if (event != null) {
2521 events.add(event);
2522 }
2523 resetParser();
2524 return;
2525 case 'm':
2526 // Generate a mouse release event
2527 event = parseMouseSGR(true);
2528 if (event != null) {
2529 events.add(event);
2530 }
2531 resetParser();
2532 return;
2533 default:
2534 break;
2535 }
2536
2537 // Unknown keystroke, ignore
2538 resetParser();
2539 return;
2540
2541 case CSI_PARAM:
2542 // Numbers - parameter values
2543 if ((ch >= '0') && (ch <= '9')) {
2544 params.set(params.size() - 1,
2545 params.get(params.size() - 1) + ch);
2546 state = ParseState.CSI_PARAM;
2547 return;
2548 }
2549 // Parameter separator
2550 if (ch == ';') {
2551 params.add("");
2552 return;
2553 }
2554
2555 if (ch == '~') {
2556 events.add(csiFnKey());
2557 resetParser();
2558 return;
2559 }
2560
2561 if ((ch >= 0x30) && (ch <= 0x7E)) {
2562 switch (ch) {
2563 case 'A':
2564 // Up
2565 if (params.size() > 1) {
2566 shift = csiIsShift(params.get(1));
2567 alt = csiIsAlt(params.get(1));
2568 ctrl = csiIsCtrl(params.get(1));
2569 }
2570 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
2571 resetParser();
2572 return;
2573 case 'B':
2574 // Down
2575 if (params.size() > 1) {
2576 shift = csiIsShift(params.get(1));
2577 alt = csiIsAlt(params.get(1));
2578 ctrl = csiIsCtrl(params.get(1));
2579 }
2580 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
2581 resetParser();
2582 return;
2583 case 'C':
2584 // Right
2585 if (params.size() > 1) {
2586 shift = csiIsShift(params.get(1));
2587 alt = csiIsAlt(params.get(1));
2588 ctrl = csiIsCtrl(params.get(1));
2589 }
2590 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
2591 resetParser();
2592 return;
2593 case 'D':
2594 // Left
2595 if (params.size() > 1) {
2596 shift = csiIsShift(params.get(1));
2597 alt = csiIsAlt(params.get(1));
2598 ctrl = csiIsCtrl(params.get(1));
2599 }
2600 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
2601 resetParser();
2602 return;
2603 case 'H':
2604 // Home
2605 if (params.size() > 1) {
2606 shift = csiIsShift(params.get(1));
2607 alt = csiIsAlt(params.get(1));
2608 ctrl = csiIsCtrl(params.get(1));
2609 }
2610 events.add(new TKeypressEvent(kbHome, alt, ctrl, shift));
2611 resetParser();
2612 return;
2613 case 'F':
2614 // End
2615 if (params.size() > 1) {
2616 shift = csiIsShift(params.get(1));
2617 alt = csiIsAlt(params.get(1));
2618 ctrl = csiIsCtrl(params.get(1));
2619 }
2620 events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift));
2621 resetParser();
2622 return;
2623 case 't':
2624 // windowOps
2625 if ((params.size() > 2) && (params.get(0).equals("4"))) {
2626 if (debugToStderr) {
2627 System.err.printf("windowOp pixels: " +
2628 "height %s width %s\n",
2629 params.get(1), params.get(2));
2630 }
2631 try {
2632 widthPixels = Integer.parseInt(params.get(2));
2633 heightPixels = Integer.parseInt(params.get(1));
2634 } catch (NumberFormatException e) {
2635 if (debugToStderr) {
2636 e.printStackTrace();
2637 }
2638 }
2639 if (widthPixels <= 0) {
2640 widthPixels = 640;
2641 }
2642 if (heightPixels <= 0) {
2643 heightPixels = 400;
2644 }
2645 }
2646 resetParser();
2647 return;
2648 default:
2649 break;
2650 }
2651 }
2652
2653 // Unknown keystroke, ignore
2654 resetParser();
2655 return;
2656
2657 case MOUSE:
2658 params.set(0, params.get(params.size() - 1) + ch);
2659 if (params.get(0).length() == 3) {
2660 // We have enough to generate a mouse event
2661 events.add(parseMouse());
2662 resetParser();
2663 }
2664 return;
2665
2666 default:
2667 break;
2668 }
2669
2670 // This "should" be impossible to reach
2671 return;
2672 }
2673
2674 /**
2675 * Request (u)xterm to report the current window size dimensions.
2676 *
2677 * @return the string to emit to xterm
2678 */
2679 private String xtermReportWindowPixelDimensions() {
2680 return "\033[14t";
2681 }
2682
2683 /**
2684 * Tell (u)xterm that we want alt- keystrokes to send escape + character
2685 * rather than set the 8th bit. Anyone who wants UTF8 should want this
2686 * enabled.
2687 *
2688 * @param on if true, enable metaSendsEscape
2689 * @return the string to emit to xterm
2690 */
2691 private String xtermMetaSendsEscape(final boolean on) {
2692 if (on) {
2693 return "\033[?1036h\033[?1034l";
2694 }
2695 return "\033[?1036l";
2696 }
2697
2698 /**
2699 * Create an xterm OSC sequence to change the window title.
2700 *
2701 * @param title the new title
2702 * @return the string to emit to xterm
2703 */
2704 private String getSetTitleString(final String title) {
2705 return "\033]2;" + title + "\007";
2706 }
2707
2708 // ------------------------------------------------------------------------
2709 // Sixel output support ---------------------------------------------------
2710 // ------------------------------------------------------------------------
2711
2712 /**
2713 * Start a sixel string for display one row's worth of bitmap data.
2714 *
2715 * @param x column coordinate. 0 is the left-most column.
2716 * @param y row coordinate. 0 is the top-most row.
2717 * @return the string to emit to an ANSI / ECMA-style terminal
2718 */
2719 private String startSixel(final int x, final int y) {
2720 StringBuilder sb = new StringBuilder();
2721
2722 assert (sixel == true);
2723
2724 // Place the cursor
2725 sb.append(gotoXY(x, y));
2726
2727 // DCS
2728 sb.append("\033Pq");
2729
2730 if (palette == null) {
2731 palette = new SixelPalette();
2732 }
2733
2734 return sb.toString();
2735 }
2736
2737 /**
2738 * End a sixel string for display one row's worth of bitmap data.
2739 *
2740 * @return the string to emit to an ANSI / ECMA-style terminal
2741 */
2742 private String endSixel() {
2743 assert (sixel == true);
2744
2745 // ST
2746 return ("\033\\");
2747 }
2748
2749 /**
2750 * Create a sixel string representing a row of several cells containing
2751 * bitmap data.
2752 *
2753 * @param x column coordinate. 0 is the left-most column.
2754 * @param y row coordinate. 0 is the top-most row.
2755 * @param cells the cells containing the bitmap data
2756 * @return the string to emit to an ANSI / ECMA-style terminal
2757 */
2758 private String toSixel(final int x, final int y,
2759 final ArrayList<Cell> cells) {
2760
2761 StringBuilder sb = new StringBuilder();
2762
2763 assert (cells != null);
2764 assert (cells.size() > 0);
2765 assert (cells.get(0).getImage() != null);
2766
2767 if (sixel == false) {
2768 sb.append(normal());
2769 sb.append(gotoXY(x, y));
2770 for (int i = 0; i < cells.size(); i++) {
2771 sb.append(' ');
2772 }
2773 return sb.toString();
2774 }
2775
2776 if (sixelCache == null) {
2777 sixelCache = new SixelCache(height * 10);
2778 }
2779
2780 // Save and get rows to/from the cache that do NOT have inverted
2781 // cells.
2782 boolean saveInCache = true;
2783 for (Cell cell: cells) {
2784 if (cell.isInvertedImage()) {
2785 saveInCache = false;
2786 }
2787 }
2788 if (saveInCache) {
2789 String cachedResult = sixelCache.get(cells);
2790 if (cachedResult != null) {
2791 // System.err.println("CACHE HIT");
2792 sb.append(startSixel(x, y));
2793 sb.append(cachedResult);
2794 sb.append(endSixel());
2795 return sb.toString();
2796 }
2797 // System.err.println("CACHE MISS");
2798 }
2799
2800 int imageWidth = cells.get(0).getImage().getWidth();
2801 int imageHeight = cells.get(0).getImage().getHeight();
2802
2803 // cells.get(x).getImage() has a dithered bitmap containing indexes
2804 // into the color palette. Piece these together into one larger
2805 // image for final rendering.
2806 int totalWidth = 0;
2807 int fullWidth = cells.size() * getTextWidth();
2808 int fullHeight = getTextHeight();
2809 for (int i = 0; i < cells.size(); i++) {
2810 totalWidth += cells.get(i).getImage().getWidth();
2811 }
2812
2813 BufferedImage image = new BufferedImage(fullWidth,
2814 fullHeight, BufferedImage.TYPE_INT_ARGB);
2815
2816 int [] rgbArray;
2817 for (int i = 0; i < cells.size() - 1; i++) {
2818 if (cells.get(i).isInvertedImage()) {
2819 rgbArray = new int[imageWidth * imageHeight];
2820 for (int j = 0; j < rgbArray.length; j++) {
2821 rgbArray[j] = 0xFFFFFF;
2822 }
2823 } else {
2824 rgbArray = cells.get(i).getImage().getRGB(0, 0,
2825 imageWidth, imageHeight, null, 0, imageWidth);
2826 }
2827
2828 /*
2829 System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
2830 i * imageWidth, 0, imageWidth, imageHeight,
2831 0, imageWidth);
2832 System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
2833 fullWidth, fullHeight, cells.size(), getTextWidth());
2834 */
2835
2836 image.setRGB(i * imageWidth, 0, imageWidth, imageHeight,
2837 rgbArray, 0, imageWidth);
2838 if (imageHeight < fullHeight) {
2839 int backgroundColor = cells.get(i).getBackground().getRGB();
2840 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
2841 for (int imageY = imageHeight; imageY < fullHeight;
2842 imageY++) {
2843
2844 image.setRGB(imageX, imageY, backgroundColor);
2845 }
2846 }
2847 }
2848 }
2849 totalWidth -= ((cells.size() - 1) * imageWidth);
2850 if (cells.get(cells.size() - 1).isInvertedImage()) {
2851 rgbArray = new int[totalWidth * imageHeight];
2852 for (int j = 0; j < rgbArray.length; j++) {
2853 rgbArray[j] = 0xFFFFFF;
2854 }
2855 } else {
2856 rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
2857 totalWidth, imageHeight, null, 0, totalWidth);
2858 }
2859 image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
2860 imageHeight, rgbArray, 0, totalWidth);
2861
2862 if (totalWidth < getTextWidth()) {
2863 int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
2864
2865 for (int imageX = image.getWidth() - totalWidth;
2866 imageX < image.getWidth(); imageX++) {
2867
2868 for (int imageY = 0; imageY < fullHeight; imageY++) {
2869 image.setRGB(imageX, imageY, backgroundColor);
2870 }
2871 }
2872 }
2873
2874 // Dither the image. It is ok to lose the original here.
2875 if (palette == null) {
2876 palette = new SixelPalette();
2877 }
2878 image = palette.ditherImage(image);
2879
2880 // Emit the palette, but only for the colors actually used by these
2881 // cells.
2882 boolean [] usedColors = new boolean[MAX_COLOR_REGISTERS];
2883 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
2884 for (int imageY = 0; imageY < image.getHeight(); imageY++) {
2885 usedColors[image.getRGB(imageX, imageY)] = true;
2886 }
2887 }
2888 palette.emitPalette(sb, usedColors);
2889
2890 // Render the entire row of cells.
2891 for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
2892 int [][] sixels = new int[image.getWidth()][6];
2893
2894 // See which colors are actually used in this band of sixels.
2895 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
2896 for (int imageY = 0;
2897 (imageY < 6) && (imageY + currentRow < fullHeight);
2898 imageY++) {
2899
2900 int colorIdx = image.getRGB(imageX, imageY + currentRow);
2901 assert (colorIdx >= 0);
2902 assert (colorIdx < MAX_COLOR_REGISTERS);
2903
2904 sixels[imageX][imageY] = colorIdx;
2905 }
2906 }
2907
2908 for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
2909 boolean isUsed = false;
2910 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
2911 for (int j = 0; j < 6; j++) {
2912 if (sixels[imageX][j] == i) {
2913 isUsed = true;
2914 }
2915 }
2916 }
2917 if (isUsed == false) {
2918 continue;
2919 }
2920
2921 // Set to the beginning of scan line for the next set of
2922 // colored pixels, and select the color.
2923 sb.append(String.format("$#%d", i));
2924
2925 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
2926
2927 // Add up all the pixels that match this color.
2928 int data = 0;
2929 for (int j = 0;
2930 (j < 6) && (currentRow + j < fullHeight);
2931 j++) {
2932
2933 if (sixels[imageX][j] == i) {
2934 switch (j) {
2935 case 0:
2936 data += 1;
2937 break;
2938 case 1:
2939 data += 2;
2940 break;
2941 case 2:
2942 data += 4;
2943 break;
2944 case 3:
2945 data += 8;
2946 break;
2947 case 4:
2948 data += 16;
2949 break;
2950 case 5:
2951 data += 32;
2952 break;
2953 }
2954 }
2955 }
2956 assert (data >= 0);
2957 assert (data < 127);
2958 data += 63;
2959 sb.append((char) data);
2960 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
2961 } // for (int i = 0; i < MAX_COLOR_REGISTERS; i++)
2962
2963 // Advance to the next scan line.
2964 sb.append("-");
2965
2966 } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
2967
2968 // Kill the very last "-", because it is unnecessary.
2969 sb.deleteCharAt(sb.length() - 1);
2970
2971 if (saveInCache) {
2972 // This row is OK to save into the cache.
2973 sixelCache.put(cells, sb.toString());
2974 }
2975
2976 return (startSixel(x, y) + sb.toString() + endSixel());
2977 }
2978
2979 // ------------------------------------------------------------------------
2980 // End sixel output support -----------------------------------------------
2981 // ------------------------------------------------------------------------
2982
2983 /**
2984 * Create a SGR parameter sequence for a single color change.
2985 *
2986 * @param bold if true, set bold
2987 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
2988 * @param foreground if true, this is a foreground color
2989 * @return the string to emit to an ANSI / ECMA-style terminal,
2990 * e.g. "\033[42m"
2991 */
2992 private String color(final boolean bold, final Color color,
2993 final boolean foreground) {
2994 return color(color, foreground, true) +
2995 rgbColor(bold, color, foreground);
2996 }
2997
2998 /**
2999 * Create a T.416 RGB parameter sequence for a single color change.
3000 *
3001 * @param colorRGB a 24-bit RGB value for foreground color
3002 * @param foreground if true, this is a foreground color
3003 * @return the string to emit to an ANSI / ECMA-style terminal,
3004 * e.g. "\033[42m"
3005 */
3006 private String colorRGB(final int colorRGB, final boolean foreground) {
3007
3008 int colorRed = (colorRGB >>> 16) & 0xFF;
3009 int colorGreen = (colorRGB >>> 8) & 0xFF;
3010 int colorBlue = colorRGB & 0xFF;
3011
3012 StringBuilder sb = new StringBuilder();
3013 if (foreground) {
3014 sb.append("\033[38;2;");
3015 } else {
3016 sb.append("\033[48;2;");
3017 }
3018 sb.append(String.format("%d;%d;%dm", colorRed, colorGreen, colorBlue));
3019 return sb.toString();
3020 }
3021
3022 /**
3023 * Create a T.416 RGB parameter sequence for both foreground and
3024 * background color change.
3025 *
3026 * @param foreColorRGB a 24-bit RGB value for foreground color
3027 * @param backColorRGB a 24-bit RGB value for foreground color
3028 * @return the string to emit to an ANSI / ECMA-style terminal,
3029 * e.g. "\033[42m"
3030 */
3031 private String colorRGB(final int foreColorRGB, final int backColorRGB) {
3032 int foreColorRed = (foreColorRGB >>> 16) & 0xFF;
3033 int foreColorGreen = (foreColorRGB >>> 8) & 0xFF;
3034 int foreColorBlue = foreColorRGB & 0xFF;
3035 int backColorRed = (backColorRGB >>> 16) & 0xFF;
3036 int backColorGreen = (backColorRGB >>> 8) & 0xFF;
3037 int backColorBlue = backColorRGB & 0xFF;
3038
3039 StringBuilder sb = new StringBuilder();
3040 sb.append(String.format("\033[38;2;%d;%d;%dm",
3041 foreColorRed, foreColorGreen, foreColorBlue));
3042 sb.append(String.format("\033[48;2;%d;%d;%dm",
3043 backColorRed, backColorGreen, backColorBlue));
3044 return sb.toString();
3045 }
3046
3047 /**
3048 * Create a T.416 RGB parameter sequence for a single color change.
3049 *
3050 * @param bold if true, set bold
3051 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3052 * @param foreground if true, this is a foreground color
3053 * @return the string to emit to an xterm terminal with RGB support,
3054 * e.g. "\033[38;2;RR;GG;BBm"
3055 */
3056 private String rgbColor(final boolean bold, final Color color,
3057 final boolean foreground) {
3058 if (doRgbColor == false) {
3059 return "";
3060 }
3061 StringBuilder sb = new StringBuilder("\033[");
3062 if (bold) {
3063 // Bold implies foreground only
3064 sb.append("38;2;");
3065 if (color.equals(Color.BLACK)) {
3066 sb.append("84;84;84");
3067 } else if (color.equals(Color.RED)) {
3068 sb.append("252;84;84");
3069 } else if (color.equals(Color.GREEN)) {
3070 sb.append("84;252;84");
3071 } else if (color.equals(Color.YELLOW)) {
3072 sb.append("252;252;84");
3073 } else if (color.equals(Color.BLUE)) {
3074 sb.append("84;84;252");
3075 } else if (color.equals(Color.MAGENTA)) {
3076 sb.append("252;84;252");
3077 } else if (color.equals(Color.CYAN)) {
3078 sb.append("84;252;252");
3079 } else if (color.equals(Color.WHITE)) {
3080 sb.append("252;252;252");
3081 }
3082 } else {
3083 if (foreground) {
3084 sb.append("38;2;");
3085 } else {
3086 sb.append("48;2;");
3087 }
3088 if (color.equals(Color.BLACK)) {
3089 sb.append("0;0;0");
3090 } else if (color.equals(Color.RED)) {
3091 sb.append("168;0;0");
3092 } else if (color.equals(Color.GREEN)) {
3093 sb.append("0;168;0");
3094 } else if (color.equals(Color.YELLOW)) {
3095 sb.append("168;84;0");
3096 } else if (color.equals(Color.BLUE)) {
3097 sb.append("0;0;168");
3098 } else if (color.equals(Color.MAGENTA)) {
3099 sb.append("168;0;168");
3100 } else if (color.equals(Color.CYAN)) {
3101 sb.append("0;168;168");
3102 } else if (color.equals(Color.WHITE)) {
3103 sb.append("168;168;168");
3104 }
3105 }
3106 sb.append("m");
3107 return sb.toString();
3108 }
3109
3110 /**
3111 * Create a T.416 RGB parameter sequence for both foreground and
3112 * background color change.
3113 *
3114 * @param bold if true, set bold
3115 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3116 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3117 * @return the string to emit to an xterm terminal with RGB support,
3118 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
3119 */
3120 private String rgbColor(final boolean bold, final Color foreColor,
3121 final Color backColor) {
3122 if (doRgbColor == false) {
3123 return "";
3124 }
3125
3126 return rgbColor(bold, foreColor, true) +
3127 rgbColor(false, backColor, false);
3128 }
3129
3130 /**
3131 * Create a SGR parameter sequence for a single color change.
3132 *
3133 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
3134 * @param foreground if true, this is a foreground color
3135 * @param header if true, make the full header, otherwise just emit the
3136 * color parameter e.g. "42;"
3137 * @return the string to emit to an ANSI / ECMA-style terminal,
3138 * e.g. "\033[42m"
3139 */
3140 private String color(final Color color, final boolean foreground,
3141 final boolean header) {
3142
3143 int ecmaColor = color.getValue();
3144
3145 // Convert Color.* values to SGR numerics
3146 if (foreground) {
3147 ecmaColor += 30;
3148 } else {
3149 ecmaColor += 40;
3150 }
3151
3152 if (header) {
3153 return String.format("\033[%dm", ecmaColor);
3154 } else {
3155 return String.format("%d;", ecmaColor);
3156 }
3157 }
3158
3159 /**
3160 * Create a SGR parameter sequence for both foreground and background
3161 * color change.
3162 *
3163 * @param bold if true, set bold
3164 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3165 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3166 * @return the string to emit to an ANSI / ECMA-style terminal,
3167 * e.g. "\033[31;42m"
3168 */
3169 private String color(final boolean bold, final Color foreColor,
3170 final Color backColor) {
3171 return color(foreColor, backColor, true) +
3172 rgbColor(bold, foreColor, backColor);
3173 }
3174
3175 /**
3176 * Create a SGR parameter sequence for both foreground and
3177 * background color change.
3178 *
3179 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3180 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3181 * @param header if true, make the full header, otherwise just emit the
3182 * color parameter e.g. "31;42;"
3183 * @return the string to emit to an ANSI / ECMA-style terminal,
3184 * e.g. "\033[31;42m"
3185 */
3186 private String color(final Color foreColor, final Color backColor,
3187 final boolean header) {
3188
3189 int ecmaForeColor = foreColor.getValue();
3190 int ecmaBackColor = backColor.getValue();
3191
3192 // Convert Color.* values to SGR numerics
3193 ecmaBackColor += 40;
3194 ecmaForeColor += 30;
3195
3196 if (header) {
3197 return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
3198 } else {
3199 return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
3200 }
3201 }
3202
3203 /**
3204 * Create a SGR parameter sequence for foreground, background, and
3205 * several attributes. This sequence first resets all attributes to
3206 * default, then sets attributes as per the parameters.
3207 *
3208 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
3209 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
3210 * @param bold if true, set bold
3211 * @param reverse if true, set reverse
3212 * @param blink if true, set blink
3213 * @param underline if true, set underline
3214 * @return the string to emit to an ANSI / ECMA-style terminal,
3215 * e.g. "\033[0;1;31;42m"
3216 */
3217 private String color(final Color foreColor, final Color backColor,
3218 final boolean bold, final boolean reverse, final boolean blink,
3219 final boolean underline) {
3220
3221 int ecmaForeColor = foreColor.getValue();
3222 int ecmaBackColor = backColor.getValue();
3223
3224 // Convert Color.* values to SGR numerics
3225 ecmaBackColor += 40;
3226 ecmaForeColor += 30;
3227
3228 StringBuilder sb = new StringBuilder();
3229 if ( bold && reverse && blink && !underline ) {
3230 sb.append("\033[0;1;7;5;");
3231 } else if ( bold && reverse && !blink && !underline ) {
3232 sb.append("\033[0;1;7;");
3233 } else if ( !bold && reverse && blink && !underline ) {
3234 sb.append("\033[0;7;5;");
3235 } else if ( bold && !reverse && blink && !underline ) {
3236 sb.append("\033[0;1;5;");
3237 } else if ( bold && !reverse && !blink && !underline ) {
3238 sb.append("\033[0;1;");
3239 } else if ( !bold && reverse && !blink && !underline ) {
3240 sb.append("\033[0;7;");
3241 } else if ( !bold && !reverse && blink && !underline) {
3242 sb.append("\033[0;5;");
3243 } else if ( bold && reverse && blink && underline ) {
3244 sb.append("\033[0;1;7;5;4;");
3245 } else if ( bold && reverse && !blink && underline ) {
3246 sb.append("\033[0;1;7;4;");
3247 } else if ( !bold && reverse && blink && underline ) {
3248 sb.append("\033[0;7;5;4;");
3249 } else if ( bold && !reverse && blink && underline ) {
3250 sb.append("\033[0;1;5;4;");
3251 } else if ( bold && !reverse && !blink && underline ) {
3252 sb.append("\033[0;1;4;");
3253 } else if ( !bold && reverse && !blink && underline ) {
3254 sb.append("\033[0;7;4;");
3255 } else if ( !bold && !reverse && blink && underline) {
3256 sb.append("\033[0;5;4;");
3257 } else if ( !bold && !reverse && !blink && underline) {
3258 sb.append("\033[0;4;");
3259 } else {
3260 assert (!bold && !reverse && !blink && !underline);
3261 sb.append("\033[0;");
3262 }
3263 sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
3264 sb.append(rgbColor(bold, foreColor, backColor));
3265 return sb.toString();
3266 }
3267
3268 /**
3269 * Create a SGR parameter sequence for foreground, background, and
3270 * several attributes. This sequence first resets all attributes to
3271 * default, then sets attributes as per the parameters.
3272 *
3273 * @param foreColorRGB a 24-bit RGB value for foreground color
3274 * @param backColorRGB a 24-bit RGB value for foreground color
3275 * @param bold if true, set bold
3276 * @param reverse if true, set reverse
3277 * @param blink if true, set blink
3278 * @param underline if true, set underline
3279 * @return the string to emit to an ANSI / ECMA-style terminal,
3280 * e.g. "\033[0;1;31;42m"
3281 */
3282 private String colorRGB(final int foreColorRGB, final int backColorRGB,
3283 final boolean bold, final boolean reverse, final boolean blink,
3284 final boolean underline) {
3285
3286 int foreColorRed = (foreColorRGB >>> 16) & 0xFF;
3287 int foreColorGreen = (foreColorRGB >>> 8) & 0xFF;
3288 int foreColorBlue = foreColorRGB & 0xFF;
3289 int backColorRed = (backColorRGB >>> 16) & 0xFF;
3290 int backColorGreen = (backColorRGB >>> 8) & 0xFF;
3291 int backColorBlue = backColorRGB & 0xFF;
3292
3293 StringBuilder sb = new StringBuilder();
3294 if ( bold && reverse && blink && !underline ) {
3295 sb.append("\033[0;1;7;5;");
3296 } else if ( bold && reverse && !blink && !underline ) {
3297 sb.append("\033[0;1;7;");
3298 } else if ( !bold && reverse && blink && !underline ) {
3299 sb.append("\033[0;7;5;");
3300 } else if ( bold && !reverse && blink && !underline ) {
3301 sb.append("\033[0;1;5;");
3302 } else if ( bold && !reverse && !blink && !underline ) {
3303 sb.append("\033[0;1;");
3304 } else if ( !bold && reverse && !blink && !underline ) {
3305 sb.append("\033[0;7;");
3306 } else if ( !bold && !reverse && blink && !underline) {
3307 sb.append("\033[0;5;");
3308 } else if ( bold && reverse && blink && underline ) {
3309 sb.append("\033[0;1;7;5;4;");
3310 } else if ( bold && reverse && !blink && underline ) {
3311 sb.append("\033[0;1;7;4;");
3312 } else if ( !bold && reverse && blink && underline ) {
3313 sb.append("\033[0;7;5;4;");
3314 } else if ( bold && !reverse && blink && underline ) {
3315 sb.append("\033[0;1;5;4;");
3316 } else if ( bold && !reverse && !blink && underline ) {
3317 sb.append("\033[0;1;4;");
3318 } else if ( !bold && reverse && !blink && underline ) {
3319 sb.append("\033[0;7;4;");
3320 } else if ( !bold && !reverse && blink && underline) {
3321 sb.append("\033[0;5;4;");
3322 } else if ( !bold && !reverse && !blink && underline) {
3323 sb.append("\033[0;4;");
3324 } else {
3325 assert (!bold && !reverse && !blink && !underline);
3326 sb.append("\033[0;");
3327 }
3328
3329 sb.append("m\033[38;2;");
3330 sb.append(String.format("%d;%d;%d", foreColorRed, foreColorGreen,
3331 foreColorBlue));
3332 sb.append("m\033[48;2;");
3333 sb.append(String.format("%d;%d;%d", backColorRed, backColorGreen,
3334 backColorBlue));
3335 sb.append("m");
3336 return sb.toString();
3337 }
3338
3339 /**
3340 * Create a SGR parameter sequence to reset to defaults.
3341 *
3342 * @return the string to emit to an ANSI / ECMA-style terminal,
3343 * e.g. "\033[0m"
3344 */
3345 private String normal() {
3346 return normal(true) + rgbColor(false, Color.WHITE, Color.BLACK);
3347 }
3348
3349 /**
3350 * Create a SGR parameter sequence to reset to defaults.
3351 *
3352 * @param header if true, make the full header, otherwise just emit the
3353 * bare parameter e.g. "0;"
3354 * @return the string to emit to an ANSI / ECMA-style terminal,
3355 * e.g. "\033[0m"
3356 */
3357 private String normal(final boolean header) {
3358 if (header) {
3359 return "\033[0;37;40m";
3360 }
3361 return "0;37;40";
3362 }
3363
3364 /**
3365 * Create a SGR parameter sequence for enabling the visible cursor.
3366 *
3367 * @param on if true, turn on cursor
3368 * @return the string to emit to an ANSI / ECMA-style terminal
3369 */
3370 private String cursor(final boolean on) {
3371 if (on && !cursorOn) {
3372 cursorOn = true;
3373 return "\033[?25h";
3374 }
3375 if (!on && cursorOn) {
3376 cursorOn = false;
3377 return "\033[?25l";
3378 }
3379 return "";
3380 }
3381
3382 /**
3383 * Clear the entire screen. Because some terminals use back-color-erase,
3384 * set the color to white-on-black beforehand.
3385 *
3386 * @return the string to emit to an ANSI / ECMA-style terminal
3387 */
3388 private String clearAll() {
3389 return "\033[0;37;40m\033[2J";
3390 }
3391
3392 /**
3393 * Clear the line from the cursor (inclusive) to the end of the screen.
3394 * Because some terminals use back-color-erase, set the color to
3395 * white-on-black beforehand.
3396 *
3397 * @return the string to emit to an ANSI / ECMA-style terminal
3398 */
3399 private String clearRemainingLine() {
3400 return "\033[0;37;40m\033[K";
3401 }
3402
3403 /**
3404 * Move the cursor to (x, y).
3405 *
3406 * @param x column coordinate. 0 is the left-most column.
3407 * @param y row coordinate. 0 is the top-most row.
3408 * @return the string to emit to an ANSI / ECMA-style terminal
3409 */
3410 private String gotoXY(final int x, final int y) {
3411 return String.format("\033[%d;%dH", y + 1, x + 1);
3412 }
3413
3414 /**
3415 * Tell (u)xterm that we want to receive mouse events based on "Any event
3416 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
3417 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
3418 * See
3419 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
3420 *
3421 * Note that this also sets the alternate/primary screen buffer.
3422 *
3423 * Finally, also emit a Privacy Message sequence that Jexer recognizes to
3424 * mean "hide the mouse pointer." We have to use our own sequence to do
3425 * this because there is no standard in xterm for unilaterally hiding the
3426 * pointer all the time (regardless of typing).
3427 *
3428 * @param on If true, enable mouse report and use the alternate screen
3429 * buffer. If false disable mouse reporting and use the primary screen
3430 * buffer.
3431 * @return the string to emit to xterm
3432 */
3433 private String mouse(final boolean on) {
3434 if (on) {
3435 return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
3436 }
3437 return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";
3438 }
3439
3440 }