2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2017 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.backend
;
31 import java
.io
.BufferedReader
;
32 import java
.io
.FileDescriptor
;
33 import java
.io
.FileInputStream
;
34 import java
.io
.InputStream
;
35 import java
.io
.InputStreamReader
;
36 import java
.io
.IOException
;
37 import java
.io
.OutputStream
;
38 import java
.io
.OutputStreamWriter
;
39 import java
.io
.PrintWriter
;
40 import java
.io
.Reader
;
41 import java
.io
.UnsupportedEncodingException
;
42 import java
.util
.ArrayList
;
43 import java
.util
.List
;
44 import java
.util
.LinkedList
;
46 import jexer
.bits
.Cell
;
47 import jexer
.bits
.CellAttributes
;
48 import jexer
.bits
.Color
;
49 import jexer
.event
.TInputEvent
;
50 import jexer
.event
.TKeypressEvent
;
51 import jexer
.event
.TMouseEvent
;
52 import jexer
.event
.TResizeEvent
;
53 import static jexer
.TKeypress
.*;
56 * This class reads keystrokes and mouse events and emits output to ANSI
57 * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
59 public final class ECMA48Terminal
extends LogicalScreen
60 implements TerminalReader
, Runnable
{
63 * Emit debugging to stderr.
65 private boolean debugToStderr
= false;
68 * If true, emit T.416-style RGB colors. This is a) expensive in
69 * bandwidth, and b) potentially terrible looking for non-xterms.
71 private static boolean doRgbColor
= false;
74 * The session information.
76 private SessionInfo sessionInfo
;
79 * Getter for sessionInfo.
81 * @return the SessionInfo
83 public SessionInfo
getSessionInfo() {
88 * The event queue, filled up by a thread reading on input.
90 private List
<TInputEvent
> eventQueue
;
93 * If true, we want the reader thread to exit gracefully.
95 private boolean stopReaderThread
;
100 private Thread readerThread
;
103 * Parameters being collected. E.g. if the string is \033[1;3m, then
104 * params[0] will be 1 and params[1] will be 3.
106 private ArrayList
<String
> params
;
109 * States in the input parser.
111 private enum ParseState
{
122 * Current parsing state.
124 private ParseState state
;
127 * The time we entered ESCAPE. If we get a bare escape without a code
128 * following it, this is used to return that bare escape.
130 private long escapeTime
;
133 * The time we last checked the window size. We try not to spawn stty
134 * more than once per second.
136 private long windowSizeTime
;
139 * true if mouse1 was down. Used to report mouse1 on the release event.
141 private boolean mouse1
;
144 * true if mouse2 was down. Used to report mouse2 on the release event.
146 private boolean mouse2
;
149 * true if mouse3 was down. Used to report mouse3 on the release event.
151 private boolean mouse3
;
154 * Cache the cursor visibility value so we only emit the sequence when we
157 private boolean cursorOn
= true;
160 * Cache the last window size to figure out if a TResizeEvent needs to be
163 private TResizeEvent windowResize
= null;
166 * If true, then we changed System.in and need to change it back.
168 private boolean setRawMode
;
171 * The terminal's input. If an InputStream is not specified in the
172 * constructor, then this InputStreamReader will be bound to System.in
173 * with UTF-8 encoding.
175 private Reader input
;
178 * The terminal's raw InputStream. If an InputStream is not specified in
179 * the constructor, then this InputReader will be bound to System.in.
180 * This is used by run() to see if bytes are available() before calling
181 * (Reader)input.read().
183 private InputStream inputStream
;
186 * The terminal's output. If an OutputStream is not specified in the
187 * constructor, then this PrintWriter will be bound to System.out with
190 private PrintWriter output
;
193 * The listening object that run() wakes up on new input.
195 private Object listener
;
198 * Set listener to a different Object.
200 * @param listener the new listening object that run() wakes up on new
203 public void setListener(final Object listener
) {
204 this.listener
= listener
;
208 * Get the output writer.
212 public PrintWriter
getOutput() {
217 * Check if there are events in the queue.
219 * @return if true, getEvents() has something to return to the backend
221 public boolean hasEvents() {
222 synchronized (eventQueue
) {
223 return (eventQueue
.size() > 0);
228 * Call 'stty' to set cooked mode.
230 * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
232 private void sttyCooked() {
237 * Call 'stty' to set raw mode.
239 * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
240 * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
241 * -parenb cs8 min 1 < /dev/tty'
243 private void sttyRaw() {
248 * Call 'stty' to set raw or cooked mode.
250 * @param mode if true, set raw mode, otherwise set cooked mode
252 private void doStty(final boolean mode
) {
254 "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
256 String
[] cmdCooked
= {
257 "/bin/sh", "-c", "stty sane cooked < /dev/tty"
262 process
= Runtime
.getRuntime().exec(cmdRaw
);
264 process
= Runtime
.getRuntime().exec(cmdCooked
);
266 BufferedReader in
= new BufferedReader(new InputStreamReader(process
.getInputStream(), "UTF-8"));
267 String line
= in
.readLine();
268 if ((line
!= null) && (line
.length() > 0)) {
269 System
.err
.println("WEIRD?! Normal output from stty: " + line
);
272 BufferedReader err
= new BufferedReader(new InputStreamReader(process
.getErrorStream(), "UTF-8"));
273 line
= err
.readLine();
274 if ((line
!= null) && (line
.length() > 0)) {
275 System
.err
.println("Error output from stty: " + line
);
280 } catch (InterruptedException e
) {
284 int rc
= process
.exitValue();
286 System
.err
.println("stty returned error code: " + rc
);
288 } catch (IOException e
) {
294 * Constructor sets up state for getEvent().
296 * @param listener the object this backend needs to wake up when new
298 * @param input an InputStream connected to the remote user, or null for
299 * System.in. If System.in is used, then on non-Windows systems it will
300 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
301 * mode. input is always converted to a Reader with UTF-8 encoding.
302 * @param output an OutputStream connected to the remote user, or null
303 * for System.out. output is always converted to a Writer with UTF-8
305 * @param windowWidth the number of text columns to start with
306 * @param windowHeight the number of text rows to start with
307 * @throws UnsupportedEncodingException if an exception is thrown when
308 * creating the InputStreamReader
310 public ECMA48Terminal(final Object listener
, final InputStream input
,
311 final OutputStream output
, final int windowWidth
,
312 final int windowHeight
) throws UnsupportedEncodingException
{
314 this(listener
, input
, output
);
316 // Send dtterm/xterm sequences, which will probably not work because
317 // allowWindowOps is defaulted to false.
318 String resizeString
= String
.format("\033[8;%d;%dt", windowHeight
,
320 this.output
.write(resizeString
);
325 * Constructor sets up state for getEvent().
327 * @param listener the object this backend needs to wake up when new
329 * @param input an InputStream connected to the remote user, or null for
330 * System.in. If System.in is used, then on non-Windows systems it will
331 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
332 * mode. input is always converted to a Reader with UTF-8 encoding.
333 * @param output an OutputStream connected to the remote user, or null
334 * for System.out. output is always converted to a Writer with UTF-8
336 * @throws UnsupportedEncodingException if an exception is thrown when
337 * creating the InputStreamReader
339 public ECMA48Terminal(final Object listener
, final InputStream input
,
340 final OutputStream output
) throws UnsupportedEncodingException
{
346 stopReaderThread
= false;
347 this.listener
= listener
;
350 // inputStream = System.in;
351 inputStream
= new FileInputStream(FileDescriptor
.in
);
357 this.input
= new InputStreamReader(inputStream
, "UTF-8");
359 if (input
instanceof SessionInfo
) {
360 // This is a TelnetInputStream that exposes window size and
361 // environment variables from the telnet layer.
362 sessionInfo
= (SessionInfo
) input
;
364 if (sessionInfo
== null) {
366 // Reading right off the tty
367 sessionInfo
= new TTYSessionInfo();
369 sessionInfo
= new TSessionInfo();
373 if (output
== null) {
374 this.output
= new PrintWriter(new OutputStreamWriter(System
.out
,
377 this.output
= new PrintWriter(new OutputStreamWriter(output
,
381 // Enable mouse reporting and metaSendsEscape
382 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
385 // Query the screen size
386 sessionInfo
.queryWindowSize();
387 setDimensions(sessionInfo
.getWindowWidth(),
388 sessionInfo
.getWindowHeight());
390 // Hang onto the window size
391 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
392 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
394 // Permit RGB colors only if externally requested
395 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
396 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
403 // Spin up the input reader
404 eventQueue
= new LinkedList
<TInputEvent
>();
405 readerThread
= new Thread(this);
406 readerThread
.start();
409 this.output
.write(clearAll());
414 * Constructor sets up state for getEvent().
416 * @param listener the object this backend needs to wake up when new
418 * @param input the InputStream underlying 'reader'. Its available()
419 * method is used to determine if reader.read() will block or not.
420 * @param reader a Reader connected to the remote user.
421 * @param writer a PrintWriter connected to the remote user.
422 * @param setRawMode if true, set System.in into raw mode with stty.
423 * This should in general not be used. It is here solely for Demo3,
424 * which uses System.in.
425 * @throws IllegalArgumentException if input, reader, or writer are null.
427 public ECMA48Terminal(final Object listener
, final InputStream input
,
428 final Reader reader
, final PrintWriter writer
,
429 final boolean setRawMode
) {
432 throw new IllegalArgumentException("InputStream must be specified");
434 if (reader
== null) {
435 throw new IllegalArgumentException("Reader must be specified");
437 if (writer
== null) {
438 throw new IllegalArgumentException("Writer must be specified");
444 stopReaderThread
= false;
445 this.listener
= listener
;
450 if (setRawMode
== true) {
453 this.setRawMode
= setRawMode
;
455 if (input
instanceof SessionInfo
) {
456 // This is a TelnetInputStream that exposes window size and
457 // environment variables from the telnet layer.
458 sessionInfo
= (SessionInfo
) input
;
460 if (sessionInfo
== null) {
461 if (setRawMode
== true) {
462 // Reading right off the tty
463 sessionInfo
= new TTYSessionInfo();
465 sessionInfo
= new TSessionInfo();
469 this.output
= writer
;
471 // Enable mouse reporting and metaSendsEscape
472 this.output
.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
475 // Query the screen size
476 sessionInfo
.queryWindowSize();
477 setDimensions(sessionInfo
.getWindowWidth(),
478 sessionInfo
.getWindowHeight());
480 // Hang onto the window size
481 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
482 sessionInfo
.getWindowWidth(), sessionInfo
.getWindowHeight());
484 // Permit RGB colors only if externally requested
485 if (System
.getProperty("jexer.ECMA48.rgbColor") != null) {
486 if (System
.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
493 // Spin up the input reader
494 eventQueue
= new LinkedList
<TInputEvent
>();
495 readerThread
= new Thread(this);
496 readerThread
.start();
499 this.output
.write(clearAll());
504 * Constructor sets up state for getEvent().
506 * @param listener the object this backend needs to wake up when new
508 * @param input the InputStream underlying 'reader'. Its available()
509 * method is used to determine if reader.read() will block or not.
510 * @param reader a Reader connected to the remote user.
511 * @param writer a PrintWriter connected to the remote user.
512 * @throws IllegalArgumentException if input, reader, or writer are null.
514 public ECMA48Terminal(final Object listener
, final InputStream input
,
515 final Reader reader
, final PrintWriter writer
) {
517 this(listener
, input
, reader
, writer
, false);
521 * Restore terminal to normal state.
523 public void closeTerminal() {
525 // System.err.println("=== shutdown() ==="); System.err.flush();
527 // Tell the reader thread to stop looking at input
528 stopReaderThread
= true;
531 } catch (InterruptedException e
) {
535 // Disable mouse reporting and show cursor
536 output
.printf("%s%s%s", mouse(false), cursor(true), normal());
542 // We don't close System.in/out
544 // Shut down the streams, this should wake up the reader thread
551 if (output
!= null) {
555 } catch (IOException e
) {
564 public void flush() {
569 * Perform a somewhat-optimal rendering of a line.
571 * @param y row coordinate. 0 is the top-most row.
572 * @param sb StringBuilder to write escape sequences to
573 * @param lastAttr cell attributes from the last call to flushLine
575 private void flushLine(final int y
, final StringBuilder sb
,
576 CellAttributes lastAttr
) {
580 for (int x
= 0; x
< width
; x
++) {
581 Cell lCell
= logical
[x
][y
];
582 if (!lCell
.isBlank()) {
586 // Push textEnd to first column beyond the text area
590 // reallyCleared = true;
592 for (int x
= 0; x
< width
; x
++) {
593 Cell lCell
= logical
[x
][y
];
594 Cell pCell
= physical
[x
][y
];
596 if (!lCell
.equals(pCell
) || reallyCleared
) {
599 System
.err
.printf("\n--\n");
600 System
.err
.printf(" Y: %d X: %d\n", y
, x
);
601 System
.err
.printf(" lCell: %s\n", lCell
);
602 System
.err
.printf(" pCell: %s\n", pCell
);
603 System
.err
.printf(" ==== \n");
606 if (lastAttr
== null) {
607 lastAttr
= new CellAttributes();
612 if ((lastX
!= (x
- 1)) || (lastX
== -1)) {
613 // Advancing at least one cell, or the first gotoXY
614 sb
.append(gotoXY(x
, y
));
617 assert (lastAttr
!= null);
619 if ((x
== textEnd
) && (textEnd
< width
- 1)) {
620 assert (lCell
.isBlank());
622 for (int i
= x
; i
< width
; i
++) {
623 assert (logical
[i
][y
].isBlank());
624 // Physical is always updated
625 physical
[i
][y
].reset();
628 // Clear remaining line
629 sb
.append(clearRemainingLine());
634 // Now emit only the modified attributes
635 if ((lCell
.getForeColor() != lastAttr
.getForeColor())
636 && (lCell
.getBackColor() != lastAttr
.getBackColor())
637 && (lCell
.isBold() == lastAttr
.isBold())
638 && (lCell
.isReverse() == lastAttr
.isReverse())
639 && (lCell
.isUnderline() == lastAttr
.isUnderline())
640 && (lCell
.isBlink() == lastAttr
.isBlink())
642 // Both colors changed, attributes the same
643 sb
.append(color(lCell
.isBold(),
644 lCell
.getForeColor(), lCell
.getBackColor()));
647 System
.err
.printf("1 Change only fore/back colors\n");
649 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
650 && (lCell
.getBackColor() != lastAttr
.getBackColor())
651 && (lCell
.isBold() != lastAttr
.isBold())
652 && (lCell
.isReverse() != lastAttr
.isReverse())
653 && (lCell
.isUnderline() != lastAttr
.isUnderline())
654 && (lCell
.isBlink() != lastAttr
.isBlink())
656 // Everything is different
657 sb
.append(color(lCell
.getForeColor(),
658 lCell
.getBackColor(),
659 lCell
.isBold(), lCell
.isReverse(),
661 lCell
.isUnderline()));
664 System
.err
.printf("2 Set all attributes\n");
666 } else if ((lCell
.getForeColor() != lastAttr
.getForeColor())
667 && (lCell
.getBackColor() == lastAttr
.getBackColor())
668 && (lCell
.isBold() == lastAttr
.isBold())
669 && (lCell
.isReverse() == lastAttr
.isReverse())
670 && (lCell
.isUnderline() == lastAttr
.isUnderline())
671 && (lCell
.isBlink() == lastAttr
.isBlink())
674 // Attributes same, foreColor different
675 sb
.append(color(lCell
.isBold(),
676 lCell
.getForeColor(), true));
679 System
.err
.printf("3 Change foreColor\n");
681 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
682 && (lCell
.getBackColor() != lastAttr
.getBackColor())
683 && (lCell
.isBold() == lastAttr
.isBold())
684 && (lCell
.isReverse() == lastAttr
.isReverse())
685 && (lCell
.isUnderline() == lastAttr
.isUnderline())
686 && (lCell
.isBlink() == lastAttr
.isBlink())
688 // Attributes same, backColor different
689 sb
.append(color(lCell
.isBold(),
690 lCell
.getBackColor(), false));
693 System
.err
.printf("4 Change backColor\n");
695 } else if ((lCell
.getForeColor() == lastAttr
.getForeColor())
696 && (lCell
.getBackColor() == lastAttr
.getBackColor())
697 && (lCell
.isBold() == lastAttr
.isBold())
698 && (lCell
.isReverse() == lastAttr
.isReverse())
699 && (lCell
.isUnderline() == lastAttr
.isUnderline())
700 && (lCell
.isBlink() == lastAttr
.isBlink())
703 // All attributes the same, just print the char
707 System
.err
.printf("5 Only emit character\n");
710 // Just reset everything again
711 sb
.append(color(lCell
.getForeColor(),
712 lCell
.getBackColor(),
716 lCell
.isUnderline()));
719 System
.err
.printf("6 Change all attributes\n");
722 // Emit the character
723 sb
.append(lCell
.getChar());
725 // Save the last rendered cell
727 lastAttr
.setTo(lCell
);
729 // Physical is always updated
730 physical
[x
][y
].setTo(lCell
);
732 } // if (!lCell.equals(pCell) || (reallyCleared == true))
734 } // for (int x = 0; x < width; x++)
738 * Render the screen to a string that can be emitted to something that
739 * knows how to process ECMA-48/ANSI X3.64 escape sequences.
741 * @return escape sequences string that provides the updates to the
744 private String
flushString() {
745 CellAttributes attr
= null;
747 StringBuilder sb
= new StringBuilder();
749 attr
= new CellAttributes();
750 sb
.append(clearAll());
753 for (int y
= 0; y
< height
; y
++) {
754 flushLine(y
, sb
, attr
);
757 reallyCleared
= false;
759 String result
= sb
.toString();
761 System
.err
.printf("flushString(): %s\n", result
);
767 * Push the logical screen to the physical device.
770 public void flushPhysical() {
771 String result
= flushString();
775 && (cursorY
<= height
- 1)
776 && (cursorX
<= width
- 1)
778 result
+= cursor(true);
779 result
+= gotoXY(cursorX
, cursorY
);
781 result
+= cursor(false);
783 output
.write(result
);
788 * Set the window title.
790 * @param title the new title
792 public void setTitle(final String title
) {
793 output
.write(getSetTitleString(title
));
798 * Reset keyboard/mouse input parser.
800 private void resetParser() {
801 state
= ParseState
.GROUND
;
802 params
= new ArrayList
<String
>();
808 * Produce a control character or one of the special ones (ENTER, TAB,
811 * @param ch Unicode code point
812 * @param alt if true, set alt on the TKeypress
813 * @return one TKeypress event, either a control character (e.g. isKey ==
814 * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
817 private TKeypressEvent
controlChar(final char ch
, final boolean alt
) {
818 // System.err.printf("controlChar: %02x\n", ch);
822 // Carriage return --> ENTER
823 return new TKeypressEvent(kbEnter
, alt
, false, false);
825 // Linefeed --> ENTER
826 return new TKeypressEvent(kbEnter
, alt
, false, false);
829 return new TKeypressEvent(kbEsc
, alt
, false, false);
832 return new TKeypressEvent(kbTab
, alt
, false, false);
834 // Make all other control characters come back as the alphabetic
835 // character with the ctrl field set. So SOH would be 'A' +
837 return new TKeypressEvent(false, 0, (char)(ch
+ 0x40),
843 * Produce special key from CSI Pn ; Pm ; ... ~
845 * @return one KEYPRESS event representing a special key
847 private TInputEvent
csiFnKey() {
849 if (params
.size() > 0) {
850 key
= Integer
.parseInt(params
.get(0));
853 boolean ctrl
= false;
854 boolean shift
= false;
855 if (params
.size() > 1) {
856 shift
= csiIsShift(params
.get(1));
857 alt
= csiIsAlt(params
.get(1));
858 ctrl
= csiIsCtrl(params
.get(1));
863 return new TKeypressEvent(kbHome
, alt
, ctrl
, shift
);
865 return new TKeypressEvent(kbIns
, alt
, ctrl
, shift
);
867 return new TKeypressEvent(kbDel
, alt
, ctrl
, shift
);
869 return new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
);
871 return new TKeypressEvent(kbPgUp
, alt
, ctrl
, shift
);
873 return new TKeypressEvent(kbPgDn
, alt
, ctrl
, shift
);
875 return new TKeypressEvent(kbF5
, alt
, ctrl
, shift
);
877 return new TKeypressEvent(kbF6
, alt
, ctrl
, shift
);
879 return new TKeypressEvent(kbF7
, alt
, ctrl
, shift
);
881 return new TKeypressEvent(kbF8
, alt
, ctrl
, shift
);
883 return new TKeypressEvent(kbF9
, alt
, ctrl
, shift
);
885 return new TKeypressEvent(kbF10
, alt
, ctrl
, shift
);
887 return new TKeypressEvent(kbF11
, alt
, ctrl
, shift
);
889 return new TKeypressEvent(kbF12
, alt
, ctrl
, shift
);
897 * Produce mouse events based on "Any event tracking" and UTF-8
899 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
901 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
903 private TInputEvent
parseMouse() {
904 int buttons
= params
.get(0).charAt(0) - 32;
905 int x
= params
.get(0).charAt(1) - 32 - 1;
906 int y
= params
.get(0).charAt(2) - 32 - 1;
908 // Clamp X and Y to the physical screen coordinates.
909 if (x
>= windowResize
.getWidth()) {
910 x
= windowResize
.getWidth() - 1;
912 if (y
>= windowResize
.getHeight()) {
913 y
= windowResize
.getHeight() - 1;
916 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
917 boolean eventMouse1
= false;
918 boolean eventMouse2
= false;
919 boolean eventMouse3
= false;
920 boolean eventMouseWheelUp
= false;
921 boolean eventMouseWheelDown
= false;
923 // System.err.printf("buttons: %04x\r\n", buttons);
940 if (!mouse1
&& !mouse2
&& !mouse3
) {
941 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
943 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
960 // Dragging with mouse1 down
963 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
967 // Dragging with mouse2 down
970 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
974 // Dragging with mouse3 down
977 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
981 // Dragging with mouse2 down after wheelUp
984 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
988 // Dragging with mouse2 down after wheelDown
991 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
995 eventMouseWheelUp
= true;
999 eventMouseWheelDown
= true;
1003 // Unknown, just make it motion
1004 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1007 return new TMouseEvent(eventType
, x
, y
, x
, y
,
1008 eventMouse1
, eventMouse2
, eventMouse3
,
1009 eventMouseWheelUp
, eventMouseWheelDown
);
1013 * Produce mouse events based on "Any event tracking" and SGR
1015 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1017 * @param release if true, this was a release ('m')
1018 * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
1020 private TInputEvent
parseMouseSGR(final boolean release
) {
1021 // SGR extended coordinates - mode 1006
1022 if (params
.size() < 3) {
1023 // Invalid position, bail out.
1026 int buttons
= Integer
.parseInt(params
.get(0));
1027 int x
= Integer
.parseInt(params
.get(1)) - 1;
1028 int y
= Integer
.parseInt(params
.get(2)) - 1;
1030 // Clamp X and Y to the physical screen coordinates.
1031 if (x
>= windowResize
.getWidth()) {
1032 x
= windowResize
.getWidth() - 1;
1034 if (y
>= windowResize
.getHeight()) {
1035 y
= windowResize
.getHeight() - 1;
1038 TMouseEvent
.Type eventType
= TMouseEvent
.Type
.MOUSE_DOWN
;
1039 boolean eventMouse1
= false;
1040 boolean eventMouse2
= false;
1041 boolean eventMouse3
= false;
1042 boolean eventMouseWheelUp
= false;
1043 boolean eventMouseWheelDown
= false;
1046 eventType
= TMouseEvent
.Type
.MOUSE_UP
;
1060 // Motion only, no buttons down
1061 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1065 // Dragging with mouse1 down
1067 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1071 // Dragging with mouse2 down
1073 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1077 // Dragging with mouse3 down
1079 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1083 // Dragging with mouse2 down after wheelUp
1085 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1089 // Dragging with mouse2 down after wheelDown
1091 eventType
= TMouseEvent
.Type
.MOUSE_MOTION
;
1095 eventMouseWheelUp
= true;
1099 eventMouseWheelDown
= true;
1103 // Unknown, bail out
1106 return new TMouseEvent(eventType
, x
, y
, x
, y
,
1107 eventMouse1
, eventMouse2
, eventMouse3
,
1108 eventMouseWheelUp
, eventMouseWheelDown
);
1112 * Return any events in the IO queue.
1114 * @param queue list to append new events to
1116 public void getEvents(final List
<TInputEvent
> queue
) {
1117 synchronized (eventQueue
) {
1118 if (eventQueue
.size() > 0) {
1119 synchronized (queue
) {
1120 queue
.addAll(eventQueue
);
1128 * Return any events in the IO queue due to timeout.
1130 * @param queue list to append new events to
1132 private void getIdleEvents(final List
<TInputEvent
> queue
) {
1133 long nowTime
= System
.currentTimeMillis();
1135 // Check for new window size
1136 long windowSizeDelay
= nowTime
- windowSizeTime
;
1137 if (windowSizeDelay
> 1000) {
1138 sessionInfo
.queryWindowSize();
1139 int newWidth
= sessionInfo
.getWindowWidth();
1140 int newHeight
= sessionInfo
.getWindowHeight();
1142 if ((newWidth
!= windowResize
.getWidth())
1143 || (newHeight
!= windowResize
.getHeight())
1146 if (debugToStderr
) {
1147 System
.err
.println("Screen size changed, old size " +
1149 System
.err
.println(" new size " +
1150 newWidth
+ " x " + newHeight
);
1153 TResizeEvent event
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1154 newWidth
, newHeight
);
1155 windowResize
= new TResizeEvent(TResizeEvent
.Type
.SCREEN
,
1156 newWidth
, newHeight
);
1159 windowSizeTime
= nowTime
;
1162 // ESCDELAY type timeout
1163 if (state
== ParseState
.ESCAPE
) {
1164 long escDelay
= nowTime
- escapeTime
;
1165 if (escDelay
> 100) {
1166 // After 0.1 seconds, assume a true escape character
1167 queue
.add(controlChar((char)0x1B, false));
1174 * Returns true if the CSI parameter for a keyboard command means that
1177 private boolean csiIsShift(final String x
) {
1189 * Returns true if the CSI parameter for a keyboard command means that
1192 private boolean csiIsAlt(final String x
) {
1204 * Returns true if the CSI parameter for a keyboard command means that
1207 private boolean csiIsCtrl(final String x
) {
1219 * Parses the next character of input to see if an InputEvent is
1222 * @param events list to append new events to
1223 * @param ch Unicode code point
1225 private void processChar(final List
<TInputEvent
> events
, final char ch
) {
1227 // ESCDELAY type timeout
1228 long nowTime
= System
.currentTimeMillis();
1229 if (state
== ParseState
.ESCAPE
) {
1230 long escDelay
= nowTime
- escapeTime
;
1231 if (escDelay
> 250) {
1232 // After 0.25 seconds, assume a true escape character
1233 events
.add(controlChar((char)0x1B, false));
1239 boolean ctrl
= false;
1240 boolean alt
= false;
1241 boolean shift
= false;
1243 // System.err.printf("state: %s ch %c\r\n", state, ch);
1249 state
= ParseState
.ESCAPE
;
1250 escapeTime
= nowTime
;
1255 // Control character
1256 events
.add(controlChar(ch
, false));
1263 events
.add(new TKeypressEvent(false, 0, ch
,
1264 false, false, false));
1273 // ALT-Control character
1274 events
.add(controlChar(ch
, true));
1280 // This will be one of the function keys
1281 state
= ParseState
.ESCAPE_INTERMEDIATE
;
1285 // '[' goes to CSI_ENTRY
1287 state
= ParseState
.CSI_ENTRY
;
1291 // Everything else is assumed to be Alt-keystroke
1292 if ((ch
>= 'A') && (ch
<= 'Z')) {
1296 events
.add(new TKeypressEvent(false, 0, ch
, alt
, ctrl
, shift
));
1300 case ESCAPE_INTERMEDIATE
:
1301 if ((ch
>= 'P') && (ch
<= 'S')) {
1305 events
.add(new TKeypressEvent(kbF1
));
1308 events
.add(new TKeypressEvent(kbF2
));
1311 events
.add(new TKeypressEvent(kbF3
));
1314 events
.add(new TKeypressEvent(kbF4
));
1323 // Unknown keystroke, ignore
1328 // Numbers - parameter values
1329 if ((ch
>= '0') && (ch
<= '9')) {
1330 params
.set(params
.size() - 1,
1331 params
.get(params
.size() - 1) + ch
);
1332 state
= ParseState
.CSI_PARAM
;
1335 // Parameter separator
1341 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1345 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1350 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1355 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1360 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1365 events
.add(new TKeypressEvent(kbHome
));
1370 events
.add(new TKeypressEvent(kbEnd
));
1374 // CBT - Cursor backward X tab stops (default 1)
1375 events
.add(new TKeypressEvent(kbBackTab
));
1380 state
= ParseState
.MOUSE
;
1383 // Mouse position, SGR (1006) coordinates
1384 state
= ParseState
.MOUSE_SGR
;
1391 // Unknown keystroke, ignore
1396 // Numbers - parameter values
1397 if ((ch
>= '0') && (ch
<= '9')) {
1398 params
.set(params
.size() - 1,
1399 params
.get(params
.size() - 1) + ch
);
1402 // Parameter separator
1410 // Generate a mouse press event
1411 TInputEvent event
= parseMouseSGR(false);
1412 if (event
!= null) {
1418 // Generate a mouse release event
1419 event
= parseMouseSGR(true);
1420 if (event
!= null) {
1429 // Unknown keystroke, ignore
1434 // Numbers - parameter values
1435 if ((ch
>= '0') && (ch
<= '9')) {
1436 params
.set(params
.size() - 1,
1437 params
.get(params
.size() - 1) + ch
);
1438 state
= ParseState
.CSI_PARAM
;
1441 // Parameter separator
1448 events
.add(csiFnKey());
1453 if ((ch
>= 0x30) && (ch
<= 0x7E)) {
1457 if (params
.size() > 1) {
1458 shift
= csiIsShift(params
.get(1));
1459 alt
= csiIsAlt(params
.get(1));
1460 ctrl
= csiIsCtrl(params
.get(1));
1462 events
.add(new TKeypressEvent(kbUp
, alt
, ctrl
, shift
));
1467 if (params
.size() > 1) {
1468 shift
= csiIsShift(params
.get(1));
1469 alt
= csiIsAlt(params
.get(1));
1470 ctrl
= csiIsCtrl(params
.get(1));
1472 events
.add(new TKeypressEvent(kbDown
, alt
, ctrl
, shift
));
1477 if (params
.size() > 1) {
1478 shift
= csiIsShift(params
.get(1));
1479 alt
= csiIsAlt(params
.get(1));
1480 ctrl
= csiIsCtrl(params
.get(1));
1482 events
.add(new TKeypressEvent(kbRight
, alt
, ctrl
, shift
));
1487 if (params
.size() > 1) {
1488 shift
= csiIsShift(params
.get(1));
1489 alt
= csiIsAlt(params
.get(1));
1490 ctrl
= csiIsCtrl(params
.get(1));
1492 events
.add(new TKeypressEvent(kbLeft
, alt
, ctrl
, shift
));
1497 if (params
.size() > 1) {
1498 shift
= csiIsShift(params
.get(1));
1499 alt
= csiIsAlt(params
.get(1));
1500 ctrl
= csiIsCtrl(params
.get(1));
1502 events
.add(new TKeypressEvent(kbHome
, alt
, ctrl
, shift
));
1507 if (params
.size() > 1) {
1508 shift
= csiIsShift(params
.get(1));
1509 alt
= csiIsAlt(params
.get(1));
1510 ctrl
= csiIsCtrl(params
.get(1));
1512 events
.add(new TKeypressEvent(kbEnd
, alt
, ctrl
, shift
));
1520 // Unknown keystroke, ignore
1525 params
.set(0, params
.get(params
.size() - 1) + ch
);
1526 if (params
.get(0).length() == 3) {
1527 // We have enough to generate a mouse event
1528 events
.add(parseMouse());
1537 // This "should" be impossible to reach
1542 * Tell (u)xterm that we want alt- keystrokes to send escape + character
1543 * rather than set the 8th bit. Anyone who wants UTF8 should want this
1546 * @param on if true, enable metaSendsEscape
1547 * @return the string to emit to xterm
1549 private String
xtermMetaSendsEscape(final boolean on
) {
1551 return "\033[?1036h\033[?1034l";
1553 return "\033[?1036l";
1557 * Create an xterm OSC sequence to change the window title.
1559 * @param title the new title
1560 * @return the string to emit to xterm
1562 private String
getSetTitleString(final String title
) {
1563 return "\033]2;" + title
+ "\007";
1567 * Create a SGR parameter sequence for a single color change.
1569 * @param bold if true, set bold
1570 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1571 * @param foreground if true, this is a foreground color
1572 * @return the string to emit to an ANSI / ECMA-style terminal,
1575 private String
color(final boolean bold
, final Color color
,
1576 final boolean foreground
) {
1577 return color(color
, foreground
, true) +
1578 rgbColor(bold
, color
, foreground
);
1582 * Create a T.416 RGB parameter sequence for a single color change.
1584 * @param bold if true, set bold
1585 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1586 * @param foreground if true, this is a foreground color
1587 * @return the string to emit to an xterm terminal with RGB support,
1588 * e.g. "\033[38;2;RR;GG;BBm"
1590 private String
rgbColor(final boolean bold
, final Color color
,
1591 final boolean foreground
) {
1592 if (doRgbColor
== false) {
1595 StringBuilder sb
= new StringBuilder("\033[");
1597 // Bold implies foreground only
1599 if (color
.equals(Color
.BLACK
)) {
1600 sb
.append("84;84;84");
1601 } else if (color
.equals(Color
.RED
)) {
1602 sb
.append("252;84;84");
1603 } else if (color
.equals(Color
.GREEN
)) {
1604 sb
.append("84;252;84");
1605 } else if (color
.equals(Color
.YELLOW
)) {
1606 sb
.append("252;252;84");
1607 } else if (color
.equals(Color
.BLUE
)) {
1608 sb
.append("84;84;252");
1609 } else if (color
.equals(Color
.MAGENTA
)) {
1610 sb
.append("252;84;252");
1611 } else if (color
.equals(Color
.CYAN
)) {
1612 sb
.append("84;252;252");
1613 } else if (color
.equals(Color
.WHITE
)) {
1614 sb
.append("252;252;252");
1622 if (color
.equals(Color
.BLACK
)) {
1624 } else if (color
.equals(Color
.RED
)) {
1625 sb
.append("168;0;0");
1626 } else if (color
.equals(Color
.GREEN
)) {
1627 sb
.append("0;168;0");
1628 } else if (color
.equals(Color
.YELLOW
)) {
1629 sb
.append("168;84;0");
1630 } else if (color
.equals(Color
.BLUE
)) {
1631 sb
.append("0;0;168");
1632 } else if (color
.equals(Color
.MAGENTA
)) {
1633 sb
.append("168;0;168");
1634 } else if (color
.equals(Color
.CYAN
)) {
1635 sb
.append("0;168;168");
1636 } else if (color
.equals(Color
.WHITE
)) {
1637 sb
.append("168;168;168");
1641 return sb
.toString();
1645 * Create a T.416 RGB parameter sequence for both foreground and
1646 * background color change.
1648 * @param bold if true, set bold
1649 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1650 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1651 * @return the string to emit to an xterm terminal with RGB support,
1652 * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
1654 private String
rgbColor(final boolean bold
, final Color foreColor
,
1655 final Color backColor
) {
1656 if (doRgbColor
== false) {
1660 return rgbColor(bold
, foreColor
, true) +
1661 rgbColor(false, backColor
, false);
1665 * Create a SGR parameter sequence for a single color change.
1667 * @param color one of the Color.WHITE, Color.BLUE, etc. constants
1668 * @param foreground if true, this is a foreground color
1669 * @param header if true, make the full header, otherwise just emit the
1670 * color parameter e.g. "42;"
1671 * @return the string to emit to an ANSI / ECMA-style terminal,
1674 private String
color(final Color color
, final boolean foreground
,
1675 final boolean header
) {
1677 int ecmaColor
= color
.getValue();
1679 // Convert Color.* values to SGR numerics
1687 return String
.format("\033[%dm", ecmaColor
);
1689 return String
.format("%d;", ecmaColor
);
1694 * Create a SGR parameter sequence for both foreground and background
1697 * @param bold if true, set bold
1698 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1699 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1700 * @return the string to emit to an ANSI / ECMA-style terminal,
1701 * e.g. "\033[31;42m"
1703 private String
color(final boolean bold
, final Color foreColor
,
1704 final Color backColor
) {
1705 return color(foreColor
, backColor
, true) +
1706 rgbColor(bold
, foreColor
, backColor
);
1710 * Create a SGR parameter sequence for both foreground and
1711 * background color change.
1713 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1714 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1715 * @param header if true, make the full header, otherwise just emit the
1716 * color parameter e.g. "31;42;"
1717 * @return the string to emit to an ANSI / ECMA-style terminal,
1718 * e.g. "\033[31;42m"
1720 private String
color(final Color foreColor
, final Color backColor
,
1721 final boolean header
) {
1723 int ecmaForeColor
= foreColor
.getValue();
1724 int ecmaBackColor
= backColor
.getValue();
1726 // Convert Color.* values to SGR numerics
1727 ecmaBackColor
+= 40;
1728 ecmaForeColor
+= 30;
1731 return String
.format("\033[%d;%dm", ecmaForeColor
, ecmaBackColor
);
1733 return String
.format("%d;%d;", ecmaForeColor
, ecmaBackColor
);
1738 * Create a SGR parameter sequence for foreground, background, and
1739 * several attributes. This sequence first resets all attributes to
1740 * default, then sets attributes as per the parameters.
1742 * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
1743 * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
1744 * @param bold if true, set bold
1745 * @param reverse if true, set reverse
1746 * @param blink if true, set blink
1747 * @param underline if true, set underline
1748 * @return the string to emit to an ANSI / ECMA-style terminal,
1749 * e.g. "\033[0;1;31;42m"
1751 private String
color(final Color foreColor
, final Color backColor
,
1752 final boolean bold
, final boolean reverse
, final boolean blink
,
1753 final boolean underline
) {
1755 int ecmaForeColor
= foreColor
.getValue();
1756 int ecmaBackColor
= backColor
.getValue();
1758 // Convert Color.* values to SGR numerics
1759 ecmaBackColor
+= 40;
1760 ecmaForeColor
+= 30;
1762 StringBuilder sb
= new StringBuilder();
1763 if ( bold
&& reverse
&& blink
&& !underline
) {
1764 sb
.append("\033[0;1;7;5;");
1765 } else if ( bold
&& reverse
&& !blink
&& !underline
) {
1766 sb
.append("\033[0;1;7;");
1767 } else if ( !bold
&& reverse
&& blink
&& !underline
) {
1768 sb
.append("\033[0;7;5;");
1769 } else if ( bold
&& !reverse
&& blink
&& !underline
) {
1770 sb
.append("\033[0;1;5;");
1771 } else if ( bold
&& !reverse
&& !blink
&& !underline
) {
1772 sb
.append("\033[0;1;");
1773 } else if ( !bold
&& reverse
&& !blink
&& !underline
) {
1774 sb
.append("\033[0;7;");
1775 } else if ( !bold
&& !reverse
&& blink
&& !underline
) {
1776 sb
.append("\033[0;5;");
1777 } else if ( bold
&& reverse
&& blink
&& underline
) {
1778 sb
.append("\033[0;1;7;5;4;");
1779 } else if ( bold
&& reverse
&& !blink
&& underline
) {
1780 sb
.append("\033[0;1;7;4;");
1781 } else if ( !bold
&& reverse
&& blink
&& underline
) {
1782 sb
.append("\033[0;7;5;4;");
1783 } else if ( bold
&& !reverse
&& blink
&& underline
) {
1784 sb
.append("\033[0;1;5;4;");
1785 } else if ( bold
&& !reverse
&& !blink
&& underline
) {
1786 sb
.append("\033[0;1;4;");
1787 } else if ( !bold
&& reverse
&& !blink
&& underline
) {
1788 sb
.append("\033[0;7;4;");
1789 } else if ( !bold
&& !reverse
&& blink
&& underline
) {
1790 sb
.append("\033[0;5;4;");
1791 } else if ( !bold
&& !reverse
&& !blink
&& underline
) {
1792 sb
.append("\033[0;4;");
1794 assert (!bold
&& !reverse
&& !blink
&& !underline
);
1795 sb
.append("\033[0;");
1797 sb
.append(String
.format("%d;%dm", ecmaForeColor
, ecmaBackColor
));
1798 sb
.append(rgbColor(bold
, foreColor
, backColor
));
1799 return sb
.toString();
1803 * Create a SGR parameter sequence to reset to defaults.
1805 * @return the string to emit to an ANSI / ECMA-style terminal,
1808 private String
normal() {
1809 return normal(true) + rgbColor(false, Color
.WHITE
, Color
.BLACK
);
1813 * Create a SGR parameter sequence to reset to defaults.
1815 * @param header if true, make the full header, otherwise just emit the
1816 * bare parameter e.g. "0;"
1817 * @return the string to emit to an ANSI / ECMA-style terminal,
1820 private String
normal(final boolean header
) {
1822 return "\033[0;37;40m";
1828 * Create a SGR parameter sequence for enabling the visible cursor.
1830 * @param on if true, turn on cursor
1831 * @return the string to emit to an ANSI / ECMA-style terminal
1833 private String
cursor(final boolean on
) {
1834 if (on
&& !cursorOn
) {
1838 if (!on
&& cursorOn
) {
1846 * Clear the entire screen. Because some terminals use back-color-erase,
1847 * set the color to white-on-black beforehand.
1849 * @return the string to emit to an ANSI / ECMA-style terminal
1851 private String
clearAll() {
1852 return "\033[0;37;40m\033[2J";
1856 * Clear the line from the cursor (inclusive) to the end of the screen.
1857 * Because some terminals use back-color-erase, set the color to
1858 * white-on-black beforehand.
1860 * @return the string to emit to an ANSI / ECMA-style terminal
1862 private String
clearRemainingLine() {
1863 return "\033[0;37;40m\033[K";
1867 * Move the cursor to (x, y).
1869 * @param x column coordinate. 0 is the left-most column.
1870 * @param y row coordinate. 0 is the top-most row.
1871 * @return the string to emit to an ANSI / ECMA-style terminal
1873 private String
gotoXY(final int x
, final int y
) {
1874 return String
.format("\033[%d;%dH", y
+ 1, x
+ 1);
1878 * Tell (u)xterm that we want to receive mouse events based on "Any event
1879 * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
1880 * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
1882 * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
1884 * Note that this also sets the alternate/primary screen buffer.
1886 * @param on If true, enable mouse report and use the alternate screen
1887 * buffer. If false disable mouse reporting and use the primary screen
1889 * @return the string to emit to xterm
1891 private String
mouse(final boolean on
) {
1893 return "\033[?1002;1003;1005;1006h\033[?1049h";
1895 return "\033[?1002;1003;1006;1005l\033[?1049l";
1899 * Read function runs on a separate thread.
1902 boolean done
= false;
1903 // available() will often return > 1, so we need to read in chunks to
1905 char [] readBuffer
= new char[128];
1906 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
1908 while (!done
&& !stopReaderThread
) {
1910 // We assume that if inputStream has bytes available, then
1911 // input won't block on read().
1912 int n
= inputStream
.available();
1915 System.err.printf("inputStream.available(): %d\n", n);
1920 if (readBuffer
.length
< n
) {
1921 // The buffer wasn't big enough, make it huger
1922 readBuffer
= new char[readBuffer
.length
* 2];
1925 // System.err.printf("BEFORE read()\n"); System.err.flush();
1927 int rc
= input
.read(readBuffer
, 0, readBuffer
.length
);
1930 System.err.printf("AFTER read() %d\n", rc);
1938 for (int i
= 0; i
< rc
; i
++) {
1939 int ch
= readBuffer
[i
];
1940 processChar(events
, (char)ch
);
1942 getIdleEvents(events
);
1943 if (events
.size() > 0) {
1944 // Add to the queue for the backend thread to
1945 // be able to obtain.
1946 synchronized (eventQueue
) {
1947 eventQueue
.addAll(events
);
1949 if (listener
!= null) {
1950 synchronized (listener
) {
1951 listener
.notifyAll();
1958 getIdleEvents(events
);
1959 if (events
.size() > 0) {
1960 synchronized (eventQueue
) {
1961 eventQueue
.addAll(events
);
1963 if (listener
!= null) {
1964 synchronized (listener
) {
1965 listener
.notifyAll();
1971 // Wait 20 millis for more data
1974 // System.err.println("end while loop"); System.err.flush();
1975 } catch (InterruptedException e
) {
1977 } catch (IOException e
) {
1978 e
.printStackTrace();
1981 } // while ((done == false) && (stopReaderThread == false))
1982 // System.err.println("*** run() exiting..."); System.err.flush();