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