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