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