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

org.jline.consoleui.prompt.AbstractPrompt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2024, the original author(s).
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * https://opensource.org/licenses/BSD-3-Clause
 */
package org.jline.consoleui.prompt;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jline.consoleui.elements.Checkbox;
import org.jline.consoleui.elements.ConfirmChoice;
import org.jline.consoleui.elements.ExpandableChoice;
import org.jline.consoleui.elements.InputValue;
import org.jline.consoleui.elements.items.CheckboxItemIF;
import org.jline.consoleui.elements.items.ChoiceItemIF;
import org.jline.consoleui.elements.items.ConsoleUIItemIF;
import org.jline.consoleui.elements.items.ListItemIF;
import org.jline.consoleui.elements.items.impl.CheckboxItem;
import org.jline.consoleui.elements.items.impl.ChoiceItem;
import org.jline.consoleui.elements.items.impl.Separator;
import org.jline.keymap.BindingReader;
import org.jline.keymap.KeyMap;
import org.jline.reader.*;
import org.jline.reader.impl.CompletionMatcherImpl;
import org.jline.reader.impl.ReaderUtils;
import org.jline.terminal.Size;
import org.jline.terminal.Terminal;
import org.jline.utils.*;

import static org.jline.keymap.KeyMap.*;

/**
 * Classes for all prompt implementations.
 */
public abstract class AbstractPrompt {
    protected final Terminal terminal;
    protected final BindingReader bindingReader;
    private final List header;
    private final AttributedString message;
    protected final List items;
    protected int firstItemRow;
    private final Size size = new Size();
    protected final ConsolePrompt.UiConfig config;
    private Display display;
    private ListRange range = null;

    public AbstractPrompt(
            Terminal terminal, List header, AttributedString message, ConsolePrompt.UiConfig cfg) {
        this(terminal, header, message, new ArrayList<>(), cfg);
    }

    public AbstractPrompt(
            Terminal terminal,
            List header,
            AttributedString message,
            List items,
            ConsolePrompt.UiConfig cfg) {
        this.terminal = terminal;
        this.bindingReader = new BindingReader(terminal.reader());
        this.size.copy(terminal.getSize());
        int listSpace = Math.min(size.getRows(), 10);
        this.header = header.size() > size.getRows() - listSpace
                ? header.subList(header.size() - size.getRows() + listSpace, header.size())
                : header;
        this.message = message;
        this.items = items;
        this.firstItemRow = this.header.size() + 1;
        this.config = cfg;
    }

    protected void resetHeader() {
        this.firstItemRow = header.size() + 1;
    }

    protected void resetDisplay() {
        display = new Display(terminal, true);
        size.copy(terminal.getSize());
        display.clear();
        display.reset();
    }

    protected void refreshDisplay(int row) {
        refreshDisplay(row, 0, null, false);
    }

    protected void refreshDisplay(int row, Set selected) {
        display.resize(size.getRows(), size.getColumns());
        display.reset();
        display.update(
                displayLines(row, selected),
                size.cursorPos(Math.min(size.getRows() - 1, firstItemRow + items.size()), 0));
    }

    protected void refreshDisplay(int row, int column, String buffer, boolean newline) {
        display.resize(size.getRows(), size.getColumns());
        AttributedStringBuilder asb = new AttributedStringBuilder();
        int crow = column == 0 ? Math.min(size.getRows() - 1, firstItemRow + items.size()) : row;
        if (buffer != null) {
            if (newline && !buffer.isEmpty()) {
                asb.style(config.style(".pr")).append(">> ");
            }
            asb.style(AttributedStyle.DEFAULT).append(buffer);
        }
        display.update(displayLines(row, asb.toAttributedString(), newline), size.cursorPos(crow, column));
    }

    protected void refreshDisplay(
            int buffRow, int buffCol, String buffer, int candRow, int candCol, List candidates) {
        display.resize(size.getRows(), size.getColumns());
        AttributedStringBuilder asb = new AttributedStringBuilder();
        if (buffer != null) {
            asb.style(AttributedStyle.DEFAULT).append(buffer);
        }
        display.update(
                displayLines(candRow, candCol, asb.toAttributedString(), candidates), size.cursorPos(buffRow, buffCol));
    }

    private int candidateStartPosition(int candidatesColumn, String buffer, List cands) {
        List values = cands.stream()
                .map(c -> AttributedString.stripAnsi(c.displ()))
                .filter(c -> !c.matches("\\w+") && c.length() > 1)
                .collect(Collectors.toList());
        Set notDelimiters = new HashSet<>();
        values.forEach(v -> v.substring(0, v.length() - 1)
                .chars()
                .filter(c -> !Character.isDigit(c) && !Character.isAlphabetic(c))
                .forEach(c -> notDelimiters.add(Character.toString((char) c))));
        int out = candidatesColumn;
        for (int i = buffer.length(); i > 0; i--) {
            if (buffer.substring(0, i).matches(".*\\W") && !notDelimiters.contains(buffer.substring(i - 1, i))) {
                out += i;
                break;
            }
        }
        return out;
    }

    private List displayLines(
            int cursorRow, int candidatesColumn, AttributedString buffer, List candidates) {
        computeListRange(cursorRow, candidates.size());
        List out = new ArrayList<>();
        for (int i = range.headerStart; i < header.size(); i++) {
            out.add(header.get(i));
        }
        AttributedStringBuilder asb = new AttributedStringBuilder();
        asb.append(message);
        asb.append(buffer);
        out.add(asb.toAttributedString());
        int listStart;
        if (cursorRow - firstItemRow >= 0 && !candidates.isEmpty() && cursorRow - firstItemRow < candidates.size()) {
            String dc = candidates.get(cursorRow - firstItemRow).displ();
            listStart = candidatesColumn
                    + buffer.columnLength()
                    - display.wcwidth(dc)
                    + (AttributedString.stripAnsi(dc).endsWith("*") ? 1 : 0);
        } else {
            listStart = candidateStartPosition(candidatesColumn, buffer.toString(), candidates);
        }
        int width = Math.max(
                candidates.stream()
                        .map(Candidate::displ)
                        .mapToInt(display::wcwidth)
                        .max()
                        .orElse(20),
                20);
        for (int i = range.first; i < range.last - 1; i++) {
            if (candidates.isEmpty() || i > candidates.size() - 1) {
                break;
            }
            Candidate c = candidates.get(i);
            asb = new AttributedStringBuilder();
            AttributedStringBuilder tmp = new AttributedStringBuilder();
            tmp.ansiAppend(c.displ());
            asb.style(tmp.styleAt(0));
            if (i + firstItemRow == cursorRow) {
                asb.style(new AttributedStyle().inverse());
            }
            asb.append(AttributedString.stripAnsi(c.displ()));
            int cl = asb.columnLength();
            for (int k = cl; k < width; k++) {
                asb.append(" ");
            }
            AttributedStringBuilder asb2 = new AttributedStringBuilder();
            asb2.tabs(listStart);
            asb2.append("\t");
            asb2.style(config.style(".cb"));
            asb2.append(asb).append(" ");
            out.add(asb2.toAttributedString());
        }
        addFooter(out, candidates.size());
        return out;
    }

    private List displayLines(int cursorRow, Set selected) {
        computeListRange(cursorRow, items.size());
        List out = new ArrayList<>();
        for (int i = range.headerStart; i < header.size(); i++) {
            out.add(header.get(i));
        }
        AttributedStringBuilder asb = new AttributedStringBuilder();
        asb.append(message);
        out.add(asb.toAttributedString());
        for (int i = range.first; i < range.last - 1; i++) {
            if (items.isEmpty() || i > items.size() - 1) {
                break;
            }
            ConsoleUIItemIF s = items.get(i);
            asb = new AttributedStringBuilder();
            if (s.isSelectable()) {
                asb = i + firstItemRow == cursorRow
                        ? asb.append(config.indicator())
                                .style(AttributedStyle.DEFAULT)
                                .append(" ")
                        : fillIndicatorSpace(asb).append(" ");
                asb = selected.contains(s.getName())
                        ? asb.append(config.checkedBox())
                        : asb.append(config.uncheckedBox());
            } else if (s instanceof CheckboxItem) {
                fillIndicatorSpace(asb);
                asb.append(" ");
                if (s.isDisabled()) {
                    asb.append(config.unavailable());
                } else {
                    fillCheckboxSpace(asb);
                }
            }
            asb.append(s.getText()).toAttributedString();
            if (s.isDisabled()) {
                asb.append(" (").append(s.getDisabledText()).append(")");
            }
            out.add(asb.toAttributedString());
        }
        addFooter(out, items.size()); // footer is necessary for making the long item list scroll correctly
        return out;
    }

    private AttributedStringBuilder fillIndicatorSpace(AttributedStringBuilder asb) {
        for (int i = 0; i < config.indicator().length(); i++) {
            asb.append(" ");
        }
        return asb;
    }

    private void fillCheckboxSpace(AttributedStringBuilder asb) {
        for (int i = 0; i < config.checkedBox().length(); i++) {
            asb.append(" ");
        }
    }

    private static class ListRange {
        final int first;
        final int last;
        final int headerStart;

        public ListRange(int headerStart, int first, int last) {
            this.headerStart = headerStart;
            this.first = first;
            this.last = last;
        }
    }

    private void computeListRange(int cursorRow, int itemsSize) {
        if (range != null && range.first <= cursorRow - firstItemRow && range.last - 1 > cursorRow - firstItemRow) {
            return;
        }
        range = new ListRange(0, 0, itemsSize + 1);
        if (size.getRows() < header.size() + itemsSize + 1) {
            int itemId = cursorRow - firstItemRow;
            int headerStart = header.size() + 1 > 10 ? header.size() - 9 : 0;
            firstItemRow = firstItemRow - headerStart;
            int forList = size.getRows() - header.size() + headerStart - 1;
            if (itemId < forList - 1) {
                range = new ListRange(headerStart, 0, forList);
            } else {
                range = new ListRange(headerStart, itemId - forList + 2, itemId + 2);
            }
        }
    }

    private void addFooter(List lines, int itemsSize) {
        if (size.getRows() < header.size() + itemsSize + 1) {
            AttributedStringBuilder asb = new AttributedStringBuilder();
            lines.add(asb.append(".").toAttributedString());
        }
    }

    private List displayLines(int cursorRow, AttributedString buffer, boolean newline) {
        computeListRange(cursorRow, items.size());
        List out = new ArrayList<>();
        for (int i = range.headerStart; i < header.size(); i++) {
            out.add(header.get(i));
        }
        AttributedStringBuilder asb = new AttributedStringBuilder();
        asb.append(message);
        if (buffer != null && !newline) {
            asb.append(buffer);
        }
        out.add(asb.toAttributedString());
        if (buffer != null && newline) {
            asb = new AttributedStringBuilder();
            asb.append(buffer);
            out.add(asb.toAttributedString());
        }
        for (int i = range.first; i < range.last - 1; i++) {
            ConsoleUIItemIF s = items.get(i);
            asb = new AttributedStringBuilder();
            String key = s instanceof ChoiceItem ? ((ChoiceItem) s).getKey() + " - " : "";
            if (i + firstItemRow == cursorRow) {
                out.add(asb.append(config.indicator())
                        .style(config.style(".se"))
                        .append(" ")
                        .append(key)
                        .append(s.getText())
                        .toAttributedString());
            } else if (!(s instanceof Separator)) {
                fillIndicatorSpace(asb);
                out.add(asb.append(" ").append(key).append(s.getText()).toAttributedString());
            } else {
                out.add(asb.append(s.getText()).toAttributedString());
            }
        }
        addFooter(out, items.size());
        return out;
    }

    protected static class ExpandableChoicePrompt extends AbstractPrompt {
        private enum Operation {
            INSERT,
            EXIT
        }

        private final int startColumn;
        private final List items;
        private final ConsolePrompt.UiConfig config;

        private ExpandableChoicePrompt(
                Terminal terminal,
                List header,
                AttributedString message,
                ExpandableChoice expandableChoice,
                ConsolePrompt.UiConfig cfg) {
            super(terminal, header, message, cfg);
            startColumn = message.columnLength();
            items = expandableChoice.getChoiceItems();
            config = cfg;
        }

        public static ExpandableChoicePrompt getPrompt(
                Terminal terminal,
                List header,
                AttributedString message,
                ExpandableChoice expandableChoice,
                ConsolePrompt.UiConfig cfg) {
            return new ExpandableChoicePrompt(terminal, header, message, expandableChoice, cfg);
        }

        private void bindKeys(KeyMap map) {
            for (char i = 32; i < KEYMAP_LENGTH; i++) {
                map.bind(Operation.INSERT, Character.toString(i));
            }
            map.bind(Operation.EXIT, "\r");
        }

        public ExpandableChoiceResult execute() {
            resetDisplay();
            int row = firstItemRow - 1;
            KeyMap keyMap = new KeyMap<>();
            bindKeys(keyMap);
            StringBuilder buffer = new StringBuilder();
            String selectedId = null;
            boolean expandChoiceList = false;
            for (ChoiceItemIF cu : items) {
                if (cu.isSelectable() && cu.isDefaultChoice()) {
                    selectedId = cu.getName();
                    break;
                }
            }
            while (true) {
                refreshDisplay(row, startColumn, buffer.toString(), true);
                Operation op = bindingReader.readBinding(keyMap);
                buffer = new StringBuilder();
                switch (op) {
                    case INSERT:
                        String ch = bindingReader.getLastBinding();
                        if (ch.equals("h")) {
                            expandChoiceList = true;
                            buffer.append(config.resourceBundle().getString("help.list.all.options"));
                        } else {
                            selectedId = null;
                            expandChoiceList = false;
                            boolean found = false;
                            for (ChoiceItemIF cu : items) {
                                if (cu.isSelectable() && cu.getKey().toString().equals(ch)) {
                                    selectedId = cu.getName();
                                    buffer.append(selectedId);
                                    found = true;
                                    break;
                                }
                            }
                            if (!found) {
                                buffer.append(config.resourceBundle().getString("please.enter.a.valid.command"));
                            }
                        }
                        break;
                    case EXIT:
                        if (selectedId == null || expandChoiceList) {
                            if (expandChoiceList) {
                                throw new ExpandableChoiceException();
                            }
                            break;
                        }
                        return new ExpandableChoiceResult(selectedId);
                }
            }
        }
    }

    @SuppressWarnings("serial")
    protected static class ExpandableChoiceException extends RuntimeException {}

    protected static class ConfirmPrompt extends AbstractPrompt {
        private enum Operation {
            NO,
            YES,
            EXIT
        }

        private final int startColumn;
        private final ConfirmChoice.ConfirmationValue defaultValue;
        private final ConsolePrompt.UiConfig config;

        private ConfirmPrompt(
                Terminal terminal,
                List header,
                AttributedString message,
                ConfirmChoice confirmChoice,
                ConsolePrompt.UiConfig cfg) {
            super(terminal, header, message, cfg);
            startColumn = message.columnLength();
            defaultValue = confirmChoice.getDefaultConfirmation();
            config = cfg;
        }

        public static ConfirmPrompt getPrompt(
                Terminal terminal,
                List header,
                AttributedString message,
                ConfirmChoice confirmChoice,
                ConsolePrompt.UiConfig cfg) {
            return new ConfirmPrompt(terminal, header, message, confirmChoice, cfg);
        }

        private void bindKeys(KeyMap map) {
            String yes = config.resourceBundle().getString("confirmation_yes_key");
            String no = config.resourceBundle().getString("confirmation_no_key");
            map.bind(Operation.YES, yes, yes.toUpperCase());
            map.bind(Operation.NO, no, no.toUpperCase());
            map.bind(Operation.EXIT, "\r");
        }

        public ConfirmResult execute() {
            resetDisplay();
            int row = firstItemRow - 1;
            int column = startColumn;
            KeyMap keyMap = new KeyMap<>();
            bindKeys(keyMap);
            StringBuilder buffer = new StringBuilder();
            ConfirmChoice.ConfirmationValue confirm = defaultValue;
            while (true) {
                refreshDisplay(row, column, buffer.toString(), false);
                Operation op = bindingReader.readBinding(keyMap);
                buffer = new StringBuilder();
                switch (op) {
                    case YES:
                        buffer.append(config.resourceBundle().getString("confirmation_yes_answer"));
                        confirm = ConfirmChoice.ConfirmationValue.YES;
                        column = startColumn + 3;
                        break;
                    case NO:
                        buffer.append(config.resourceBundle().getString("confirmation_no_answer"));
                        confirm = ConfirmChoice.ConfirmationValue.NO;
                        column = startColumn + 2;
                        break;
                    case EXIT:
                        if (confirm == null) {
                            break;
                        }
                        return new ConfirmResult(confirm);
                }
            }
        }
    }

    protected static class InputValuePrompt extends AbstractPrompt {
        private enum Operation {
            INSERT,
            BACKSPACE,
            DELETE,
            RIGHT,
            LEFT,
            BEGINNING_OF_LINE,
            END_OF_LINE,
            SELECT_CANDIDATE,
            EXIT
        }

        private enum SelectOp {
            FORWARD_ONE_LINE,
            BACKWARD_ONE_LINE,
            EXIT
        }

        private final int startColumn;
        private final String defaultValue;
        private final Character mask;
        private final LineReader reader;
        private final Completer completer;

        private InputValuePrompt(
                LineReader reader,
                Terminal terminal,
                List header,
                AttributedString message,
                InputValue inputValue,
                ConsolePrompt.UiConfig cfg) {
            super(terminal, header, message, cfg);
            this.reader = reader;
            defaultValue = inputValue.getDefaultValue();
            startColumn = message.columnLength();
            mask = inputValue.getMask();
            this.completer = inputValue.getCompleter();
        }

        public static InputValuePrompt getPrompt(
                LineReader reader,
                Terminal terminal,
                List header,
                AttributedString message,
                InputValue inputValue,
                ConsolePrompt.UiConfig cfg) {
            return new InputValuePrompt(reader, terminal, header, message, inputValue, cfg);
        }

        private void bindKeys(KeyMap map) {
            map.setUnicode(Operation.INSERT);
            for (char i = 32; i < KEYMAP_LENGTH; i++) {
                map.bind(Operation.INSERT, Character.toString(i));
            }
            map.bind(Operation.BACKSPACE, del());
            map.bind(Operation.DELETE, ctrl('D'), key(terminal, InfoCmp.Capability.key_dc));
            map.bind(Operation.BACKSPACE, ctrl('H'));
            map.bind(Operation.EXIT, "\r");
            map.bind(Operation.RIGHT, key(terminal, InfoCmp.Capability.key_right));
            map.bind(Operation.LEFT, key(terminal, InfoCmp.Capability.key_left));
            map.bind(Operation.BEGINNING_OF_LINE, ctrl('A'), key(terminal, InfoCmp.Capability.key_home));
            map.bind(Operation.END_OF_LINE, ctrl('E'), key(terminal, InfoCmp.Capability.key_end));
            map.bind(Operation.RIGHT, ctrl('F'));
            map.bind(Operation.LEFT, ctrl('B'));
            map.bind(Operation.SELECT_CANDIDATE, "\t");
        }

        private void bindSelectKeys(KeyMap map) {
            map.bind(SelectOp.FORWARD_ONE_LINE, "\t", "e", ctrl('E'), key(terminal, InfoCmp.Capability.key_down));
            map.bind(SelectOp.BACKWARD_ONE_LINE, "y", ctrl('Y'), key(terminal, InfoCmp.Capability.key_up));
            map.bind(SelectOp.EXIT, "\r");
        }

        public InputResult execute() {
            resetDisplay();
            int row = firstItemRow - 1;
            int column = startColumn;
            List matches = new ArrayList<>();
            KeyMap keyMap = new KeyMap<>();
            bindKeys(keyMap);
            StringBuilder buffer = new StringBuilder();
            CompletionMatcher completionMatcher = new CompletionMatcherImpl();
            boolean tabCompletion = completer != null && reader != null;
            while (true) {
                boolean displayCandidates = true;
                if (tabCompletion) {
                    List possible = new ArrayList<>();
                    CompletingWord completingWord = new CompletingWord(buffer.toString());
                    completer.complete(reader, completingWord, possible);
                    completionMatcher.compile(config.readerOptions(), false, completingWord, false, 0, null);
                    matches = completionMatcher.matches(possible).stream()
                            .sorted(Comparator.naturalOrder())
                            .collect(Collectors.toList());
                    if (matches.size() > ReaderUtils.getInt(reader, LineReader.MENU_LIST_MAX, 10)) {
                        displayCandidates = false;
                    }
                }
                refreshDisplay(
                        firstItemRow - 1,
                        column,
                        buffer.toString(),
                        row,
                        startColumn,
                        displayCandidates ? matches : new ArrayList<>());
                Operation op = bindingReader.readBinding(keyMap);
                switch (op) {
                    case LEFT:
                        if (column > startColumn) {
                            column--;
                        }
                        break;
                    case RIGHT:
                        if (column < startColumn + buffer.length()) {
                            column++;
                        }
                        break;
                    case INSERT:
                        buffer.insert(column - startColumn, mask == null ? bindingReader.getLastBinding() : mask);
                        column++;
                        break;
                    case BACKSPACE:
                        if (column > startColumn) {
                            buffer.deleteCharAt(column - startColumn - 1);
                            column--;
                        }
                        break;
                    case DELETE:
                        if (column < startColumn + buffer.length() && column >= startColumn) {
                            buffer.deleteCharAt(column - startColumn);
                        }
                        break;
                    case BEGINNING_OF_LINE:
                        column = startColumn;
                        break;
                    case END_OF_LINE:
                        column = startColumn + buffer.length();
                        break;
                    case SELECT_CANDIDATE:
                        if (tabCompletion && matches.size() < ReaderUtils.getInt(reader, LineReader.LIST_MAX, 50)) {
                            String selected =
                                    selectCandidate(firstItemRow - 1, buffer.toString(), row + 1, startColumn, matches);
                            resetHeader();
                            buffer.delete(0, buffer.length());
                            buffer.append(selected);
                            column = startColumn + buffer.length();
                        }
                        break;
                    case EXIT:
                        if (buffer.toString().isEmpty()) {
                            buffer.append(defaultValue);
                        }
                        return new InputResult(buffer.toString());
                }
            }
        }

        String selectCandidate(int buffRow, String buffer, int row, int column, List candidates) {
            if (candidates.isEmpty()) {
                return buffer;
            } else if (candidates.size() == 1) {
                return candidates.get(0).value();
            }
            KeyMap keyMap = new KeyMap<>();
            bindSelectKeys(keyMap);
            while (true) {
                String selected = candidates.get(row - buffRow - 1).value();
                refreshDisplay(buffRow, column + selected.length(), selected, row, column, candidates);
                SelectOp op = bindingReader.readBinding(keyMap);
                switch (op) {
                    case FORWARD_ONE_LINE:
                        if (row < buffRow + candidates.size()) {
                            row++;
                        } else {
                            row = buffRow + 1;
                        }
                        break;
                    case BACKWARD_ONE_LINE:
                        if (row > buffRow + 1) {
                            row--;
                        } else {
                            row = buffRow + candidates.size();
                        }
                        break;
                    case EXIT:
                        return selected;
                }
            }
        }
    }

    private static class CompletingWord implements CompletingParsedLine {
        private final String word;

        public CompletingWord(String word) {
            this.word = word;
        }

        @Override
        public CharSequence escape(CharSequence candidate, boolean complete) {
            return null;
        }

        @Override
        public int rawWordCursor() {
            return word.length();
        }

        @Override
        public int rawWordLength() {
            return word.length();
        }

        @Override
        public String word() {
            return word;
        }

        @Override
        public int wordCursor() {
            return word.length();
        }

        @Override
        public int wordIndex() {
            return 0;
        }

        @Override
        public List words() {
            return new ArrayList<>(Collections.singletonList(word));
        }

        @Override
        public String line() {
            return word;
        }

        @Override
        public int cursor() {
            return word.length();
        }
    }

    private static  int nextRow(int row, int firstItemRow, List items) {
        int itemsSize = items.size();
        int next;
        for (next = row + 1;
                next - firstItemRow < itemsSize
                        && !items.get(next - firstItemRow).isSelectable();
                next++) {}
        if (next - firstItemRow >= itemsSize) {
            for (next = firstItemRow;
                    next - firstItemRow < itemsSize
                            && !items.get(next - firstItemRow).isSelectable();
                    next++) {}
        }
        return next;
    }

    private static  int prevRow(int row, int firstItemRow, List items) {
        int itemsSize = items.size();
        int prev;
        for (prev = row - 1;
                prev - firstItemRow >= 0 && !items.get(prev - firstItemRow).isSelectable();
                prev--) {}
        if (prev - firstItemRow < 0) {
            for (prev = firstItemRow + itemsSize - 1;
                    prev - firstItemRow >= 0 && !items.get(prev - firstItemRow).isSelectable();
                    prev--) {}
        }
        return prev;
    }

    protected static class ListChoicePrompt extends AbstractPrompt {
        private enum Operation {
            FORWARD_ONE_LINE,
            BACKWARD_ONE_LINE,
            INSERT,
            EXIT
        }

        private final List items;

        private ListChoicePrompt(
                Terminal terminal,
                List header,
                AttributedString message,
                List listItems,
                ConsolePrompt.UiConfig cfg) {
            super(terminal, header, message, listItems, cfg);
            items = listItems;
        }

        public static  ListChoicePrompt getPrompt(
                Terminal terminal,
                List header,
                AttributedString message,
                List listItems,
                ConsolePrompt.UiConfig cfg) {
            return new ListChoicePrompt<>(terminal, header, message, listItems, cfg);
        }

        private void bindKeys(KeyMap map) {
            for (char i = 32; i < KEYMAP_LENGTH; i++) {
                map.bind(Operation.INSERT, Character.toString(i));
            }
            map.bind(Operation.FORWARD_ONE_LINE, "e", ctrl('E'), key(terminal, InfoCmp.Capability.key_down));
            map.bind(Operation.BACKWARD_ONE_LINE, "y", ctrl('Y'), key(terminal, InfoCmp.Capability.key_up));
            map.bind(Operation.EXIT, "\r");
        }

        public ListResult execute() {
            resetDisplay();
            int selectRow = nextRow(firstItemRow - 1, firstItemRow, items);
            KeyMap keyMap = new KeyMap<>();
            bindKeys(keyMap);
            while (true) {
                refreshDisplay(selectRow);
                Operation op = bindingReader.readBinding(keyMap);
                switch (op) {
                    case FORWARD_ONE_LINE:
                        selectRow = nextRow(selectRow, firstItemRow, items);
                        break;
                    case BACKWARD_ONE_LINE:
                        selectRow = prevRow(selectRow, firstItemRow, items);
                        break;
                    case INSERT:
                        String ch = bindingReader.getLastBinding();
                        int id = 0;
                        for (ListItemIF cu : items) {
                            if (cu instanceof ChoiceItem) {
                                ChoiceItem ci = (ChoiceItem) cu;
                                if (ci.isSelectable() && ci.getKey().toString().equals(ch)) {
                                    selectRow = firstItemRow + id;
                                    break;
                                }
                            }
                            id++;
                        }
                        break;
                    case EXIT:
                        T listItem = items.get(selectRow - firstItemRow);
                        return new ListResult(listItem.getName());
                }
            }
        }
    }

    protected static class CheckboxPrompt extends AbstractPrompt {
        private enum Operation {
            FORWARD_ONE_LINE,
            BACKWARD_ONE_LINE,
            TOGGLE,
            EXIT
        }

        private final List items;

        private CheckboxPrompt(
                Terminal terminal,
                List header,
                AttributedString message,
                Checkbox checkbox,
                ConsolePrompt.UiConfig cfg) {
            super(terminal, header, message, checkbox.getCheckboxItemList(), cfg);
            items = checkbox.getCheckboxItemList();
        }

        public static CheckboxPrompt getPrompt(
                Terminal terminal,
                List header,
                AttributedString message,
                Checkbox checkbox,
                ConsolePrompt.UiConfig cfg) {
            return new CheckboxPrompt(terminal, header, message, checkbox, cfg);
        }

        private void bindKeys(KeyMap map) {
            map.bind(Operation.FORWARD_ONE_LINE, "e", ctrl('E'), key(terminal, InfoCmp.Capability.key_down));
            map.bind(Operation.BACKWARD_ONE_LINE, "y", ctrl('Y'), key(terminal, InfoCmp.Capability.key_up));
            map.bind(Operation.TOGGLE, " ");
            map.bind(Operation.EXIT, "\r");
        }

        public CheckboxResult execute() {
            resetDisplay();
            int selectRow = nextRow(firstItemRow - 1, firstItemRow, items);
            Set selected = items.stream()
                    .filter(CheckboxItemIF::isChecked)
                    .flatMap(it -> Stream.of(it.getName()))
                    .collect(Collectors.toSet());
            KeyMap keyMap = new KeyMap<>();
            bindKeys(keyMap);
            while (true) {
                refreshDisplay(selectRow, selected);
                Operation op = bindingReader.readBinding(keyMap);
                switch (op) {
                    case FORWARD_ONE_LINE:
                        selectRow = nextRow(selectRow, firstItemRow, items);
                        break;
                    case BACKWARD_ONE_LINE:
                        selectRow = prevRow(selectRow, firstItemRow, items);
                        break;
                    case TOGGLE:
                        if (selected.contains(
                                items.get(selectRow - firstItemRow).getName())) {
                            selected.remove(items.get(selectRow - firstItemRow).getName());
                        } else {
                            selected.add(items.get(selectRow - firstItemRow).getName());
                        }
                        break;
                    case EXIT:
                        return new CheckboxResult(selected);
                }
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy