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