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