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