Attempt to redraw the active commandline when needed

The goal is to have the prompt and any text remain visible even
when asynchronous processes spit out output.

Bug: 6205785
Change-Id: I2d06a8ce57f744a6bf705745b604eb6d21c5fb13
diff --git a/src/com/android/tradefed/command/Console.java b/src/com/android/tradefed/command/Console.java
index bcfee01..5747ebe 100644
--- a/src/com/android/tradefed/command/Console.java
+++ b/src/com/android/tradefed/command/Console.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceManager;
 import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.log.ConsoleReaderOutputStream;
 import com.android.tradefed.log.LogRegistry;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.QuotationAwareTokenizer;
@@ -34,6 +35,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -164,7 +166,9 @@
      */
     private static ConsoleReader getReader() {
         try {
-            return new ConsoleReader();
+            final ConsoleReader reader = new ConsoleReader();
+            System.setOut(new PrintStream(new ConsoleReaderOutputStream(reader)));
+            return reader;
         } catch (IOException e) {
             System.err.format("Failed to initialize ConsoleReader: %s\n", e.getMessage());
             return null;
@@ -642,7 +646,6 @@
     protected void printLine(String output) {
         if (mConsoleReader != null) {
             try {
-                mConsoleReader.printString(output);
                 mConsoleReader.printNewline();
             } catch (IOException e) {
                 // not guaranteed to work, but worth a try
diff --git a/src/com/android/tradefed/log/ConsoleReaderOutputStream.java b/src/com/android/tradefed/log/ConsoleReaderOutputStream.java
new file mode 100644
index 0000000..4527e1b
--- /dev/null
+++ b/src/com/android/tradefed/log/ConsoleReaderOutputStream.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.log;
+
+import jline.ConsoleReader;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An OutputStream that can be used to make {@code System.out.print()} play nice with the user's
+ * {@link ConsoleReader} buffer.
+ * <p />
+ * In trivial performance tests, this class did not have a measurable performance impact.
+ */
+public class ConsoleReaderOutputStream extends OutputStream {
+    /**
+     * ANSI "clear line" (Esc + "[2K") followed by carriage return
+     * See: http://ascii-table.com/ansi-escape-sequences-vt-100.php
+     */
+    private static final String ANSI_CR = "\u001b[2K\r";
+    private static final String CR = "\r";
+    private final ConsoleReader mConsoleReader;
+
+    public ConsoleReaderOutputStream(ConsoleReader reader) {
+        if (reader == null) throw new NullPointerException();
+        mConsoleReader = reader;
+    }
+
+    @Override
+    public void flush() {
+        try {
+            mConsoleReader.flushConsole();
+        } catch (IOException e) {
+            // ignore
+        }
+    }
+
+    /**
+     * A special implementation to keep the user's command buffer visible when asynchronous tasks
+     * write to stdout.
+     * <p />
+     * If a full-line write is detected (one that terminates with "\n"), we:
+     * <ol>
+     *   <li>Clear the current line (which will contain the prompt and the user's buffer</li>
+     *   <li>Print the full line(s), which will drop us on a new line</li>
+     *   <li>Redraw the prompt and the user's buffer</li>
+     * </ol>
+     * <p />
+     * By doing so, we never skip any asynchronously-logged output, but we still keep the prompt and
+     * the user's buffer as the last items on the screen.
+     */
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+        final boolean fullLine = b[off + len - 1] == '\n';
+        if (fullLine) {
+            if (mConsoleReader.getTerminal().isANSISupported()) {
+                // use ANSI escape codes to clear the line and jump to the beginning
+                mConsoleReader.printString(ANSI_CR);
+            } else {
+                // Just jump to the beginning of the line to print the message
+                mConsoleReader.printString(CR);
+            }
+        }
+
+        mConsoleReader.printString(new String(b, off, len));
+
+        if (fullLine) {
+            mConsoleReader.drawLine();
+            mConsoleReader.flushConsole();
+        }
+    }
+
+    // FIXME: it'd be nice if ConsoleReader had a way to write a character rather than just a
+    // FIXME: String.  As is, this method makes me cringe.  Especially since the first thing
+    // FIXME: ConsoleReader does is convert it back into a char array :o(
+    @Override
+    public void write(int b) throws IOException {
+        char[] str = new char[] {(char)(b & 0xff)};
+        mConsoleReader.printString(new String(str));
+    }
+}
+