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