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