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