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