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