Bug fixes
[nikiroo-utils.git] / src / jexer / backend / ECMA48Terminal.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2017 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.backend;
30
31 import java.io.BufferedReader;
32 import java.io.FileDescriptor;
33 import java.io.FileInputStream;
34 import java.io.InputStream;
35 import java.io.InputStreamReader;
36 import java.io.IOException;
37 import java.io.OutputStream;
38 import java.io.OutputStreamWriter;
39 import java.io.PrintWriter;
40 import java.io.Reader;
41 import java.io.UnsupportedEncodingException;
42 import java.util.ArrayList;
43 import java.util.Date;
44 import java.util.List;
45 import java.util.LinkedList;
46
47 import jexer.bits.Cell;
48 import jexer.bits.CellAttributes;
49 import jexer.bits.Color;
50 import jexer.event.TInputEvent;
51 import jexer.event.TKeypressEvent;
52 import jexer.event.TMouseEvent;
53 import jexer.event.TResizeEvent;
54 import static jexer.TKeypress.*;
55
56 /**
57 * This class reads keystrokes and mouse events and emits output to ANSI
58 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
59 */
60 public final class ECMA48Terminal extends LogicalScreen
61 implements TerminalReader, Runnable {
62
63 /**
64 * Emit debugging to stderr.
65 */
66 private boolean debugToStderr = false;
67
68 /**
69 * If true, emit T.416-style RGB colors. This is a) expensive in
70 * bandwidth, and b) potentially terrible looking for non-xterms.
71 */
72 private static boolean doRgbColor = false;
73
74 /**
75 * The session information.
76 */
77 private SessionInfo sessionInfo;
78
79 /**
80 * Getter for sessionInfo.
81 *
82 * @return the SessionInfo
83 */
84 public SessionInfo getSessionInfo() {
85 return sessionInfo;
86 }
87
88 /**
89 * The event queue, filled up by a thread reading on input.
90 */
91 private List<TInputEvent> eventQueue;
92
93 /**
94 * If true, we want the reader thread to exit gracefully.
95 */
96 private boolean stopReaderThread;
97
98 /**
99 * The reader thread.
100 */
101 private Thread readerThread;
102
103 /**
104 * Parameters being collected. E.g. if the string is \033[1;3m, then
105 * params[0] will be 1 and params[1] will be 3.
106 */
107 private ArrayList<String> params;
108
109 /**
110 * States in the input parser.
111 */
112 private enum ParseState {
113 GROUND,
114 ESCAPE,
115 ESCAPE_INTERMEDIATE,
116 CSI_ENTRY,
117 CSI_PARAM,
118 MOUSE,
119 MOUSE_SGR,
120 }
121
122 /**
123 * Current parsing state.
124 */
125 private ParseState state;
126
127 /**
128 * The time we entered ESCAPE. If we get a bare escape without a code
129 * following it, this is used to return that bare escape.
130 */
131 private long escapeTime;
132
133 /**
134 * The time we last checked the window size. We try not to spawn stty
135 * more than once per second.
136 */
137 private long windowSizeTime;
138
139 /**
140 * true if mouse1 was down. Used to report mouse1 on the release event.
141 */
142 private boolean mouse1;
143
144 /**
145 * true if mouse2 was down. Used to report mouse2 on the release event.
146 */
147 private boolean mouse2;
148
149 /**
150 * true if mouse3 was down. Used to report mouse3 on the release event.
151 */
152 private boolean mouse3;
153
154 /**
155 * Cache the cursor visibility value so we only emit the sequence when we
156 * need to.
157 */
158 private boolean cursorOn = true;
159
160 /**
161 * Cache the last window size to figure out if a TResizeEvent needs to be
162 * generated.
163 */
164 private TResizeEvent windowResize = null;
165
166 /**
167 * If true, then we changed System.in and need to change it back.
168 */
169 private boolean setRawMode;
170
171 /**
172 * The terminal's input. If an InputStream is not specified in the
173 * constructor, then this InputStreamReader will be bound to System.in
174 * with UTF-8 encoding.
175 */
176 private Reader input;
177
178 /**
179 * The terminal's raw InputStream. If an InputStream is not specified in
180 * the constructor, then this InputReader will be bound to System.in.
181 * This is used by run() to see if bytes are available() before calling
182 * (Reader)input.read().
183 */
184 private InputStream inputStream;
185
186 /**
187 * The terminal's output. If an OutputStream is not specified in the
188 * constructor, then this PrintWriter will be bound to System.out with
189 * UTF-8 encoding.
190 */
191 private PrintWriter output;
192
193 /**
194 * The listening object that run() wakes up on new input.
195 */
196 private Object listener;
197
198 /**
199 * Set listener to a different Object.
200 *
201 * @param listener the new listening object that run() wakes up on new
202 * input
203 */
204 public void setListener(final Object listener) {
205 this.listener = listener;
206 }
207
208 /**
209 * Get the output writer.
210 *
211 * @return the Writer
212 */
213 public PrintWriter getOutput() {
214 return output;
215 }
216
217 /**
218 * Check if there are events in the queue.
219 *
220 * @return if true, getEvents() has something to return to the backend
221 */
222 public boolean hasEvents() {
223 synchronized (eventQueue) {
224 return (eventQueue.size() > 0);
225 }
226 }
227
228 /**
229 * Call 'stty' to set cooked mode.
230 *
231 * <p>Actually executes '/bin/sh -c stty sane cooked &lt; /dev/tty'
232 */
233 private void sttyCooked() {
234 doStty(false);
235 }
236
237 /**
238 * Call 'stty' to set raw mode.
239 *
240 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
241 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
242 * -parenb cs8 min 1 &lt; /dev/tty'
243 */
244 private void sttyRaw() {
245 doStty(true);
246 }
247
248 /**
249 * Call 'stty' to set raw or cooked mode.
250 *
251 * @param mode if true, set raw mode, otherwise set cooked mode
252 */
253 private void doStty(final boolean mode) {
254 String [] cmdRaw = {
255 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
256 };
257 String [] cmdCooked = {
258 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
259 };
260 try {
261 Process process;
262 if (mode) {
263 process = Runtime.getRuntime().exec(cmdRaw);
264 } else {
265 process = Runtime.getRuntime().exec(cmdCooked);
266 }
267 BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
268 String line = in.readLine();
269 if ((line != null) && (line.length() > 0)) {
270 System.err.println("WEIRD?! Normal output from stty: " + line);
271 }
272 while (true) {
273 BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
274 line = err.readLine();
275 if ((line != null) && (line.length() > 0)) {
276 System.err.println("Error output from stty: " + line);
277 }
278 try {
279 process.waitFor();
280 break;
281 } catch (InterruptedException e) {
282 e.printStackTrace();
283 }
284 }
285 int rc = process.exitValue();
286 if (rc != 0) {
287 System.err.println("stty returned error code: " + rc);
288 }
289 } catch (IOException e) {
290 e.printStackTrace();
291 }
292 }
293
294 /**
295 * Constructor sets up state for getEvent().
296 *
297 * @param listener the object this backend needs to wake up when new
298 * input comes in
299 * @param input an InputStream connected to the remote user, or null for
300 * System.in. If System.in is used, then on non-Windows systems it will
301 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
302 * mode. input is always converted to a Reader with UTF-8 encoding.
303 * @param output an OutputStream connected to the remote user, or null
304 * for System.out. output is always converted to a Writer with UTF-8
305 * encoding.
306 * @throws UnsupportedEncodingException if an exception is thrown when
307 * creating the InputStreamReader
308 */
309 public ECMA48Terminal(final Object listener, final InputStream input,
310 final OutputStream output) throws UnsupportedEncodingException {
311
312 resetParser();
313 mouse1 = false;
314 mouse2 = false;
315 mouse3 = false;
316 stopReaderThread = false;
317 this.listener = listener;
318
319 if (input == null) {
320 // inputStream = System.in;
321 inputStream = new FileInputStream(FileDescriptor.in);
322 sttyRaw();
323 setRawMode = true;
324 } else {
325 inputStream = input;
326 }
327 this.input = new InputStreamReader(inputStream, "UTF-8");
328
329 if (input instanceof SessionInfo) {
330 // This is a TelnetInputStream that exposes window size and
331 // environment variables from the telnet layer.
332 sessionInfo = (SessionInfo) input;
333 }
334 if (sessionInfo == null) {
335 if (input == null) {
336 // Reading right off the tty
337 sessionInfo = new TTYSessionInfo();
338 } else {
339 sessionInfo = new TSessionInfo();
340 }
341 }
342
343 if (output == null) {
344 this.output = new PrintWriter(new OutputStreamWriter(System.out,
345 "UTF-8"));
346 } else {
347 this.output = new PrintWriter(new OutputStreamWriter(output,
348 "UTF-8"));
349 }
350
351 // Enable mouse reporting and metaSendsEscape
352 this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
353 this.output.flush();
354
355 // Query the screen size
356 sessionInfo.queryWindowSize();
357 setDimensions(sessionInfo.getWindowWidth(),
358 sessionInfo.getWindowHeight());
359
360 // Hang onto the window size
361 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
362 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
363
364 // Permit RGB colors only if externally requested
365 if (System.getProperty("jexer.ECMA48.rgbColor") != null) {
366 if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
367 doRgbColor = true;
368 } else {
369 doRgbColor = false;
370 }
371 }
372
373 // Spin up the input reader
374 eventQueue = new LinkedList<TInputEvent>();
375 readerThread = new Thread(this);
376 readerThread.start();
377
378 // Clear the screen
379 this.output.write(clearAll());
380 this.output.flush();
381 }
382
383 /**
384 * Constructor sets up state for getEvent().
385 *
386 * @param listener the object this backend needs to wake up when new
387 * input comes in
388 * @param input the InputStream underlying 'reader'. Its available()
389 * method is used to determine if reader.read() will block or not.
390 * @param reader a Reader connected to the remote user.
391 * @param writer a PrintWriter connected to the remote user.
392 * @param setRawMode if true, set System.in into raw mode with stty.
393 * This should in general not be used. It is here solely for Demo3,
394 * which uses System.in.
395 * @throws IllegalArgumentException if input, reader, or writer are null.
396 */
397 public ECMA48Terminal(final Object listener, final InputStream input,
398 final Reader reader, final PrintWriter writer,
399 final boolean setRawMode) {
400
401 if (input == null) {
402 throw new IllegalArgumentException("InputStream must be specified");
403 }
404 if (reader == null) {
405 throw new IllegalArgumentException("Reader must be specified");
406 }
407 if (writer == null) {
408 throw new IllegalArgumentException("Writer must be specified");
409 }
410 resetParser();
411 mouse1 = false;
412 mouse2 = false;
413 mouse3 = false;
414 stopReaderThread = false;
415 this.listener = listener;
416
417 inputStream = input;
418 this.input = reader;
419
420 if (setRawMode == true) {
421 sttyRaw();
422 }
423 this.setRawMode = setRawMode;
424
425 if (input instanceof SessionInfo) {
426 // This is a TelnetInputStream that exposes window size and
427 // environment variables from the telnet layer.
428 sessionInfo = (SessionInfo) input;
429 }
430 if (sessionInfo == null) {
431 if (setRawMode == true) {
432 // Reading right off the tty
433 sessionInfo = new TTYSessionInfo();
434 } else {
435 sessionInfo = new TSessionInfo();
436 }
437 }
438
439 this.output = writer;
440
441 // Enable mouse reporting and metaSendsEscape
442 this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
443 this.output.flush();
444
445 // Query the screen size
446 sessionInfo.queryWindowSize();
447 setDimensions(sessionInfo.getWindowWidth(),
448 sessionInfo.getWindowHeight());
449
450 // Hang onto the window size
451 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
452 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
453
454 // Permit RGB colors only if externally requested
455 if (System.getProperty("jexer.ECMA48.rgbColor") != null) {
456 if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
457 doRgbColor = true;
458 } else {
459 doRgbColor = false;
460 }
461 }
462
463 // Spin up the input reader
464 eventQueue = new LinkedList<TInputEvent>();
465 readerThread = new Thread(this);
466 readerThread.start();
467
468 // Clear the screen
469 this.output.write(clearAll());
470 this.output.flush();
471 }
472
473 /**
474 * Constructor sets up state for getEvent().
475 *
476 * @param listener the object this backend needs to wake up when new
477 * input comes in
478 * @param input the InputStream underlying 'reader'. Its available()
479 * method is used to determine if reader.read() will block or not.
480 * @param reader a Reader connected to the remote user.
481 * @param writer a PrintWriter connected to the remote user.
482 * @throws IllegalArgumentException if input, reader, or writer are null.
483 */
484 public ECMA48Terminal(final Object listener, final InputStream input,
485 final Reader reader, final PrintWriter writer) {
486
487 this(listener, input, reader, writer, false);
488 }
489
490 /**
491 * Restore terminal to normal state.
492 */
493 public void closeTerminal() {
494
495 // System.err.println("=== shutdown() ==="); System.err.flush();
496
497 // Tell the reader thread to stop looking at input
498 stopReaderThread = true;
499 try {
500 readerThread.join();
501 } catch (InterruptedException e) {
502 e.printStackTrace();
503 }
504
505 // Disable mouse reporting and show cursor
506 output.printf("%s%s%s", mouse(false), cursor(true), normal());
507 output.flush();
508
509 if (setRawMode) {
510 sttyCooked();
511 setRawMode = false;
512 // We don't close System.in/out
513 } else {
514 // Shut down the streams, this should wake up the reader thread
515 // and make it exit.
516 try {
517 if (input != null) {
518 input.close();
519 input = null;
520 }
521 if (output != null) {
522 output.close();
523 output = null;
524 }
525 } catch (IOException e) {
526 e.printStackTrace();
527 }
528 }
529 }
530
531 /**
532 * Flush output.
533 */
534 public void flush() {
535 output.flush();
536 }
537
538 /**
539 * Perform a somewhat-optimal rendering of a line.
540 *
541 * @param y row coordinate. 0 is the top-most row.
542 * @param sb StringBuilder to write escape sequences to
543 * @param lastAttr cell attributes from the last call to flushLine
544 */
545 private void flushLine(final int y, final StringBuilder sb,
546 CellAttributes lastAttr) {
547
548 int lastX = -1;
549 int textEnd = 0;
550 for (int x = 0; x < width; x++) {
551 Cell lCell = logical[x][y];
552 if (!lCell.isBlank()) {
553 textEnd = x;
554 }
555 }
556 // Push textEnd to first column beyond the text area
557 textEnd++;
558
559 // DEBUG
560 // reallyCleared = true;
561
562 for (int x = 0; x < width; x++) {
563 Cell lCell = logical[x][y];
564 Cell pCell = physical[x][y];
565
566 if (!lCell.equals(pCell) || reallyCleared) {
567
568 if (debugToStderr) {
569 System.err.printf("\n--\n");
570 System.err.printf(" Y: %d X: %d\n", y, x);
571 System.err.printf(" lCell: %s\n", lCell);
572 System.err.printf(" pCell: %s\n", pCell);
573 System.err.printf(" ==== \n");
574 }
575
576 if (lastAttr == null) {
577 lastAttr = new CellAttributes();
578 sb.append(normal());
579 }
580
581 // Place the cell
582 if ((lastX != (x - 1)) || (lastX == -1)) {
583 // Advancing at least one cell, or the first gotoXY
584 sb.append(gotoXY(x, y));
585 }
586
587 assert (lastAttr != null);
588
589 if ((x == textEnd) && (textEnd < width - 1)) {
590 assert (lCell.isBlank());
591
592 for (int i = x; i < width; i++) {
593 assert (logical[i][y].isBlank());
594 // Physical is always updated
595 physical[i][y].reset();
596 }
597
598 // Clear remaining line
599 sb.append(clearRemainingLine());
600 lastAttr.reset();
601 return;
602 }
603
604 // Now emit only the modified attributes
605 if ((lCell.getForeColor() != lastAttr.getForeColor())
606 && (lCell.getBackColor() != lastAttr.getBackColor())
607 && (lCell.isBold() == lastAttr.isBold())
608 && (lCell.isReverse() == lastAttr.isReverse())
609 && (lCell.isUnderline() == lastAttr.isUnderline())
610 && (lCell.isBlink() == lastAttr.isBlink())
611 ) {
612 // Both colors changed, attributes the same
613 sb.append(color(lCell.isBold(),
614 lCell.getForeColor(), lCell.getBackColor()));
615
616 if (debugToStderr) {
617 System.err.printf("1 Change only fore/back colors\n");
618 }
619 } else if ((lCell.getForeColor() != lastAttr.getForeColor())
620 && (lCell.getBackColor() != lastAttr.getBackColor())
621 && (lCell.isBold() != lastAttr.isBold())
622 && (lCell.isReverse() != lastAttr.isReverse())
623 && (lCell.isUnderline() != lastAttr.isUnderline())
624 && (lCell.isBlink() != lastAttr.isBlink())
625 ) {
626 // Everything is different
627 sb.append(color(lCell.getForeColor(),
628 lCell.getBackColor(),
629 lCell.isBold(), lCell.isReverse(),
630 lCell.isBlink(),
631 lCell.isUnderline()));
632
633 if (debugToStderr) {
634 System.err.printf("2 Set all attributes\n");
635 }
636 } else if ((lCell.getForeColor() != lastAttr.getForeColor())
637 && (lCell.getBackColor() == lastAttr.getBackColor())
638 && (lCell.isBold() == lastAttr.isBold())
639 && (lCell.isReverse() == lastAttr.isReverse())
640 && (lCell.isUnderline() == lastAttr.isUnderline())
641 && (lCell.isBlink() == lastAttr.isBlink())
642 ) {
643
644 // Attributes same, foreColor different
645 sb.append(color(lCell.isBold(),
646 lCell.getForeColor(), true));
647
648 if (debugToStderr) {
649 System.err.printf("3 Change foreColor\n");
650 }
651 } else if ((lCell.getForeColor() == lastAttr.getForeColor())
652 && (lCell.getBackColor() != lastAttr.getBackColor())
653 && (lCell.isBold() == lastAttr.isBold())
654 && (lCell.isReverse() == lastAttr.isReverse())
655 && (lCell.isUnderline() == lastAttr.isUnderline())
656 && (lCell.isBlink() == lastAttr.isBlink())
657 ) {
658 // Attributes same, backColor different
659 sb.append(color(lCell.isBold(),
660 lCell.getBackColor(), false));
661
662 if (debugToStderr) {
663 System.err.printf("4 Change backColor\n");
664 }
665 } else if ((lCell.getForeColor() == lastAttr.getForeColor())
666 && (lCell.getBackColor() == lastAttr.getBackColor())
667 && (lCell.isBold() == lastAttr.isBold())
668 && (lCell.isReverse() == lastAttr.isReverse())
669 && (lCell.isUnderline() == lastAttr.isUnderline())
670 && (lCell.isBlink() == lastAttr.isBlink())
671 ) {
672
673 // All attributes the same, just print the char
674 // NOP
675
676 if (debugToStderr) {
677 System.err.printf("5 Only emit character\n");
678 }
679 } else {
680 // Just reset everything again
681 sb.append(color(lCell.getForeColor(),
682 lCell.getBackColor(),
683 lCell.isBold(),
684 lCell.isReverse(),
685 lCell.isBlink(),
686 lCell.isUnderline()));
687
688 if (debugToStderr) {
689 System.err.printf("6 Change all attributes\n");
690 }
691 }
692 // Emit the character
693 sb.append(lCell.getChar());
694
695 // Save the last rendered cell
696 lastX = x;
697 lastAttr.setTo(lCell);
698
699 // Physical is always updated
700 physical[x][y].setTo(lCell);
701
702 } // if (!lCell.equals(pCell) || (reallyCleared == true))
703
704 } // for (int x = 0; x < width; x++)
705 }
706
707 /**
708 * Render the screen to a string that can be emitted to something that
709 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
710 *
711 * @return escape sequences string that provides the updates to the
712 * physical screen
713 */
714 private String flushString() {
715 if (!dirty) {
716 assert (!reallyCleared);
717 return "";
718 }
719
720 CellAttributes attr = null;
721
722 StringBuilder sb = new StringBuilder();
723 if (reallyCleared) {
724 attr = new CellAttributes();
725 sb.append(clearAll());
726 }
727
728 for (int y = 0; y < height; y++) {
729 flushLine(y, sb, attr);
730 }
731
732 dirty = false;
733 reallyCleared = false;
734
735 String result = sb.toString();
736 if (debugToStderr) {
737 System.err.printf("flushString(): %s\n", result);
738 }
739 return result;
740 }
741
742 /**
743 * Push the logical screen to the physical device.
744 */
745 @Override
746 public void flushPhysical() {
747 String result = flushString();
748 if ((cursorVisible)
749 && (cursorY >= 0)
750 && (cursorX >= 0)
751 && (cursorY <= height - 1)
752 && (cursorX <= width - 1)
753 ) {
754 result += cursor(true);
755 result += gotoXY(cursorX, cursorY);
756 } else {
757 result += cursor(false);
758 }
759 output.write(result);
760 flush();
761 }
762
763 /**
764 * Set the window title.
765 *
766 * @param title the new title
767 */
768 public void setTitle(final String title) {
769 output.write(getSetTitleString(title));
770 flush();
771 }
772
773 /**
774 * Reset keyboard/mouse input parser.
775 */
776 private void resetParser() {
777 state = ParseState.GROUND;
778 params = new ArrayList<String>();
779 params.clear();
780 params.add("");
781 }
782
783 /**
784 * Produce a control character or one of the special ones (ENTER, TAB,
785 * etc.).
786 *
787 * @param ch Unicode code point
788 * @param alt if true, set alt on the TKeypress
789 * @return one TKeypress event, either a control character (e.g. isKey ==
790 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
791 * fnKey == ESC)
792 */
793 private TKeypressEvent controlChar(final char ch, final boolean alt) {
794 // System.err.printf("controlChar: %02x\n", ch);
795
796 switch (ch) {
797 case 0x0D:
798 // Carriage return --> ENTER
799 return new TKeypressEvent(kbEnter, alt, false, false);
800 case 0x0A:
801 // Linefeed --> ENTER
802 return new TKeypressEvent(kbEnter, alt, false, false);
803 case 0x1B:
804 // ESC
805 return new TKeypressEvent(kbEsc, alt, false, false);
806 case '\t':
807 // TAB
808 return new TKeypressEvent(kbTab, alt, false, false);
809 default:
810 // Make all other control characters come back as the alphabetic
811 // character with the ctrl field set. So SOH would be 'A' +
812 // ctrl.
813 return new TKeypressEvent(false, 0, (char)(ch + 0x40),
814 alt, true, false);
815 }
816 }
817
818 /**
819 * Produce special key from CSI Pn ; Pm ; ... ~
820 *
821 * @return one KEYPRESS event representing a special key
822 */
823 private TInputEvent csiFnKey() {
824 int key = 0;
825 if (params.size() > 0) {
826 key = Integer.parseInt(params.get(0));
827 }
828 boolean alt = false;
829 boolean ctrl = false;
830 boolean shift = false;
831 if (params.size() > 1) {
832 shift = csiIsShift(params.get(1));
833 alt = csiIsAlt(params.get(1));
834 ctrl = csiIsCtrl(params.get(1));
835 }
836
837 switch (key) {
838 case 1:
839 return new TKeypressEvent(kbHome, alt, ctrl, shift);
840 case 2:
841 return new TKeypressEvent(kbIns, alt, ctrl, shift);
842 case 3:
843 return new TKeypressEvent(kbDel, alt, ctrl, shift);
844 case 4:
845 return new TKeypressEvent(kbEnd, alt, ctrl, shift);
846 case 5:
847 return new TKeypressEvent(kbPgUp, alt, ctrl, shift);
848 case 6:
849 return new TKeypressEvent(kbPgDn, alt, ctrl, shift);
850 case 15:
851 return new TKeypressEvent(kbF5, alt, ctrl, shift);
852 case 17:
853 return new TKeypressEvent(kbF6, alt, ctrl, shift);
854 case 18:
855 return new TKeypressEvent(kbF7, alt, ctrl, shift);
856 case 19:
857 return new TKeypressEvent(kbF8, alt, ctrl, shift);
858 case 20:
859 return new TKeypressEvent(kbF9, alt, ctrl, shift);
860 case 21:
861 return new TKeypressEvent(kbF10, alt, ctrl, shift);
862 case 23:
863 return new TKeypressEvent(kbF11, alt, ctrl, shift);
864 case 24:
865 return new TKeypressEvent(kbF12, alt, ctrl, shift);
866 default:
867 // Unknown
868 return null;
869 }
870 }
871
872 /**
873 * Produce mouse events based on "Any event tracking" and UTF-8
874 * coordinates. See
875 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
876 *
877 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
878 */
879 private TInputEvent parseMouse() {
880 int buttons = params.get(0).charAt(0) - 32;
881 int x = params.get(0).charAt(1) - 32 - 1;
882 int y = params.get(0).charAt(2) - 32 - 1;
883
884 // Clamp X and Y to the physical screen coordinates.
885 if (x >= windowResize.getWidth()) {
886 x = windowResize.getWidth() - 1;
887 }
888 if (y >= windowResize.getHeight()) {
889 y = windowResize.getHeight() - 1;
890 }
891
892 TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
893 boolean eventMouse1 = false;
894 boolean eventMouse2 = false;
895 boolean eventMouse3 = false;
896 boolean eventMouseWheelUp = false;
897 boolean eventMouseWheelDown = false;
898
899 // System.err.printf("buttons: %04x\r\n", buttons);
900
901 switch (buttons) {
902 case 0:
903 eventMouse1 = true;
904 mouse1 = true;
905 break;
906 case 1:
907 eventMouse2 = true;
908 mouse2 = true;
909 break;
910 case 2:
911 eventMouse3 = true;
912 mouse3 = true;
913 break;
914 case 3:
915 // Release or Move
916 if (!mouse1 && !mouse2 && !mouse3) {
917 eventType = TMouseEvent.Type.MOUSE_MOTION;
918 } else {
919 eventType = TMouseEvent.Type.MOUSE_UP;
920 }
921 if (mouse1) {
922 mouse1 = false;
923 eventMouse1 = true;
924 }
925 if (mouse2) {
926 mouse2 = false;
927 eventMouse2 = true;
928 }
929 if (mouse3) {
930 mouse3 = false;
931 eventMouse3 = true;
932 }
933 break;
934
935 case 32:
936 // Dragging with mouse1 down
937 eventMouse1 = true;
938 mouse1 = true;
939 eventType = TMouseEvent.Type.MOUSE_MOTION;
940 break;
941
942 case 33:
943 // Dragging with mouse2 down
944 eventMouse2 = true;
945 mouse2 = true;
946 eventType = TMouseEvent.Type.MOUSE_MOTION;
947 break;
948
949 case 34:
950 // Dragging with mouse3 down
951 eventMouse3 = true;
952 mouse3 = true;
953 eventType = TMouseEvent.Type.MOUSE_MOTION;
954 break;
955
956 case 96:
957 // Dragging with mouse2 down after wheelUp
958 eventMouse2 = true;
959 mouse2 = true;
960 eventType = TMouseEvent.Type.MOUSE_MOTION;
961 break;
962
963 case 97:
964 // Dragging with mouse2 down after wheelDown
965 eventMouse2 = true;
966 mouse2 = true;
967 eventType = TMouseEvent.Type.MOUSE_MOTION;
968 break;
969
970 case 64:
971 eventMouseWheelUp = true;
972 break;
973
974 case 65:
975 eventMouseWheelDown = true;
976 break;
977
978 default:
979 // Unknown, just make it motion
980 eventType = TMouseEvent.Type.MOUSE_MOTION;
981 break;
982 }
983 return new TMouseEvent(eventType, x, y, x, y,
984 eventMouse1, eventMouse2, eventMouse3,
985 eventMouseWheelUp, eventMouseWheelDown);
986 }
987
988 /**
989 * Produce mouse events based on "Any event tracking" and SGR
990 * coordinates. See
991 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
992 *
993 * @param release if true, this was a release ('m')
994 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
995 */
996 private TInputEvent parseMouseSGR(final boolean release) {
997 // SGR extended coordinates - mode 1006
998 if (params.size() < 3) {
999 // Invalid position, bail out.
1000 return null;
1001 }
1002 int buttons = Integer.parseInt(params.get(0));
1003 int x = Integer.parseInt(params.get(1)) - 1;
1004 int y = Integer.parseInt(params.get(2)) - 1;
1005
1006 // Clamp X and Y to the physical screen coordinates.
1007 if (x >= windowResize.getWidth()) {
1008 x = windowResize.getWidth() - 1;
1009 }
1010 if (y >= windowResize.getHeight()) {
1011 y = windowResize.getHeight() - 1;
1012 }
1013
1014 TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
1015 boolean eventMouse1 = false;
1016 boolean eventMouse2 = false;
1017 boolean eventMouse3 = false;
1018 boolean eventMouseWheelUp = false;
1019 boolean eventMouseWheelDown = false;
1020
1021 if (release) {
1022 eventType = TMouseEvent.Type.MOUSE_UP;
1023 }
1024
1025 switch (buttons) {
1026 case 0:
1027 eventMouse1 = true;
1028 break;
1029 case 1:
1030 eventMouse2 = true;
1031 break;
1032 case 2:
1033 eventMouse3 = true;
1034 break;
1035 case 35:
1036 // Motion only, no buttons down
1037 eventType = TMouseEvent.Type.MOUSE_MOTION;
1038 break;
1039
1040 case 32:
1041 // Dragging with mouse1 down
1042 eventMouse1 = true;
1043 eventType = TMouseEvent.Type.MOUSE_MOTION;
1044 break;
1045
1046 case 33:
1047 // Dragging with mouse2 down
1048 eventMouse2 = true;
1049 eventType = TMouseEvent.Type.MOUSE_MOTION;
1050 break;
1051
1052 case 34:
1053 // Dragging with mouse3 down
1054 eventMouse3 = true;
1055 eventType = TMouseEvent.Type.MOUSE_MOTION;
1056 break;
1057
1058 case 96:
1059 // Dragging with mouse2 down after wheelUp
1060 eventMouse2 = true;
1061 eventType = TMouseEvent.Type.MOUSE_MOTION;
1062 break;
1063
1064 case 97:
1065 // Dragging with mouse2 down after wheelDown
1066 eventMouse2 = true;
1067 eventType = TMouseEvent.Type.MOUSE_MOTION;
1068 break;
1069
1070 case 64:
1071 eventMouseWheelUp = true;
1072 break;
1073
1074 case 65:
1075 eventMouseWheelDown = true;
1076 break;
1077
1078 default:
1079 // Unknown, bail out
1080 return null;
1081 }
1082 return new TMouseEvent(eventType, x, y, x, y,
1083 eventMouse1, eventMouse2, eventMouse3,
1084 eventMouseWheelUp, eventMouseWheelDown);
1085 }
1086
1087 /**
1088 * Return any events in the IO queue.
1089 *
1090 * @param queue list to append new events to
1091 */
1092 public void getEvents(final List<TInputEvent> queue) {
1093 synchronized (eventQueue) {
1094 if (eventQueue.size() > 0) {
1095 synchronized (queue) {
1096 queue.addAll(eventQueue);
1097 }
1098 eventQueue.clear();
1099 }
1100 }
1101 }
1102
1103 /**
1104 * Return any events in the IO queue due to timeout.
1105 *
1106 * @param queue list to append new events to
1107 */
1108 private void getIdleEvents(final List<TInputEvent> queue) {
1109 Date now = new Date();
1110
1111 // Check for new window size
1112 long windowSizeDelay = now.getTime() - windowSizeTime;
1113 if (windowSizeDelay > 1000) {
1114 sessionInfo.queryWindowSize();
1115 int newWidth = sessionInfo.getWindowWidth();
1116 int newHeight = sessionInfo.getWindowHeight();
1117 if ((newWidth != windowResize.getWidth())
1118 || (newHeight != windowResize.getHeight())
1119 ) {
1120 TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
1121 newWidth, newHeight);
1122 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
1123 newWidth, newHeight);
1124 queue.add(event);
1125 }
1126 windowSizeTime = now.getTime();
1127 }
1128
1129 // ESCDELAY type timeout
1130 if (state == ParseState.ESCAPE) {
1131 long escDelay = now.getTime() - escapeTime;
1132 if (escDelay > 100) {
1133 // After 0.1 seconds, assume a true escape character
1134 queue.add(controlChar((char)0x1B, false));
1135 resetParser();
1136 }
1137 }
1138 }
1139
1140 /**
1141 * Returns true if the CSI parameter for a keyboard command means that
1142 * shift was down.
1143 */
1144 private boolean csiIsShift(final String x) {
1145 if ((x.equals("2"))
1146 || (x.equals("4"))
1147 || (x.equals("6"))
1148 || (x.equals("8"))
1149 ) {
1150 return true;
1151 }
1152 return false;
1153 }
1154
1155 /**
1156 * Returns true if the CSI parameter for a keyboard command means that
1157 * alt was down.
1158 */
1159 private boolean csiIsAlt(final String x) {
1160 if ((x.equals("3"))
1161 || (x.equals("4"))
1162 || (x.equals("7"))
1163 || (x.equals("8"))
1164 ) {
1165 return true;
1166 }
1167 return false;
1168 }
1169
1170 /**
1171 * Returns true if the CSI parameter for a keyboard command means that
1172 * ctrl was down.
1173 */
1174 private boolean csiIsCtrl(final String x) {
1175 if ((x.equals("5"))
1176 || (x.equals("6"))
1177 || (x.equals("7"))
1178 || (x.equals("8"))
1179 ) {
1180 return true;
1181 }
1182 return false;
1183 }
1184
1185 /**
1186 * Parses the next character of input to see if an InputEvent is
1187 * fully here.
1188 *
1189 * @param events list to append new events to
1190 * @param ch Unicode code point
1191 */
1192 private void processChar(final List<TInputEvent> events, final char ch) {
1193
1194 // ESCDELAY type timeout
1195 Date now = new Date();
1196 if (state == ParseState.ESCAPE) {
1197 long escDelay = now.getTime() - escapeTime;
1198 if (escDelay > 250) {
1199 // After 0.25 seconds, assume a true escape character
1200 events.add(controlChar((char)0x1B, false));
1201 resetParser();
1202 }
1203 }
1204
1205 // TKeypress fields
1206 boolean ctrl = false;
1207 boolean alt = false;
1208 boolean shift = false;
1209
1210 // System.err.printf("state: %s ch %c\r\n", state, ch);
1211
1212 switch (state) {
1213 case GROUND:
1214
1215 if (ch == 0x1B) {
1216 state = ParseState.ESCAPE;
1217 escapeTime = now.getTime();
1218 return;
1219 }
1220
1221 if (ch <= 0x1F) {
1222 // Control character
1223 events.add(controlChar(ch, false));
1224 resetParser();
1225 return;
1226 }
1227
1228 if (ch >= 0x20) {
1229 // Normal character
1230 events.add(new TKeypressEvent(false, 0, ch,
1231 false, false, false));
1232 resetParser();
1233 return;
1234 }
1235
1236 break;
1237
1238 case ESCAPE:
1239 if (ch <= 0x1F) {
1240 // ALT-Control character
1241 events.add(controlChar(ch, true));
1242 resetParser();
1243 return;
1244 }
1245
1246 if (ch == 'O') {
1247 // This will be one of the function keys
1248 state = ParseState.ESCAPE_INTERMEDIATE;
1249 return;
1250 }
1251
1252 // '[' goes to CSI_ENTRY
1253 if (ch == '[') {
1254 state = ParseState.CSI_ENTRY;
1255 return;
1256 }
1257
1258 // Everything else is assumed to be Alt-keystroke
1259 if ((ch >= 'A') && (ch <= 'Z')) {
1260 shift = true;
1261 }
1262 alt = true;
1263 events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift));
1264 resetParser();
1265 return;
1266
1267 case ESCAPE_INTERMEDIATE:
1268 if ((ch >= 'P') && (ch <= 'S')) {
1269 // Function key
1270 switch (ch) {
1271 case 'P':
1272 events.add(new TKeypressEvent(kbF1));
1273 break;
1274 case 'Q':
1275 events.add(new TKeypressEvent(kbF2));
1276 break;
1277 case 'R':
1278 events.add(new TKeypressEvent(kbF3));
1279 break;
1280 case 'S':
1281 events.add(new TKeypressEvent(kbF4));
1282 break;
1283 default:
1284 break;
1285 }
1286 resetParser();
1287 return;
1288 }
1289
1290 // Unknown keystroke, ignore
1291 resetParser();
1292 return;
1293
1294 case CSI_ENTRY:
1295 // Numbers - parameter values
1296 if ((ch >= '0') && (ch <= '9')) {
1297 params.set(params.size() - 1,
1298 params.get(params.size() - 1) + ch);
1299 state = ParseState.CSI_PARAM;
1300 return;
1301 }
1302 // Parameter separator
1303 if (ch == ';') {
1304 params.add("");
1305 return;
1306 }
1307
1308 if ((ch >= 0x30) && (ch <= 0x7E)) {
1309 switch (ch) {
1310 case 'A':
1311 // Up
1312 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
1313 resetParser();
1314 return;
1315 case 'B':
1316 // Down
1317 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
1318 resetParser();
1319 return;
1320 case 'C':
1321 // Right
1322 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
1323 resetParser();
1324 return;
1325 case 'D':
1326 // Left
1327 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
1328 resetParser();
1329 return;
1330 case 'H':
1331 // Home
1332 events.add(new TKeypressEvent(kbHome));
1333 resetParser();
1334 return;
1335 case 'F':
1336 // End
1337 events.add(new TKeypressEvent(kbEnd));
1338 resetParser();
1339 return;
1340 case 'Z':
1341 // CBT - Cursor backward X tab stops (default 1)
1342 events.add(new TKeypressEvent(kbBackTab));
1343 resetParser();
1344 return;
1345 case 'M':
1346 // Mouse position
1347 state = ParseState.MOUSE;
1348 return;
1349 case '<':
1350 // Mouse position, SGR (1006) coordinates
1351 state = ParseState.MOUSE_SGR;
1352 return;
1353 default:
1354 break;
1355 }
1356 }
1357
1358 // Unknown keystroke, ignore
1359 resetParser();
1360 return;
1361
1362 case MOUSE_SGR:
1363 // Numbers - parameter values
1364 if ((ch >= '0') && (ch <= '9')) {
1365 params.set(params.size() - 1,
1366 params.get(params.size() - 1) + ch);
1367 return;
1368 }
1369 // Parameter separator
1370 if (ch == ';') {
1371 params.add("");
1372 return;
1373 }
1374
1375 switch (ch) {
1376 case 'M':
1377 // Generate a mouse press event
1378 TInputEvent event = parseMouseSGR(false);
1379 if (event != null) {
1380 events.add(event);
1381 }
1382 resetParser();
1383 return;
1384 case 'm':
1385 // Generate a mouse release event
1386 event = parseMouseSGR(true);
1387 if (event != null) {
1388 events.add(event);
1389 }
1390 resetParser();
1391 return;
1392 default:
1393 break;
1394 }
1395
1396 // Unknown keystroke, ignore
1397 resetParser();
1398 return;
1399
1400 case CSI_PARAM:
1401 // Numbers - parameter values
1402 if ((ch >= '0') && (ch <= '9')) {
1403 params.set(params.size() - 1,
1404 params.get(params.size() - 1) + ch);
1405 state = ParseState.CSI_PARAM;
1406 return;
1407 }
1408 // Parameter separator
1409 if (ch == ';') {
1410 params.add("");
1411 return;
1412 }
1413
1414 if (ch == '~') {
1415 events.add(csiFnKey());
1416 resetParser();
1417 return;
1418 }
1419
1420 if ((ch >= 0x30) && (ch <= 0x7E)) {
1421 switch (ch) {
1422 case 'A':
1423 // Up
1424 if (params.size() > 1) {
1425 shift = csiIsShift(params.get(1));
1426 alt = csiIsAlt(params.get(1));
1427 ctrl = csiIsCtrl(params.get(1));
1428 }
1429 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
1430 resetParser();
1431 return;
1432 case 'B':
1433 // Down
1434 if (params.size() > 1) {
1435 shift = csiIsShift(params.get(1));
1436 alt = csiIsAlt(params.get(1));
1437 ctrl = csiIsCtrl(params.get(1));
1438 }
1439 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
1440 resetParser();
1441 return;
1442 case 'C':
1443 // Right
1444 if (params.size() > 1) {
1445 shift = csiIsShift(params.get(1));
1446 alt = csiIsAlt(params.get(1));
1447 ctrl = csiIsCtrl(params.get(1));
1448 }
1449 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
1450 resetParser();
1451 return;
1452 case 'D':
1453 // Left
1454 if (params.size() > 1) {
1455 shift = csiIsShift(params.get(1));
1456 alt = csiIsAlt(params.get(1));
1457 ctrl = csiIsCtrl(params.get(1));
1458 }
1459 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
1460 resetParser();
1461 return;
1462 case 'H':
1463 // Home
1464 if (params.size() > 1) {
1465 shift = csiIsShift(params.get(1));
1466 alt = csiIsAlt(params.get(1));
1467 ctrl = csiIsCtrl(params.get(1));
1468 }
1469 events.add(new TKeypressEvent(kbHome, alt, ctrl, shift));
1470 resetParser();
1471 return;
1472 case 'F':
1473 // End
1474 if (params.size() > 1) {
1475 shift = csiIsShift(params.get(1));
1476 alt = csiIsAlt(params.get(1));
1477 ctrl = csiIsCtrl(params.get(1));
1478 }
1479 events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift));
1480 resetParser();
1481 return;
1482 default:
1483 break;
1484 }
1485 }
1486
1487 // Unknown keystroke, ignore
1488 resetParser();
1489 return;
1490
1491 case MOUSE:
1492 params.set(0, params.get(params.size() - 1) + ch);
1493 if (params.get(0).length() == 3) {
1494 // We have enough to generate a mouse event
1495 events.add(parseMouse());
1496 resetParser();
1497 }
1498 return;
1499
1500 default:
1501 break;
1502 }
1503
1504 // This "should" be impossible to reach
1505 return;
1506 }
1507
1508 /**
1509 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1510 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1511 * enabled.
1512 *
1513 * @param on if true, enable metaSendsEscape
1514 * @return the string to emit to xterm
1515 */
1516 private String xtermMetaSendsEscape(final boolean on) {
1517 if (on) {
1518 return "\033[?1036h\033[?1034l";
1519 }
1520 return "\033[?1036l";
1521 }
1522
1523 /**
1524 * Create an xterm OSC sequence to change the window title.
1525 *
1526 * @param title the new title
1527 * @return the string to emit to xterm
1528 */
1529 private String getSetTitleString(final String title) {
1530 return "\033]2;" + title + "\007";
1531 }
1532
1533 /**
1534 * Create a SGR parameter sequence for a single color change.
1535 *
1536 * @param bold if true, set bold
1537 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1538 * @param foreground if true, this is a foreground color
1539 * @return the string to emit to an ANSI / ECMA-style terminal,
1540 * e.g. "\033[42m"
1541 */
1542 private String color(final boolean bold, final Color color,
1543 final boolean foreground) {
1544 return color(color, foreground, true) +
1545 rgbColor(bold, color, foreground);
1546 }
1547
1548 /**
1549 * Create a T.416 RGB parameter sequence for a single color change.
1550 *
1551 * @param bold if true, set bold
1552 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1553 * @param foreground if true, this is a foreground color
1554 * @return the string to emit to an xterm terminal with RGB support,
1555 * e.g. "\033[38;2;RR;GG;BBm"
1556 */
1557 private String rgbColor(final boolean bold, final Color color,
1558 final boolean foreground) {
1559 if (doRgbColor == false) {
1560 return "";
1561 }
1562 StringBuilder sb = new StringBuilder("\033[");
1563 if (bold) {
1564 // Bold implies foreground only
1565 sb.append("38;2;");
1566 if (color.equals(Color.BLACK)) {
1567 sb.append("84;84;84");
1568 } else if (color.equals(Color.RED)) {
1569 sb.append("252;84;84");
1570 } else if (color.equals(Color.GREEN)) {
1571 sb.append("84;252;84");
1572 } else if (color.equals(Color.YELLOW)) {
1573 sb.append("252;252;84");
1574 } else if (color.equals(Color.BLUE)) {
1575 sb.append("84;84;252");
1576 } else if (color.equals(Color.MAGENTA)) {
1577 sb.append("252;84;252");
1578 } else if (color.equals(Color.CYAN)) {
1579 sb.append("84;252;252");
1580 } else if (color.equals(Color.WHITE)) {
1581 sb.append("252;252;252");
1582 }
1583 } else {
1584 if (foreground) {
1585 sb.append("38;2;");
1586 } else {
1587 sb.append("48;2;");
1588 }
1589 if (color.equals(Color.BLACK)) {
1590 sb.append("0;0;0");
1591 } else if (color.equals(Color.RED)) {
1592 sb.append("168;0;0");
1593 } else if (color.equals(Color.GREEN)) {
1594 sb.append("0;168;0");
1595 } else if (color.equals(Color.YELLOW)) {
1596 sb.append("168;84;0");
1597 } else if (color.equals(Color.BLUE)) {
1598 sb.append("0;0;168");
1599 } else if (color.equals(Color.MAGENTA)) {
1600 sb.append("168;0;168");
1601 } else if (color.equals(Color.CYAN)) {
1602 sb.append("0;168;168");
1603 } else if (color.equals(Color.WHITE)) {
1604 sb.append("168;168;168");
1605 }
1606 }
1607 sb.append("m");
1608 return sb.toString();
1609 }
1610
1611 /**
1612 * Create a T.416 RGB parameter sequence for both foreground and
1613 * background color change.
1614 *
1615 * @param bold if true, set bold
1616 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1617 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1618 * @return the string to emit to an xterm terminal with RGB support,
1619 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
1620 */
1621 private String rgbColor(final boolean bold, final Color foreColor,
1622 final Color backColor) {
1623 if (doRgbColor == false) {
1624 return "";
1625 }
1626
1627 return rgbColor(bold, foreColor, true) +
1628 rgbColor(false, backColor, false);
1629 }
1630
1631 /**
1632 * Create a SGR parameter sequence for a single color change.
1633 *
1634 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1635 * @param foreground if true, this is a foreground color
1636 * @param header if true, make the full header, otherwise just emit the
1637 * color parameter e.g. "42;"
1638 * @return the string to emit to an ANSI / ECMA-style terminal,
1639 * e.g. "\033[42m"
1640 */
1641 private String color(final Color color, final boolean foreground,
1642 final boolean header) {
1643
1644 int ecmaColor = color.getValue();
1645
1646 // Convert Color.* values to SGR numerics
1647 if (foreground) {
1648 ecmaColor += 30;
1649 } else {
1650 ecmaColor += 40;
1651 }
1652
1653 if (header) {
1654 return String.format("\033[%dm", ecmaColor);
1655 } else {
1656 return String.format("%d;", ecmaColor);
1657 }
1658 }
1659
1660 /**
1661 * Create a SGR parameter sequence for both foreground and background
1662 * color change.
1663 *
1664 * @param bold if true, set bold
1665 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1666 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1667 * @return the string to emit to an ANSI / ECMA-style terminal,
1668 * e.g. "\033[31;42m"
1669 */
1670 private String color(final boolean bold, final Color foreColor,
1671 final Color backColor) {
1672 return color(foreColor, backColor, true) +
1673 rgbColor(bold, foreColor, backColor);
1674 }
1675
1676 /**
1677 * Create a SGR parameter sequence for both foreground and
1678 * background color change.
1679 *
1680 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1681 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1682 * @param header if true, make the full header, otherwise just emit the
1683 * color parameter e.g. "31;42;"
1684 * @return the string to emit to an ANSI / ECMA-style terminal,
1685 * e.g. "\033[31;42m"
1686 */
1687 private String color(final Color foreColor, final Color backColor,
1688 final boolean header) {
1689
1690 int ecmaForeColor = foreColor.getValue();
1691 int ecmaBackColor = backColor.getValue();
1692
1693 // Convert Color.* values to SGR numerics
1694 ecmaBackColor += 40;
1695 ecmaForeColor += 30;
1696
1697 if (header) {
1698 return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
1699 } else {
1700 return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
1701 }
1702 }
1703
1704 /**
1705 * Create a SGR parameter sequence for foreground, background, and
1706 * several attributes. This sequence first resets all attributes to
1707 * default, then sets attributes as per the parameters.
1708 *
1709 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1710 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1711 * @param bold if true, set bold
1712 * @param reverse if true, set reverse
1713 * @param blink if true, set blink
1714 * @param underline if true, set underline
1715 * @return the string to emit to an ANSI / ECMA-style terminal,
1716 * e.g. "\033[0;1;31;42m"
1717 */
1718 private String color(final Color foreColor, final Color backColor,
1719 final boolean bold, final boolean reverse, final boolean blink,
1720 final boolean underline) {
1721
1722 int ecmaForeColor = foreColor.getValue();
1723 int ecmaBackColor = backColor.getValue();
1724
1725 // Convert Color.* values to SGR numerics
1726 ecmaBackColor += 40;
1727 ecmaForeColor += 30;
1728
1729 StringBuilder sb = new StringBuilder();
1730 if ( bold && reverse && blink && !underline ) {
1731 sb.append("\033[0;1;7;5;");
1732 } else if ( bold && reverse && !blink && !underline ) {
1733 sb.append("\033[0;1;7;");
1734 } else if ( !bold && reverse && blink && !underline ) {
1735 sb.append("\033[0;7;5;");
1736 } else if ( bold && !reverse && blink && !underline ) {
1737 sb.append("\033[0;1;5;");
1738 } else if ( bold && !reverse && !blink && !underline ) {
1739 sb.append("\033[0;1;");
1740 } else if ( !bold && reverse && !blink && !underline ) {
1741 sb.append("\033[0;7;");
1742 } else if ( !bold && !reverse && blink && !underline) {
1743 sb.append("\033[0;5;");
1744 } else if ( bold && reverse && blink && underline ) {
1745 sb.append("\033[0;1;7;5;4;");
1746 } else if ( bold && reverse && !blink && underline ) {
1747 sb.append("\033[0;1;7;4;");
1748 } else if ( !bold && reverse && blink && underline ) {
1749 sb.append("\033[0;7;5;4;");
1750 } else if ( bold && !reverse && blink && underline ) {
1751 sb.append("\033[0;1;5;4;");
1752 } else if ( bold && !reverse && !blink && underline ) {
1753 sb.append("\033[0;1;4;");
1754 } else if ( !bold && reverse && !blink && underline ) {
1755 sb.append("\033[0;7;4;");
1756 } else if ( !bold && !reverse && blink && underline) {
1757 sb.append("\033[0;5;4;");
1758 } else if ( !bold && !reverse && !blink && underline) {
1759 sb.append("\033[0;4;");
1760 } else {
1761 assert (!bold && !reverse && !blink && !underline);
1762 sb.append("\033[0;");
1763 }
1764 sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
1765 sb.append(rgbColor(bold, foreColor, backColor));
1766 return sb.toString();
1767 }
1768
1769 /**
1770 * Create a SGR parameter sequence to reset to defaults.
1771 *
1772 * @return the string to emit to an ANSI / ECMA-style terminal,
1773 * e.g. "\033[0m"
1774 */
1775 private String normal() {
1776 return normal(true) + rgbColor(false, Color.WHITE, Color.BLACK);
1777 }
1778
1779 /**
1780 * Create a SGR parameter sequence to reset to defaults.
1781 *
1782 * @param header if true, make the full header, otherwise just emit the
1783 * bare parameter e.g. "0;"
1784 * @return the string to emit to an ANSI / ECMA-style terminal,
1785 * e.g. "\033[0m"
1786 */
1787 private String normal(final boolean header) {
1788 if (header) {
1789 return "\033[0;37;40m";
1790 }
1791 return "0;37;40";
1792 }
1793
1794 /**
1795 * Create a SGR parameter sequence for enabling the visible cursor.
1796 *
1797 * @param on if true, turn on cursor
1798 * @return the string to emit to an ANSI / ECMA-style terminal
1799 */
1800 private String cursor(final boolean on) {
1801 if (on && !cursorOn) {
1802 cursorOn = true;
1803 return "\033[?25h";
1804 }
1805 if (!on && cursorOn) {
1806 cursorOn = false;
1807 return "\033[?25l";
1808 }
1809 return "";
1810 }
1811
1812 /**
1813 * Clear the entire screen. Because some terminals use back-color-erase,
1814 * set the color to white-on-black beforehand.
1815 *
1816 * @return the string to emit to an ANSI / ECMA-style terminal
1817 */
1818 private String clearAll() {
1819 return "\033[0;37;40m\033[2J";
1820 }
1821
1822 /**
1823 * Clear the line from the cursor (inclusive) to the end of the screen.
1824 * Because some terminals use back-color-erase, set the color to
1825 * white-on-black beforehand.
1826 *
1827 * @return the string to emit to an ANSI / ECMA-style terminal
1828 */
1829 private String clearRemainingLine() {
1830 return "\033[0;37;40m\033[K";
1831 }
1832
1833 /**
1834 * Move the cursor to (x, y).
1835 *
1836 * @param x column coordinate. 0 is the left-most column.
1837 * @param y row coordinate. 0 is the top-most row.
1838 * @return the string to emit to an ANSI / ECMA-style terminal
1839 */
1840 private String gotoXY(final int x, final int y) {
1841 return String.format("\033[%d;%dH", y + 1, x + 1);
1842 }
1843
1844 /**
1845 * Tell (u)xterm that we want to receive mouse events based on "Any event
1846 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1847 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1848 * See
1849 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1850 *
1851 * Note that this also sets the alternate/primary screen buffer.
1852 *
1853 * @param on If true, enable mouse report and use the alternate screen
1854 * buffer. If false disable mouse reporting and use the primary screen
1855 * buffer.
1856 * @return the string to emit to xterm
1857 */
1858 private String mouse(final boolean on) {
1859 if (on) {
1860 return "\033[?1002;1003;1005;1006h\033[?1049h";
1861 }
1862 return "\033[?1002;1003;1006;1005l\033[?1049l";
1863 }
1864
1865 /**
1866 * Read function runs on a separate thread.
1867 */
1868 public void run() {
1869 boolean done = false;
1870 // available() will often return > 1, so we need to read in chunks to
1871 // stay caught up.
1872 char [] readBuffer = new char[128];
1873 List<TInputEvent> events = new LinkedList<TInputEvent>();
1874
1875 while (!done && !stopReaderThread) {
1876 try {
1877 // We assume that if inputStream has bytes available, then
1878 // input won't block on read().
1879 int n = inputStream.available();
1880 if (n > 0) {
1881 if (readBuffer.length < n) {
1882 // The buffer wasn't big enough, make it huger
1883 readBuffer = new char[readBuffer.length * 2];
1884 }
1885
1886 int rc = input.read(readBuffer, 0, readBuffer.length);
1887 // System.err.printf("read() %d", rc); System.err.flush();
1888 if (rc == -1) {
1889 // This is EOF
1890 done = true;
1891 } else {
1892 for (int i = 0; i < rc; i++) {
1893 int ch = readBuffer[i];
1894 processChar(events, (char)ch);
1895 }
1896 getIdleEvents(events);
1897 if (events.size() > 0) {
1898 // Add to the queue for the backend thread to
1899 // be able to obtain.
1900 synchronized (eventQueue) {
1901 eventQueue.addAll(events);
1902 }
1903 if (listener != null) {
1904 synchronized (listener) {
1905 listener.notifyAll();
1906 }
1907 }
1908 events.clear();
1909 }
1910 }
1911 } else {
1912 getIdleEvents(events);
1913 if (events.size() > 0) {
1914 synchronized (eventQueue) {
1915 eventQueue.addAll(events);
1916 }
1917 events.clear();
1918 if (listener != null) {
1919 synchronized (listener) {
1920 listener.notifyAll();
1921 }
1922 }
1923 }
1924
1925 // Wait 10 millis for more data
1926 Thread.sleep(10);
1927 }
1928 // System.err.println("end while loop"); System.err.flush();
1929 } catch (InterruptedException e) {
1930 // SQUASH
1931 } catch (IOException e) {
1932 e.printStackTrace();
1933 done = true;
1934 }
1935 } // while ((done == false) && (stopReaderThread == false))
1936 // System.err.println("*** run() exiting..."); System.err.flush();
1937 }
1938
1939 }