immutable TKeypressEvent
[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
49import jexer.TKeypress;
50import jexer.bits.Color;
51import jexer.event.TInputEvent;
52import jexer.event.TKeypressEvent;
53import jexer.event.TMouseEvent;
54import jexer.event.TResizeEvent;
55import jexer.session.SessionInfo;
56import jexer.session.TSessionInfo;
57import jexer.session.TTYSessionInfo;
58import static jexer.TKeypress.*;
59
60/**
7b5261bc
KL
61 * This class reads keystrokes and mouse events and emits output to ANSI
62 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
b1589621 63 */
4328bb42 64public class ECMA48Terminal implements Runnable {
b1589621
KL
65
66 /**
7b5261bc
KL
67 * The session information.
68 */
69 private SessionInfo sessionInfo;
70
71 /**
72 * Getter for sessionInfo.
73 *
74 * @return the SessionInfo
b1589621 75 */
7b5261bc
KL
76 public final SessionInfo getSessionInfo() {
77 return sessionInfo;
78 }
b1589621 79
4328bb42 80 /**
7b5261bc 81 * The event queue, filled up by a thread reading on input.
4328bb42
KL
82 */
83 private List<TInputEvent> eventQueue;
84
85 /**
86 * If true, we want the reader thread to exit gracefully.
87 */
88 private boolean stopReaderThread;
89
90 /**
7b5261bc 91 * The reader thread.
4328bb42
KL
92 */
93 private Thread readerThread;
94
b1589621
KL
95 /**
96 * Parameters being collected. E.g. if the string is \033[1;3m, then
97 * params[0] will be 1 and params[1] will be 3.
98 */
99 private ArrayList<String> params;
100
101 /**
102 * params[paramI] is being appended to.
103 */
104 private int paramI;
105
106 /**
7b5261bc 107 * States in the input parser.
b1589621
KL
108 */
109 private enum ParseState {
7b5261bc
KL
110 GROUND,
111 ESCAPE,
112 ESCAPE_INTERMEDIATE,
113 CSI_ENTRY,
114 CSI_PARAM,
115 // CSI_INTERMEDIATE,
116 MOUSE
b1589621
KL
117 }
118
119 /**
7b5261bc 120 * Current parsing state.
b1589621
KL
121 */
122 private ParseState state;
123
124 /**
7b5261bc
KL
125 * The time we entered ESCAPE. If we get a bare escape without a code
126 * following it, this is used to return that bare escape.
b1589621
KL
127 */
128 private long escapeTime;
129
130 /**
131 * true if mouse1 was down. Used to report mouse1 on the release event.
132 */
133 private boolean mouse1;
134
135 /**
136 * true if mouse2 was down. Used to report mouse2 on the release event.
137 */
138 private boolean mouse2;
139
140 /**
141 * true if mouse3 was down. Used to report mouse3 on the release event.
142 */
143 private boolean mouse3;
144
145 /**
146 * Cache the cursor visibility value so we only emit the sequence when we
147 * need to.
148 */
149 private boolean cursorOn = true;
150
151 /**
152 * Cache the last window size to figure out if a TResizeEvent needs to be
153 * generated.
154 */
155 private TResizeEvent windowResize = null;
156
157 /**
158 * If true, then we changed System.in and need to change it back.
159 */
160 private boolean setRawMode;
161
162 /**
163 * The terminal's input. If an InputStream is not specified in the
4328bb42
KL
164 * constructor, then this InputStreamReader will be bound to System.in
165 * with UTF-8 encoding.
b1589621
KL
166 */
167 private Reader input;
168
4328bb42
KL
169 /**
170 * The terminal's raw InputStream. If an InputStream is not specified in
171 * the constructor, then this InputReader will be bound to System.in.
172 * This is used by run() to see if bytes are available() before calling
173 * (Reader)input.read().
174 */
175 private InputStream inputStream;
176
b1589621
KL
177 /**
178 * The terminal's output. If an OutputStream is not specified in the
179 * constructor, then this PrintWriter will be bound to System.out with
180 * UTF-8 encoding.
181 */
182 private PrintWriter output;
183
184 /**
185 * When true, the terminal is sending non-UTF8 bytes when reporting mouse
186 * events.
187 *
188 * TODO: Add broken mouse detection back into the reader.
189 */
190 private boolean brokenTerminalUTFMouse = false;
191
217c6107
KL
192 /**
193 * Get the output writer.
194 *
195 * @return the Writer
196 */
197 public PrintWriter getOutput() {
7b5261bc 198 return output;
217c6107
KL
199 }
200
623a1bd1
KL
201 /**
202 * Check if there are events in the queue.
203 *
204 * @return if true, getEvents() has something to return to the backend
205 */
206 public boolean hasEvents() {
7b5261bc
KL
207 synchronized (eventQueue) {
208 return (eventQueue.size() > 0);
209 }
623a1bd1
KL
210 }
211
b1589621 212 /**
7b5261bc
KL
213 * Call 'stty' to set cooked mode.
214 *
215 * <p>Actually executes '/bin/sh -c stty sane cooked &lt; /dev/tty'
b1589621
KL
216 */
217 private void sttyCooked() {
7b5261bc 218 doStty(false);
b1589621
KL
219 }
220
221 /**
7b5261bc
KL
222 * Call 'stty' to set raw mode.
223 *
224 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
225 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
226 * -parenb cs8 min 1 &lt; /dev/tty'
b1589621
KL
227 */
228 private void sttyRaw() {
7b5261bc 229 doStty(true);
b1589621
KL
230 }
231
232 /**
233 * Call 'stty' to set raw or cooked mode.
234 *
235 * @param mode if true, set raw mode, otherwise set cooked mode
236 */
7b5261bc
KL
237 private void doStty(final boolean mode) {
238 String [] cmdRaw = {
239 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
240 };
241 String [] cmdCooked = {
242 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
243 };
244 try {
245 Process process;
246 if (mode == true) {
247 process = Runtime.getRuntime().exec(cmdRaw);
248 } else {
249 process = Runtime.getRuntime().exec(cmdCooked);
250 }
251 BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
252 String line = in.readLine();
253 if ((line != null) && (line.length() > 0)) {
254 System.err.println("WEIRD?! Normal output from stty: " + line);
255 }
256 while (true) {
257 BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
258 line = err.readLine();
259 if ((line != null) && (line.length() > 0)) {
260 System.err.println("Error output from stty: " + line);
261 }
262 try {
263 process.waitFor();
264 break;
265 } catch (InterruptedException e) {
266 e.printStackTrace();
267 }
268 }
269 int rc = process.exitValue();
270 if (rc != 0) {
271 System.err.println("stty returned error code: " + rc);
272 }
273 } catch (IOException e) {
274 e.printStackTrace();
275 }
b1589621
KL
276 }
277
278 /**
7b5261bc 279 * Constructor sets up state for getEvent().
b1589621
KL
280 *
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 */
7b5261bc
KL
291 public ECMA48Terminal(final InputStream input,
292 final OutputStream output) throws UnsupportedEncodingException {
293
294 reset();
295 mouse1 = false;
296 mouse2 = false;
297 mouse3 = false;
298 stopReaderThread = false;
299
300 if (input == null) {
301 // inputStream = System.in;
302 inputStream = new FileInputStream(FileDescriptor.in);
303 sttyRaw();
304 setRawMode = true;
305 } else {
306 inputStream = input;
307 }
308 this.input = new InputStreamReader(inputStream, "UTF-8");
309
310 // TODO: include TelnetSocket from NIB and have it implement
311 // SessionInfo
312 if (input instanceof SessionInfo) {
313 sessionInfo = (SessionInfo) input;
314 }
315 if (sessionInfo == null) {
316 if (input == null) {
317 // Reading right off the tty
318 sessionInfo = new TTYSessionInfo();
319 } else {
320 sessionInfo = new TSessionInfo();
321 }
322 }
323
324 if (output == null) {
325 this.output = new PrintWriter(new OutputStreamWriter(System.out,
326 "UTF-8"));
327 } else {
328 this.output = new PrintWriter(new OutputStreamWriter(output,
329 "UTF-8"));
330 }
331
332 // Enable mouse reporting and metaSendsEscape
333 this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
334
335 // Hang onto the window size
336 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
337 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
338
339 // Spin up the input reader
340 eventQueue = new LinkedList<TInputEvent>();
341 readerThread = new Thread(this);
342 readerThread.start();
b1589621
KL
343 }
344
345 /**
7b5261bc 346 * Restore terminal to normal state.
b1589621
KL
347 */
348 public void shutdown() {
4328bb42 349
7b5261bc
KL
350 // System.err.println("=== shutdown() ==="); System.err.flush();
351
352 // Tell the reader thread to stop looking at input
353 stopReaderThread = true;
354 try {
355 readerThread.join();
356 } catch (InterruptedException e) {
357 e.printStackTrace();
358 }
359
360 // Disable mouse reporting and show cursor
361 output.printf("%s%s%s", mouse(false), cursor(true), normal());
362 output.flush();
363
364 if (setRawMode) {
365 sttyCooked();
366 setRawMode = false;
367 // We don't close System.in/out
368 } else {
369 // Shut down the streams, this should wake up the reader thread
370 // and make it exit.
371 try {
372 if (input != null) {
373 input.close();
374 input = null;
375 }
376 if (output != null) {
377 output.close();
378 output = null;
379 }
380 } catch (IOException e) {
381 e.printStackTrace();
382 }
383 }
b1589621
KL
384 }
385
386 /**
7b5261bc 387 * Flush output.
b1589621
KL
388 */
389 public void flush() {
7b5261bc 390 output.flush();
b1589621
KL
391 }
392
393 /**
7b5261bc 394 * Reset keyboard/mouse input parser.
b1589621
KL
395 */
396 private void reset() {
7b5261bc
KL
397 state = ParseState.GROUND;
398 params = new ArrayList<String>();
399 paramI = 0;
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 }
477
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
533 TMouseEvent event = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN);
534 event.x = x;
535 event.y = y;
536 event.absoluteX = x;
537 event.absoluteY = y;
538
539 // System.err.printf("buttons: %04x\r\n", buttons);
540
541 switch (buttons) {
542 case 0:
543 event.mouse1 = true;
544 mouse1 = true;
545 break;
546 case 1:
547 event.mouse2 = true;
548 mouse2 = true;
549 break;
550 case 2:
551 event.mouse3 = true;
552 mouse3 = true;
553 break;
554 case 3:
555 // Release or Move
556 if (!mouse1 && !mouse2 && !mouse3) {
557 event.type = TMouseEvent.Type.MOUSE_MOTION;
558 } else {
559 event.type = TMouseEvent.Type.MOUSE_UP;
560 }
561 if (mouse1) {
562 mouse1 = false;
563 event.mouse1 = true;
564 }
565 if (mouse2) {
566 mouse2 = false;
567 event.mouse2 = true;
568 }
569 if (mouse3) {
570 mouse3 = false;
571 event.mouse3 = true;
572 }
573 break;
574
575 case 32:
576 // Dragging with mouse1 down
577 event.mouse1 = true;
578 mouse1 = true;
579 event.type = TMouseEvent.Type.MOUSE_MOTION;
580 break;
581
582 case 33:
583 // Dragging with mouse2 down
584 event.mouse2 = true;
585 mouse2 = true;
586 event.type = TMouseEvent.Type.MOUSE_MOTION;
587 break;
588
589 case 34:
590 // Dragging with mouse3 down
591 event.mouse3 = true;
592 mouse3 = true;
593 event.type = TMouseEvent.Type.MOUSE_MOTION;
594 break;
595
596 case 96:
597 // Dragging with mouse2 down after wheelUp
598 event.mouse2 = true;
599 mouse2 = true;
600 event.type = TMouseEvent.Type.MOUSE_MOTION;
601 break;
602
603 case 97:
604 // Dragging with mouse2 down after wheelDown
605 event.mouse2 = true;
606 mouse2 = true;
607 event.type = TMouseEvent.Type.MOUSE_MOTION;
608 break;
609
610 case 64:
611 event.mouseWheelUp = true;
612 break;
613
614 case 65:
615 event.mouseWheelDown = true;
616 break;
617
618 default:
619 // Unknown, just make it motion
620 event.type = TMouseEvent.Type.MOUSE_MOTION;
621 break;
622 }
623 return event;
b1589621
KL
624 }
625
626 /**
05dbb28d 627 * Return any events in the IO queue.
b1589621 628 *
623a1bd1 629 * @param queue list to append new events to
05dbb28d 630 */
7b5261bc
KL
631 public void getEvents(final List<TInputEvent> queue) {
632 synchronized (eventQueue) {
633 if (eventQueue.size() > 0) {
634 queue.addAll(eventQueue);
635 eventQueue.clear();
636 }
637 }
05dbb28d
KL
638 }
639
640 /**
623a1bd1 641 * Return any events in the IO queue due to timeout.
b1589621 642 *
623a1bd1 643 * @param queue list to append new events to
b1589621 644 */
7b5261bc
KL
645 public void getIdleEvents(final List<TInputEvent> queue) {
646
647 // Check for new window size
648 sessionInfo.queryWindowSize();
649 int newWidth = sessionInfo.getWindowWidth();
650 int newHeight = sessionInfo.getWindowHeight();
651 if ((newWidth != windowResize.getWidth())
652 || (newHeight != windowResize.getHeight())
653 ) {
654 TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
655 newWidth, newHeight);
656 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
657 newWidth, newHeight);
658 synchronized (eventQueue) {
659 eventQueue.add(event);
660 }
661 }
662
663 synchronized (eventQueue) {
664 if (eventQueue.size() > 0) {
665 queue.addAll(eventQueue);
666 eventQueue.clear();
667 }
668 }
b1589621
KL
669 }
670
671 /**
672 * Parses the next character of input to see if an InputEvent is
673 * fully here.
674 *
623a1bd1 675 * @param events list to append new events to
05dbb28d 676 * @param ch Unicode code point
b1589621 677 */
7b5261bc
KL
678 private void processChar(final List<TInputEvent> events, final char ch) {
679
7b5261bc 680 // ESCDELAY type timeout
b299e69c 681 Date now = new Date();
7b5261bc
KL
682 if (state == ParseState.ESCAPE) {
683 long escDelay = now.getTime() - escapeTime;
684 if (escDelay > 250) {
685 // After 0.25 seconds, assume a true escape character
b299e69c 686 events.add(controlChar((char)0x1B, false));
7b5261bc
KL
687 reset();
688 }
689 }
690
b299e69c
KL
691 // TKeypress fields
692 boolean ctrl = false;
693 boolean alt = false;
694 boolean shift = false;
695 char keyCh = ch;
696 TKeypress key;
697
7b5261bc
KL
698 // System.err.printf("state: %s ch %c\r\n", state, ch);
699
700 switch (state) {
701 case GROUND:
702
703 if (ch == 0x1B) {
704 state = ParseState.ESCAPE;
705 escapeTime = now.getTime();
706 return;
707 }
708
709 if (ch <= 0x1F) {
710 // Control character
b299e69c 711 events.add(controlChar(ch, false));
7b5261bc
KL
712 reset();
713 return;
714 }
715
716 if (ch >= 0x20) {
717 // Normal character
b299e69c
KL
718 events.add(new TKeypressEvent(false, 0, ch,
719 false, false, false));
7b5261bc
KL
720 reset();
721 return;
722 }
723
724 break;
725
726 case ESCAPE:
727 if (ch <= 0x1F) {
728 // ALT-Control character
b299e69c 729 events.add(controlChar(ch, true));
7b5261bc
KL
730 reset();
731 return;
732 }
733
734 if (ch == 'O') {
735 // This will be one of the function keys
736 state = ParseState.ESCAPE_INTERMEDIATE;
737 return;
738 }
739
740 // '[' goes to CSI_ENTRY
741 if (ch == '[') {
742 state = ParseState.CSI_ENTRY;
743 return;
744 }
745
746 // Everything else is assumed to be Alt-keystroke
7b5261bc 747 if ((ch >= 'A') && (ch <= 'Z')) {
b299e69c 748 shift = true;
7b5261bc 749 }
b299e69c
KL
750 alt = true;
751 events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift));
7b5261bc
KL
752 reset();
753 return;
754
755 case ESCAPE_INTERMEDIATE:
756 if ((ch >= 'P') && (ch <= 'S')) {
757 // Function key
7b5261bc
KL
758 switch (ch) {
759 case 'P':
b299e69c 760 events.add(new TKeypressEvent(kbF1));
7b5261bc
KL
761 break;
762 case 'Q':
b299e69c 763 events.add(new TKeypressEvent(kbF2));
7b5261bc
KL
764 break;
765 case 'R':
b299e69c 766 events.add(new TKeypressEvent(kbF3));
7b5261bc
KL
767 break;
768 case 'S':
b299e69c 769 events.add(new TKeypressEvent(kbF4));
7b5261bc
KL
770 break;
771 default:
772 break;
773 }
7b5261bc
KL
774 reset();
775 return;
776 }
777
778 // Unknown keystroke, ignore
779 reset();
780 return;
781
782 case CSI_ENTRY:
783 // Numbers - parameter values
784 if ((ch >= '0') && (ch <= '9')) {
785 params.set(paramI, params.get(paramI) + ch);
786 state = ParseState.CSI_PARAM;
787 return;
788 }
789 // Parameter separator
790 if (ch == ';') {
791 paramI++;
792 params.set(paramI, "");
793 return;
794 }
795
796 if ((ch >= 0x30) && (ch <= 0x7E)) {
797 switch (ch) {
798 case 'A':
799 // Up
7b5261bc
KL
800 if (params.size() > 1) {
801 if (params.get(1).equals("2")) {
b299e69c 802 shift = true;
7b5261bc
KL
803 }
804 if (params.get(1).equals("5")) {
b299e69c 805 ctrl = true;
7b5261bc
KL
806 }
807 if (params.get(1).equals("3")) {
b299e69c 808 alt = true;
7b5261bc
KL
809 }
810 }
b299e69c 811 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
7b5261bc
KL
812 reset();
813 return;
814 case 'B':
815 // Down
7b5261bc
KL
816 if (params.size() > 1) {
817 if (params.get(1).equals("2")) {
b299e69c 818 shift = true;
7b5261bc
KL
819 }
820 if (params.get(1).equals("5")) {
b299e69c 821 ctrl = true;
7b5261bc
KL
822 }
823 if (params.get(1).equals("3")) {
b299e69c 824 alt = true;
7b5261bc
KL
825 }
826 }
b299e69c 827 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
7b5261bc
KL
828 reset();
829 return;
830 case 'C':
831 // Right
7b5261bc
KL
832 if (params.size() > 1) {
833 if (params.get(1).equals("2")) {
b299e69c 834 shift = true;
7b5261bc
KL
835 }
836 if (params.get(1).equals("5")) {
b299e69c 837 ctrl = true;
7b5261bc
KL
838 }
839 if (params.get(1).equals("3")) {
b299e69c 840 alt = true;
7b5261bc
KL
841 }
842 }
b299e69c 843 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
7b5261bc
KL
844 reset();
845 return;
846 case 'D':
847 // Left
7b5261bc
KL
848 if (params.size() > 1) {
849 if (params.get(1).equals("2")) {
b299e69c 850 shift = true;
7b5261bc
KL
851 }
852 if (params.get(1).equals("5")) {
b299e69c 853 ctrl = true;
7b5261bc
KL
854 }
855 if (params.get(1).equals("3")) {
b299e69c 856 alt = true;
7b5261bc
KL
857 }
858 }
b299e69c 859 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
7b5261bc
KL
860 reset();
861 return;
862 case 'H':
863 // Home
b299e69c 864 events.add(new TKeypressEvent(kbHome));
7b5261bc
KL
865 reset();
866 return;
867 case 'F':
868 // End
b299e69c 869 events.add(new TKeypressEvent(kbEnd));
7b5261bc
KL
870 reset();
871 return;
872 case 'Z':
873 // CBT - Cursor backward X tab stops (default 1)
b299e69c 874 events.add(new TKeypressEvent(kbBackTab));
7b5261bc
KL
875 reset();
876 return;
877 case 'M':
878 // Mouse position
879 state = ParseState.MOUSE;
880 return;
881 default:
882 break;
883 }
884 }
885
886 // Unknown keystroke, ignore
887 reset();
888 return;
889
890 case CSI_PARAM:
891 // Numbers - parameter values
892 if ((ch >= '0') && (ch <= '9')) {
893 params.set(paramI, params.get(paramI) + ch);
894 state = ParseState.CSI_PARAM;
895 return;
896 }
897 // Parameter separator
898 if (ch == ';') {
899 paramI++;
900 params.set(paramI, "");
901 return;
902 }
903
904 if (ch == '~') {
905 events.add(csiFnKey());
906 reset();
907 return;
908 }
909
910 if ((ch >= 0x30) && (ch <= 0x7E)) {
911 switch (ch) {
912 case 'A':
913 // Up
7b5261bc
KL
914 if (params.size() > 1) {
915 if (params.get(1).equals("2")) {
b299e69c 916 shift = true;
7b5261bc
KL
917 }
918 if (params.get(1).equals("5")) {
b299e69c 919 ctrl = true;
7b5261bc
KL
920 }
921 if (params.get(1).equals("3")) {
b299e69c 922 alt = true;
7b5261bc
KL
923 }
924 }
b299e69c 925 events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
7b5261bc
KL
926 reset();
927 return;
928 case 'B':
929 // Down
7b5261bc
KL
930 if (params.size() > 1) {
931 if (params.get(1).equals("2")) {
b299e69c 932 shift = true;
7b5261bc
KL
933 }
934 if (params.get(1).equals("5")) {
b299e69c 935 ctrl = true;
7b5261bc
KL
936 }
937 if (params.get(1).equals("3")) {
b299e69c 938 alt = true;
7b5261bc
KL
939 }
940 }
b299e69c 941 events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
7b5261bc
KL
942 reset();
943 return;
944 case 'C':
945 // Right
7b5261bc
KL
946 if (params.size() > 1) {
947 if (params.get(1).equals("2")) {
b299e69c 948 shift = true;
7b5261bc
KL
949 }
950 if (params.get(1).equals("5")) {
b299e69c 951 ctrl = true;
7b5261bc
KL
952 }
953 if (params.get(1).equals("3")) {
b299e69c 954 alt = true;
7b5261bc
KL
955 }
956 }
b299e69c 957 events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
7b5261bc
KL
958 reset();
959 return;
960 case 'D':
961 // Left
7b5261bc
KL
962 if (params.size() > 1) {
963 if (params.get(1).equals("2")) {
b299e69c 964 shift = true;
7b5261bc
KL
965 }
966 if (params.get(1).equals("5")) {
b299e69c 967 ctrl = true;
7b5261bc
KL
968 }
969 if (params.get(1).equals("3")) {
b299e69c 970 alt = true;
7b5261bc
KL
971 }
972 }
b299e69c 973 events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
7b5261bc
KL
974 reset();
975 return;
976 default:
977 break;
978 }
979 }
980
981 // Unknown keystroke, ignore
982 reset();
983 return;
984
985 case MOUSE:
986 params.set(0, params.get(paramI) + ch);
987 if (params.get(0).length() == 3) {
988 // We have enough to generate a mouse event
989 events.add(parseMouse());
990 reset();
991 }
992 return;
993
994 default:
995 break;
996 }
997
998 // This "should" be impossible to reach
999 return;
b1589621
KL
1000 }
1001
1002 /**
05dbb28d
KL
1003 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1004 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1005 * enabled.
b1589621 1006 *
05dbb28d
KL
1007 * @param on if true, enable metaSendsEscape
1008 * @return the string to emit to xterm
b1589621 1009 */
7b5261bc
KL
1010 public String xtermMetaSendsEscape(final boolean on) {
1011 if (on) {
1012 return "\033[?1036h\033[?1034l";
1013 }
1014 return "\033[?1036l";
b1589621
KL
1015 }
1016
1017 /**
05dbb28d
KL
1018 * Convert a list of SGR parameters into a full escape sequence. This
1019 * also eliminates a trailing ';' which would otherwise reset everything
1020 * to white-on-black not-bold.
b1589621 1021 *
05dbb28d
KL
1022 * @param str string of parameters, e.g. "31;1;"
1023 * @return the string to emit to an ANSI / ECMA-style terminal,
1024 * e.g. "\033[31;1m"
b1589621 1025 */
7b5261bc
KL
1026 public String addHeaderSGR(String str) {
1027 if (str.length() > 0) {
1028 // Nix any trailing ';' because that resets all attributes
1029 while (str.endsWith(":")) {
1030 str = str.substring(0, str.length() - 1);
1031 }
1032 }
1033 return "\033[" + str + "m";
b1589621
KL
1034 }
1035
1036 /**
1037 * Create a SGR parameter sequence for a single color change.
1038 *
05dbb28d
KL
1039 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1040 * @param foreground if true, this is a foreground color
1041 * @return the string to emit to an ANSI / ECMA-style terminal,
1042 * e.g. "\033[42m"
b1589621 1043 */
7b5261bc
KL
1044 public String color(final Color color, final boolean foreground) {
1045 return color(color, foreground, true);
b1589621
KL
1046 }
1047
1048 /**
1049 * Create a SGR parameter sequence for a single color change.
1050 *
05dbb28d
KL
1051 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1052 * @param foreground if true, this is a foreground color
1053 * @param header if true, make the full header, otherwise just emit the
1054 * color parameter e.g. "42;"
1055 * @return the string to emit to an ANSI / ECMA-style terminal,
1056 * e.g. "\033[42m"
b1589621 1057 */
7b5261bc
KL
1058 public String color(final Color color, final boolean foreground,
1059 final boolean header) {
1060
1061 int ecmaColor = color.getValue();
1062
1063 // Convert Color.* values to SGR numerics
1064 if (foreground) {
1065 ecmaColor += 30;
1066 } else {
1067 ecmaColor += 40;
1068 }
1069
1070 if (header) {
1071 return String.format("\033[%dm", ecmaColor);
1072 } else {
1073 return String.format("%d;", ecmaColor);
1074 }
b1589621
KL
1075 }
1076
1077 /**
1078 * Create a SGR parameter sequence for both foreground and
1079 * background color change.
1080 *
05dbb28d
KL
1081 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1082 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1083 * @return the string to emit to an ANSI / ECMA-style terminal,
1084 * e.g. "\033[31;42m"
b1589621 1085 */
7b5261bc
KL
1086 public String color(final Color foreColor, final Color backColor) {
1087 return color(foreColor, backColor, true);
b1589621
KL
1088 }
1089
1090 /**
1091 * Create a SGR parameter sequence for both foreground and
1092 * background color change.
1093 *
05dbb28d
KL
1094 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1095 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1096 * @param header if true, make the full header, otherwise just emit the
1097 * color parameter e.g. "31;42;"
1098 * @return the string to emit to an ANSI / ECMA-style terminal,
1099 * e.g. "\033[31;42m"
b1589621 1100 */
7b5261bc
KL
1101 public String color(final Color foreColor, final Color backColor,
1102 final boolean header) {
b1589621 1103
7b5261bc
KL
1104 int ecmaForeColor = foreColor.getValue();
1105 int ecmaBackColor = backColor.getValue();
b1589621 1106
7b5261bc
KL
1107 // Convert Color.* values to SGR numerics
1108 ecmaBackColor += 40;
1109 ecmaForeColor += 30;
b1589621 1110
7b5261bc
KL
1111 if (header) {
1112 return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
1113 } else {
1114 return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
1115 }
b1589621
KL
1116 }
1117
1118 /**
1119 * Create a SGR parameter sequence for foreground, background, and
05dbb28d
KL
1120 * several attributes. This sequence first resets all attributes to
1121 * default, then sets attributes as per the parameters.
b1589621 1122 *
05dbb28d
KL
1123 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1124 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1125 * @param bold if true, set bold
1126 * @param reverse if true, set reverse
1127 * @param blink if true, set blink
1128 * @param underline if true, set underline
1129 * @return the string to emit to an ANSI / ECMA-style terminal,
1130 * e.g. "\033[0;1;31;42m"
b1589621 1131 */
7b5261bc
KL
1132 public String color(final Color foreColor, final Color backColor,
1133 final boolean bold, final boolean reverse, final boolean blink,
1134 final boolean underline) {
1135
1136 int ecmaForeColor = foreColor.getValue();
1137 int ecmaBackColor = backColor.getValue();
1138
1139 // Convert Color.* values to SGR numerics
1140 ecmaBackColor += 40;
1141 ecmaForeColor += 30;
1142
1143 StringBuilder sb = new StringBuilder();
1144 if ( bold && reverse && blink && !underline ) {
1145 sb.append("\033[0;1;7;5;");
1146 } else if ( bold && reverse && !blink && !underline ) {
1147 sb.append("\033[0;1;7;");
1148 } else if ( !bold && reverse && blink && !underline ) {
1149 sb.append("\033[0;7;5;");
1150 } else if ( bold && !reverse && blink && !underline ) {
1151 sb.append("\033[0;1;5;");
1152 } else if ( bold && !reverse && !blink && !underline ) {
1153 sb.append("\033[0;1;");
1154 } else if ( !bold && reverse && !blink && !underline ) {
1155 sb.append("\033[0;7;");
1156 } else if ( !bold && !reverse && blink && !underline) {
1157 sb.append("\033[0;5;");
1158 } else if ( bold && reverse && blink && underline ) {
1159 sb.append("\033[0;1;7;5;4;");
1160 } else if ( bold && reverse && !blink && underline ) {
1161 sb.append("\033[0;1;7;4;");
1162 } else if ( !bold && reverse && blink && underline ) {
1163 sb.append("\033[0;7;5;4;");
1164 } else if ( bold && !reverse && blink && underline ) {
1165 sb.append("\033[0;1;5;4;");
1166 } else if ( bold && !reverse && !blink && underline ) {
1167 sb.append("\033[0;1;4;");
1168 } else if ( !bold && reverse && !blink && underline ) {
1169 sb.append("\033[0;7;4;");
1170 } else if ( !bold && !reverse && blink && underline) {
1171 sb.append("\033[0;5;4;");
1172 } else if ( !bold && !reverse && !blink && underline) {
1173 sb.append("\033[0;4;");
1174 } else {
1175 assert (!bold && !reverse && !blink && !underline);
1176 sb.append("\033[0;");
1177 }
1178 sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
1179 return sb.toString();
b1589621
KL
1180 }
1181
1182 /**
1183 * Create a SGR parameter sequence for enabling reverse color.
1184 *
05dbb28d
KL
1185 * @param on if true, turn on reverse
1186 * @return the string to emit to an ANSI / ECMA-style terminal,
1187 * e.g. "\033[7m"
b1589621 1188 */
7b5261bc
KL
1189 public String reverse(final boolean on) {
1190 if (on) {
1191 return "\033[7m";
1192 }
1193 return "\033[27m";
b1589621
KL
1194 }
1195
1196 /**
1197 * Create a SGR parameter sequence to reset to defaults.
1198 *
05dbb28d
KL
1199 * @return the string to emit to an ANSI / ECMA-style terminal,
1200 * e.g. "\033[0m"
b1589621 1201 */
7b5261bc
KL
1202 public String normal() {
1203 return normal(true);
b1589621
KL
1204 }
1205
1206 /**
1207 * Create a SGR parameter sequence to reset to defaults.
1208 *
05dbb28d
KL
1209 * @param header if true, make the full header, otherwise just emit the
1210 * bare parameter e.g. "0;"
1211 * @return the string to emit to an ANSI / ECMA-style terminal,
1212 * e.g. "\033[0m"
b1589621 1213 */
7b5261bc
KL
1214 public String normal(final boolean header) {
1215 if (header) {
1216 return "\033[0;37;40m";
1217 }
1218 return "0;37;40";
b1589621
KL
1219 }
1220
1221 /**
1222 * Create a SGR parameter sequence for enabling boldface.
1223 *
05dbb28d
KL
1224 * @param on if true, turn on bold
1225 * @return the string to emit to an ANSI / ECMA-style terminal,
1226 * e.g. "\033[1m"
b1589621 1227 */
7b5261bc
KL
1228 public String bold(final boolean on) {
1229 return bold(on, true);
b1589621
KL
1230 }
1231
1232 /**
1233 * Create a SGR parameter sequence for enabling boldface.
1234 *
05dbb28d
KL
1235 * @param on if true, turn on bold
1236 * @param header if true, make the full header, otherwise just emit the
1237 * bare parameter e.g. "1;"
1238 * @return the string to emit to an ANSI / ECMA-style terminal,
1239 * e.g. "\033[1m"
b1589621 1240 */
7b5261bc
KL
1241 public String bold(final boolean on, final boolean header) {
1242 if (header) {
1243 if (on) {
1244 return "\033[1m";
1245 }
1246 return "\033[22m";
1247 }
1248 if (on) {
1249 return "1;";
1250 }
1251 return "22;";
b1589621
KL
1252 }
1253
1254 /**
1255 * Create a SGR parameter sequence for enabling blinking text.
1256 *
05dbb28d
KL
1257 * @param on if true, turn on blink
1258 * @return the string to emit to an ANSI / ECMA-style terminal,
1259 * e.g. "\033[5m"
b1589621 1260 */
7b5261bc
KL
1261 public String blink(final boolean on) {
1262 return blink(on, true);
b1589621
KL
1263 }
1264
1265 /**
1266 * Create a SGR parameter sequence for enabling blinking text.
1267 *
05dbb28d
KL
1268 * @param on if true, turn on blink
1269 * @param header if true, make the full header, otherwise just emit the
1270 * bare parameter e.g. "5;"
1271 * @return the string to emit to an ANSI / ECMA-style terminal,
1272 * e.g. "\033[5m"
b1589621 1273 */
7b5261bc
KL
1274 public String blink(final boolean on, final boolean header) {
1275 if (header) {
1276 if (on) {
1277 return "\033[5m";
1278 }
1279 return "\033[25m";
1280 }
1281 if (on) {
1282 return "5;";
1283 }
1284 return "25;";
b1589621
KL
1285 }
1286
1287 /**
05dbb28d
KL
1288 * Create a SGR parameter sequence for enabling underline / underscored
1289 * text.
b1589621 1290 *
05dbb28d
KL
1291 * @param on if true, turn on underline
1292 * @return the string to emit to an ANSI / ECMA-style terminal,
1293 * e.g. "\033[4m"
b1589621 1294 */
7b5261bc
KL
1295 public String underline(final boolean on) {
1296 if (on) {
1297 return "\033[4m";
1298 }
1299 return "\033[24m";
b1589621
KL
1300 }
1301
1302 /**
1303 * Create a SGR parameter sequence for enabling the visible cursor.
1304 *
05dbb28d
KL
1305 * @param on if true, turn on cursor
1306 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1307 */
7b5261bc
KL
1308 public String cursor(final boolean on) {
1309 if (on && !cursorOn) {
1310 cursorOn = true;
1311 return "\033[?25h";
1312 }
1313 if (!on && cursorOn) {
1314 cursorOn = false;
1315 return "\033[?25l";
1316 }
1317 return "";
b1589621
KL
1318 }
1319
1320 /**
1321 * Clear the entire screen. Because some terminals use back-color-erase,
1322 * set the color to white-on-black beforehand.
1323 *
05dbb28d 1324 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1325 */
7b5261bc
KL
1326 public String clearAll() {
1327 return "\033[0;37;40m\033[2J";
b1589621
KL
1328 }
1329
1330 /**
1331 * Clear the line from the cursor (inclusive) to the end of the screen.
1332 * Because some terminals use back-color-erase, set the color to
1333 * white-on-black beforehand.
1334 *
05dbb28d 1335 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1336 */
7b5261bc
KL
1337 public String clearRemainingLine() {
1338 return "\033[0;37;40m\033[K";
b1589621
KL
1339 }
1340
1341 /**
1342 * Clear the line up the cursor (inclusive). Because some terminals use
1343 * back-color-erase, set the color to white-on-black beforehand.
1344 *
05dbb28d 1345 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1346 */
7b5261bc
KL
1347 public String clearPreceedingLine() {
1348 return "\033[0;37;40m\033[1K";
b1589621
KL
1349 }
1350
1351 /**
1352 * Clear the line. Because some terminals use back-color-erase, set the
1353 * color to white-on-black beforehand.
1354 *
05dbb28d 1355 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1356 */
7b5261bc
KL
1357 public String clearLine() {
1358 return "\033[0;37;40m\033[2K";
b1589621
KL
1359 }
1360
1361 /**
1362 * Move the cursor to the top-left corner.
1363 *
05dbb28d 1364 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1365 */
7b5261bc
KL
1366 public String home() {
1367 return "\033[H";
b1589621
KL
1368 }
1369
1370 /**
1371 * Move the cursor to (x, y).
1372 *
05dbb28d
KL
1373 * @param x column coordinate. 0 is the left-most column.
1374 * @param y row coordinate. 0 is the top-most row.
1375 * @return the string to emit to an ANSI / ECMA-style terminal
b1589621 1376 */
7b5261bc
KL
1377 public String gotoXY(final int x, final int y) {
1378 return String.format("\033[%d;%dH", y + 1, x + 1);
b1589621
KL
1379 }
1380
1381 /**
05dbb28d
KL
1382 * Tell (u)xterm that we want to receive mouse events based on "Any event
1383 * tracking" and UTF-8 coordinates. See
b1589621
KL
1384 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1385 *
05dbb28d 1386 * Note that this also sets the alternate/primary screen buffer.
b1589621 1387 *
05dbb28d
KL
1388 * @param on If true, enable mouse report and use the alternate screen
1389 * buffer. If false disable mouse reporting and use the primary screen
1390 * buffer.
1391 * @return the string to emit to xterm
b1589621 1392 */
7b5261bc
KL
1393 public String mouse(final boolean on) {
1394 if (on) {
1395 return "\033[?1003;1005h\033[?1049h";
1396 }
1397 return "\033[?1003;1005l\033[?1049l";
b1589621
KL
1398 }
1399
4328bb42
KL
1400 /**
1401 * Read function runs on a separate thread.
1402 */
1403 public void run() {
7b5261bc
KL
1404 boolean done = false;
1405 // available() will often return > 1, so we need to read in chunks to
1406 // stay caught up.
1407 char [] readBuffer = new char[128];
1408 List<TInputEvent> events = new LinkedList<TInputEvent>();
1409
1410 while (!done && !stopReaderThread) {
1411 try {
1412 // We assume that if inputStream has bytes available, then
1413 // input won't block on read().
1414 int n = inputStream.available();
1415 if (n > 0) {
1416 if (readBuffer.length < n) {
1417 // The buffer wasn't big enough, make it huger
1418 readBuffer = new char[readBuffer.length * 2];
1419 }
1420
1421 int rc = input.read(readBuffer, 0, n);
1422 // System.err.printf("read() %d", rc); System.err.flush();
1423 if (rc == -1) {
1424 // This is EOF
1425 done = true;
1426 } else {
1427 for (int i = 0; i < rc; i++) {
1428 int ch = readBuffer[i];
1429 processChar(events, (char)ch);
1430 if (events.size() > 0) {
1431 // Add to the queue for the backend thread to
1432 // be able to obtain.
1433 synchronized (eventQueue) {
1434 eventQueue.addAll(events);
1435 }
1436 // Now wake up the backend
1437 synchronized (this) {
1438 this.notifyAll();
1439 }
1440 events.clear();
1441 }
1442 }
1443 }
1444 } else {
1445 // Wait 5 millis for more data
1446 Thread.sleep(5);
1447 }
1448 // System.err.println("end while loop"); System.err.flush();
1449 } catch (InterruptedException e) {
1450 // SQUASH
1451 } catch (IOException e) {
1452 e.printStackTrace();
1453 done = true;
1454 }
1455 } // while ((done == false) && (stopReaderThread == false))
1456 // System.err.println("*** run() exiting..."); System.err.flush();
4328bb42
KL
1457 }
1458
b1589621 1459}