InputConfirmation.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;
import java.util.function.BooleanSupplier;

/**
 *
 */
public class InputConfirmation implements BooleanSupplier {
    /**
     * Constructor for simple confirmation.
     *
     * @param terminal     Terminal to use.
     * @param confirmation Message to print.
     */
    public InputConfirmation(Terminal terminal, String confirmation) {
        this(terminal, confirmation, null);
    }

    /**
     * Constructor for complete line-input.
     *
     * @param terminal     Terminal to use.
     * @param confirmation Message to print.
     * @param defaultValue Default value to return on e.g. 'enter'.
     */
    public InputConfirmation(Terminal terminal, String confirmation, Boolean defaultValue) {
        this.terminal = terminal;
        this.confirmation = confirmation;
        this.defaultValue = defaultValue;
    }

    @Override
    public boolean getAsBoolean() {
        terminal.formatln("%s", confirmation);

        try {
            for (; ; ) {
                Char c = terminal.read();
                if (c == null) {
                    throw new IOException("End of stream.");
                }
                handleInterrupt(c);
                if (isConfirmation(c)) {
                    printConfirmation(true);
                    return true;
                } else if (isRejection(c)) {
                    printConfirmation(false);
                    return false;
                } else if (isDefault(c)) {
                    if (defaultValue != null) {
                        printConfirmation(defaultValue);
                        return defaultValue;
                    } else {
                        // ignore this letter if no default value exists.
                        continue;
                    }
                }
                printMessage(String.format("'%s' is not valid input.", c.asString()));
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * Handle backspace. These are not control sequences, so must be handled separately
     * from those.
     *
     * @param c The char instance.
     * @return True if default value should be used.
     */
    protected boolean isDefault(Char c) {
        return c.codepoint() == '\n' || c.codepoint() == ' ';
    }

    /**
     * @param c The char instance.
     * @return True if confirmation is rejected.
     */
    protected boolean isRejection(Char c) {
        return c.codepoint() == 'n' || c.codepoint() == Char.DEL || c.codepoint() == Char.BS;
    }

    /**
     * If the provided char indicates confirmation.
     *
     * @param c The char instance.
     * @return True if confirmation is accepted.
     */
    protected boolean isConfirmation(Char c) {
        return c.codepoint() == 'y';
    }

    /**
     * Handle user interrupts.
     *
     * @param c The char instance.
     * @throws IOException If interrupted.
     */
    protected void handleInterrupt(Char c) throws IOException {
        if (c.codepoint() == Char.ESC || c.codepoint() == Char.ABR || c.codepoint() == Char.EOF) {
            printMessage("User interrupted.");
            throw new IOException("User interrupted: " + c.asString());
        }
    }

    /**
     * Print confirmation.
     *
     * @param result The result to be printed.
     */
    protected void printConfirmation(boolean result) {
        if (result) {
            printMessage("Yes.");
        } else {
            printMessage("No.");
        }
    }

    private void printMessage(String message) {
        if (printedMessage == null) {
            terminal.format(" %s", message);
        } else {
            terminal.format("\r%s%s %s", Control.CURSOR_ERASE, this.confirmation, message);
        }
        printedMessage = message;
    }

    private final Terminal terminal;
    private final String   confirmation;
    private final Boolean  defaultValue;
    private       String   printedMessage;
}