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