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