InputPassword.java

/*
 * Copyright (c) 2016, Stein Eldar Johnsen
 *
 * 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 net.morimekta.terminal.input;

import net.morimekta.strings.chr.Char;
import net.morimekta.strings.chr.Control;
import net.morimekta.terminal.Terminal;

import java.io.IOException;
import java.io.UncheckedIOException;

/**
 * Class that handled reading a password from terminal.
 * <p>
 * It will not print out the password (hidden input) on the terminal,
 * but may print out the length of it and current position based
 * on char replacement. The char replacement must be a single char, or
 * an empty string.
 * <p>
 * See {@link InputLine} for details on input controls.
 */
public class InputPassword {
    /**
     * Constructor for simple line-input.
     *
     * @param terminal Terminal to use.
     * @param message Message to print.
     */
    public InputPassword(Terminal terminal,
                         String message) {
        this(terminal, message, "*");
    }

    /**
     * Constructor for complete line-input.
     *
     * @param terminal Terminal to use.
     * @param message Message to print.
     * @param charReplacement The character replacement string, e.g. "*".
     */
    public InputPassword(Terminal terminal,
                         String message,
                         String charReplacement) {
        this.terminal = terminal;
        this.message = message;
        this.charReplacement = charReplacement;
    }

    /**
     * Read password from terminal.
     *
     * @return The resulting line.
     */
    public String readPassword() {
        this.before = "";
        this.after = "";

        terminal.formatln("%s: ", message);

        try {
            for (; ; ) {
                Char c = terminal.read();
                if (c == null) {
                    throw new IOException("End of input.");
                }

                int ch = c.codepoint();

                if (ch == Char.CR) {
                    return before + after;
                }

                if (ch == Char.DEL || ch == Char.BS) {
                    // backspace...
                    if (before.length() > 0) {
                        before = before.substring(0, before.length() - 1);
                        printInputLine();
                    }
                    continue;
                }

                if (c instanceof Control) {
                    if (c.equals(Control.DELETE)) {
                        if (after.length() > 0) {
                            after = after.substring(1);
                        }
                    } else if (c.equals(Control.LEFT)) {
                        if (before.length() > 0) {
                            after = "" + before.charAt(before.length() - 1) + after;
                            before = before.substring(0, before.length() - 1);
                        }
                    } else if (c.equals(Control.HOME)) {
                        after = before + after;
                        before = "";
                    } else if (c.equals(Control.RIGHT)) {
                        if (after.length() > 0) {
                            before = before + after.charAt(0);
                            after = after.substring(1);
                        }
                    } else if (c.equals(Control.END)) {
                        before = before + after;
                        after = "";
                    } else {
                        // Silently ignore unknown control chars.
                        continue;
                    }
                    printInputLine();
                    continue;
                }

                if (ch == Char.ESC || ch == Char.ABR || ch == Char.EOF) {
                    throw new IOException("User interrupted: " + c.asString());
                }

                if (ch < 0x20) {
                    // Silently ignore unknown ASCII control characters.
                    continue;
                }

                before = before + c;
                printInputLine();
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private void printInputLine() {
        String bf = charReplacement.repeat(before.length());
        String af = charReplacement.repeat(after.length());
        terminal.format("\r%s%s: %s",
                        Control.CURSOR_ERASE,
                        message, bf);
        if (af.length() > 0) {
            terminal.format("%s%s", af, Control.cursorLeft(af.length()));
        }
    }

    private final Terminal terminal;
    private final String message;
    private final String charReplacement;

    private String before;
    private String after;
}