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