ConsoleIkePrompter.java

package network.ike.plugin.support;

import org.apache.maven.api.plugin.Log;

import java.io.BufferedReader;
import java.io.Console;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * Environment-aware {@link IkePrompter} (IKE-Network/ike-issues#385).
 *
 * <p>Detects the input channel with {@link System#console()} and
 * renders accordingly:
 *
 * <ul>
 *   <li><b>{@code System.console() != null}</b> — a real terminal (a
 *       shell, or IntelliJ's Terminal tool window). The label is
 *       written and the input read through {@link Console}, so the
 *       prompt renders <em>inline</em> with the cursor.</li>
 *   <li><b>{@code System.console() == null}</b> — a piped runner
 *       (IntelliJ's Maven tool window). The label is written through
 *       the Maven {@link Log} — the one channel a piped IDE console
 *       renders correctly and in order with {@code [INFO]} output —
 *       and input is read from {@code System.in}. The label lands on
 *       its own line: visible, ordered, answerable.</li>
 * </ul>
 *
 * <p>The Maven 4 {@code Prompter} service is intentionally not used:
 * it writes through JLine to raw file descriptors, uncoordinated with
 * the logger, and misrenders in piped runners.
 */
public final class ConsoleIkePrompter implements IkePrompter {

    private final Log log;
    private final boolean interactive;
    private BufferedReader pipedReader;

    /**
     * Creates a prompter.
     *
     * @param log         the Maven logger — used for the own-line
     *                    label in the piped (non-console) case
     * @param interactive whether Maven is in an interactive context;
     *                    pass {@code false} for batch mode (the
     *                    prompter then declines all prompts)
     */
    public ConsoleIkePrompter(Log log, boolean interactive) {
        this.log = log;
        this.interactive = interactive;
    }

    @Override
    public boolean isInteractive() {
        return interactive;
    }

    @Override
    public String prompt(String label) {
        if (!interactive) {
            return null;
        }
        Console console = System.console();
        String line;
        if (console != null) {
            // Real terminal — inline prompt through the Console.
            line = console.readLine("%s", label);
        } else {
            // Piped runner — label through the logging channel,
            // input from stdin (ike-issues#385).
            log.info(label);
            try {
                line = pipedReader().readLine();
            } catch (IOException e) {
                throw new UncheckedIOException(
                        "Could not read input from System.in", e);
            }
        }
        return (line == null || line.isBlank()) ? null : line.trim();
    }

    @Override
    public boolean confirm(String label, boolean defaultYes) {
        String suffix = defaultYes ? " [Y/n]: " : " [y/N]: ";
        String answer = prompt(label + suffix);
        if (answer == null) {
            return defaultYes;
        }
        return switch (answer.toLowerCase()) {
            case "y", "yes" -> true;
            case "n", "no" -> false;
            default -> defaultYes;
        };
    }

    @Override
    public String select(String label, List<String> options) {
        if (options == null || options.isEmpty()) {
            return null;
        }
        log.info(label);
        for (int i = 0; i < options.size(); i++) {
            log.info("  " + (i + 1) + ") " + options.get(i));
        }
        String answer = prompt("Select [1-" + options.size() + "]: ");
        if (answer == null) {
            return null;
        }
        try {
            int index = Integer.parseInt(answer.trim()) - 1;
            return (index >= 0 && index < options.size())
                    ? options.get(index) : null;
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private BufferedReader pipedReader() {
        if (pipedReader == null) {
            pipedReader = new BufferedReader(
                    new InputStreamReader(System.in, StandardCharsets.UTF_8));
        }
        return pipedReader;
    }
}