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