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