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