+ * Constructor sets up state for getEvent().
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public ECMA48Terminal(final Object listener, final InputStream input,
+ final Reader reader, final PrintWriter writer) {
+
+ this(listener, input, reader, writer, false);
+ }
+
+ // ------------------------------------------------------------------------
+ // LogicalScreen ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the window title.
+ *
+ * @param title the new title
+ */
+ @Override
+ public void setTitle(final String title) {
+ output.write(getSetTitleString(title));
+ flush();
+ }
+
+ /**
+ * Push the logical screen to the physical device.
+ */
+ @Override
+ public void flushPhysical() {
+ StringBuilder sb = new StringBuilder();
+ if ((cursorVisible)
+ && (cursorY >= 0)
+ && (cursorX >= 0)
+ && (cursorY <= height - 1)
+ && (cursorX <= width - 1)
+ ) {
+ flushString(sb);
+ sb.append(cursor(true));
+ sb.append(gotoXY(cursorX, cursorY));
+ } else {
+ sb.append(cursor(false));
+ flushString(sb);
+ }
+ output.write(sb.toString());
+ flush();
+ }
+
+ /**
+ * Resize the physical screen to match the logical screen dimensions.
+ */
+ @Override
+ public void resizeToScreen() {
+ // Send dtterm/xterm sequences, which will probably not work because
+ // allowWindowOps is defaulted to false.
+ String resizeString = String.format("\033[8;%d;%dt", getHeight(),
+ getWidth());
+ this.output.write(resizeString);
+ this.output.flush();
+ }
+
+ // ------------------------------------------------------------------------
+ // TerminalReader ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the backend
+ */
+ public boolean hasEvents() {
+ synchronized (eventQueue) {
+ return (eventQueue.size() > 0);
+ }
+ }
+
+ /**
+ * Return any events in the IO queue.
+ *
+ * @param queue list to append new events to
+ */
+ public void getEvents(final List<TInputEvent> queue) {
+ synchronized (eventQueue) {
+ if (eventQueue.size() > 0) {
+ synchronized (queue) {
+ queue.addAll(eventQueue);
+ }
+ eventQueue.clear();
+ }
+ }
+ }
+
+ /**
+ * Restore terminal to normal state.
+ */
+ public void closeTerminal() {
+
+ // System.err.println("=== shutdown() ==="); System.err.flush();
+
+ // Tell the reader thread to stop looking at input
+ stopReaderThread = true;
+ try {
+ readerThread.join();
+ } catch (InterruptedException e) {
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
+ }
+
+ // Disable mouse reporting and show cursor. Defensive null check
+ // here in case closeTerminal() is called twice.
+ if (output != null) {
+ output.printf("%s%s%s", mouse(false), cursor(true), normal());
+ output.flush();
+ }
+
+ if (setRawMode) {
+ sttyCooked();
+ setRawMode = false;
+ // We don't close System.in/out
+ } else {
+ // Shut down the streams, this should wake up the reader thread
+ // and make it exit.
+ if (input != null) {
+ try {
+ input.close();
+ } catch (IOException e) {
+ // SQUASH
+ }
+ input = null;
+ }
+ if (output != null) {
+ output.close();
+ output = null;
+ }
+ }
+ }
+
+ /**
+ * Set listener to a different Object.
+ *
+ * @param listener the new listening object that run() wakes up on new
+ * input
+ */
+ public void setListener(final Object listener) {
+ this.listener = listener;
+ }
+
+ /**
+ * Reload options from System properties.
+ */
+ public void reloadOptions() {
+ // Permit RGB colors only if externally requested.
+ if (System.getProperty("jexer.ECMA48.rgbColor",
+ "false").equals("true")
+ ) {
+ doRgbColor = true;
+ } else {
+ doRgbColor = false;
+ }
+
+ // Pull the system properties for sixel output.
+ if (System.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
+ sixel = true;
+ } else {
+ sixel = false;
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Runnable ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Read function runs on a separate thread.
+ */
+ public void run() {
+ boolean done = false;
+ // available() will often return > 1, so we need to read in chunks to
+ // stay caught up.
+ char [] readBuffer = new char[128];
+ List<TInputEvent> events = new ArrayList<TInputEvent>();
+
+ while (!done && !stopReaderThread) {
+ try {
+ // We assume that if inputStream has bytes available, then
+ // input won't block on read().
+ int n = inputStream.available();
+
+ /*
+ System.err.printf("inputStream.available(): %d\n", n);
+ System.err.flush();
+ */
+
+ if (n > 0) {
+ if (readBuffer.length < n) {
+ // The buffer wasn't big enough, make it huger
+ readBuffer = new char[readBuffer.length * 2];
+ }
+
+ // System.err.printf("BEFORE read()\n"); System.err.flush();
+
+ int rc = input.read(readBuffer, 0, readBuffer.length);
+
+ /*
+ System.err.printf("AFTER read() %d\n", rc);
+ System.err.flush();
+ */
+
+ if (rc == -1) {
+ // This is EOF
+ done = true;
+ } else {
+ for (int i = 0; i < rc; i++) {
+ int ch = readBuffer[i];
+ processChar(events, (char)ch);
+ }
+ getIdleEvents(events);
+ if (events.size() > 0) {
+ // Add to the queue for the backend thread to
+ // be able to obtain.
+ synchronized (eventQueue) {
+ eventQueue.addAll(events);
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ events.clear();
+ }
+ }
+ } else {
+ getIdleEvents(events);
+ if (events.size() > 0) {
+ synchronized (eventQueue) {
+ eventQueue.addAll(events);
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ events.clear();
+ }
+
+ if (output.checkError()) {
+ // This is EOF.
+ done = true;
+ }
+
+ // Wait 20 millis for more data
+ Thread.sleep(20);
+ }
+ // System.err.println("end while loop"); System.err.flush();
+ } catch (InterruptedException e) {
+ // SQUASH
+ } catch (IOException e) {
+ e.printStackTrace();
+ done = true;
+ }
+ } // while ((done == false) && (stopReaderThread == false))
+
+ // Pass an event up to TApplication to tell it this Backend is done.
+ synchronized (eventQueue) {
+ eventQueue.add(new TCommandEvent(cmBackendDisconnect));
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+
+ // System.err.println("*** run() exiting..."); System.err.flush();
+ }
+
+ // ------------------------------------------------------------------------
+ // ECMA48Terminal ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the width of a character cell in pixels.
+ *
+ * @return the width in pixels of a character cell
+ */
+ public int getTextWidth() {
+ return (widthPixels / sessionInfo.getWindowWidth());
+ }
+
+ /**
+ * Get the height of a character cell in pixels.
+ *
+ * @return the height in pixels of a character cell
+ */
+ public int getTextHeight() {
+ return (heightPixels / sessionInfo.getWindowHeight());
+ }
+
+ /**
+ * Getter for sessionInfo.
+ *
+ * @return the SessionInfo
+ */
+ public SessionInfo getSessionInfo() {
+ return sessionInfo;
+ }
+
+ /**
+ * Get the output writer.
+ *
+ * @return the Writer
+ */
+ public PrintWriter getOutput() {
+ return output;
+ }
+
+ /**
+ * Call 'stty' to set cooked mode.
+ *
+ * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
+ */
+ private void sttyCooked() {
+ doStty(false);
+ }
+
+ /**
+ * Call 'stty' to set raw mode.