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