All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.termd.core.readline.Readline Maven / Gradle / Ivy

Go to download

An open source terminal daemon library providing terminal handling in Java, back ported to Alibaba by core engine team to support running on JDK 6+.

There is a newer version: 1.1.7.14
Show newest version
/*
 * Copyright 2015 Julien Viet
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.termd.core.readline;

import io.termd.core.function.BiConsumer;
import io.termd.core.function.Consumer;
import io.termd.core.tty.TtyConnection;
import io.termd.core.tty.TtyEvent;
import io.termd.core.util.Logging;
import io.termd.core.util.Vector;
import io.termd.core.util.Helper;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Make this class thread safe as SSH will access this class with different threds [sic].
 *
 * @author Julien Viet
 */
public class Readline {

  /**
   * The max number of history item that will be saved in memory.
   */
  private static final int MAX_HISTORY_SIZE = 500;

  // private final Device device;
  private final Map functions = new HashMap();
  private final EventQueue decoder;
  private Interaction interaction;
  private Vector size;
  private volatile List history;

  public Readline(Keymap keymap) {
    // https://github.com/alibaba/termd/issues/42
    // this.device = TermInfo.defaultInfo().getDevice("xterm"); // For now use xterm
    this.decoder = new EventQueue(keymap);
    this.history = new ArrayList();
    addFunction(ACCEPT_LINE);
  }

  /**
   * @return the current history
   */
  public List getHistory() {
    return history;
  }

  /**
   * Set the history
   *
   * @param history the history
   */
  public void setHistory(List history) {
    this.history = history;
  }

  /**
   * @return the last known size
   */
  public Vector size() {
    return size;
  }

  public Readline addFunction(Function function) {
    functions.put(function.name(), function);
    return this;
  }

  public Readline addFunctions(Iterable functions) {
    for (Function function : functions) {
      addFunction(function);
    }
    return this;
  }

  /**
   * Cancel the current readline interaction if there one, the request handler is called with {@code null}.
   */
  public boolean cancel() {
    Interaction interaction;
    synchronized (this) {
      interaction = this.interaction;
      if (interaction == null) {
        return false;
      }
    }
    return interaction.end(null);
  }

  private void deliver() {
    while (true) {
      Interaction handler;
      KeyEvent event;
      synchronized (this) {
        if (decoder.hasNext() && interaction != null && !interaction.paused) {
          event = decoder.next();
          handler = interaction;
        } else {
          return;
        }
      }
      handler.handle(event);
    }
  }

  /**
   * Read a line until a request can be processed.
   *
   * @param requestHandler the requestHandler
   */
  public void readline(TtyConnection conn, String prompt, Consumer requestHandler) {
    readline(conn, prompt, requestHandler, null);
  }

  /**
   * Read a line until a request can be processed.
   *
   * @param requestHandler the requestHandler
   */
  public void readline(TtyConnection conn, String prompt, Consumer requestHandler, Consumer completionHandler) {
    synchronized (this) {
      if (interaction != null) {
        throw new IllegalStateException("Already reading a line");
      }
      interaction = new Interaction(conn, prompt, requestHandler, completionHandler);
    }
    interaction.install();
    conn.write(prompt);
    schedulePendingEvent();
  }

  /**
   * Schedule delivery of pending events in the event queue.
   */
  public void schedulePendingEvent() {
    TtyConnection conn;
    synchronized (this) {
      if (interaction == null) {
        throw new IllegalStateException("No interaction!");
      }
      if (decoder.hasNext()) {
        conn = interaction.conn;
      } else {
        return;
      }
    }
    conn.execute(new Runnable() {
      @Override
      public void run() {
        deliver();
      }
    });
  }

  public synchronized Readline queueEvent(int[] codePoints) {
    decoder.append(codePoints);
    return this;
  }

  public synchronized boolean hasEvent() {
    return decoder.hasNext();
  }

  public synchronized KeyEvent nextEvent() {
    return decoder.next();
  }

  public class Interaction {

    final TtyConnection conn;
    private Consumer prevReadHandler;
    private Consumer prevSizeHandler;
    private BiConsumer prevEventHandler;
    private final String prompt;
    private final Consumer requestHandler;
    private final Consumer completionHandler;
    private final Map data;
    private final LineBuffer line = new LineBuffer();
    private final LineBuffer buffer = new LineBuffer();
    private int historyIndex = -1;
    private String currentPrompt;
    private boolean paused;

    private Interaction(
        TtyConnection conn,
        String prompt,
        Consumer requestHandler,
        Consumer completionHandler) {
      this.conn = conn;
      this.prompt = prompt;
      this.data = new HashMap();
      this.currentPrompt = prompt;
      this.requestHandler = requestHandler;
      this.completionHandler = completionHandler;
    }

    /**
     * End the current interaction with a callback.
     *
     * @param s the
     */
    private boolean end(String s) {
      synchronized (Readline.this) {
        if (interaction == null) {
          return false;
        }
        interaction = null;
        conn.setStdinHandler(prevReadHandler);
        conn.setSizeHandler(prevSizeHandler);
        conn.setEventHandler(prevEventHandler);
      }
      requestHandler.accept(s);
      return true;
    }

    private void handle(KeyEvent event) {

      // Very specific behavior that cannot be encapsulated in a function flow
      if (event.length() == 1) {
        if (event.getCodePointAt(0) == 4 && buffer.getSize() == 0) {
          // Specific behavior for Ctrl-D with empty line
          end(null);
          return;
        } else if (event.getCodePointAt(0) == 3) {
          // Specific behavior Ctrl-C
          line.clear();
          buffer.clear();
          data.clear();
          historyIndex = -1;
          currentPrompt = prompt;
          conn.stdoutHandler().accept(new int[]{'\n'});
          conn.write(interaction.prompt);
          return;
        }
        else if (event.getCodePointAt(0) == 12) {
          // Specific behavior Ctrl-L

          // \033 is the control character, \033[H means move the cursor to (0,0), \033[2J means clear screen
          conn.write("\033[H\033[2J");

          this.redraw();

          return;
        }
      }
      if (event instanceof FunctionEvent) {
        FunctionEvent fname = (FunctionEvent) event;
        Function function = functions.get(fname.name());
        if (function != null) {
          synchronized (this) {
            paused = true;
          }
          function.apply(this);
        } else {
          Logging.READLINE.warn("Unimplemented function " + fname.name());
        }
      } else {
        LineBuffer buf = buffer.copy();
        for (int i = 0;i < event.length();i++) {
          int codePoint = event.getCodePointAt(i);
          try {
            buf.insert(codePoint);
          } catch (IllegalArgumentException e) {
            conn.stdoutHandler().accept(new int[]{'\007'});
          }
        }
        refresh(buf);
      }
    }

    void resize(int oldWith, int newWidth) {

      // Erase screen
      LineBuffer abc = new LineBuffer(buffer.getCapacity());
      abc.insert(currentPrompt);
      abc.insert(buffer.toArray());
      abc.setCursor(currentPrompt.length() + buffer.getCursor());

      // Recompute new cursor
      Vector pos = abc.getCursorPosition(newWidth);
      int curWidth = pos.x();
      int curHeight = pos.y();

      // Recompute new end
      Vector end = abc.getPosition(abc.getSize(), oldWith);
      int endHeight = end.y() + end.x() / newWidth;

      // Position at the bottom / right
      Consumer out = conn.stdoutHandler();
      out.accept(new int[]{'\r'});
      while (curHeight != endHeight) {
        if (curHeight > endHeight) {
          out.accept(new int[]{'\033','[','1','A'});
          curHeight--;
        } else {
          out.accept(new int[]{'\n'});
          curHeight++;
        }
      }

      // Now erase and redraw
      while (curHeight > 0) {
        out.accept(new int[]{'\033','[','1','K'});
        out.accept(new int[]{'\033','[','1','A'});
        curHeight--;
      }
      out.accept(new int[]{'\033','[','1','K'});

      // Now redraw
      out.accept(Helper.toCodePoints(currentPrompt));
      refresh(new LineBuffer(), newWidth);
    }

    public Consumer completionHandler() {
      return completionHandler;
    }

    public Map data() {
      return data;
    }

    public List history() {
      return history;
    }

    public int getHistoryIndex() {
      return historyIndex;
    }

    public void setHistoryIndex(int historyIndex) {
      this.historyIndex = historyIndex;
    }

    public LineBuffer line() {
      return line;
    }

    public LineBuffer buffer() {
      return buffer;
    }

    public String currentPrompt() {
      return currentPrompt;
    }

    public Vector size() {
      return size;
    }

    /**
     * Redraw the current line.
     */
    public void redraw() {
      LineBuffer toto = new LineBuffer(buffer.getCapacity());
      toto.insert(Helper.toCodePoints(currentPrompt));
      toto.insert(buffer.toArray());
      toto.setCursor(currentPrompt.length() + buffer.getCursor());
      LineBuffer abc = new LineBuffer(toto.getCapacity());
      abc.update(toto, conn.stdoutHandler(), size.x());
    }

    /**
     * Refresh the current buffer with the argument buffer.
     *
     * @param buffer the new buffer
     */
    public Interaction refresh(LineBuffer buffer) {
      refresh(buffer, size.x());
      return this;
    }

    private void refresh(LineBuffer update, int width) {
      LineBuffer copy3 = new LineBuffer(update.getCapacity());
      final List codePoints = new LinkedList();
      copy3.insert(Helper.toCodePoints(currentPrompt));
      copy3.insert(buffer().toArray());
      copy3.setCursor(currentPrompt.length() + buffer().getCursor());
      LineBuffer copy2 = new LineBuffer(copy3.getCapacity());
      copy2.insert(Helper.toCodePoints(currentPrompt));
      copy2.insert(update.toArray());
      copy2.setCursor(currentPrompt.length() + update.getCursor());
      copy3.update(copy2, new Consumer() {
        @Override
        public void accept(int[] data) {
          for (int cp : data) {
            codePoints.add(cp);
          }
        }
      }, width);
      conn.stdoutHandler().accept(Helper.convert(codePoints));
      buffer.clear();
      buffer.insert(update.toArray());
      buffer.setCursor(update.getCursor());
    }

    public void resume() {
      synchronized (Readline.this) {
        if (!paused) {
          throw new IllegalStateException();
        }
        paused = false;
      }
      schedulePendingEvent();
    }

    private void install() {
      prevReadHandler = conn.getStdinHandler();
      prevSizeHandler = conn.getSizeHandler();
      prevEventHandler = conn.getEventHandler();
      conn.setStdinHandler(new Consumer() {
        @Override
        public void accept(int[] data) {
          synchronized (Readline.this) {
            decoder.append(data);
          }
          deliver();
        }
      });
      size = conn.size();
      conn.setSizeHandler(new Consumer() {
        @Override
        public void accept(Vector dim) {
          if (size != null) {
            // Not supported for now
            // interaction.resize(size.width(), dim.width());
          }
          size = dim;
        }
      });
      conn.setEventHandler(null);
    }
  }

  // Need to access internal state
  private final Function ACCEPT_LINE = new Function() {

    @Override
    public String name() {
      return "accept-line";
    }

    @Override
    public void apply(Interaction interaction) {
      interaction.line.insert(interaction.buffer.toArray());
      LineStatus pb = new LineStatus();
      for (int i = 0;i < interaction.line.getSize();i++) {
        pb.accept(interaction.line.getAt(i));
      }
      interaction.buffer.clear();
      if (pb.isEscaping()) {
        interaction.line.delete(-1); // Remove \
        interaction.currentPrompt = "> ";
        interaction.conn.write("\n> ");
        interaction.resume();
      } else {
        if (pb.isQuoted()) {
          interaction.line.insert('\n');
          interaction.conn.write("\n> ");
          interaction.currentPrompt = "> ";
          interaction.resume();
        } else {
          String raw = interaction.line.toString();
          if (interaction.line.getSize() > 0) {
            addToHistory(interaction.line.toArray());
          }
          interaction.line.clear();
          interaction.conn.write("\n");
          interaction.end(raw);
        }
      }
    }

      private void addToHistory(int[] command) {
          // copy and save. https://github.com/alibaba/termd/issues/44
          synchronized (Readline.class) {
              List tmp = new ArrayList(history.size());
              // add to first
              tmp.add(command);

              for (int[] c : history) {
                  tmp.add(c);
                  if (tmp.size() >= MAX_HISTORY_SIZE) {
                      break;
                  }
              }
              history = tmp;
          }
      }

  };
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy