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