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