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