More PMD warnings
[fanfix.git] / src / jexer / backend / ECMA48Terminal.java
CommitLineData
daa4106c 1/*
b1589621
KL
2 * Jexer - Java Text User Interface
3 *
e16dda65 4 * The MIT License (MIT)
b1589621 5 *
a2018e99 6 * Copyright (C) 2017 Kevin Lamonte
b1589621 7 *
e16dda65
KL
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:
b1589621 14 *
e16dda65
KL
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
b1589621 17 *
e16dda65
KL
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.
7b5261bc
KL
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
b1589621 28 */
42873e30 29package jexer.backend;
b1589621
KL
30
31import java.io.BufferedReader;
4328bb42
KL
32import java.io.FileDescriptor;
33import java.io.FileInputStream;
b1589621
KL
34import java.io.InputStream;
35import java.io.InputStreamReader;
36import java.io.IOException;
37import java.io.OutputStream;
38import java.io.OutputStreamWriter;
39import java.io.PrintWriter;
40import java.io.Reader;
41import java.io.UnsupportedEncodingException;
42import java.util.ArrayList;
b1589621
KL
43import java.util.List;
44import java.util.LinkedList;
45
42873e30
KL
46import jexer.bits.Cell;
47import jexer.bits.CellAttributes;
b1589621
KL
48import jexer.bits.Color;
49import jexer.event.TInputEvent;
50import jexer.event.TKeypressEvent;
51import jexer.event.TMouseEvent;
52import jexer.event.TResizeEvent;
b1589621
KL
53import static jexer.TKeypress.*;
54
55/**
7b5261bc
KL
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.
b1589621 58 */
42873e30
KL
59public final class ECMA48Terminal extends LogicalScreen
60 implements TerminalReader, Runnable {
61
d36057df
KL
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
42873e30
KL
83 /**
84 * Emit debugging to stderr.
85 */
86 private boolean debugToStderr = false;
b1589621 87
15ea4d73
KL
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
b1589621 94 /**
7b5261bc
KL
95 * The session information.
96 */
97 private SessionInfo sessionInfo;
98
4328bb42 99 /**
7b5261bc 100 * The event queue, filled up by a thread reading on input.
4328bb42
KL
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 /**
7b5261bc 110 * The reader thread.
4328bb42
KL
111 */
112 private Thread readerThread;
113
b1589621
KL
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 */
d36057df 118 private List<String> params;
b1589621
KL
119
120 /**
7b5261bc 121 * Current parsing state.
b1589621
KL
122 */
123 private ParseState state;
124
125 /**
7b5261bc
KL
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.
b1589621
KL
128 */
129 private long escapeTime;
130
a83fea2b
KL
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
b1589621
KL
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
4328bb42
KL
171 * constructor, then this InputStreamReader will be bound to System.in
172 * with UTF-8 encoding.
b1589621
KL
173 */
174 private Reader input;
175
4328bb42
KL
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
b1589621
KL
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
92554d64
KL
191 /**
192 * The listening object that run() wakes up on new input.
193 */
194 private Object listener;
195
d36057df
KL
196 // ------------------------------------------------------------------------
197 // Constructors -----------------------------------------------------------
198 // ------------------------------------------------------------------------
b1589621
KL
199
200 /**
7b5261bc 201 * Constructor sets up state for getEvent().
eb29bbb5
KL
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().
b1589621 233 *
92554d64
KL
234 * @param listener the object this backend needs to wake up when new
235 * input comes in
b1589621
KL
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.
7b5261bc
KL
243 * @throws UnsupportedEncodingException if an exception is thrown when
244 * creating the InputStreamReader
b1589621 245 */
92554d64 246 public ECMA48Terminal(final Object listener, final InputStream input,
7b5261bc
KL
247 final OutputStream output) throws UnsupportedEncodingException {
248
42873e30 249 resetParser();
7b5261bc
KL
250 mouse1 = false;
251 mouse2 = false;
252 mouse3 = false;
253 stopReaderThread = false;
92554d64 254 this.listener = listener;
7b5261bc
KL
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
7b5261bc 266 if (input instanceof SessionInfo) {
ea91242c
KL
267 // This is a TelnetInputStream that exposes window size and
268 // environment variables from the telnet layer.
7b5261bc
KL
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));
55b4f29b 290 this.output.flush();
7b5261bc 291
3e074355
KL
292 // Query the screen size
293 sessionInfo.queryWindowSize();
294 setDimensions(sessionInfo.getWindowWidth(),
295 sessionInfo.getWindowHeight());
296
7b5261bc
KL
297 // Hang onto the window size
298 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
299 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
300
15ea4d73
KL
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;
3e074355
KL
305 } else {
306 doRgbColor = false;
15ea4d73
KL
307 }
308 }
309
7b5261bc
KL
310 // Spin up the input reader
311 eventQueue = new LinkedList<TInputEvent>();
312 readerThread = new Thread(this);
313 readerThread.start();
42873e30 314
42873e30
KL
315 // Clear the screen
316 this.output.write(clearAll());
317 this.output.flush();
b1589621
KL
318 }
319
6985c572
KL
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 }
42873e30 347 resetParser();
6985c572
KL
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) {
31b7033c
KL
368 if (setRawMode == true) {
369 // Reading right off the tty
370 sessionInfo = new TTYSessionInfo();
371 } else {
372 sessionInfo = new TSessionInfo();
373 }
6985c572
KL
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
3e074355
KL
382 // Query the screen size
383 sessionInfo.queryWindowSize();
384 setDimensions(sessionInfo.getWindowWidth(),
385 sessionInfo.getWindowHeight());
386
6985c572
KL
387 // Hang onto the window size
388 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
389 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
390
15ea4d73
KL
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;
3e074355
KL
395 } else {
396 doRgbColor = false;
15ea4d73
KL
397 }
398 }
399
6985c572
KL
400 // Spin up the input reader
401 eventQueue = new LinkedList<TInputEvent>();
402 readerThread = new Thread(this);
403 readerThread.start();
42873e30 404
42873e30
KL
405 // Clear the screen
406 this.output.write(clearAll());
407 this.output.flush();
6985c572
KL
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
d36057df
KL
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
b1589621 494 /**
7b5261bc 495 * Restore terminal to normal state.
b1589621 496 */
42873e30 497 public void closeTerminal() {
4328bb42 498
7b5261bc
KL
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 }
b1589621
KL
533 }
534
d36057df
KL
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
b1589621 724 /**
7b5261bc 725 * Flush output.
b1589621
KL
726 */
727 public void flush() {
7b5261bc 728 output.flush();
b1589621
KL
729 }
730
42873e30
KL
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() {
42873e30
KL
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
42873e30
KL
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
b1589621 929 /**
7b5261bc 930 * Reset keyboard/mouse input parser.
b1589621 931 */
42873e30 932 private void resetParser() {
7b5261bc
KL
933 state = ParseState.GROUND;
934 params = new ArrayList<String>();
7b5261bc
KL
935 params.clear();
936 params.add("");
b1589621
KL
937 }
938
939 /**
940 * Produce a control character or one of the special ones (ENTER, TAB,
7b5261bc 941 * etc.).
b1589621
KL
942 *
943 * @param ch Unicode code point
b299e69c 944 * @param alt if true, set alt on the TKeypress
7b5261bc 945 * @return one TKeypress event, either a control character (e.g. isKey ==
b1589621
KL
946 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
947 * fnKey == ESC)
948 */
b299e69c 949 private TKeypressEvent controlChar(final char ch, final boolean alt) {
7b5261bc
KL
950 // System.err.printf("controlChar: %02x\n", ch);
951
952 switch (ch) {
953 case 0x0D:
954 // Carriage return --> ENTER
b299e69c 955 return new TKeypressEvent(kbEnter, alt, false, false);
7b5261bc
KL
956 case 0x0A:
957 // Linefeed --> ENTER
b299e69c 958 return new TKeypressEvent(kbEnter, alt, false, false);
7b5261bc
KL
959 case 0x1B:
960 // ESC
b299e69c 961 return new TKeypressEvent(kbEsc, alt, false, false);
7b5261bc
KL
962 case '\t':
963 // TAB
b299e69c 964 return new TKeypressEvent(kbTab, alt, false, false);
7b5261bc
KL
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.
b299e69c
KL
969 return new TKeypressEvent(false, 0, (char)(ch + 0x40),
970 alt, true, false);
7b5261bc 971 }
b1589621
KL
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() {
7b5261bc 980 int key = 0;
7b5261bc
KL
981 if (params.size() > 0) {
982 key = Integer.parseInt(params.get(0));
983 }
b299e69c
KL
984 boolean alt = false;
985 boolean ctrl = false;
986 boolean shift = false;
32437017
KL
987 if (params.size() > 1) {
988 shift = csiIsShift(params.get(1));
989 alt = csiIsAlt(params.get(1));
990 ctrl = csiIsCtrl(params.get(1));
b299e69c 991 }
d4a29741 992
b299e69c
KL
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);
7b5261bc
KL
1022 default:
1023 // Unknown
1024 return null;
1025 }
b1589621
KL
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() {
7b5261bc
KL
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
d4a29741
KL
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;
7b5261bc
KL
1054
1055 // System.err.printf("buttons: %04x\r\n", buttons);
1056
1057 switch (buttons) {
1058 case 0:
d4a29741 1059 eventMouse1 = true;
7b5261bc
KL
1060 mouse1 = true;
1061 break;
1062 case 1:
d4a29741 1063 eventMouse2 = true;
7b5261bc
KL
1064 mouse2 = true;
1065 break;
1066 case 2:
d4a29741 1067 eventMouse3 = true;
7b5261bc
KL
1068 mouse3 = true;
1069 break;
1070 case 3:
1071 // Release or Move
1072 if (!mouse1 && !mouse2 && !mouse3) {
d4a29741 1073 eventType = TMouseEvent.Type.MOUSE_MOTION;
7b5261bc 1074 } else {
d4a29741 1075 eventType = TMouseEvent.Type.MOUSE_UP;
7b5261bc
KL
1076 }
1077 if (mouse1) {
1078 mouse1 = false;
d4a29741 1079 eventMouse1 = true;
7b5261bc
KL
1080 }
1081 if (mouse2) {
1082 mouse2 = false;
d4a29741 1083 eventMouse2 = true;
7b5261bc
KL
1084 }
1085 if (mouse3) {
1086 mouse3 = false;
d4a29741 1087 eventMouse3 = true;
7b5261bc
KL
1088 }
1089 break;
1090
1091 case 32:
1092 // Dragging with mouse1 down
d4a29741 1093 eventMouse1 = true;
7b5261bc 1094 mouse1 = true;
d4a29741 1095 eventType = TMouseEvent.Type.MOUSE_MOTION;
7b5261bc
KL
1096 break;
1097
1098 case 33:
1099 // Dragging with mouse2 down
d4a29741 1100 eventMouse2 = true;
7b5261bc 1101 mouse2 = true;
d4a29741 1102 eventType = TMouseEvent.Type.MOUSE_MOTION;
7b5261bc
KL
1103 break;
1104
1105 case 34:
1106 // Dragging with mouse3 down
d4a29741 1107 eventMouse3 = true;
7b5261bc 1108 mouse3 = true;
d4a29741 1109 eventType = TMouseEvent.Type.MOUSE_MOTION;
7b5261bc
KL
1110 break;
1111
1112 case 96:
1113 // Dragging with mouse2 down after wheelUp
d4a29741 1114 eventMouse2 = true;
7b5261bc 1115 mouse2 = true;
d4a29741 1116 eventType = TMouseEvent.Type.MOUSE_MOTION;
7b5261bc
KL
1117 break;
1118
1119 case 97:
1120 // Dragging with mouse2 down after wheelDown
d4a29741 1121 eventMouse2 = true;
7b5261bc 1122 mouse2 = true;
d4a29741 1123 eventType = TMouseEvent.Type.MOUSE_MOTION;
7b5261bc
KL
1124 break;
1125
1126 case 64:
d4a29741 1127 eventMouseWheelUp = true;
7b5261bc
KL
1128 break;
1129
1130 case 65:
d4a29741 1131 eventMouseWheelDown = true;
7b5261bc
KL
1132 break;
1133
1134 default:
1135 // Unknown, just make it motion
d4a29741 1136 eventType = TMouseEvent.Type.MOUSE_MOTION;
7b5261bc
KL
1137 break;
1138 }
d4a29741
KL
1139 return new TMouseEvent(eventType, x, y, x, y,
1140 eventMouse1, eventMouse2, eventMouse3,
1141 eventMouseWheelUp, eventMouseWheelDown);
b1589621
KL
1142 }
1143
b5f2a6db
KL
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
05dbb28d 1243 /**
623a1bd1 1244 * Return any events in the IO queue due to timeout.
b1589621 1245 *
623a1bd1 1246 * @param queue list to append new events to
b1589621 1247 */
a83fea2b 1248 private void getIdleEvents(final List<TInputEvent> queue) {
be72cb5c 1249 long nowTime = System.currentTimeMillis();
7b5261bc
KL
1250
1251 // Check for new window size
be72cb5c 1252 long windowSizeDelay = nowTime - windowSizeTime;
a83fea2b
KL
1253 if (windowSizeDelay > 1000) {
1254 sessionInfo.queryWindowSize();
1255 int newWidth = sessionInfo.getWindowWidth();
1256 int newHeight = sessionInfo.getWindowHeight();
85c07c5e 1257
a83fea2b
KL
1258 if ((newWidth != windowResize.getWidth())
1259 || (newHeight != windowResize.getHeight())
1260 ) {
85c07c5e
KL
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
a83fea2b
KL
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);
7b5261bc 1274 }
be72cb5c 1275 windowSizeTime = nowTime;
7b5261bc
KL
1276 }
1277
a83fea2b
KL
1278 // ESCDELAY type timeout
1279 if (state == ParseState.ESCAPE) {
be72cb5c 1280 long escDelay = nowTime - escapeTime;
a83fea2b
KL
1281 if (escDelay > 100) {
1282 // After 0.1 seconds, assume a true escape character
1283 queue.add(controlChar((char)0x1B, false));
42873e30 1284 resetParser();
7b5261bc
KL
1285 }
1286 }
b1589621
KL
1287 }
1288
32437017
KL
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
b1589621
KL
1334 /**
1335 * Parses the next character of input to see if an InputEvent is
1336 * fully here.
1337 *
623a1bd1 1338 * @param events list to append new events to
05dbb28d 1339 * @param ch Unicode code point
b1589621 1340 */
7b5261bc
KL
1341 private void processChar(final List<TInputEvent> events, final char ch) {
1342
7b5261bc 1343 // ESCDELAY type timeout
be72cb5c 1344 long nowTime = System.currentTimeMillis();
7b5261bc 1345 if (state == ParseState.ESCAPE) {
be72cb5c 1346 long escDelay = nowTime - escapeTime;
7b5261bc
KL
1347 if (escDelay > 250) {
1348 // After 0.25 seconds, assume a true escape character
b299e69c 1349 events.add(controlChar((char)0x1B, false));
42873e30 1350 resetParser();
7b5261bc
KL
1351 }
1352 }
1353
b299e69c
KL
1354 // TKeypress fields
1355 boolean ctrl = false;
1356 boolean alt = false;
1357 boolean shift = false;
b299e69c 1358
7b5261bc
KL
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;
be72cb5c 1366 escapeTime = nowTime;
7b5261bc
KL
1367 return;
1368 }
1369
1370 if (ch <= 0x1F) {
1371 // Control character
b299e69c 1372 events.add(controlChar(ch, false));
42873e30 1373 resetParser();
7b5261bc
KL
1374 return;
1375 }
1376
1377 if (ch >= 0x20) {
1378 // Normal character
b299e69c
KL
1379 events.add(new TKeypressEvent(false, 0, ch,
1380 false, false, false));
42873e30 1381 resetParser();
7b5261bc
KL
1382 return;
1383 }
1384
1385 break;
1386
1387 case ESCAPE:
1388 if (ch <= 0x1F) {
1389 // ALT-Control character
b299e69c 1390 events.add(controlChar(ch, true));
42873e30 1391 resetParser();
7b5261bc
KL
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
7b5261bc 1408 if ((ch >= 'A') && (ch <= 'Z')) {
b299e69c 1409 shift = true;
7b5261bc 1410 }
b299e69c
KL
1411 alt = true;
1412 events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift));
42873e30 1413 resetParser();
7b5261bc
KL
1414 return;
1415
1416 case ESCAPE_INTERMEDIATE:
1417 if ((ch >= 'P') && (ch <= 'S')) {
1418 // Function key
7b5261bc
KL
1419 switch (ch) {
1420 case 'P':
b299e69c 1421 events.add(new TKeypressEvent(kbF1));
7b5261bc
KL
1422 break;
1423 case 'Q':
b299e69c 1424 events.add(new TKeypressEvent(kbF2));
7b5261bc
KL
1425 break;
1426 case 'R':
b299e69c 1427 events.add(new TKeypressEvent(kbF3));
7b5261bc
KL
1428 break;
1429 case 'S':
b299e69c 1430 events.add(new TKeypressEvent(kbF4));
7b5261bc
KL
1431 break;
1432 default:
1433 break;
1434 }
42873e30 1435 resetParser();
7b5261bc
KL
1436 return;
1437 }
1438
1439 // Unknown keystroke, ignore
42873e30 1440 resetParser();
7b5261bc
KL
1441 return;
1442
1443 case CSI_ENTRY:
1444 // Numbers - parameter values
1445 if ((ch >= '0') && (ch <= '9')) {
92554d64
KL
1446 params.set(params.size() - 1,
1447 params.get(params.size() - 1) + ch);
7b5261bc
KL
1448 state = ParseState.CSI_PARAM;
1449 return;
1450 }
1451 // Parameter separator
1452 if (ch == ';') {
a83fea2b 1453 params.add("");
7b5261bc
KL
1454 return;
1455 }
1456
1457 if ((ch >= 0x30) && (ch <= 0x7E)) {
1458 switch (ch) {
1459 case 'A':
1460 // Up
b299e69c 1461 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
42873e30 1462 resetParser();
7b5261bc
KL
1463 return;
1464 case 'B':
1465 // Down
b299e69c 1466 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
42873e30 1467 resetParser();
7b5261bc
KL
1468 return;
1469 case 'C':
1470 // Right
b299e69c 1471 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
42873e30 1472 resetParser();
7b5261bc
KL
1473 return;
1474 case 'D':
1475 // Left
b299e69c 1476 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
42873e30 1477 resetParser();
7b5261bc
KL
1478 return;
1479 case 'H':
1480 // Home
b299e69c 1481 events.add(new TKeypressEvent(kbHome));
42873e30 1482 resetParser();
7b5261bc
KL
1483 return;
1484 case 'F':
1485 // End
b299e69c 1486 events.add(new TKeypressEvent(kbEnd));
42873e30 1487 resetParser();
7b5261bc
KL
1488 return;
1489 case 'Z':
1490 // CBT - Cursor backward X tab stops (default 1)
b299e69c 1491 events.add(new TKeypressEvent(kbBackTab));
42873e30 1492 resetParser();
7b5261bc
KL
1493 return;
1494 case 'M':
1495 // Mouse position
1496 state = ParseState.MOUSE;
1497 return;
b5f2a6db
KL
1498 case '<':
1499 // Mouse position, SGR (1006) coordinates
1500 state = ParseState.MOUSE_SGR;
1501 return;
7b5261bc
KL
1502 default:
1503 break;
1504 }
1505 }
1506
1507 // Unknown keystroke, ignore
42873e30 1508 resetParser();
7b5261bc
KL
1509 return;
1510
b5f2a6db
KL
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 }
42873e30 1531 resetParser();
b5f2a6db
KL
1532 return;
1533 case 'm':
1534 // Generate a mouse release event
1535 event = parseMouseSGR(true);
1536 if (event != null) {
1537 events.add(event);
1538 }
42873e30 1539 resetParser();
b5f2a6db
KL
1540 return;
1541 default:
1542 break;
1543 }
1544
1545 // Unknown keystroke, ignore
42873e30 1546 resetParser();
b5f2a6db
KL
1547 return;
1548
7b5261bc
KL
1549 case CSI_PARAM:
1550 // Numbers - parameter values
1551 if ((ch >= '0') && (ch <= '9')) {
92554d64
KL
1552 params.set(params.size() - 1,
1553 params.get(params.size() - 1) + ch);
7b5261bc
KL
1554 state = ParseState.CSI_PARAM;
1555 return;
1556 }
1557 // Parameter separator
1558 if (ch == ';') {
92554d64 1559 params.add("");
7b5261bc
KL
1560 return;
1561 }
1562
1563 if (ch == '~') {
1564 events.add(csiFnKey());
42873e30 1565 resetParser();
7b5261bc
KL
1566 return;
1567 }
1568
1569 if ((ch >= 0x30) && (ch <= 0x7E)) {
1570 switch (ch) {
1571 case 'A':
1572 // Up
7b5261bc 1573 if (params.size() > 1) {
32437017
KL
1574 shift = csiIsShift(params.get(1));
1575 alt = csiIsAlt(params.get(1));
1576 ctrl = csiIsCtrl(params.get(1));
7b5261bc 1577 }
b299e69c 1578 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
42873e30 1579 resetParser();
7b5261bc
KL
1580 return;
1581 case 'B':
1582 // Down
7b5261bc 1583 if (params.size() > 1) {
32437017
KL
1584 shift = csiIsShift(params.get(1));
1585 alt = csiIsAlt(params.get(1));
1586 ctrl = csiIsCtrl(params.get(1));
7b5261bc 1587 }
b299e69c 1588 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
42873e30 1589 resetParser();
7b5261bc
KL
1590 return;
1591 case 'C':
1592 // Right
7b5261bc 1593 if (params.size() > 1) {
32437017
KL
1594 shift = csiIsShift(params.get(1));
1595 alt = csiIsAlt(params.get(1));
1596 ctrl = csiIsCtrl(params.get(1));
7b5261bc 1597 }
b299e69c 1598 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
42873e30 1599 resetParser();
7b5261bc
KL
1600 return;
1601 case 'D':
1602 // Left
7b5261bc 1603 if (params.size() > 1) {
32437017
KL
1604 shift = csiIsShift(params.get(1));
1605 alt = csiIsAlt(params.get(1));
1606 ctrl = csiIsCtrl(params.get(1));
7b5261bc 1607 }
b299e69c 1608 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
42873e30 1609 resetParser();
7b5261bc 1610 return;
32437017
KL
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));
42873e30 1619 resetParser();
32437017
KL
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));
42873e30 1629 resetParser();
32437017 1630 return;
7b5261bc
KL
1631 default:
1632 break;
1633 }
1634 }
1635
1636 // Unknown keystroke, ignore
42873e30 1637 resetParser();
7b5261bc
KL
1638 return;
1639
1640 case MOUSE:
92554d64 1641 params.set(0, params.get(params.size() - 1) + ch);
7b5261bc
KL
1642 if (params.get(0).length() == 3) {
1643 // We have enough to generate a mouse event
1644 events.add(parseMouse());
42873e30 1645 resetParser();
7b5261bc
KL
1646 }
1647 return;
1648
1649 default:
1650 break;
1651 }
1652
1653 // This "should" be impossible to reach
1654 return;
b1589621
KL
1655 }
1656
1657 /**
05dbb28d
KL
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.
b1589621 1661 *
05dbb28d
KL
1662 * @param on if true, enable metaSendsEscape
1663 * @return the string to emit to xterm
b1589621 1664 */
b5f2a6db 1665 private String xtermMetaSendsEscape(final boolean on) {
7b5261bc
KL
1666 if (on) {
1667 return "\033[?1036h\033[?1034l";
1668 }
1669 return "\033[?1036l";
b1589621
KL
1670 }
1671
55d2b2c2 1672 /**
42873e30 1673 * Create an xterm OSC sequence to change the window title.
55d2b2c2
KL
1674 *
1675 * @param title the new title
1676 * @return the string to emit to xterm
1677 */
42873e30 1678 private String getSetTitleString(final String title) {
55d2b2c2
KL
1679 return "\033]2;" + title + "\007";
1680 }
1681
b1589621 1682 /**
42873e30 1683 * Create a SGR parameter sequence for a single color change.
b1589621 1684 *
15ea4d73 1685 * @param bold if true, set bold
05dbb28d
KL
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"
b1589621 1690 */
42873e30 1691 private String color(final boolean bold, final Color color,
15ea4d73
KL
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)) {
6ba187ac 1716 sb.append("84;84;84");
15ea4d73 1717 } else if (color.equals(Color.RED)) {
6ba187ac 1718 sb.append("252;84;84");
15ea4d73 1719 } else if (color.equals(Color.GREEN)) {
6ba187ac 1720 sb.append("84;252;84");
15ea4d73 1721 } else if (color.equals(Color.YELLOW)) {
6ba187ac 1722 sb.append("252;252;84");
15ea4d73 1723 } else if (color.equals(Color.BLUE)) {
6ba187ac 1724 sb.append("84;84;252");
15ea4d73 1725 } else if (color.equals(Color.MAGENTA)) {
6ba187ac 1726 sb.append("252;84;252");
15ea4d73 1727 } else if (color.equals(Color.CYAN)) {
6ba187ac 1728 sb.append("84;252;252");
15ea4d73
KL
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)) {
6ba187ac 1745 sb.append("168;84;0");
15ea4d73
KL
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);
b1589621
KL
1778 }
1779
1780 /**
1781 * Create a SGR parameter sequence for a single color change.
1782 *
05dbb28d
KL
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"
b1589621 1789 */
b5f2a6db 1790 private String color(final Color color, final boolean foreground,
7b5261bc
KL
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 }
b1589621
KL
1807 }
1808
1809 /**
b5f2a6db 1810 * Create a SGR parameter sequence for both foreground and background
42873e30 1811 * color change.
b1589621 1812 *
15ea4d73 1813 * @param bold if true, set bold
05dbb28d
KL
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"
b1589621 1818 */
42873e30 1819 private String color(final boolean bold, final Color foreColor,
15ea4d73
KL
1820 final Color backColor) {
1821 return color(foreColor, backColor, true) +
1822 rgbColor(bold, foreColor, backColor);
b1589621
KL
1823 }
1824
1825 /**
1826 * Create a SGR parameter sequence for both foreground and
1827 * background color change.
1828 *
05dbb28d
KL
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"
b1589621 1835 */
b5f2a6db 1836 private String color(final Color foreColor, final Color backColor,
7b5261bc 1837 final boolean header) {
b1589621 1838
7b5261bc
KL
1839 int ecmaForeColor = foreColor.getValue();
1840 int ecmaBackColor = backColor.getValue();
b1589621 1841
7b5261bc
KL
1842 // Convert Color.* values to SGR numerics
1843 ecmaBackColor += 40;
1844 ecmaForeColor += 30;
b1589621 1845
7b5261bc
KL
1846 if (header) {
1847 return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
1848 } else {
1849 return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
1850 }
b1589621
KL
1851 }
1852
1853 /**
1854 * Create a SGR parameter sequence for foreground, background, and
05dbb28d 1855 * several attributes. This sequence first resets all attributes to
42873e30 1856 * default, then sets attributes as per the parameters.
b1589621 1857 *
05dbb28d
KL
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"
b1589621 1866 */
42873e30 1867 private String color(final Color foreColor, final Color backColor,
7b5261bc
KL
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));
8a632d71
KL
1914 sb.append(rgbColor(bold, foreColor, backColor));
1915 return sb.toString();
b1589621
KL
1916 }
1917
b1589621 1918 /**
42873e30 1919 * Create a SGR parameter sequence to reset to defaults.
b1589621 1920 *
05dbb28d
KL
1921 * @return the string to emit to an ANSI / ECMA-style terminal,
1922 * e.g. "\033[0m"
b1589621 1923 */
42873e30 1924 private String normal() {
8a632d71 1925 return normal(true) + rgbColor(false, Color.WHITE, Color.BLACK);
b1589621
KL
1926 }
1927
1928 /**
1929 * Create a SGR parameter sequence to reset to defaults.
1930 *
05dbb28d
KL
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"
b1589621 1935 */
b5f2a6db 1936 private String normal(final boolean header) {
7b5261bc
KL
1937 if (header) {
1938 return "\033[0;37;40m";
1939 }
1940 return "0;37;40";
b1589621
KL
1941 }
1942
b1589621 1943 /**
42873e30 1944 * Create a SGR parameter sequence for enabling the visible cursor.
b1589621 1945 *
05dbb28d
KL
1946 * @param on if true, turn on cursor
1947 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1948 */
42873e30 1949 private String cursor(final boolean on) {
7b5261bc
KL
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 "";
b1589621
KL
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 *
05dbb28d 1965 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1966 */
42873e30 1967 private String clearAll() {
7b5261bc 1968 return "\033[0;37;40m\033[2J";
b1589621
KL
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
42873e30 1974 * white-on-black beforehand.
b1589621 1975 *
05dbb28d 1976 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1977 */
42873e30 1978 private String clearRemainingLine() {
7b5261bc 1979 return "\033[0;37;40m\033[K";
b1589621
KL
1980 }
1981
b1589621 1982 /**
42873e30 1983 * Move the cursor to (x, y).
b1589621 1984 *
05dbb28d
KL
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
b1589621 1988 */
42873e30 1989 private String gotoXY(final int x, final int y) {
7b5261bc 1990 return String.format("\033[%d;%dH", y + 1, x + 1);
b1589621
KL
1991 }
1992
1993 /**
05dbb28d 1994 * Tell (u)xterm that we want to receive mouse events based on "Any event
b5f2a6db
KL
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
b1589621
KL
1998 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1999 *
05dbb28d 2000 * Note that this also sets the alternate/primary screen buffer.
b1589621 2001 *
05dbb28d
KL
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
b1589621 2006 */
b5f2a6db 2007 private String mouse(final boolean on) {
7b5261bc 2008 if (on) {
32437017 2009 return "\033[?1002;1003;1005;1006h\033[?1049h";
7b5261bc 2010 }
32437017 2011 return "\033[?1002;1003;1006;1005l\033[?1049l";
b1589621
KL
2012 }
2013
2014}