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