FELIX-2625 First shot at creating a Gogo Shell Plugin for the Web Console

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1238456 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole-plugins/gogo/src/main/appended-resources/META-INF/DEPENDENCIES b/webconsole-plugins/gogo/src/main/appended-resources/META-INF/DEPENDENCIES
new file mode 100644
index 0000000..1ebfb84
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/appended-resources/META-INF/DEPENDENCIES
@@ -0,0 +1,23 @@
+
+I. Included Software
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+Licensed under the Apache License 2.0.
+
+This product includes software from https://github.com/jline/jline2
+Copyright (c) 2002-2006, Marc Prud'hommeaux <mwp1@cornell.edu>
+All rights reserved.
+Licensed under the BSD License
+
+
+II. Used Software
+
+This product uses software developed at
+The OSGi Alliance (http://www.osgi.org/).
+Copyright (c) OSGi Alliance (2000, 2009).
+Licensed under the Apache License 2.0.
+
+
+III. License Summary
+- Apache License 2.0
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Activator.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Activator.java
new file mode 100644
index 0000000..b8de725
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Activator.java
@@ -0,0 +1,35 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+import org.fusesource.jansi.AnsiConsole;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+public class Activator implements BundleActivator {
+
+    public void start(BundleContext context) throws Exception {
+        AnsiConsole.systemInstall();
+    }
+
+    public void stop(BundleContext context) throws Exception {
+        AnsiConsole.systemUninstall();
+    }
+
+}
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/AggregateCompleter.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/AggregateCompleter.java
new file mode 100644
index 0000000..b8ee42e
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/AggregateCompleter.java
@@ -0,0 +1,89 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Collection;
+
+import jline.console.completer.Completer;
+
+/**
+ * Completer which contains multipule completers and aggregates them together.
+ */
+public class AggregateCompleter implements Completer
+{
+    private final Collection<Completer> completers;
+
+    public AggregateCompleter(final Collection<Completer> completers) {
+        assert completers != null;
+        this.completers = completers;
+    }
+
+    public int complete(final String buffer, final int cursor, final List candidates) {
+        // buffer could be null
+        assert candidates != null;
+
+        List<Completion> completions = new ArrayList<Completion>(completers.size());
+
+        // Run each completer, saving its completion results
+        int max = -1;
+        for (Completer completer : completers) {
+            Completion completion = new Completion(candidates);
+            completion.complete(completer, buffer, cursor);
+
+            // Compute the max cursor position
+            max = Math.max(max, completion.cursor);
+
+            completions.add(completion);
+        }
+
+        // Append candiates from completions which have the same cursor position as max
+        for (Completion completion : completions) {
+            if (completion.cursor == max) {
+                // noinspection unchecked
+                candidates.addAll(completion.candidates);
+            }
+        }
+
+        return max;
+    }
+
+    private class Completion
+    {
+        public final List<CharSequence> candidates;
+
+        public int cursor;
+
+        public Completion(final List candidates) {
+            assert candidates != null;
+
+            // noinspection unchecked
+            this.candidates = new LinkedList<CharSequence>(candidates);
+        }
+
+        public void complete(final Completer completer, final String buffer, final int cursor) {
+            assert completer != null;
+
+            this.cursor = completer.complete(buffer, cursor, candidates);
+        }
+    }
+}
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/CloseShellException.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/CloseShellException.java
new file mode 100644
index 0000000..f3540a2
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/CloseShellException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+
+@SuppressWarnings("serial")
+public class CloseShellException extends Exception {
+
+}
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/CommandSessionHolder.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/CommandSessionHolder.java
new file mode 100644
index 0000000..0c04196
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/CommandSessionHolder.java
@@ -0,0 +1,38 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+import org.apache.felix.service.command.CommandSession;
+
+public class CommandSessionHolder {
+
+    private static final ThreadLocal<CommandSession> session = new ThreadLocal<CommandSession>();
+
+    public static CommandSession getSession() {
+        return session.get();
+    }
+
+    public static void setSession(CommandSession commandSession) {
+        session.set(commandSession);
+    }
+
+    public static void unset() {
+        session.remove();
+    }
+}
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/CommandsCompleter.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/CommandsCompleter.java
new file mode 100644
index 0000000..0a55a35
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/CommandsCompleter.java
@@ -0,0 +1,85 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import jline.console.completer.Completer;
+
+import org.apache.felix.service.command.CommandSession;
+
+public class CommandsCompleter implements Completer {
+
+    private CommandSession session;
+    private final List<Completer> completers = new ArrayList<Completer>();
+    private final Set<String> commands = new HashSet<String>();
+
+    public CommandsCompleter(CommandSession session) {
+        this.session = session;
+    }
+
+
+    public int complete(String buffer, int cursor, List<CharSequence> candidates) {
+        checkData();
+        int res = new AggregateCompleter(completers).complete(buffer, cursor, candidates);
+//        TODO: Collections.sort(candidates);
+        return res;
+    }
+
+    protected synchronized void checkData() {
+        // Copy the set to avoid concurrent modification exceptions
+        // TODO: fix that in gogo instead
+        Set<String> names = new HashSet<String>((Set<String>) session.get(".commands" /* CommandSessionImpl.COMMANDS */));
+        if (!names.equals(commands)) {
+            commands.clear();
+            completers.clear();
+
+            // get command aliases
+            Set<String> aliases = this.getAliases();
+            completers.add(new StringsCompleter(aliases));
+
+            // add argument completers for each command
+            for (String command : names) {
+                commands.add(command);
+            }
+        }
+    }
+
+    /**
+     * Get the aliases defined in the console session.
+     *
+     * @return the aliases set
+     */
+    private Set<String> getAliases() {
+        Set<String> vars = (Set<String>) session.get(null);
+        Set<String> aliases = new HashSet<String>();
+        for (String var : vars) {
+            Object content = session.get(var);
+            if (content != null && content.getClass().getName().equals("org.apache.felix.gogo.runtime.Closure"))  {
+                aliases.add(var);
+            }
+        }
+        return aliases;
+    }
+
+}
+
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Console.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Console.java
new file mode 100644
index 0000000..c9f37e2
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Console.java
@@ -0,0 +1,442 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+import java.io.CharArrayWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import jline.Terminal;
+import jline.UnsupportedTerminal;
+import jline.console.ConsoleReader;
+import jline.console.completer.Completer;
+import jline.console.history.PersistentHistory;
+
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Converter;
+import org.fusesource.jansi.Ansi;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Console implements Runnable {
+
+    public static final String SHELL_INIT_SCRIPT = "karaf.shell.init.script";
+
+    public static final String PROMPT = "PROMPT";
+
+    public static final String DEFAULT_PROMPT = "\u001B[1m${USER}\u001B[0m@${APPLICATION}> ";
+
+    public static final String PRINT_STACK_TRACES = "karaf.printStackTraces";
+
+    public static final String LAST_EXCEPTION = "karaf.lastException";
+
+    public static final String IGNORE_INTERRUPTS = "karaf.ignoreInterrupts";
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Console.class);
+
+    protected CommandSession session;
+
+    private ConsoleReader reader;
+
+    private BlockingQueue<Integer> queue;
+
+    private boolean interrupt;
+
+    private Thread pipe;
+
+    volatile private boolean running;
+
+    volatile private boolean eof;
+
+    private Runnable closeCallback;
+
+    private Terminal terminal;
+
+    private InputStream consoleInput;
+
+    private InputStream in;
+
+    private PrintStream out;
+
+    private PrintStream err;
+
+    private Thread thread;
+
+    public Console(CommandProcessor processor, InputStream in, PrintStream out, PrintStream err, Terminal term,
+            Runnable closeCallback) throws Exception {
+        this.in = in;
+        this.out = out;
+        this.err = err;
+        this.queue = new ArrayBlockingQueue<Integer>(1024);
+        this.terminal = term == null ? new UnsupportedTerminal() : term;
+        this.consoleInput = new ConsoleInputStream();
+        this.session = processor.createSession(this.consoleInput, this.out, this.err);
+        this.session.put("SCOPE", "shell:osgi:*");
+        this.closeCallback = closeCallback;
+
+        reader = new ConsoleReader(this.consoleInput, new PrintWriter(this.out), getClass().getResourceAsStream(
+            "keybinding.properties"), this.terminal);
+
+        session.put(".jline.history", reader.getHistory());
+        Completer completer = createCompleter();
+        if (completer != null) {
+            reader.addCompleter(completer);
+        }
+        if (Boolean.getBoolean("jline.nobell")) {
+            reader.setBellEnabled(false);
+        }
+        pipe = new Thread(new Pipe());
+        pipe.setName("gogo shell pipe thread");
+        pipe.setDaemon(true);
+    }
+
+    public CommandSession getSession() {
+        return session;
+    }
+
+    public void close() {
+        // System.err.println("Closing");
+        if (reader.getHistory() instanceof PersistentHistory) {
+            try {
+                ((PersistentHistory) reader.getHistory()).flush();
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+        running = false;
+        CommandSessionHolder.unset();
+        pipe.interrupt();
+    }
+
+    public void run() {
+        ThreadLocal<CommandSessionHolder> consoleState = new ThreadLocal<CommandSessionHolder>();
+        thread = Thread.currentThread();
+        CommandSessionHolder.setSession(session);
+        running = true;
+        pipe.start();
+        welcome();
+        setSessionProperties();
+        String scriptFileName = System.getProperty(SHELL_INIT_SCRIPT);
+        if (scriptFileName != null) {
+            Reader r = null;
+            try {
+                File scriptFile = new File(scriptFileName);
+                r = new InputStreamReader(new FileInputStream(scriptFile));
+                CharArrayWriter w = new CharArrayWriter();
+                int n;
+                char[] buf = new char[8192];
+                while ((n = r.read(buf)) > 0) {
+                    w.write(buf, 0, n);
+                }
+                session.execute(new String(w.toCharArray()));
+            } catch (Exception e) {
+                LOGGER.debug("Error in initialization script", e);
+                System.err.println("Error in initialization script: " + e.getMessage());
+            } finally {
+                if (r != null) {
+                    try {
+                        r.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+        while (running) {
+            try {
+                String command = null;
+                boolean loop = true;
+                boolean first = true;
+                while (loop) {
+                    checkInterrupt();
+                    String line = reader.readLine(first ? getPrompt() : "> ");
+                    if (line == null) {
+                        break;
+                    }
+                    if (command == null) {
+                        command = line;
+                    } else {
+                        command += " " + line;
+                    }
+                    if (reader.getHistory().size() == 0) {
+                        reader.getHistory().add(command);
+                    } else {
+                        reader.getHistory().replace(command);
+                    }
+                    try {
+                        new Parser(command, 0).program();
+                        loop = false;
+                    } catch (Exception e) {
+                        loop = true;
+                        first = false;
+                    }
+                }
+                if (command == null) {
+                    break;
+                }
+                // session.getConsole().println("Executing: " + line);
+                Object result = session.execute(command);
+                if (result != null) {
+                    session.getConsole().println(session.format(result, Converter.INSPECT));
+                }
+            } catch (InterruptedIOException e) {
+                // System.err.println("^C");
+                // TODO: interrupt current thread
+            } catch (CloseShellException e) {
+                break;
+            } catch (Exception t) {
+                try {
+                    LOGGER.info("Exception caught while executing command", t);
+                    session.put(LAST_EXCEPTION, t);
+                    session.getConsole().print(Ansi.ansi().fg(Ansi.Color.RED).toString());
+                    session.getConsole().println(
+                        "Error executing command: "
+                            + (t.getMessage() != null ? t.getMessage() : t.getClass().getName()));
+                    session.getConsole().print(Ansi.ansi().fg(Ansi.Color.DEFAULT).toString());
+                } catch (Exception ignore) {
+                    // ignore
+                }
+            }
+        }
+        close();
+        // System.err.println("Exiting console...");
+        if (closeCallback != null) {
+            closeCallback.run();
+        }
+    }
+
+    protected boolean getBoolean(String name) {
+        Object s = session.get(name);
+        if (s == null) {
+            s = System.getProperty(name);
+        }
+        if (s == null) {
+            return false;
+        }
+        if (s instanceof Boolean) {
+            return (Boolean) s;
+        }
+        return Boolean.parseBoolean(s.toString());
+    }
+
+    protected void welcome() {
+        Properties props = loadBrandingProperties();
+        String welcome = props.getProperty("welcome");
+        if (welcome != null && welcome.length() > 0) {
+            session.getConsole().println(welcome);
+        }
+    }
+
+    protected void setSessionProperties() {
+        Properties props = loadBrandingProperties();
+        for (Map.Entry<Object, Object> entry : props.entrySet()) {
+            String key = (String) entry.getKey();
+            if (key.startsWith("session.")) {
+                session.put(key.substring("session.".length()), entry.getValue());
+            }
+        }
+    }
+
+    protected Completer createCompleter() {
+        return new CommandsCompleter(session);
+    }
+
+    protected Properties loadBrandingProperties() {
+        Properties props = new Properties();
+        loadProps(props, "org/apache/karaf/shell/console/branding.properties");
+        loadProps(props, "org/apache/karaf/branding/branding.properties");
+        return props;
+    }
+
+    protected void loadProps(Properties props, String resource) {
+        InputStream is = null;
+        try {
+            is = getClass().getClassLoader().getResourceAsStream(resource);
+            if (is != null) {
+                props.load(is);
+            }
+        } catch (IOException e) {
+            // ignore
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }
+        }
+    }
+
+    protected String getPrompt() {
+        try {
+            String prompt;
+            try {
+                Object p = session.get(PROMPT);
+                if (p != null) {
+                    prompt = p.toString();
+                } else {
+                    Properties properties = loadBrandingProperties();
+                    if (properties.getProperty("prompt") != null) {
+                        prompt = properties.getProperty("prompt");
+                        // we put the PROMPT in ConsoleSession to avoid to read
+                        // the properties file each time.
+                        session.put(PROMPT, prompt);
+                    } else {
+                        prompt = DEFAULT_PROMPT;
+                    }
+                }
+            } catch (Throwable t) {
+                prompt = DEFAULT_PROMPT;
+            }
+            Matcher matcher = Pattern.compile("\\$\\{([^}]+)\\}").matcher(prompt);
+            while (matcher.find()) {
+                Object rep = session.get(matcher.group(1));
+                if (rep != null) {
+                    prompt = prompt.replace(matcher.group(0), rep.toString());
+                    matcher.reset(prompt);
+                }
+            }
+            return prompt;
+        } catch (Throwable t) {
+            return "$ ";
+        }
+    }
+
+    private void checkInterrupt() throws IOException {
+        if (Thread.interrupted() || interrupt) {
+            interrupt = false;
+            throw new InterruptedIOException("Keyboard interruption");
+        }
+    }
+
+    private void interrupt() {
+        interrupt = true;
+        thread.interrupt();
+    }
+
+    private class ConsoleInputStream extends InputStream {
+        private int read(boolean wait) throws IOException {
+            if (!running) {
+                return -1;
+            }
+            checkInterrupt();
+            if (eof && queue.isEmpty()) {
+                return -1;
+            }
+            Integer i;
+            if (wait) {
+                try {
+                    i = queue.take();
+                } catch (InterruptedException e) {
+                    throw new InterruptedIOException();
+                }
+                checkInterrupt();
+            } else {
+                i = queue.poll();
+            }
+            if (i == null) {
+                return -1;
+            }
+            return i;
+        }
+
+        @Override
+        public int read() throws IOException {
+            return read(true);
+        }
+
+        @Override
+        public int read(byte b[], int off, int len) throws IOException {
+            if (b == null) {
+                throw new NullPointerException();
+            } else if (off < 0 || len < 0 || len > b.length - off) {
+                throw new IndexOutOfBoundsException();
+            } else if (len == 0) {
+                return 0;
+            }
+
+            int nb = 1;
+            int i = read(true);
+            if (i < 0) {
+                return -1;
+            }
+            b[off++] = (byte) i;
+            while (nb < len) {
+                i = read(false);
+                if (i < 0) {
+                    return nb;
+                }
+                b[off++] = (byte) i;
+                nb++;
+            }
+            return nb;
+        }
+
+        @Override
+        public int available() throws IOException {
+            return queue.size();
+        }
+    }
+
+    private class Pipe implements Runnable {
+        public void run() {
+            try {
+                while (running) {
+                    try {
+                        int c = terminal.readCharacter(in);
+                        if (c == -1) {
+                            return;
+                        } else if (c == 4 && !getBoolean(IGNORE_INTERRUPTS)) {
+                            err.println("^D");
+                        } else if (c == 3 && !getBoolean(IGNORE_INTERRUPTS)) {
+                            err.println("^C");
+                            reader.getCursorBuffer().clear();
+                            interrupt();
+                        }
+                        queue.put(c);
+                    } catch (Throwable t) {
+                        return;
+                    }
+                }
+            } finally {
+                eof = true;
+                try {
+                    queue.put(-1);
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+    }
+
+}
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/GogoPlugin.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/GogoPlugin.java
new file mode 100644
index 0000000..323bf49
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/GogoPlugin.java
@@ -0,0 +1,236 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.zip.GZIPOutputStream;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.webconsole.SimpleWebConsolePlugin;
+import org.apache.felix.webconsole.WebConsoleConstants;
+import org.osgi.framework.BundleContext;
+
+/**
+ * The <code>GogoPlugin</code>
+ */
+@Component
+@Service(Servlet.class)
+public class GogoPlugin extends SimpleWebConsolePlugin {
+
+    /** Pseudo class version ID to keep the IDE quite. */
+    private static final long serialVersionUID = 1L;
+
+    @Property(name=WebConsoleConstants.PLUGIN_LABEL)
+    public static final String LABEL = "gogo";
+
+    @Property(name=WebConsoleConstants.PLUGIN_TITLE)
+    public static final String TITLE = "Gogo";
+
+    public static final int TERM_WIDTH = 120;
+    public static final int TERM_HEIGHT = 39;
+
+    @Reference
+    private CommandProcessor commandProcessor;
+
+    public GogoPlugin() {
+        super(LABEL, TITLE, null);
+    }
+
+    @Override
+    @Activate
+    public void activate(BundleContext bundleContext) {
+        super.activate(bundleContext);
+    }
+
+    @Override
+    @Deactivate
+    public void deactivate() {
+        super.deactivate();
+    }
+
+    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>" );
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        String encoding = request.getHeader("Accept-Encoding");
+        boolean supportsGzip = (encoding != null && encoding.toLowerCase().indexOf("gzip") > -1);
+        SessionTerminal st = getSessionTerminal(request);
+        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());
+            }
+        }
+    }
+
+    private SessionTerminal getSessionTerminal(final HttpServletRequest request) throws IOException{
+        final Object terminal = request.getSession(true).getAttribute("terminal");
+        if (terminal instanceof SessionTerminal) {
+            final SessionTerminal st = (SessionTerminal) terminal;
+            if (!st.isClosed()) {
+                return st;
+            }
+        }
+
+        final SessionTerminal st = new SessionTerminal(request.getRemoteUser());
+        request.getSession().setAttribute("terminal", st);
+        return st;
+    }
+
+    public class SessionTerminal implements Runnable {
+
+        private Terminal terminal;
+        private Console console;
+        private PipedOutputStream in;
+        private PipedInputStream out;
+        private boolean closed;
+
+        public SessionTerminal(final String user) 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),
+                                      null);
+                CommandSession session = console.getSession();
+                session.put("APPLICATION", System.getProperty("karaf.name", "root"));
+                session.put("USER", user);
+                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 (IOException) new IOException().initCause(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/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/NameScoping.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/NameScoping.java
new file mode 100644
index 0000000..3d4b025
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/NameScoping.java
@@ -0,0 +1,73 @@
+/**
+ *
+ * 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.webconsole.plugins.gogo.impl;
+
+import org.apache.felix.service.command.CommandSession;
+
+
+/**
+ * A helper class for name scoping
+ */
+public class NameScoping {
+
+    public static final String MULTI_SCOPE_MODE_KEY = "MULTI_SCOPE_MODE";
+
+    /**
+     * Returns the name of the command which can omit the global scope prefix if the command starts with the
+     * same prefix as the current application
+     */
+    public static String getCommandNameWithoutGlobalPrefix(CommandSession session, String key) {
+        if (!isMultiScopeMode(session)) {
+            String globalScope = (String) session.get("APPLICATION");
+            if (globalScope != null) {
+                String prefix = globalScope + ":";
+                if (key.startsWith(prefix)) {
+                    // TODO we may only want to do this for single-scope mode when outside of OSGi?
+                    // so we may want to also check for a isMultiScope mode == false
+                    return key.substring(prefix.length());
+                }
+            }
+        }
+        return key;
+    }
+
+    /**
+     * Returns true if the given scope is the global scope so that it can be hidden from help messages
+     */
+    public static boolean isGlobalScope(CommandSession session, String scope) {
+        if (!isMultiScopeMode(session)) {
+            String globalScope = (String) session.get("APPLICATION");
+            if (globalScope != null) {
+                return scope.equals(globalScope);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if we are in multi-scope mode (the default) or if we are in single scope mode which means we
+     * avoid prefixing commands with their scope
+     */
+    public static boolean isMultiScopeMode(CommandSession session) {
+        Object value = session.get(MULTI_SCOPE_MODE_KEY);
+        if (value != null && value.equals("false")) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Parser.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Parser.java
new file mode 100644
index 0000000..d2df531
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Parser.java
@@ -0,0 +1,473 @@
+/*
+ * 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.
+ */
+// DWB14: parser loops if // comment at start of program
+// DWB15: allow program to have trailing ';'
+package org.apache.felix.webconsole.plugins.gogo.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Parser
+{
+    int current = 0;
+    String text;
+    boolean escaped;
+    static final String SPECIAL = "<;|{[\"'$`(=";
+
+    List<List<List<String>>> program;
+    List<List<String>> statements;
+    List<String> statement;
+    int cursor;
+    int start = -1;
+    int c0;
+    int c1;
+    int c2;
+    int c3;
+
+    public Parser(String text, int cursor)
+    {
+        this.text = text;
+        this.cursor = cursor;
+    }
+
+    void ws()
+    {
+        // derek: BUGFIX: loop if comment  at beginning of input
+        //while (!eof() && Character.isWhitespace(peek())) {
+        while (!eof() && (!escaped && Character.isWhitespace(peek()) || current == 0))
+        {
+            if (current != 0 || !escaped && Character.isWhitespace(peek()))
+            {
+                current++;
+            }
+            if (peek() == '/' && current < text.length() - 2
+                && text.charAt(current + 1) == '/')
+            {
+                comment();
+            }
+            if (current == 0)
+            {
+                break;
+            }
+        }
+    }
+
+    private void comment()
+    {
+        while (!eof() && peek() != '\n' && peek() != '\r')
+        {
+            next();
+        }
+    }
+
+    boolean eof()
+    {
+        return current >= text.length();
+    }
+
+    char peek()
+    {
+        return peek(false);
+    }
+
+    char peek(boolean increment)
+    {
+        escaped = false;
+        if (eof())
+        {
+            return 0;
+        }
+
+        int last = current;
+        char c = text.charAt(current++);
+
+        if (c == '\\')
+        {
+            escaped = true;
+            if (eof())
+            {
+                throw new RuntimeException("Eof found after \\"); // derek
+            }
+
+            c = text.charAt(current++);
+
+            switch (c)
+            {
+                case 't':
+                    c = '\t';
+                    break;
+                case '\r':
+                case '\n':
+                    c = ' ';
+                    break;
+                case 'b':
+                    c = '\b';
+                    break;
+                case 'f':
+                    c = '\f';
+                    break;
+                case 'n':
+                    c = '\n';
+                    break;
+                case 'r':
+                    c = '\r';
+                    break;
+                case 'u':
+                    c = unicode();
+                    current += 4;
+                    break;
+                default:
+                    // We just take the next character literally
+                    // but have the escaped flag set, important for {},[] etc
+            }
+        }
+        if (cursor > last && cursor <= current)
+        {
+            c0 = program != null ? program.size() : 0;
+            c1 = statements != null ? statements.size() : 0;
+            c2 = statement != null ? statement.size() : 0;
+            c3 = (start >= 0) ? current - start : 0;
+        }
+        if (!increment)
+        {
+            current = last;
+        }
+        return c;
+    }
+
+    public List<List<List<String>>> program()
+    {
+        program = new ArrayList<List<List<String>>>();
+        ws();
+        if (!eof())
+        {
+            program.add(pipeline());
+            while (peek() == ';')
+            {
+                current++;
+                List<List<String>> pipeline = pipeline();
+                program.add(pipeline);
+            }
+        }
+        if (!eof())
+        {
+            throw new RuntimeException("Program has trailing text: " + context(current));
+        }
+
+        List<List<List<String>>> p = program;
+        program = null;
+        return p;
+    }
+
+    CharSequence context(int around)
+    {
+        return text.subSequence(Math.max(0, current - 20), Math.min(text.length(),
+            current + 4));
+    }
+
+    public List<List<String>> pipeline()
+    {
+        statements = new ArrayList<List<String>>();
+        statements.add(statement());
+        while (peek() == '|')
+        {
+            current++;
+            ws();
+            if (!eof())
+            {
+                statements.add(statement());
+            }
+            else
+            {
+                statements.add(new ArrayList<String>());
+                break;
+            }
+        }
+        List<List<String>> s = statements;
+        statements = null;
+        return s;
+    }
+
+    public List<String> statement()
+    {
+        statement = new ArrayList<String>();
+        statement.add(value());
+        while (!eof())
+        {
+            ws();
+            if (peek() == '|' || peek() == ';')
+            {
+                break;
+            }
+
+            if (!eof())
+            {
+                statement.add(messy());
+            }
+        }
+        List<String> s = statement;
+        statement = null;
+        return s;
+    }
+
+    public String messy()
+    {
+        start = current;
+        char c = peek();
+        if (c > 0 && SPECIAL.indexOf(c) < 0)
+        {
+            current++;
+            try {
+                while (!eof())
+                {
+                    c = peek();
+                    if (!escaped && (c == ';' || c == '|' || Character.isWhitespace(c)))
+                    {
+                        break;
+                    }
+                    next();
+                }
+                return text.substring(start, current);
+            } finally {
+                start = -1;
+            }
+        }
+        else
+        {
+            return value();
+        }
+    }
+
+    String value()
+    {
+        ws();
+
+        start = current;
+        try {
+            char c = next();
+            if (!escaped)
+            {
+                switch (c)
+                {
+                    case '{':
+                        return text.substring(start, find('}', '{'));
+                    case '(':
+                        return text.substring(start, find(')', '('));
+                    case '[':
+                        return text.substring(start, find(']', '['));
+                    case '<':
+                        return text.substring(start, find('>', '<'));
+                    case '=':
+                        return text.substring(start, current);
+                    case '"':
+                    case '\'':
+                        quote(c);
+                        break;
+                }
+            }
+
+            // Some identifier or number
+            while (!eof())
+            {
+                c = peek();
+                if (!escaped)
+                {
+                    if (Character.isWhitespace(c) || c == ';' || c == '|' || c == '=')
+                    {
+                        break;
+                    }
+                    else if (c == '{')
+                    {
+                        next();
+                        find('}', '{');
+                    }
+                    else if (c == '(')
+                    {
+                        next();
+                        find(')', '(');
+                    }
+                    else if (c == '<')
+                    {
+                        next();
+                        find('>', '<');
+                    }
+                    else if (c == '[')
+                    {
+                        next();
+                        find(']', '[');
+                    }
+                    else if (c == '\'' || c == '"')
+                    {
+                        next();
+                        quote(c);
+                        next();
+                    }
+                    else
+                    {
+                        next();
+                    }
+                }
+                else
+                {
+                    next();
+                }
+            }
+            return text.substring(start, current);
+        } finally {
+            start = -1;
+        }
+    }
+
+    boolean escaped()
+    {
+        return escaped;
+    }
+
+    char next()
+    {
+        return peek(true);
+    }
+
+    char unicode()
+    {
+        if (current + 4 > text.length())
+        {
+            throw new IllegalArgumentException("Unicode \\u escape at eof at pos ..."
+                + context(current) + "...");
+        }
+
+        String s = text.subSequence(current, current + 4).toString();
+        int n = Integer.parseInt(s, 16);
+        return (char) n;
+    }
+
+    int find(char target, char deeper)
+    {
+        int start = current;
+        int level = 1;
+
+        while (level != 0)
+        {
+            if (eof())
+            {
+                throw new RuntimeException("Eof found in the middle of a compound for '"
+                    + target + deeper + "', begins at " + context(start));
+            }
+
+            char c = next();
+            if (!escaped)
+            {
+                if (c == target)
+                {
+                    level--;
+                }
+                else
+                {
+                    if (c == deeper)
+                    {
+                        level++;
+                    }
+                    else
+                    {
+                        if (c == '"')
+                        {
+                            quote('"');
+                        }
+                        else
+                        {
+                            if (c == '\'')
+                            {
+                                quote('\'');
+                            }
+                            else
+                            {
+                                if (c == '`')
+                                {
+                                    quote('`');
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return current;
+    }
+
+    int quote(char which)
+    {
+        while (!eof() && (peek() != which || escaped))
+        {
+            next();
+        }
+
+        return current++;
+    }
+
+    CharSequence findVar()
+    {
+        int start = current;
+        char c = peek();
+
+        if (c == '{')
+        {
+            next();
+            int end = find('}', '{');
+            return text.subSequence(start, end);
+        }
+        if (c == '(')
+        {
+            next();
+            int end = find(')', '(');
+            return text.subSequence(start, end);
+        }
+
+        if (Character.isJavaIdentifierPart(c))
+        {
+            while (c == '$')
+            {
+                c = next();
+            }
+            while (!eof() && (Character.isJavaIdentifierPart(c) || c == '.') && c != '$')
+            {
+                next();
+                c = peek();
+            }
+            return text.subSequence(start, current);
+        }
+        throw new IllegalArgumentException(
+            "Reference to variable does not match syntax of a variable: "
+                + context(start));
+    }
+
+    public String toString()
+    {
+        return "..." + context(current) + "...";
+    }
+
+    public String unescape()
+    {
+        StringBuilder sb = new StringBuilder();
+        while (!eof())
+        {
+            sb.append(next());
+        }
+        return sb.toString();
+    }
+}
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/StringsCompleter.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/StringsCompleter.java
new file mode 100644
index 0000000..c551db2
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/StringsCompleter.java
@@ -0,0 +1,101 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import jline.console.completer.Completer;
+
+/**
+ * Completer for a set of strings.
+ */
+public class StringsCompleter implements Completer {
+
+    private final SortedSet<String> strings = new TreeSet<String>();
+
+    private final boolean caseSensitive;
+
+    public StringsCompleter() {
+        this(true);
+    }
+
+    public StringsCompleter(final boolean caseSensitive) {
+        this.caseSensitive = caseSensitive;
+    }
+
+    public StringsCompleter(final Collection<String> strings) {
+        this();
+        assert strings != null;
+        getStrings().addAll(strings);
+    }
+
+    public StringsCompleter(final String[] strings, boolean caseSensitive) {
+        this(Arrays.asList(strings), caseSensitive);
+    }
+
+    public StringsCompleter(final Collection<String> strings, boolean caseSensitive) {
+        this(caseSensitive);
+        assert strings != null;
+        getStrings().addAll(strings);
+    }
+
+    public StringsCompleter(final String[] strings) {
+        this(Arrays.asList(strings));
+    }
+
+    public SortedSet<String> getStrings() {
+        return strings;
+    }
+
+    public int complete(String buffer, final int cursor, final List candidates) {
+        // buffer could be null
+        assert candidates != null;
+
+        if (buffer == null) {
+            buffer = "";
+        }
+        if (!caseSensitive) {
+            buffer = buffer.toLowerCase();
+        }
+
+        // KARAF-421, use getStrings() instead strings field.
+        SortedSet<String> matches = getStrings().tailSet(buffer);
+
+        for (String match : matches) {
+            String s = caseSensitive ? match : match.toLowerCase();
+            if (!s.startsWith(buffer)) {
+                break;
+            }
+
+            // noinspection unchecked
+            candidates.add(match);
+        }
+
+        if (candidates.size() == 1) {
+            // noinspection unchecked
+            candidates.set(0, candidates.get(0) + " ");
+        }
+
+        return candidates.isEmpty() ? -1 : 0;
+    }
+}
diff --git a/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Terminal.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Terminal.java
new file mode 100644
index 0000000..27fc0a8
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/Terminal.java
@@ -0,0 +1,1504 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+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;
+        }
+        return 1;
+    }
+
+    //
+    // Low-level terminal functions
+    //
+
+    private int[] peek(int y0, int x0, int y1, int x1) {
+        int from = width * y0 + x0;
+        int to = width * (y1 - 1) + x1;
+        int newLength = to - from;
+        if (newLength < 0)
+            throw new IllegalArgumentException(from + " > " + to);
+        int[] copy = new int[newLength];
+        System.arraycopy(screen, from, copy, 0,
+                         Math.min(screen.length - from, newLength));
+        return copy;
+    }
+
+    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);
+        }
+    }
+
+    @SuppressWarnings("unused")
+    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();
+    }
+
+    @SuppressWarnings("unused")
+    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_saved2 = 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);
+    }
+
+    @SuppressWarnings("unused")
+    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(@SuppressWarnings("unused") String p) {
+        vt100_saved_cx = cx;
+        vt100_saved_cy = cy;
+    }
+
+    private void csi_RCP(@SuppressWarnings("unused") 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(@SuppressWarnings("unused") 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 && ps[i].length() > 0) {
+                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/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/WebTerminal.java b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/WebTerminal.java
new file mode 100644
index 0000000..b47553e
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/java/org/apache/felix/webconsole/plugins/gogo/impl/WebTerminal.java
@@ -0,0 +1,46 @@
+/*
+ * 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.webconsole.plugins.gogo.impl;
+
+import jline.TerminalSupport;
+
+public class WebTerminal extends TerminalSupport {
+
+    private int width;
+    private int height;
+
+    public WebTerminal(int width, int height) {
+        super(true);
+        this.width = width;
+        this.height = height;
+    }
+
+    public void init() throws Exception {
+    }
+
+    public void restore() throws Exception {
+    }
+
+    public int getWidth() {
+        return width;
+    }
+
+    public int getHeight() {
+        return height;
+    }
+
+}
diff --git a/webconsole-plugins/gogo/src/main/resources/res/ui/gogo.css b/webconsole-plugins/gogo/src/main/resources/res/ui/gogo.css
new file mode 100644
index 0000000..f693de7
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/resources/res/ui/gogo.css
@@ -0,0 +1,91 @@
+/*
+ * 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
+ */
+
+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/webconsole-plugins/gogo/src/main/resources/res/ui/gogo.js b/webconsole-plugins/gogo/src/main/resources/res/ui/gogo.js
new file mode 100644
index 0000000..77044a7
--- /dev/null
+++ b/webconsole-plugins/gogo/src/main/resources/res/ui/gogo.js
@@ -0,0 +1,245 @@
+//
+// 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 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, fromkeydown) {
+        // Translate to standard keycodes
+        if (!ev)
+            ev = window.event;
+        var kc;
+        if (ev.keyCode)
+            kc = ev.keyCode;
+        if (!fromkeydown && 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 (fromkeydown) {
+            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;
+            }
+        }
+
+        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;
+        }
+
+//        debug("fromkeydown=" + fromkeydown + ", ev.keyCode=" + ev.keyCode + ", " +
+//              "ev.which=" + ev.which + ", ev.ctrlKey=" + ev.ctrlKey + ", " +
+//              "kc=" + kc + ", k=" + k);
+
+        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;
+           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) {
+               keypress(ev, true);
+           }
+   }
+
+   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);
+}
+