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