FELIX-1431: Add a web console plugin to have access to the gogo shell

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@801921 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/webconsole/plugins/pom.xml b/karaf/webconsole/features/pom.xml
similarity index 88%
rename from karaf/webconsole/plugins/pom.xml
rename to karaf/webconsole/features/pom.xml
index 7025e4d..5910ace 100644
--- a/karaf/webconsole/plugins/pom.xml
+++ b/karaf/webconsole/features/pom.xml
@@ -23,16 +23,16 @@
   <modelVersion>4.0.0</modelVersion>
   
   <parent>
-    <groupId>org.apache.felix.karaf.webconsole</groupId>
-    <artifactId>webconsole</artifactId>
-    <version>1.2.0-SNAPSHOT</version>
+      <groupId>org.apache.felix.karaf.webconsole</groupId>
+      <artifactId>webconsole</artifactId>
+      <version>1.2.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.apache.felix.karaf.webconsole</groupId>
-  <artifactId>org.apache.felix.karaf.webconsole.plugins</artifactId>
+  <artifactId>org.apache.felix.karaf.webconsole.features</artifactId>
   <packaging>bundle</packaging>
   <version>1.2.0-SNAPSHOT</version>
-  <name>Apache Felix Karaf :: Web Console :: Plugins</name>
+  <name>Apache Felix Karaf :: Web Console :: Features Plugin</name>
   
   <dependencies>
     <dependency>
@@ -85,7 +85,7 @@
         <artifactId>maven-bundle-plugin</artifactId>
         <configuration>
           <instructions>
-            <Export-Package>org.apache.felix.karaf.webconsole;version=${pom.version}</Export-Package>
+            <Export-Package>org.apache.felix.karaf.webconsole.features;version=${pom.version}</Export-Package>
             <Embed-Dependency>
                <!-- Required for JSON data transfer -->
                <!-- TODO: this needs to be put in a common place for reuse. -->
diff --git a/karaf/webconsole/plugins/src/main/java/org/apache/felix/karaf/webconsole/Feature.java b/karaf/webconsole/features/src/main/java/org/apache/felix/karaf/webconsole/features/Feature.java
similarity index 95%
rename from karaf/webconsole/plugins/src/main/java/org/apache/felix/karaf/webconsole/Feature.java
rename to karaf/webconsole/features/src/main/java/org/apache/felix/karaf/webconsole/features/Feature.java
index cc2a627..67aba5e 100644
--- a/karaf/webconsole/plugins/src/main/java/org/apache/felix/karaf/webconsole/Feature.java
+++ b/karaf/webconsole/features/src/main/java/org/apache/felix/karaf/webconsole/features/Feature.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.felix.karaf.webconsole;
+package org.apache.felix.karaf.webconsole.features;
 
 /**
  * Represents a feature with a name, version and state 
diff --git a/karaf/webconsole/plugins/src/main/java/org/apache/felix/karaf/webconsole/FeaturesPlugin.java b/karaf/webconsole/features/src/main/java/org/apache/felix/karaf/webconsole/features/FeaturesPlugin.java
similarity index 99%
rename from karaf/webconsole/plugins/src/main/java/org/apache/felix/karaf/webconsole/FeaturesPlugin.java
rename to karaf/webconsole/features/src/main/java/org/apache/felix/karaf/webconsole/features/FeaturesPlugin.java
index ad22929..b4c9946 100644
--- a/karaf/webconsole/plugins/src/main/java/org/apache/felix/karaf/webconsole/FeaturesPlugin.java
+++ b/karaf/webconsole/features/src/main/java/org/apache/felix/karaf/webconsole/features/FeaturesPlugin.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.felix.karaf.webconsole;
+package org.apache.felix.karaf.webconsole.features;
 
 
 import java.io.IOException;
diff --git a/karaf/webconsole/plugins/src/main/resources/OSGI-INF/blueprint/webconsole.xml b/karaf/webconsole/features/src/main/resources/OSGI-INF/blueprint/webconsole-features.xml
similarity index 94%
rename from karaf/webconsole/plugins/src/main/resources/OSGI-INF/blueprint/webconsole.xml
rename to karaf/webconsole/features/src/main/resources/OSGI-INF/blueprint/webconsole-features.xml
index b564a35..0cd5325 100644
--- a/karaf/webconsole/plugins/src/main/resources/OSGI-INF/blueprint/webconsole.xml
+++ b/karaf/webconsole/features/src/main/resources/OSGI-INF/blueprint/webconsole-features.xml
@@ -22,7 +22,7 @@
 
     <reference id="featuresService" interface="org.apache.felix.karaf.gshell.features.FeaturesService" />
 
-    <bean id="featuresPlugin" class="org.apache.felix.karaf.webconsole.FeaturesPlugin" init-method="start" destroy-method="stop">
+    <bean id="featuresPlugin" class="org.apache.felix.karaf.webconsole.features.FeaturesPlugin" init-method="start" destroy-method="stop">
         <property name="featuresService" ref="featuresService" />
         <property name="bundleContext" ref="blueprintBundleContext" />
     </bean>
diff --git a/karaf/webconsole/plugins/src/main/resources/res/ui/features.js b/karaf/webconsole/features/src/main/resources/res/ui/features.js
similarity index 100%
rename from karaf/webconsole/plugins/src/main/resources/res/ui/features.js
rename to karaf/webconsole/features/src/main/resources/res/ui/features.js
diff --git a/karaf/webconsole/plugins/pom.xml b/karaf/webconsole/gogo/pom.xml
similarity index 87%
copy from karaf/webconsole/plugins/pom.xml
copy to karaf/webconsole/gogo/pom.xml
index 7025e4d..f7edf26 100644
--- a/karaf/webconsole/plugins/pom.xml
+++ b/karaf/webconsole/gogo/pom.xml
@@ -23,16 +23,16 @@
   <modelVersion>4.0.0</modelVersion>
   
   <parent>
-    <groupId>org.apache.felix.karaf.webconsole</groupId>
-    <artifactId>webconsole</artifactId>
-    <version>1.2.0-SNAPSHOT</version>
+      <groupId>org.apache.felix.karaf.webconsole</groupId>
+      <artifactId>webconsole</artifactId>
+      <version>1.2.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.apache.felix.karaf.webconsole</groupId>
-  <artifactId>org.apache.felix.karaf.webconsole.plugins</artifactId>
+  <artifactId>org.apache.felix.karaf.webconsole.gogo</artifactId>
   <packaging>bundle</packaging>
   <version>1.2.0-SNAPSHOT</version>
-  <name>Apache Felix Karaf :: Web Console :: Plugins</name>
+  <name>Apache Felix Karaf :: Web Console :: Gogo Plugin</name>
   
   <dependencies>
     <dependency>
@@ -62,7 +62,7 @@
     </dependency>
     <dependency>
       <groupId>org.apache.felix.karaf.gshell</groupId>
-      <artifactId>org.apache.felix.karaf.gshell.features</artifactId>
+      <artifactId>org.apache.felix.karaf.gshell.console</artifactId>
     </dependency>
     <dependency>
       <groupId>org.apache.servicemix.bundles</groupId>
@@ -85,7 +85,7 @@
         <artifactId>maven-bundle-plugin</artifactId>
         <configuration>
           <instructions>
-            <Export-Package>org.apache.felix.karaf.webconsole;version=${pom.version}</Export-Package>
+            <Export-Package>org.apache.felix.karaf.webconsole.gogo;version=${pom.version}</Export-Package>
             <Embed-Dependency>
                <!-- Required for JSON data transfer -->
                <!-- TODO: this needs to be put in a common place for reuse. -->
diff --git a/karaf/webconsole/gogo/src/main/java/org/apache/felix/karaf/webconsole/gogo/GogoPlugin.java b/karaf/webconsole/gogo/src/main/java/org/apache/felix/karaf/webconsole/gogo/GogoPlugin.java
new file mode 100644
index 0000000..6b08431
--- /dev/null
+++ b/karaf/webconsole/gogo/src/main/java/org/apache/felix/karaf/webconsole/gogo/GogoPlugin.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+ * Based on http://antony.lesuisse.org/software/ajaxterm/
+ *  Public Domain License
+ */
+
+package org.apache.felix.karaf.webconsole.gogo;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.PipedOutputStream;
+import java.io.PipedInputStream;
+import java.io.InterruptedIOException;
+import java.io.InputStreamReader;
+import java.io.ByteArrayInputStream;
+import java.io.PrintStream;
+import java.io.InputStream;
+import java.util.zip.GZIPOutputStream;
+import java.util.List;
+import java.net.URL;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletException;
+
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.apache.felix.karaf.gshell.console.jline.Console;
+import org.apache.felix.karaf.gshell.console.Completer;
+import org.apache.felix.karaf.gshell.console.completer.AggregateCompleter;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.command.CommandProcessor;
+import org.osgi.service.command.CommandSession;
+
+/**
+ * The <code>GogoPlugin</code>
+ */
+public class GogoPlugin extends AbstractWebConsolePlugin {
+
+    /** Pseudo class version ID to keep the IDE quite. */
+    private static final long serialVersionUID = 1L;
+
+    public static final String NAME = "gogo";
+
+    public static final String LABEL = "Gogo";
+
+    public static final int TERM_WIDTH = 120;
+    public static final int TERM_HEIGHT = 39;
+
+    private Log log = LogFactory.getLog(GogoPlugin.class);
+
+    private BundleContext bundleContext;
+
+    private CommandProcessor commandProcessor;
+
+    private List<Completer> completers;
+
+    public void setBundleContext(BundleContext bundleContext)
+    {
+        this.bundleContext = bundleContext;
+    }
+
+    public void setCommandProcessor(CommandProcessor commandProcessor)
+    {
+        this.commandProcessor = commandProcessor;
+    }
+
+    public void setCompleters(List<Completer> completers)
+    {
+        this.completers = completers;
+    }
+
+    /*
+    * Blueprint lifecycle callback methods
+    */
+
+    public void start()
+    {
+        super.activate( bundleContext );
+        this.log.info( LABEL + " plugin activated" );
+    }
+
+    public void stop()
+    {
+        this.log.info( LABEL + " plugin deactivated" );
+        super.deactivate();
+    }
+
+    //
+    // AbstractWebConsolePlugin interface
+    //
+    public String getLabel()
+    {
+        return NAME;
+    }
+
+
+    public String getTitle()
+    {
+        return LABEL;
+    }
+
+
+    protected void renderContent( HttpServletRequest request, HttpServletResponse response ) throws IOException
+    {
+        PrintWriter pw = response.getWriter();
+
+        String appRoot = request.getContextPath() + request.getServletPath();
+        pw.println( "<link href=\"" + appRoot + "/gogo/res/ui/gogo.css\" rel=\"stylesheet\" type=\"text/css\" />" );
+        pw.println( "<script src=\"" + appRoot + "/gogo/res/ui/gogo.js\" type=\"text/javascript\"></script>" );
+        pw.println( "<div id='console'><div id='term'></div></div>" );
+        pw.println( "<script type=\"text/javascript\"><!--" );
+        pw.println( "window.onload = function() { gogo.Terminal(document.getElementById(\"term\"), " + TERM_WIDTH + ", " + TERM_HEIGHT + "); }" );
+        pw.println( "--></script>" );
+    }
+
+    protected URL getResource( String path )
+    {
+        path = path.substring( NAME.length() + 1 );
+        URL url = this.getClass().getClassLoader().getResource( path );
+        try
+        {
+            InputStream ins = url.openStream();
+            if ( ins == null )
+            {
+                this.log.error( "failed to open " + url );
+            }
+        }
+        catch ( IOException e )
+        {
+            this.log.error( e.getMessage(), e );
+        }
+        return url;
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        String encoding = request.getHeader("Accept-Encoding");
+        boolean supportsGzip = (encoding != null && encoding.toLowerCase().indexOf("gzip") > -1);
+        SessionTerminal st = (SessionTerminal) request.getSession(true).getAttribute("terminal");
+        if (st == null || st.isClosed()) {
+            st = new SessionTerminal();
+            request.getSession().setAttribute("terminal", st);
+        }
+        String str = request.getParameter("k");
+        String f = request.getParameter("f");
+        String dump = st.handle(str, f != null && f.length() > 0);
+        if (dump != null) {
+            if (supportsGzip) {
+                response.setHeader("Content-Encoding", "gzip");
+                response.setHeader("Content-Type", "text/html");
+                try {
+                    GZIPOutputStream gzos =  new GZIPOutputStream(response.getOutputStream());
+                    gzos.write(dump.getBytes());
+                    gzos.close();
+                } catch (IOException ie) {
+                    // handle the error here
+                    ie.printStackTrace();
+                }
+            } else {
+                response.getOutputStream().write(dump.getBytes());
+            }
+        }
+    }
+
+
+    public class SessionTerminal implements Runnable {
+
+        private Terminal terminal;
+        private Console console;
+        private PipedOutputStream in;
+        private PipedInputStream out;
+        private boolean closed;
+
+        public SessionTerminal() throws IOException {
+            try {
+                this.terminal = new Terminal(TERM_WIDTH, TERM_HEIGHT);
+                terminal.write("\u001b\u005B20\u0068"); // set newline mode on
+
+                in = new PipedOutputStream();
+                out = new PipedInputStream();
+                PrintStream pipedOut = new PrintStream(new PipedOutputStream(out), true);
+
+                console = new Console(commandProcessor,
+                                      new PipedInputStream(in),
+                                      pipedOut,
+                                      pipedOut,
+                                      new WebTerminal(TERM_WIDTH, TERM_HEIGHT),
+                                      new AggregateCompleter(completers),
+                                      null);
+                CommandSession session = console.getSession();
+                session.put("APPLICATION", System.getProperty("karaf.name", "root"));
+                session.put("USER", "karaf");
+                session.put("COLUMNS", Integer.toString(TERM_WIDTH));
+                session.put("LINES", Integer.toString(TERM_HEIGHT));
+            } catch (IOException e) {
+                e.printStackTrace();
+                throw e;
+            } catch (Exception e) {
+                e.printStackTrace();
+                throw new IOException(e);
+            }
+            new Thread(console).start();
+            new Thread(this).start();
+        }
+
+        public boolean isClosed() {
+            return closed;
+        }
+
+        public String handle(String str, boolean forceDump) throws IOException {
+            try {
+                if (str != null && str.length() > 0) {
+                    String d = terminal.pipe(str);
+                    for (byte b : d.getBytes()) {
+                        in.write(b);
+                    }
+                    in.flush();
+                }
+            } catch (IOException e) {
+                closed = true;
+                throw e;
+            }
+            try {
+                return terminal.dump(10, forceDump);
+            } catch (InterruptedException e) {
+                throw new InterruptedIOException(e.toString());
+            }
+        }
+
+        public void run() {
+            try {
+                for (;;) {
+                    byte[] buf = new byte[8192];
+                    int l = out.read(buf);
+                    InputStreamReader r = new InputStreamReader(new ByteArrayInputStream(buf, 0, l));
+                    StringBuilder sb = new StringBuilder();
+                    for (;;) {
+                        int c = r.read();
+                        if (c == -1) {
+                            break;
+                        }
+                        sb.append((char) c);
+                    }
+                    if (sb.length() > 0) {
+                        terminal.write(sb.toString());
+                    }
+                    String s = terminal.read();
+                    if (s != null && s.length() > 0) {
+                        for (byte b : s.getBytes()) {
+                            in.write(b);
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                closed = true;
+                e.printStackTrace();
+            }
+        }
+
+    }
+}
diff --git a/karaf/webconsole/gogo/src/main/java/org/apache/felix/karaf/webconsole/gogo/Terminal.java b/karaf/webconsole/gogo/src/main/java/org/apache/felix/karaf/webconsole/gogo/Terminal.java
new file mode 100644
index 0000000..ecb9a9e
--- /dev/null
+++ b/karaf/webconsole/gogo/src/main/java/org/apache/felix/karaf/webconsole/gogo/Terminal.java
@@ -0,0 +1,1494 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+ * Based on http://antony.lesuisse.org/software/ajaxterm/
+ *  Public Domain License
+ */
+
+/**
+ * See http://www.ecma-international.org/publications/standards/Ecma-048.htm
+ *       and http://vt100.net/docs/vt510-rm/
+ */
+
+package org.apache.felix.karaf.webconsole.gogo;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class Terminal {
+
+    enum State {
+        None,
+        Esc,
+        Str,
+        Csi,
+    }
+
+    private int width;
+    private int height;
+    private int attr;
+    private boolean eol;
+    private int cx;
+    private int cy;
+    private int[] screen;
+    private int[] screen2;
+    private State vt100_parse_state = State.None;
+    private int vt100_parse_len;
+    private int vt100_lastchar;
+    private int vt100_parse_func;
+    private String vt100_parse_param;
+    private boolean vt100_mode_autowrap;
+    private boolean vt100_mode_insert;
+    private boolean vt100_charset_is_single_shift;
+    private boolean vt100_charset_is_graphical;
+    private boolean vt100_mode_lfnewline;
+    private boolean vt100_mode_origin;
+    private boolean vt100_mode_inverse;
+    private boolean vt100_mode_cursorkey;
+    private boolean vt100_mode_cursor;
+    private boolean vt100_mode_alt_screen;
+    private boolean vt100_mode_backspace;
+    private boolean vt100_mode_column_switch;
+    private boolean vt100_keyfilter_escape;
+    private int[] vt100_charset_graph = new int[] {
+            0x25ca, 0x2026, 0x2022, 0x3f,
+            0xb6, 0x3f, 0xb0, 0xb1,
+            0x3f, 0x3f, 0x2b, 0x2b,
+            0x2b, 0x2b, 0x2b, 0xaf,
+            0x2014, 0x2014, 0x2014, 0x5f,
+            0x2b, 0x2b, 0x2b, 0x2b,
+            0x7c, 0x2264, 0x2265, 0xb6,
+            0x2260, 0xa3, 0xb7, 0x7f
+    };
+    private int vt100_charset_g_sel;
+    private int[] vt100_charset_g = { 0, 0 };
+    private Map<String, Object> vt100_saved;
+    private Map<String, Object> vt100_saved2;
+    private int vt100_saved_cx;
+    private int vt100_saved_cy;
+    private String vt100_out;
+
+    private int scroll_area_y0;
+    private int scroll_area_y1;
+
+    private List<Integer> tab_stops;
+
+    private int utf8_char;
+    private int utf8_units_count;
+    private int utf8_units_received;
+
+    private AtomicBoolean dirty = new AtomicBoolean(true);
+
+    public Terminal() {
+        this(80, 24);
+    }
+
+    public Terminal(int width, int height) {
+        this.width = width;
+        this.height = height;
+        reset_hard();
+    }
+
+    private void reset_hard() {
+		// Attribute mask: 0x0XFB0000
+		//	X:	Bit 0 - Underlined
+		//		Bit 1 - Negative
+		//		Bit 2 - Concealed
+		//	F:	Foreground
+		//	B:	Background
+		attr = 0x00fe0000;
+		// UTF-8 decoder
+		utf8_units_count = 0;
+		utf8_units_received = 0;
+		utf8_char = 0;
+		// Key filter
+		vt100_keyfilter_escape = false;
+		// Last char
+		vt100_lastchar = 0;
+		// Control sequences
+		vt100_parse_len = 0;
+		vt100_parse_state = State.None;
+		vt100_parse_func = 0;
+		vt100_parse_param = "";
+		// Buffers
+		vt100_out = "";
+		// Invoke other resets
+        reset_screen();
+        reset_soft();
+    }
+
+    private void reset_soft() {
+        // Attribute mask: 0x0XFB0000
+        //	X:	Bit 0 - Underlined
+        //		Bit 1 - Negative
+        //		Bit 2 - Concealed
+        //	F:	Foreground
+        //	B:	Background
+        attr = 0x00fe0000;
+        // Scroll parameters
+        scroll_area_y0 = 0;
+        scroll_area_y1 = height;
+        // Character sets
+        vt100_charset_is_single_shift = false;
+        vt100_charset_is_graphical = false;
+        vt100_charset_g_sel = 0;
+        vt100_charset_g = new int[] { 0, 0 };
+		// Modes
+		vt100_mode_insert = false;
+		vt100_mode_lfnewline = false;
+		vt100_mode_cursorkey = false;
+		vt100_mode_column_switch = false;
+		vt100_mode_inverse = false;
+		vt100_mode_origin = false;
+		vt100_mode_autowrap = true;
+		vt100_mode_cursor = true;
+		vt100_mode_alt_screen = false;
+		vt100_mode_backspace = false;
+		// Init DECSC state
+		esc_DECSC();
+		vt100_saved2 = vt100_saved;
+		esc_DECSC();
+    }
+
+    private void reset_screen() {
+        // Screen
+        screen = new int[width * height];
+        Arrays.fill(screen, attr | 0x0020);
+        screen2 = new int[width * height];
+        Arrays.fill(screen2, attr | 0x0020);
+        // Scroll parameters
+        scroll_area_y0 = 0;
+        scroll_area_y1 = height;
+        // Cursor position
+        cx = 0;
+        cy = 0;
+        // Tab stops
+        tab_stops = new ArrayList<Integer>();
+        for (int i = 7; i < width; i += 8) {
+            tab_stops.add(i);
+        }
+    }
+
+    //
+    // UTF-8 functions
+    //
+
+    private String utf8_decode(String d) {
+        StringBuilder o = new StringBuilder();
+        for (char c : d.toCharArray()) {
+            if (utf8_units_count != utf8_units_received) {
+                utf8_units_received++;
+                if ((c & 0xc0) == 0x80) {
+                    utf8_char = (utf8_char << 6) | (c & 0x3f);
+                    if (utf8_units_count == utf8_units_received) {
+                        if (utf8_char < 0x10000) {
+                            o.append((char) utf8_char);
+                        }
+                        utf8_units_count = utf8_units_received = 0;
+                    }
+                } else {
+                    o.append('?');
+                    while (utf8_units_received-- > 0) {
+                        o.append('?');
+                    }
+                    utf8_units_count = 0;
+                }
+            } else {
+                if ((c & 0x80) == 0x00) {
+                    o.append(c);
+                } else if ((c & 0xe0) == 0xc0) {
+                    utf8_units_count = 1;
+                    utf8_char = c & 0x1f;
+                } else if ((c & 0xf0) == 0xe0) {
+                    utf8_units_count = 2;
+                    utf8_char = c & 0x0f;
+                } else if ((c & 0xf8) == 0xf0) {
+                    utf8_units_count = 3;
+                    utf8_char = c & 0x07;
+                } else {
+                    o.append('?');
+                }
+
+            }
+        }
+        return o.toString();
+    }
+
+    private int utf8_charwidth(int c) {
+        if (c >= 0x2e80) {
+            return 2;
+        } else {
+            return 1;
+        }
+    }
+
+    //
+    // Low-level terminal functions
+    //
+
+    private int[] peek(int y0, int x0, int y1, int x1) {
+        return Arrays.copyOfRange(screen, width * y0 + x0, width * (y1 - 1) + x1);
+    }
+
+    private void poke(int y, int x, int[] s) {
+        System.arraycopy(s, 0, screen, width * y + x, s.length);
+        setDirty();
+    }
+
+    private void fill(int y0, int x0, int y1, int x1, int c) {
+        int d0 = width * y0 + x0;
+        int d1 = width * (y1 - 1) + x1;
+        if (d0 <= d1) {
+            Arrays.fill(screen, width * y0 + x0,  width * (y1 - 1) + x1, c);
+            setDirty();
+        }
+    }
+
+    private void clear(int y0, int x0, int y1, int x1) {
+        fill(y0, x0, y1, x1, attr | 0x20);
+    }
+
+    //
+    // Scrolling functions
+    //
+
+    private void scroll_area_up(int y0, int y1) {
+        scroll_area_up(y0, y1, 1);
+    }
+
+    private void scroll_area_up(int y0, int y1, int n) {
+        n = Math.min(y1 - y0, n);
+        poke(y0, 0, peek(y0 + n, 0, y1, width));
+        clear(y1-n, 0, y1, width);
+    }
+
+    private void scroll_area_down(int y0, int y1) {
+        scroll_area_down(y0, y1, 1);
+    }
+
+    private void scroll_area_down(int y0, int y1, int n) {
+        n = Math.min(y1 - y0, n);
+        poke(y0 + n, 0, peek(y0, 0, y1-n, width));
+        clear(y0, 0, y0 + n, width);
+    }
+
+    private void scroll_area_set(int y0, int y1) {
+        y0 = Math.max(0, Math.min(height - 1, y0));
+        y1 = Math.max(1, Math.min(height, y1));
+        if (y1 > y0) {
+            scroll_area_y0 = y0;
+            scroll_area_y1 = y1;
+        }
+    }
+
+    private void scroll_line_right(int y, int x) {
+        scroll_line_right(y, x, 1);
+    }
+
+    private void scroll_line_right(int y, int x, int n) {
+        if (x < width) {
+            n = Math.min(width - cx, n);
+            poke(y, x + n, peek(y, x, y + 1, width - n));
+            clear(y, x, y + 1, x + n);
+        }
+    }
+
+    private void scroll_line_left(int y, int x) {
+        scroll_line_left(y, x, 1);
+    }
+
+    private void scroll_line_left(int y, int x, int n) {
+        if (x < width) {
+            n = Math.min(width - cx, n);
+            poke(y, x, peek(y, x + n, y + 1, width));
+            clear(y, width - n, y + 1, width);
+        }
+    }
+
+    //
+	// Cursor functions
+    //
+
+    private int[] cursor_line_width(int next_char) {
+        int wx = utf8_charwidth(next_char);
+        int lx = 0;
+        for (int x = 0; x < Math.min(cx, width); x++) {
+            int c = peek(cy, x, cy + 1, x + 1)[0] & 0xffff;
+            wx += utf8_charwidth(c);
+            lx += 1;
+        }
+        return new int[] { wx, lx };
+    }
+
+    private void cursor_up() {
+        cursor_up(1);
+    }
+
+    private void cursor_up(int n) {
+        cy = Math.max(scroll_area_y0, cy - n);
+        setDirty();
+    }
+
+    private void cursor_down() {
+        cursor_down(1);
+    }
+
+    private void cursor_down(int n) {
+        cy = Math.min(scroll_area_y1 - 1, cy + n);
+        setDirty();
+    }
+
+    private void cursor_left() {
+        cursor_left(1);
+    }
+
+    private void cursor_left(int n) {
+        eol = false;
+        cx = Math.max(0, cx - n);
+        setDirty();
+    }
+
+    private void cursor_right() {
+        cursor_right(1);
+    }
+
+    private void cursor_right(int n) {
+        eol = cx + n >= width;
+        cx = Math.min(width - 1, cx + n);
+        setDirty();
+    }
+
+    private void cursor_set_x(int x) {
+        eol = false;
+        cx = Math.max(0, x);
+        setDirty();
+    }
+
+    private void cursor_set_y(int y) {
+        cy = Math.max(0, Math.min(height - 1, y));
+        setDirty();
+    }
+
+    private void cursor_set(int y, int x) {
+        cursor_set_x(x);
+        cursor_set_y(y);
+    }
+
+    //
+    // Dumb terminal
+    //
+
+    private void ctrl_BS() {
+        int dy = (cx - 1) / width;
+        cursor_set(Math.max(scroll_area_y0, cy + dy), (cx - 1) % width);
+    }
+
+    private void ctrl_HT() {
+        ctrl_HT(1);
+    }
+
+    private void ctrl_HT(int n) {
+        if (n > 0 && cx >= width) {
+            return;
+        }
+        if (n <= 0 && cx == 0) {
+            return;
+        }
+        int ts = -1;
+        for (int i = 0; i < tab_stops.size(); i++) {
+            if (cx >= tab_stops.get(i)) {
+                ts = i;
+            }
+        }
+        ts += n;
+        if (ts < tab_stops.size() && ts >= 0) {
+            cursor_set_x(tab_stops.get(ts));
+        } else {
+            cursor_set_x(width - 1);
+        }
+    }
+
+    private void ctrl_LF() {
+        if (vt100_mode_lfnewline) {
+            ctrl_CR();
+        }
+        if (cy == scroll_area_y1 - 1) {
+            scroll_area_up(scroll_area_y0, scroll_area_y1);
+        } else {
+            cursor_down();
+        }
+    }
+
+    private void ctrl_CR() {
+        cursor_set_x(0);
+    }
+
+    private boolean dumb_write(int c) {
+        if (c < 32) {
+            if (c == 8) {
+                ctrl_BS();
+            } else if (c == 9) {
+                ctrl_HT();
+            } else if (c >= 10 && c <= 12) {
+                ctrl_LF();
+            } else if (c == 13) {
+                ctrl_CR();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private void dumb_echo(int c) {
+        if (eol) {
+            if (vt100_mode_autowrap) {
+                ctrl_CR();
+                ctrl_LF();
+            } else {
+                cx = cursor_line_width(c)[1] - 1;
+            }
+        }
+        if (vt100_mode_insert) {
+            scroll_line_right(cy, cx);
+        }
+        if (vt100_charset_is_single_shift) {
+            vt100_charset_is_single_shift = false;
+        } else if (vt100_charset_is_graphical && ((c & 0xffe0) == 0x0060)) {
+            c = vt100_charset_graph[c - 0x60];
+        }
+        poke(cy, cx, new int[] { attr | c });
+        cursor_right();
+    }
+
+    //
+    // VT100
+    //
+
+    private void vt100_charset_update() {
+        vt100_charset_is_graphical = (vt100_charset_g[vt100_charset_g_sel] == 2);
+    }
+
+    private void vt100_charset_set(int g) {
+        // Invoke active character set
+        vt100_charset_g_sel = g;
+        vt100_charset_update();
+    }
+
+    private void vt100_charset_select(int g, int charset) {
+        // Select charset
+        vt100_charset_g[g] = charset;
+        vt100_charset_update();
+    }
+
+    private void vt100_setmode(String p, boolean state) {
+        // Set VT100 mode
+        String[] ps = vt100_parse_params(p, new String[0]);
+        for (String m : ps) {
+            // 1 : GATM: Guarded area transfer
+            // 2 : KAM: Keyboard action
+            // 3 : CRM: Control representation
+            if ("4".equals(m)) {
+                // Insertion replacement mode
+                vt100_mode_insert = state;
+            // 5 : SRTM: Status reporting transfer
+            // 7 : VEM: Vertical editing
+            // 10 : HEM: Horizontal editing
+            // 11 : PUM: Positioning nit
+            // 12 : SRM: Send/receive
+            // 13 : FEAM: Format effector action
+            // 14 : FETM: Format effector transfer
+            // 15 : MATM: Multiple area transfer
+            // 16 : TTM: Transfer termination
+            // 17 : SATM: Selected area transfer
+            // 18 : TSM: Tabulation stop
+            // 19 : EBM: Editing boundary
+            } else if ("20".equals(m)) {
+                // LNM: Line feed/new line
+                vt100_mode_lfnewline = state;
+            } else if ("?1".equals(m)) {
+                // DECCKM: Cursor keys
+                vt100_mode_cursorkey = state;
+            // ?2 : DECANM: ANSI
+            } else if ("?3".equals(m)) {
+                // DECCOLM: Column
+                if (vt100_mode_column_switch) {
+                    if (state) {
+                        width = 132;
+                    } else {
+                        width = 80;
+                    }
+                    reset_screen();
+                }
+            // ?4 : DECSCLM: Scrolling
+            } else if ("?5".equals(m)) {
+                // DECSCNM: Screen
+                vt100_mode_inverse = state;
+            } else if ("?6".equals(m)) {
+                // DECOM: Origin
+                vt100_mode_origin = state;
+                if (state) {
+                    cursor_set(scroll_area_y0, 0);
+                } else {
+                    cursor_set(0, 0);
+                }
+            } else if ("?7".equals(m)) {
+                // DECAWM: Autowrap
+                vt100_mode_autowrap = state;
+            // ?8 : DECARM: Autorepeat
+            // ?9 : Interlacing
+            // ?18 : DECPFF: Print form feed
+            // ?19 : DECPEX: Printer extent
+            } else if ("?25".equals(m)) {
+                // DECTCEM: Text cursor enable
+                vt100_mode_cursor = state;
+            // ?34 : DECRLM: Cursor direction, right to left
+            // ?35 : DECHEBM: Hebrew keyboard mapping
+            // ?36 : DECHEM: Hebrew encoding mode
+            } else if ("?40".equals(m)) {
+                // Column switch control
+                vt100_mode_column_switch = state;
+            // ?42 : DECNRCM: National replacement character set
+            } else if ("?47".equals(m)) {
+                // Alternate screen mode
+                if ((state && !vt100_mode_alt_screen) || (!state && vt100_mode_alt_screen)) {
+                    int[] s = screen; screen = screen2; screen2 = s;
+                    Map<String, Object> map = vt100_saved; vt100_saved = vt100_saved2; vt100_saved = map;
+                }
+                vt100_mode_alt_screen = state;
+            // ?57 : DECNAKB: Greek keyboard mapping
+            } else if ("?67".equals(m)) {
+                // DECBKM: Backarrow key
+                vt100_mode_backspace = state;
+            }
+            // ?98 : DECARSM: auto-resize
+            // ?101 : DECCANSM: Conceal answerback message
+            // ?109 : DECCAPSLK: caps lock
+        }
+    }
+
+    private void ctrl_SO() {
+        vt100_charset_set(1);
+    }
+
+    private void ctrl_SI() {
+        vt100_charset_set(0);
+    }
+
+    private void esc_CSI() {
+        vt100_parse_reset(State.Csi);
+    }
+
+    private void esc_DECALN() {
+        fill(0, 0, height, width, 0x00fe0045);
+    }
+
+    private void esc_G0_0() {
+        vt100_charset_select(0, 0);
+    }
+    private void esc_G0_1() {
+        vt100_charset_select(0, 1);
+    }
+    private void esc_G0_2() {
+        vt100_charset_select(0, 2);
+    }
+    private void esc_G0_3() {
+        vt100_charset_select(0, 3);
+    }
+    private void esc_G0_4() {
+        vt100_charset_select(0, 4);
+    }
+
+    private void esc_G1_0() {
+        vt100_charset_select(1, 0);
+    }
+    private void esc_G1_1() {
+        vt100_charset_select(1, 1);
+    }
+    private void esc_G1_2() {
+        vt100_charset_select(1, 2);
+    }
+    private void esc_G1_3() {
+        vt100_charset_select(1, 3);
+    }
+    private void esc_G1_4() {
+        vt100_charset_select(1, 4);
+    }
+
+    private void esc_DECSC() {
+        vt100_saved = new HashMap<String, Object>();
+        vt100_saved.put("cx", cx);
+        vt100_saved.put("cy", cy);
+        vt100_saved.put("attr", attr);
+        vt100_saved.put("vt100_charset_g_sel", vt100_charset_g_sel);
+        vt100_saved.put("vt100_charset_g", vt100_charset_g);
+        vt100_saved.put("vt100_mode_autowrap", vt100_mode_autowrap);
+        vt100_saved.put("vt100_mode_origin", vt100_mode_origin);
+    }
+
+    private void esc_DECRC() {
+        cx = (Integer) vt100_saved.get("cx");
+        cy = (Integer) vt100_saved.get("cy");
+        attr = (Integer) vt100_saved.get("attr");
+        vt100_charset_g_sel = (Integer) vt100_saved.get("vt100_charset_g_sel");
+        vt100_charset_g = (int[]) vt100_saved.get("vt100_charset_g");
+        vt100_charset_update();
+        vt100_mode_autowrap = (Boolean) vt100_saved.get("vt100_mode_autowrap");
+        vt100_mode_origin = (Boolean) vt100_saved.get("vt100_mode_origin");
+    }
+
+    private void esc_IND() {
+        ctrl_LF();
+    }
+
+    private void esc_NEL() {
+        ctrl_CR();
+        ctrl_LF();
+    }
+
+    private void esc_HTS() {
+        csi_CTC("0");
+    }
+
+    private void esc_RI() {
+        if (cy == scroll_area_y0) {
+            scroll_area_down(scroll_area_y0, scroll_area_y1);
+        } else {
+            cursor_up();
+        }
+    }
+
+    private void esc_SS2() {
+        vt100_charset_is_single_shift = true;
+    }
+
+    private void esc_SS3() {
+        vt100_charset_is_single_shift = true;
+    }
+
+    private void esc_DCS() {
+        vt100_parse_reset(State.Str);
+    }
+
+    private void esc_SOS() {
+        vt100_parse_reset(State.Str);
+    }
+
+    private void esc_DECID() {
+        csi_DA("0");
+    }
+
+    private void esc_ST() {
+    }
+
+    private void esc_OSC() {
+        vt100_parse_reset(State.Str);
+    }
+
+    private void esc_PM() {
+        vt100_parse_reset(State.Str);
+    }
+
+    private void esc_APC() {
+        vt100_parse_reset(State.Str);
+    }
+
+    private void esc_RIS() {
+        reset_hard();
+    }
+
+    private void csi_ICH(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        scroll_line_right(cy, cx, ps[0]);
+    }
+
+    private void csi_CUU(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        cursor_up(Math.max(1, ps[0]));
+    }
+
+    private void csi_CUD(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        cursor_down(Math.max(1, ps[0]));
+    }
+
+    private void csi_CUF(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        cursor_right(Math.max(1, ps[0]));
+    }
+
+    private void csi_CUB(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        cursor_left(Math.max(1, ps[0]));
+    }
+
+    private void csi_CNL(String p) {
+        csi_CUD(p);
+        ctrl_CR();
+    }
+
+    private void csi_CPL(String p) {
+        csi_CUU(p);
+        ctrl_CR();
+    }
+
+    private void csi_CHA(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        cursor_set_x(ps[0] - 1);
+    }
+
+    private void csi_CUP(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1, 1 });
+        if (vt100_mode_origin) {
+            cursor_set(scroll_area_y0 + ps[0] - 1, ps[1] - 1);
+        } else {
+            cursor_set(ps[0] - 1, ps[1] - 1);
+        }
+    }
+
+    private void csi_CHT(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        ctrl_HT(Math.max(1, ps[0]));
+    }
+
+    private void csi_ED(String p) {
+        String[] ps = vt100_parse_params(p, new String[] { "0" });
+        if ("0".equals(ps[0])) {
+            clear(cy, cx, height, width);
+        } else if ("1".equals(ps[0])) {
+            clear(0, 0, cy + 1, cx + 1);
+        } else if ("2".equals(ps[0])) {
+            clear(0, 0, height, width);
+        }
+    }
+
+    private void csi_EL(String p) {
+        String[] ps = vt100_parse_params(p, new String[] { "0" });
+        if ("0".equals(ps[0])) {
+            clear(cy, cx, cy + 1, width);
+        } else if ("1".equals(ps[0])) {
+            clear(cy, 0, cy + 1, cx + 1);
+        } else if ("2".equals(ps[0])) {
+            clear(cy, 0, cy + 1, width);
+        }
+    }
+
+    private void csi_IL(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        if (cy >= scroll_area_y0 && cy < scroll_area_y1) {
+            scroll_area_down(cy, scroll_area_y1, Math.max(1, ps[0]));
+        }
+    }
+
+    private void csi_DL(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        if (cy >= scroll_area_y0 && cy < scroll_area_y1) {
+            scroll_area_up(cy, scroll_area_y1, Math.max(1, ps[0]));
+        }
+    }
+
+    private void csi_DCH(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        scroll_line_left(cy, cx, Math.max(1, ps[0]));
+    }
+
+    private void csi_SU(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        scroll_area_up(scroll_area_y0, scroll_area_y1, Math.max(1, ps[0]));
+    }
+
+    private void csi_SD(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        scroll_area_down(scroll_area_y0, scroll_area_y1, Math.max(1, ps[0]));
+    }
+
+    private void csi_CTC(String p) {
+        String[] ps = vt100_parse_params(p, new String[] { "0" });
+        for (String m : ps) {
+            if ("0".equals(m)) {
+                if (tab_stops.indexOf(cx) < 0) {
+                    tab_stops.add(cx);
+                    Collections.sort(tab_stops);
+                }
+            } else if ("2".equals(m)) {
+                tab_stops.remove(Integer.valueOf(cx));
+            } else if ("5".equals(m)) {
+                tab_stops = new ArrayList<Integer>();
+            }
+        }
+    }
+
+    private void csi_ECH(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        int n = Math.min(width - cx, Math.max(1, ps[0]));
+        clear(cy, cx, cy + 1, cx + n);
+    }
+
+    private void csi_CBT(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        ctrl_HT(1 - Math.max(1, ps[0]));
+    }
+
+    private void csi_HPA(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        cursor_set_x(ps[0] - 1);
+    }
+
+    private void csi_HPR(String p) {
+        csi_CUF(p);
+    }
+
+    private void csi_REP(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        if (vt100_lastchar < 32) {
+            return;
+        }
+        int n = Math.min(2000, Math.max(1, ps[0]));
+        while (n-- > 0) {
+            dumb_echo(vt100_lastchar);
+        }
+        vt100_lastchar = 0;
+    }
+
+    private void csi_DA(String p) {
+        String[] ps = vt100_parse_params(p, new String[] { "0" });
+        if ("0".equals(ps[0])) {
+            vt100_out = "\u001b[?1;2c";
+        } else if (">0".equals(ps[0]) || ">".equals(ps[0])) {
+            vt100_out = "\u001b[>0;184;0c";
+        }
+    }
+
+    private void csi_VPA(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1 });
+        cursor_set_y(ps[0] - 1);
+    }
+
+    private void csi_VPR(String p) {
+        csi_CUD(p);
+    }
+
+    private void csi_HVP(String p) {
+        csi_CUP(p);
+    }
+
+    private void csi_TBC(String p) {
+        String[] ps = vt100_parse_params(p, new String[] { "0" });
+        if ("0".equals(ps[0])) {
+            csi_CTC("2");
+        } else if ("3".equals(ps[0])) {
+            csi_CTC("5");
+        }
+    }
+
+    private void csi_SM(String p) {
+        vt100_setmode(p, true);
+    }
+
+    private void csi_RM(String p) {
+        vt100_setmode(p, false);
+    }
+
+    private void csi_SGR(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 0 });
+        for (int m : ps) {
+            if (m == 0) {
+                attr = 0x00fe0000;
+            } else if (m == 1) {
+                attr |= 0x08000000;
+            } else if (m == 4) {
+                attr |= 0x01000000;
+            } else if (m == 7) {
+                attr |= 0x02000000;
+            } else if (m == 8) {
+                attr |= 0x04000000;
+            } else if (m == 24) {
+                attr &= 0x7eff0000;
+            } else if (m == 27) {
+                attr &= 0x7dff0000;
+            } else if (m == 28) {
+                attr &= 0x7bff0000;
+            } else if (m >= 30 && m <= 37) {
+                attr = (attr & 0x7f0f0000) | ((m - 30) << 20);
+            } else if (m == 39) {
+                attr = (attr & 0x7f0f0000) | 0x00f00000;
+            } else if (m >= 40 && m <= 47) {
+                attr = (attr & 0x7ff00000) | ((m - 40) << 16);
+            } else if (m == 49) {
+                attr = (attr & 0x7ff00000) | 0x000e0000;
+            }
+        }
+    }
+
+    private void csi_DSR(String p) {
+        String[] ps = vt100_parse_params(p, new String[] { "0" });
+        if ("5".equals(ps[0])) {
+            vt100_out = "\u001b[0n";
+        } else if ("6".equals(ps[0])) {
+            vt100_out = "\u001b[" + (cy + 1) + ";" + (cx + 1) + "R";
+        } else if ("7".equals(ps[0])) {
+            vt100_out = "gogo-term";
+        } else if ("8".equals(ps[0])) {
+            vt100_out = "1.0-SNAPSHOT";
+        } else if ("?6".equals(ps[0])) {
+            vt100_out = "\u001b[" + (cy + 1) + ";" + (cx + 1) + ";0R";
+        } else if ("?15".equals(ps[0])) {
+            vt100_out = "\u001b[?13n";
+        } else if ("?25".equals(ps[0])) {
+            vt100_out = "\u001b[?20n";
+        } else if ("?26".equals(ps[0])) {
+            vt100_out = "\u001b[?27;1n";
+        } else if ("?53".equals(ps[0])) {
+            vt100_out = "\u001b[?53n";
+        }
+        // ?75 : Data Integrity report
+        // ?62 : Macro Space report
+        // ?63 : Memory Checksum report
+    }
+
+    private void csi_DECSTBM(String p) {
+        int[] ps = vt100_parse_params(p, new int[] { 1, height });
+        scroll_area_set(ps[0] - 1, ps[1]);
+        if (vt100_mode_origin) {
+            cursor_set(scroll_area_y0, 0);
+        } else {
+            cursor_set(0, 0);
+        }
+    }
+
+    private void csi_SCP(String p) {
+        vt100_saved_cx = cx;
+        vt100_saved_cy = cy;
+    }
+
+    private void csi_RCP(String p) {
+        cx = vt100_saved_cx;
+        cy = vt100_saved_cy;
+    }
+
+    private void csi_DECREQTPARM(String p) {
+        String[] ps = vt100_parse_params(p, new String[0]);
+        if ("0".equals(ps[0])) {
+            vt100_out = "\u001b[2;1;1;112;112;1;0x";
+        } else if ("1".equals(ps[0])) {
+            vt100_out = "\u001b[3;1;1;112;112;1;0x";
+        }
+    }
+
+    private void csi_DECSTR(String p) {
+        reset_soft();
+    }
+
+    //
+    // VT100 parser
+    //
+
+    private String[] vt100_parse_params(String p, String[] defaults) {
+        String prefix = "";
+        if (p.length() > 0) {
+            if (p.charAt(0) >= '<' && p.charAt(0) <= '?') {
+                prefix = "" + p.charAt(0);
+                p = p.substring(1);
+            }
+        }
+        String[] ps = p.split(";");
+        int n = Math.max(ps.length, defaults.length);
+        String[] values = new String[n];
+        for (int i = 0; i < n; i++) {
+            String value = null;
+            if (i < ps.length) {
+                value = prefix + ps[i];
+            }
+            if (value == null && i < defaults.length) {
+                value = defaults[i];
+            }
+            if (value == null) {
+                value = "";
+            }
+            values[i] = value;
+        }
+        return values;
+    }
+
+    private int[] vt100_parse_params(String p, int[] defaults) {
+        String prefix = "";
+        p = p == null ? "" : p;
+        if (p.length() > 0) {
+            if (p.charAt(0) >= '<' && p.charAt(0) <= '?') {
+                prefix = p.substring(0, 1);
+                p = p.substring(1);
+            }
+        }
+        String[] ps = p.split(";");
+        int n = Math.max(ps.length, defaults.length);
+        int[] values = new int[n];
+        for (int i = 0; i < n; i++) {
+            Integer value = null;
+            if (i < ps.length) {
+                String v = prefix + ps[i];
+                try {
+                    value = Integer.parseInt(v);
+                } catch (NumberFormatException e) {
+                }
+            }
+            if (value == null && i < defaults.length) {
+                value = defaults[i];
+            }
+            if (value == null) {
+                value = 0;
+            }
+            values[i] = value;
+        }
+        return values;
+    }
+
+    private void vt100_parse_reset() {
+        vt100_parse_reset(State.None);
+    }
+
+    private void vt100_parse_reset(State state) {
+        vt100_parse_state = state;
+        vt100_parse_len = 0;
+        vt100_parse_func = 0;
+        vt100_parse_param = "";
+    }
+
+    private void vt100_parse_process() {
+        if (vt100_parse_state == State.Esc) {
+            switch (vt100_parse_func) {
+                case 0x0036: /* DECBI */ break;
+                case 0x0037: esc_DECSC(); break;
+                case 0x0038: esc_DECRC(); break;
+                case 0x0042: /* BPH */ break;
+                case 0x0043: /* NBH */ break;
+                case 0x0044: esc_IND(); break;
+                case 0x0045: esc_NEL(); break;
+                case 0x0046: /* SSA */ esc_NEL(); break;
+                case 0x0048: esc_HTS(); break;
+                case 0x0049: /* HTJ */ break;
+                case 0x004A: /* VTS */ break;
+                case 0x004B: /* PLD */ break;
+                case 0x004C: /* PLU */ break;
+                case 0x004D: esc_RI(); break;
+                case 0x004E: esc_SS2(); break;
+                case 0x004F: esc_SS3(); break;
+                case 0x0050: esc_DCS(); break;
+                case 0x0051: /* PU1 */ break;
+                case 0x0052: /* PU2 */ break;
+                case 0x0053: /* STS */ break;
+                case 0x0054: /* CCH */ break;
+                case 0x0055: /* MW */ break;
+                case 0x0056: /* SPA */ break;
+                case 0x0057: /* ESA */ break;
+                case 0x0058: esc_SOS(); break;
+                case 0x005A: /* SCI */ break;
+                case 0x005B: esc_CSI(); break;
+                case 0x005C: esc_ST(); break;
+                case 0x005D: esc_OSC(); break;
+                case 0x005E: esc_PM(); break;
+                case 0x005F: esc_APC(); break;
+                case 0x0060: /* DMI */ break;
+                case 0x0061: /* INT */ break;
+                case 0x0062: /* EMI */ break;
+                case 0x0063: esc_RIS(); break;
+                case 0x0064: /* CMD */ break;
+                case 0x006C: /* RM */ break;
+                case 0x006E: /* LS2 */ break;
+                case 0x006F: /* LS3 */ break;
+                case 0x007C: /* LS3R */ break;
+                case 0x007D: /* LS2R */ break;
+                case 0x007E: /* LS1R */ break;
+                case 0x2338: esc_DECALN(); break;
+                case 0x2841: esc_G0_0(); break;
+                case 0x2842: esc_G0_1(); break;
+                case 0x2830: esc_G0_2(); break;
+                case 0x2831: esc_G0_3(); break;
+                case 0x2832: esc_G0_4(); break;
+                case 0x2930: esc_G1_2(); break;
+                case 0x2931: esc_G1_3(); break;
+                case 0x2932: esc_G1_4(); break;
+                case 0x2941: esc_G1_0(); break;
+                case 0x2942: esc_G1_1(); break;
+            }
+            if (vt100_parse_state == State.Esc) {
+                vt100_parse_reset();
+            }
+        } else {
+            switch (vt100_parse_func) {
+                case 0x0040: csi_ICH(vt100_parse_param); break;
+                case 0x0041: csi_CUU(vt100_parse_param); break;
+                case 0x0042: csi_CUD(vt100_parse_param); break;
+                case 0x0043: csi_CUF(vt100_parse_param); break;
+                case 0x0044: csi_CUB(vt100_parse_param); break;
+                case 0x0045: csi_CNL(vt100_parse_param); break;
+                case 0x0046: csi_CPL(vt100_parse_param); break;
+                case 0x0047: csi_CHA(vt100_parse_param); break;
+                case 0x0048: csi_CUP(vt100_parse_param); break;
+                case 0x0049: csi_CHT(vt100_parse_param); break;
+                case 0x004A: csi_ED(vt100_parse_param); break;
+                case 0x004B: csi_EL(vt100_parse_param); break;
+                case 0x004C: csi_IL(vt100_parse_param); break;
+                case 0x004D: csi_DL(vt100_parse_param); break;
+                case 0x004E: /* EF */ break;
+                case 0x004F: /* EA */ break;
+                case 0x0050: csi_DCH(vt100_parse_param); break;
+                case 0x0051: /* SEE */ break;
+                case 0x0052: /* CPR */ break;
+                case 0x0053: csi_SU(vt100_parse_param); break;
+                case 0x0054: csi_SD(vt100_parse_param); break;
+                case 0x0055: /* NP */ break;
+                case 0x0056: /* PP */ break;
+                case 0x0057: csi_CTC(vt100_parse_param); break;
+                case 0x0058: csi_ECH(vt100_parse_param); break;
+                case 0x0059: /* CVT */ break;
+                case 0x005A: csi_CBT(vt100_parse_param); break;
+                case 0x005B: /* SRS */ break;
+                case 0x005C: /* PTX */ break;
+                case 0x005D: /* SDS */ break;
+                case 0x005E: /* SIMD */ break;
+                case 0x0060: csi_HPA(vt100_parse_param); break;
+                case 0x0061: csi_HPR(vt100_parse_param); break;
+                case 0x0062: csi_REP(vt100_parse_param); break;
+                case 0x0063: csi_DA(vt100_parse_param); break;
+                case 0x0064: csi_VPA(vt100_parse_param); break;
+                case 0x0065: csi_VPR(vt100_parse_param); break;
+                case 0x0066: csi_HVP(vt100_parse_param); break;
+                case 0x0067: csi_TBC(vt100_parse_param); break;
+                case 0x0068: csi_SM(vt100_parse_param); break;
+                case 0x0069: /* MC */ break;
+                case 0x006A: /* HPB */ break;
+                case 0x006B: /* VPB */ break;
+                case 0x006C: csi_RM(vt100_parse_param); break;
+                case 0x006D: csi_SGR(vt100_parse_param); break;
+                case 0x006E: csi_DSR(vt100_parse_param); break;
+                case 0x006F: /* DAQ */ break;
+                case 0x0072: csi_DECSTBM(vt100_parse_param); break;
+                case 0x0073: csi_SCP(vt100_parse_param); break;
+                case 0x0075: csi_RCP(vt100_parse_param); break;
+                case 0x0078: csi_DECREQTPARM(vt100_parse_param); break;
+                case 0x2040: /* SL */ break;
+                case 0x2041: /* SR */ break;
+                case 0x2042: /* GSM */ break;
+                case 0x2043: /* GSS */ break;
+                case 0x2044: /* FNT */ break;
+                case 0x2045: /* TSS */ break;
+                case 0x2046: /* JFY */ break;
+                case 0x2047: /* SPI */ break;
+                case 0x2048: /* QUAD */ break;
+                case 0x2049: /* SSU */ break;
+                case 0x204A: /* PFS */ break;
+                case 0x204B: /* SHS */ break;
+                case 0x204C: /* SVS */ break;
+                case 0x204D: /* IGS */ break;
+                case 0x204E: /* deprecated: HTSA */ break;
+                case 0x204F: /* IDCS */ break;
+                case 0x2050: /* PPA */ break;
+                case 0x2051: /* PPR */ break;
+                case 0x2052: /* PPB */ break;
+                case 0x2053: /* SPD */ break;
+                case 0x2054: /* DTA */ break;
+                case 0x2055: /* SLH */ break;
+                case 0x2056: /* SLL */ break;
+                case 0x2057: /* FNK */ break;
+                case 0x2058: /* SPQR */ break;
+                case 0x2059: /* SEF */ break;
+                case 0x205A: /* PEC */ break;
+                case 0x205B: /* SSW */ break;
+                case 0x205C: /* SACS */ break;
+                case 0x205D: /* SAPV */ break;
+                case 0x205E: /* STAB */ break;
+                case 0x205F: /* GCC */ break;
+                case 0x2060: /* TAPE */ break;
+                case 0x2061: /* TALE */ break;
+                case 0x2062: /* TAC */ break;
+                case 0x2063: /* TCC */ break;
+                case 0x2064: /* TSR */ break;
+                case 0x2065: /* SCO */ break;
+                case 0x2066: /* SRCS */ break;
+                case 0x2067: /* SCS */ break;
+                case 0x2068: /* SLS */ break;
+                case 0x2069: /* SPH */ break;
+                case 0x206A: /* SPL */ break;
+                case 0x206B: /* SCP */ break;
+                case 0x2170: csi_DECSTR(vt100_parse_param); break;
+                case 0x2472: /* DECCARA */ break;
+                case 0x2477: /* DECRQPSR */ break;
+            }
+            if (vt100_parse_state == State.Csi) {
+                vt100_parse_reset();
+            }
+        }
+    }
+
+    private boolean vt100_write(int c) {
+        if (c < 32) {
+            if (c == 27) {
+                vt100_parse_reset(State.Esc);
+                return true;
+            } else if (c == 14) {
+                ctrl_SO();
+            } else if (c == 15) {
+                ctrl_SI();
+            }
+        } else if ((c & 0xffe0) == 0x0080) {
+            vt100_parse_reset(State.Esc);
+            vt100_parse_func = (char)(c - 0x0040);
+            vt100_parse_process();
+            return true;
+        }
+        if (vt100_parse_state != State.None) {
+            if (vt100_parse_state == State.Str) {
+                if (c >= 32) {
+                    return true;
+                }
+                vt100_parse_reset();
+            } else {
+                if (c < 32) {
+                    if (c == 24 || c == 26) {
+                        vt100_parse_reset();
+                        return true;
+                    }
+                } else {
+                    vt100_parse_len += 1;
+                    if (vt100_parse_len > 32) {
+                        vt100_parse_reset();
+                    } else {
+                        int msb = c & 0xf0;
+                        if (msb == 0x20) {
+                            vt100_parse_func <<= 8;
+                            vt100_parse_func += (char) c;
+                        } else if (msb == 0x30 && vt100_parse_state == State.Csi) {
+                            vt100_parse_param += new String(new char[] { (char) c } );
+                        } else {
+                            vt100_parse_func <<= 8;
+                            vt100_parse_func += (char) c;
+                            vt100_parse_process();
+                        }
+                        return true;
+                    }
+                }
+            }
+        }
+        vt100_lastchar = c;
+        return false;
+    }
+
+    //
+    // Dirty
+    //
+
+    private synchronized void setDirty() {
+        dirty.set(true);
+        notifyAll();
+    }
+
+    //
+    // External interface
+    //
+
+    public synchronized boolean setSize(int w, int h) {
+        if (w < 2 || w > 256 || h < 2 || h > 256) {
+            return false;
+        }
+        this.width = w;
+        this.height = h;
+        reset_screen();
+        return true;
+    }
+
+    public synchronized String read() {
+        String d = vt100_out;
+        vt100_out = "";
+        return d;
+    }
+
+    public synchronized String pipe(String d) {
+        String o = "";
+        for (char c : d.toCharArray()) {
+            if (vt100_keyfilter_escape) {
+                vt100_keyfilter_escape = false;
+                if (vt100_mode_cursorkey) {
+                    switch (c) {
+                        case '~': o += "~"; break;
+                        case 'A': o += "\u001bOA"; break;
+                        case 'B': o += "\u001bOB"; break;
+                        case 'C': o += "\u001bOC"; break;
+                        case 'D': o += "\u001bOD"; break;
+                        case 'F': o += "\u001bOF"; break;
+                        case 'H': o += "\u001bOH"; break;
+                        case '1': o += "\u001b[5~"; break;
+                        case '2': o += "\u001b[6~"; break;
+                        case '3': o += "\u001b[2~"; break;
+                        case '4': o += "\u001b[3~"; break;
+                        case 'a': o += "\u001bOP"; break;
+                        case 'b': o += "\u001bOQ"; break;
+                        case 'c': o += "\u001bOR"; break;
+                        case 'd': o += "\u001bOS"; break;
+                        case 'e': o += "\u001b[15~"; break;
+                        case 'f': o += "\u001b[17~"; break;
+                        case 'g': o += "\u001b[18~"; break;
+                        case 'h': o += "\u001b[19~"; break;
+                        case 'i': o += "\u001b[20~"; break;
+                        case 'j': o += "\u001b[21~"; break;
+                        case 'k': o += "\u001b[23~"; break;
+                        case 'l': o += "\u001b[24~"; break;
+                    }
+                } else {
+                    switch (c) {
+                        case '~': o += "~"; break;
+                        case 'A': o += "\u001b[A"; break;
+                        case 'B': o += "\u001b[B"; break;
+                        case 'C': o += "\u001b[C"; break;
+                        case 'D': o += "\u001b[D"; break;
+                        case 'F': o += "\u001b[F"; break;
+                        case 'H': o += "\u001b[H"; break;
+                        case '1': o += "\u001b[5~"; break;
+                        case '2': o += "\u001b[6~"; break;
+                        case '3': o += "\u001b[2~"; break;
+                        case '4': o += "\u001b[3~"; break;
+                        case 'a': o += "\u001bOP"; break;
+                        case 'b': o += "\u001bOQ"; break;
+                        case 'c': o += "\u001bOR"; break;
+                        case 'd': o += "\u001bOS"; break;
+                        case 'e': o += "\u001b[15~"; break;
+                        case 'f': o += "\u001b[17~"; break;
+                        case 'g': o += "\u001b[18~"; break;
+                        case 'h': o += "\u001b[19~"; break;
+                        case 'i': o += "\u001b[20~"; break;
+                        case 'j': o += "\u001b[21~"; break;
+                        case 'k': o += "\u001b[23~"; break;
+                        case 'l': o += "\u001b[24~"; break;
+                    }
+                }
+            } else if (c == '~') {
+                vt100_keyfilter_escape = true;
+            } else if (c == 127) {
+                if (vt100_mode_backspace) {
+                    o += (char) 8;
+                } else {
+                    o += (char) 127;
+                }
+            } else {
+                o += c;
+                if (vt100_mode_lfnewline && c == 13) {
+                    o += (char) 10;
+                }
+            }
+        }
+        return o;
+    }
+
+    public synchronized boolean write(String d) {
+        d = utf8_decode(d);
+        for (int c : d.toCharArray()) {
+            if (vt100_write(c)) {
+                continue;
+            }
+            if (dumb_write(c)) {
+                continue;
+            }
+            if (c <= 0xffff) {
+                dumb_echo(c);
+            }
+        }
+        return true;
+    }
+
+    public synchronized String dump(long timeout, boolean forceDump) throws InterruptedException {
+        if (!dirty.get() && timeout > 0) {
+            wait(timeout);
+        }
+        if (dirty.compareAndSet(true, false) || forceDump) {
+            StringBuilder sb = new StringBuilder();
+            int prev_attr = -1;
+            int cx = Math.min(this.cx, width - 1);
+            int cy = this.cy;
+            sb.append("<div><pre class='term'>");
+            for (int y = 0; y < height; y++) {
+                int wx = 0;
+                for (int x = 0; x < width; x++) {
+                    int d = screen[y * width + x];
+                    int c = d & 0xffff;
+                    int a = d >> 16;
+                    if (cy == y && cx == x && vt100_mode_cursor) {
+                        a = a & 0xfff0 | 0x000c;
+                    }
+                    if (a != prev_attr) {
+                        if (prev_attr != -1) {
+                            sb.append("</span>");
+                        }
+                        int bg = a & 0x000f;
+                        int fg = (a & 0x00f0) >> 4;
+                        boolean inv = (a & 0x0200) != 0;
+                        boolean inv2 = vt100_mode_inverse;
+                        if (inv && !inv2 || inv2 && !inv) {
+                            int i = fg; fg = bg; bg = i;
+                        }
+                        if ((a & 0x0400) != 0) {
+                            fg = 0x0c;
+                        }
+                        String ul;
+                        if ((a & 0x0100) != 0) {
+                            ul = " ul";
+                        } else {
+                            ul = "";
+                        }
+                        String b;
+                        if ((a & 0x0800) != 0) {
+                            b = " b";
+                        } else {
+                            b = "";
+                        }
+                        sb.append("<span class='f").append(fg).append(" b").append(bg).append(ul).append(b).append("'>");
+                        prev_attr = a;
+                    }
+                    switch (c) {
+                        case '&': sb.append("&amp;"); break;
+                        case '<': sb.append("&lt;"); break;
+                        case '>': sb.append("&gt;"); break;
+                        default:
+                            wx += utf8_charwidth(c);
+                            if (wx <= width) {
+                                sb.append((char) c);
+                            }
+                            break;
+                    }
+                }
+                sb.append("\n");
+            }
+            sb.append("</span></pre></div>");
+            return sb.toString();
+        }
+        return null;
+    }
+
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        for (int y = 0; y < height; y++) {
+            for (int x = 0; x < width; x++) {
+                sb.append((char) (screen[y * width + x] & 0xffff));
+            }
+            sb.append("\n");
+        }
+        return sb.toString();
+    }
+}
diff --git a/karaf/webconsole/gogo/src/main/java/org/apache/felix/karaf/webconsole/gogo/WebTerminal.java b/karaf/webconsole/gogo/src/main/java/org/apache/felix/karaf/webconsole/gogo/WebTerminal.java
new file mode 100644
index 0000000..550cb03
--- /dev/null
+++ b/karaf/webconsole/gogo/src/main/java/org/apache/felix/karaf/webconsole/gogo/WebTerminal.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.felix.karaf.webconsole.gogo;
+
+import java.io.InputStreamReader;
+import java.io.InputStream;
+import java.io.IOException;
+
+public class WebTerminal extends jline.Terminal {
+
+    public static final short ARROW_START = 27;
+    public static final short ARROW_PREFIX = 91;
+    public static final short ARROW_LEFT = 68;
+    public static final short ARROW_RIGHT = 67;
+    public static final short ARROW_UP = 65;
+    public static final short ARROW_DOWN = 66;
+    public static final short O_PREFIX = 79;
+    public static final short HOME_CODE = 72;
+    public static final short END_CODE = 70;
+
+    public static final short DEL_THIRD = 51;
+    public static final short DEL_SECOND = 126;
+
+    private int width;
+    private int height;
+    private boolean backspaceDeleteSwitched = false;
+    private String encoding = System.getProperty("input.encoding", "UTF-8");
+    private ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(encoding);
+    private InputStreamReader replayReader;
+
+    public WebTerminal(int width, int height) {
+        this.width = width;
+        this.height = height;
+        try {
+            replayReader = new InputStreamReader(replayStream, encoding);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void initializeTerminal() throws Exception {
+    }
+
+    public void restoreTerminal() throws Exception {
+    }
+
+    public int getTerminalWidth() {
+        return width;
+    }
+
+    public int getTerminalHeight() {
+        return height;
+    }
+
+    public boolean isSupported() {
+        return true;
+    }
+
+    public boolean getEcho() {
+        return false;
+    }
+
+    public boolean isEchoEnabled() {
+        return false;
+    }
+
+    public void enableEcho() {
+    }
+
+    public void disableEcho() {
+    }
+
+    public int readVirtualKey(InputStream in) throws IOException {
+        int c = readCharacter(in);
+
+        if (backspaceDeleteSwitched) {
+            if (c == DELETE) {
+                c = '\b';
+            } else if (c == '\b') {
+                c = DELETE;
+            }
+        }
+
+        // in Unix terminals, arrow keys are represented by
+        // a sequence of 3 characters. E.g., the up arrow
+        // key yields 27, 91, 68
+        if (c == ARROW_START) {
+            //also the escape key is 27
+            //thats why we read until we
+            //have something different than 27
+            //this is a bugfix, because otherwise
+            //pressing escape and than an arrow key
+            //was an undefined state
+            while (c == ARROW_START) {
+                c = readCharacter(in);
+            }
+            if (c == ARROW_PREFIX || c == O_PREFIX) {
+                c = readCharacter(in);
+                if (c == ARROW_UP) {
+                    return CTRL_P;
+                } else if (c == ARROW_DOWN) {
+                    return CTRL_N;
+                } else if (c == ARROW_LEFT) {
+                    return CTRL_B;
+                } else if (c == ARROW_RIGHT) {
+                    return CTRL_F;
+                } else if (c == HOME_CODE) {
+                    return CTRL_A;
+                } else if (c == END_CODE) {
+                    return CTRL_E;
+                } else if (c == DEL_THIRD) {
+                    c = readCharacter(in); // read 4th
+                    return DELETE;
+                }
+            }
+        }
+        // handle unicode characters, thanks for a patch from amyi@inf.ed.ac.uk
+        if (c > 128) {
+          // handle unicode characters longer than 2 bytes,
+          // thanks to Marc.Herbert@continuent.com
+            replayStream.setInput(c, in);
+            c = replayReader.read();
+        }
+
+        return c;
+    }
+
+    /**
+     * This is awkward and inefficient, but probably the minimal way to add
+     * UTF-8 support to JLine
+     *
+     * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a>
+     */
+    static class ReplayPrefixOneCharInputStream extends InputStream {
+
+        byte firstByte;
+        int byteLength;
+        InputStream wrappedStream;
+        int byteRead;
+
+        final String encoding;
+
+        public ReplayPrefixOneCharInputStream(String encoding) {
+            this.encoding = encoding;
+        }
+
+        public void setInput(int recorded, InputStream wrapped) throws IOException {
+            this.byteRead = 0;
+            this.firstByte = (byte) recorded;
+            this.wrappedStream = wrapped;
+
+            byteLength = 1;
+            if (encoding.equalsIgnoreCase("UTF-8")) {
+                setInputUTF8(recorded, wrapped);
+            } else if (encoding.equalsIgnoreCase("UTF-16")) {
+                byteLength = 2;
+            } else if (encoding.equalsIgnoreCase("UTF-32")) {
+                byteLength = 4;
+            }
+        }
+
+
+        public void setInputUTF8(int recorded, InputStream wrapped) throws IOException {
+            // 110yyyyy 10zzzzzz
+            if ((firstByte & (byte) 0xE0) == (byte) 0xC0) {
+                this.byteLength = 2;
+            // 1110xxxx 10yyyyyy 10zzzzzz
+            } else if ((firstByte & (byte) 0xF0) == (byte) 0xE0) {
+                this.byteLength = 3;
+            // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
+            } else if ((firstByte & (byte) 0xF8) == (byte) 0xF0) {
+                this.byteLength = 4;
+            } else {
+                throw new IOException("invalid UTF-8 first byte: " + firstByte);
+            }
+        }
+
+        public int read() throws IOException {
+            if (available() == 0) {
+                return -1;
+            }
+
+            byteRead++;
+
+            if (byteRead == 1) {
+                return firstByte;
+            }
+
+            return wrappedStream.read();
+        }
+
+        /**
+        * InputStreamReader is greedy and will try to read bytes in advance. We
+        * do NOT want this to happen since we use a temporary/"losing bytes"
+        * InputStreamReader above, that's why we hide the real
+        * wrappedStream.available() here.
+        */
+        public int available() {
+            return byteLength - byteRead;
+        }
+    }
+
+}
diff --git a/karaf/webconsole/gogo/src/main/resources/OSGI-INF/blueprint/webconsole-gogo.xml b/karaf/webconsole/gogo/src/main/resources/OSGI-INF/blueprint/webconsole-gogo.xml
new file mode 100644
index 0000000..c792c0e
--- /dev/null
+++ b/karaf/webconsole/gogo/src/main/resources/OSGI-INF/blueprint/webconsole-gogo.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+           xmlns:cm="http://www.osgi.org/xmlns/blueprint-cm/v1.0.0">
+
+    <reference id="commandProcessor" interface="org.osgi.service.command.CommandProcessor" />
+
+    <bean id="gogoPlugin" class="org.apache.felix.karaf.webconsole.gogo.GogoPlugin" init-method="start" destroy-method="stop">
+        <property name="completers">
+            <list>
+                <ref component-id="commandCompleter"/>
+            </list>
+        </property>
+        <property name="commandProcessor" ref="commandProcessor" />
+        <property name="bundleContext" ref="blueprintBundleContext" />
+    </bean>
+
+    <reference-list id="functions" filter="(&amp;(osgi.command.scope=*)(osgi.command.function=*))" availability="optional">
+        <reference-listener ref="commandCompleter"
+                            bind-method="register"
+                            unbind-method="unregister"/>
+    </reference-list>
+
+    <bean id="commandCompleter" class="org.apache.felix.karaf.gshell.console.completer.CommandsCompleter">
+        <property name="bundleContext" ref="blueprintBundleContext" />
+    </bean>
+
+    <service ref="gogoPlugin" interface="javax.servlet.Servlet" >
+        <service-properties>
+            <entry key="felix.webconsole.label" value="gogo"/>
+        </service-properties>
+    </service>
+
+</blueprint>
diff --git a/karaf/webconsole/gogo/src/main/resources/res/ui/gogo.css b/karaf/webconsole/gogo/src/main/resources/res/ui/gogo.css
new file mode 100644
index 0000000..0e806a1
--- /dev/null
+++ b/karaf/webconsole/gogo/src/main/resources/res/ui/gogo.css
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+ * Based on http://antony.lesuisse.org/software/ajaxterm/
+ *  Public Domain License
+ */
+
+body {
+    background-color: #888;
+}
+
+div#console {
+    font-size: 12px;
+    margin: 12px;
+}
+
+div#term {
+    display: inline-block;
+}
+
+pre.stat {
+	margin: 0px;
+	padding: 4px;
+	display: block;
+	font-family: monospace;
+	white-space: pre;
+	background-color: black;
+	border-top: 1px solid black;
+	color: white;
+}
+pre.stat span {
+	padding: 0px;
+}
+pre.stat .on {
+	background-color: #080;
+	font-weight: bold;
+	color: white;
+	cursor: pointer;
+}
+pre.stat .off {
+	background-color: #888;
+	font-weight: bold;
+	color: white;
+	cursor: pointer;
+}
+pre.term {
+	margin: 0px;
+	padding: 4px;
+	display: block;
+	font-family: monospace;
+	white-space: pre;
+    background:#000;
+	border-top: 1px solid white;
+	color: #eee;
+}
+pre.term span.f0  { color: #000000; }
+pre.term span.f1  { color: #c00006; }
+pre.term span.f2  { color: #1bc806; }
+pre.term span.f3  { color: #c3c609; }
+pre.term span.f4  { color: #0000c2; }
+pre.term span.f5  { color: #bf00c2; }
+pre.term span.f6  { color: #19c4c2; }
+pre.term span.f7  { color: #f2f2f2; }
+pre.term span.f12 { color: transparent; }
+pre.term span.f14 { color: #000000; }
+pre.term span.f15 { color: #bbbbbb; }
+pre.term span.b0  { background-color: #000000; }
+pre.term span.b1  { background-color: #cc2300; }
+pre.term span.b2  { background-color: #00cc00; }
+pre.term span.b3  { background-color: #cccc00; }
+pre.term span.b4  { background-color: #0e2acc; }
+pre.term span.b5  { background-color: #cc34cc; }
+pre.term span.b6  { background-color: #00cccc; }
+pre.term span.b7  { background-color: #f5f5f5; }
+pre.term span.b12 { background-color: #555555; }
+pre.term span.b14 { background-color: transparent; }
+pre.term span.b15 { background-color: #ffffff; }
+pre.term span.ul  { text-decoration: underline; }
+pre.term span.b   { font-weight:900; }
+
diff --git a/karaf/webconsole/gogo/src/main/resources/res/ui/gogo.js b/karaf/webconsole/gogo/src/main/resources/res/ui/gogo.js
new file mode 100644
index 0000000..771efe0
--- /dev/null
+++ b/karaf/webconsole/gogo/src/main/resources/res/ui/gogo.js
@@ -0,0 +1,246 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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.
+//
+
+//
+// Based on http://antony.lesuisse.org/software/ajaxterm/
+//  Public Domain License
+//
+
+gogo = { };
+
+gogo.Terminal_ctor = function(div, width, height) {
+
+   var ie = (window.ActiveXObject) ? 0 : 1;
+   var query0 = "w=" + width + "&h=" + height;
+   var query1 = query0 + "&k=";
+   var buf = "";
+   var timeout;
+   var error_timeout;
+   var keybuf = [];
+   var sending = 0;
+   var rmax = 1;
+   var force = 1;
+
+   var dstat = document.createElement('pre');
+   var sled = document.createElement('span');
+   var sdebug = document.createElement('span');
+   var dterm = document.createElement('div');
+
+   function debug(s) {
+       sdebug.innerHTML = s;
+   }
+
+   function error() {
+       sled.className = 'off';
+       debug("Connection lost timeout ts:" + ((new Date).getTime()));
+   }
+
+   function update() {
+       if (sending == 0) {
+           sending = 1;
+           sled.className = 'on';
+           var r = new XMLHttpRequest();
+           var send = "";
+           while (keybuf.length > 0) {
+               send += keybuf.pop();
+           }
+           var query = query1 + send;
+           if (force) {
+               query = query + "&f=1";
+               force = 0;
+           }
+           r.open("POST", "gogo", true);
+           r.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+           r.onreadystatechange = function () {
+               if (r.readyState == 4) {
+                   if (r.status == 200) {
+                       window.clearTimeout(error_timeout);
+                       if (r.responseText.length > 0) {
+                           dterm.innerHTML = r.responseText;
+                           rmax = 100;
+                       } else {
+                           rmax *= 2;
+                           if (rmax > 2000)
+                               rmax = 2000;
+                       }
+                       sending=0;
+                       sled.className = 'off';
+                       timeout = window.setTimeout(update, rmax);
+                   } else {
+                       debug("Connection error status:" + r.status);
+                   }
+               }
+           }
+           error_timeout = window.setTimeout(error, 5000);
+           r.send(query);
+       }
+   }
+
+   function queue(s) {
+       keybuf.unshift(s);
+       if (sending == 0) {
+           window.clearTimeout(timeout);
+           timeout = window.setTimeout(update, 1);
+       }
+   }
+
+   function keypress(ev) {
+        // Translate to standard keycodes
+        if (!ev)
+            ev = window.event;
+        var kc;
+        if (ev.keyCode)
+            kc = ev.keyCode;
+        if (ev.which)
+            kc = ev.which;
+        if (ev.ctrlKey) {
+            if (kc >= 0 && kc <= 32)
+                kc = kc;
+            else if (kc >= 65 && kc <= 90)
+                kc -= 64;
+            else if (kc >= 97 && kc <= 122)
+                kc -= 96;
+            else {
+                switch (kc) {
+                    case 54:  kc=30; break;	// Ctrl-^
+                    case 109: kc=31; break;	// Ctrl-_
+                    case 219: kc=27; break;	// Ctrl-[
+                    case 220: kc=28; break;	// Ctrl-\
+                    case 221: kc=29; break;	// Ctrl-]
+                    default: return true;
+                }
+            }
+        } else if (ev.which == 0) {
+            switch(kc) {
+                case 8: break;			     // Backspace
+                case 9: break;               // Tab
+                case 27: break;			     // ESC
+                case 33:  kc = 63276; break; // PgUp
+                case 34:  kc = 63277; break; // PgDn
+                case 35:  kc = 63275; break; // End
+                case 36:  kc = 63273; break; // Home
+                case 37:  kc = 63234; break; // Left
+                case 38:  kc = 63232; break; // Up
+                case 39:  kc = 63235; break; // Right
+                case 40:  kc = 63233; break; // Down
+                case 45:  kc = 63302; break; // Ins
+                case 46:  kc = 63272; break; // Del
+                case 112: kc = 63236; break; // F1
+                case 113: kc = 63237; break; // F2
+                case 114: kc = 63238; break; // F3
+                case 115: kc = 63239; break; // F4
+                case 116: kc = 63240; break; // F5
+                case 117: kc = 63241; break; // F6
+                case 118: kc = 63242; break; // F7
+                case 119: kc = 63243; break; // F8
+                case 120: kc = 63244; break; // F9
+                case 121: kc = 63245; break; // F10
+                case 122: kc = 63246; break; // F11
+                case 123: kc = 63247; break; // F12
+                default: return true;
+            }
+        }
+        if (kc == 8)
+            kc = 127;
+
+        var k = "";
+        // Build character
+        switch (kc) {
+            case 126:   k = "~~"; break;
+            case 63232: k = "~A"; break; // Up
+            case 63233: k = "~B"; break; // Down
+            case 63234: k = "~D"; break; // Left
+            case 63235: k = "~C"; break; // Right
+            case 63276: k = "~1"; break; // PgUp
+            case 63277: k = "~2"; break; // PgDn
+            case 63273: k = "~H"; break; // Home
+            case 63275: k = "~F"; break; // End
+            case 63302: k = "~3"; break; // Ins
+            case 63272: k = "~4"; break; // Del
+            case 63236: k = "~a"; break; // F1
+            case 63237: k = "~b"; break; // F2
+            case 63238: k = "~c"; break; // F3
+            case 63239: k = "~d"; break; // F4
+            case 63240: k = "~e"; break; // F5
+            case 63241: k = "~f"; break; // F6
+            case 63242: k = "~g"; break; // F7
+            case 63243: k = "~h"; break; // F8
+            case 63244: k = "~i"; break; // F9
+            case 63245: k = "~j"; break; // F10
+            case 63246: k = "~k"; break; // F11
+            case 63247: k = "~l"; break; // F12
+            default:    k = String.fromCharCode(kc); break;
+        }
+        queue(encodeURIComponent(k));
+
+        ev.cancelBubble = true;
+        if (ev.stopPropagation) ev.stopPropagation();
+        if (ev.preventDefault) ev.preventDefault();
+
+        return true;
+   }
+
+   function keydown(ev) {
+       if (!ev)
+          ev = window.event;
+       if (ie) {
+           o = { 9:1, 8:1, 27:1, 33:1, 34:1, 35:1, 36:1, 37:1, 38:1, 39:1, 40:1, 45:1, 46:1, 112:1,
+                 113:1, 114:1, 115:1, 116:1, 117:1, 118:1, 119:1, 120:1, 121:1, 122:1, 123:1 };
+           if (o[ev.keyCode] || ev.ctrlKey || ev.altKey) {
+               ev.which = 0;
+               return keypress(ev);
+           }
+       }
+   }
+
+   function init() {
+       if (typeof(XMLHttpRequest) == "undefined") {
+         XMLHttpRequest = function() {
+           try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); }
+             catch(e) {}
+           try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); }
+             catch(e) {}
+           try { return new ActiveXObject("Msxml2.XMLHTTP"); }
+             catch(e) {}
+           try { return new ActiveXObject("Microsoft.XMLHTTP"); }
+             catch(e) {}
+           throw new Error("This browser does not support XMLHttpRequest.");
+         };
+       }
+       sled.appendChild(document.createTextNode('\xb7'));
+       sled.className = 'off';
+       dstat.appendChild(sled);
+       dstat.appendChild(document.createTextNode(' '));
+       dstat.appendChild(sdebug);
+       dstat.className = 'stat';
+       div.appendChild(dstat);
+       var d = document.createElement('div');
+       d.appendChild(dterm);
+       div.appendChild(d);
+       document.onkeypress = keypress;
+       document.onkeydown = keydown;
+       timeout = window.setTimeout(update, 100);
+   }
+
+   init();
+
+}
+
+gogo.Terminal = function(div, width, height) {
+   return new this.Terminal_ctor(div, width, height);
+}
+
diff --git a/karaf/webconsole/pom.xml b/karaf/webconsole/pom.xml
index 225c074..24c116f 100644
--- a/karaf/webconsole/pom.xml
+++ b/karaf/webconsole/pom.xml
@@ -35,7 +35,8 @@
   <name>Apache Felix Karaf :: Web Console</name>
   
   <modules>
-    <module>plugins</module>
+    <module>features</module>
+    <module>gogo</module>
     <module>branding</module>
   </modules>