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