Skip to content

Commit

Permalink
Dynamic console-ui prompt improvements, see #1051 (#1132)
Browse files Browse the repository at this point in the history
This adds an easier way to create complex dynamic prompts
  • Loading branch information
quintesse authored Dec 18, 2024
1 parent 3c63478 commit ffce720
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.jline.builtins.Styles;
Expand Down Expand Up @@ -141,6 +142,78 @@ public Map<String, PromptResultItemIF> prompt(
}
}

/**
* Prompt a list of choices (questions). This method takes a function that given a map of interim results
* returns a list of promptable elements (typically created with {@link PromptBuilder}). Each list is then
* passed to {@link #prompt(List, List, Map)} and the result added to the map of interim results.
* The function is then called again with the updated map of results until the function returns null.
* The final result map contains the key of each promptable element and the user entry as an object
* implementing {@link PromptResultItemIF}.
*
* @param promptableElementLists a function returning lists of questions / prompts to ask the user for.
* @throws IOException may be thrown by terminal
*/
public Map<String, PromptResultItemIF> prompt(
Function<Map<String, PromptResultItemIF>, List<PromptableElementIF>> promptableElementLists)
throws IOException {
return prompt(new ArrayList<>(), promptableElementLists);
}

/**
* Prompt a list of choices (questions). This method takes a function that given a map of interim results
* returns a list of promptable elements (typically created with {@link PromptBuilder}). Each list is then
* passed to {@link #prompt(List, List, Map)} and the result added to the map of interim results.
* The function is then called again with the updated map of results until the function returns null.
* The final result map contains the key of each promptable element and the user entry as an object
* implementing {@link PromptResultItemIF}.
*
* @param headerIn info to be displayed before first prompt.
* @param promptableElementLists a function returning lists of questions / prompts to ask the user for.
* @throws IOException may be thrown by terminal
*/
public Map<String, PromptResultItemIF> prompt(
List<AttributedString> headerIn,
Function<Map<String, PromptResultItemIF>, List<PromptableElementIF>> promptableElementLists)
throws IOException {
Map<String, PromptResultItemIF> resultMap = new HashMap<>();
Deque<List<PromptableElementIF>> prevLists = new ArrayDeque<>();
Deque<Map<String, PromptResultItemIF>> prevResults = new ArrayDeque<>();
boolean cancellable = config.cancellableFirstPrompt();
// Get our first list of prompts
List<PromptableElementIF> peList = promptableElementLists.apply(new HashMap<>());
Map<String, PromptResultItemIF> peResult = new HashMap<>();
while (peList != null) {
// Second and later prompts should always be cancellable
config.setCancellableFirstPrompt(!prevLists.isEmpty() || cancellable);
// Prompt the user
prompt(headerIn, peList, peResult);
if (peResult.isEmpty()) {
// The prompt was cancelled by the user, so let's go back to the
// previous list of prompts and its results (if any)
peList = prevLists.pollFirst();
peResult = prevResults.pollFirst();
if (peResult != null) {
// Remove the results of the previous prompt from the main result map
peResult.forEach((k, v) -> resultMap.remove(k));
headerIn.remove(headerIn.size() - 1);
}
} else {
// We remember the list of prompts and their results
prevLists.push(peList);
prevResults.push(peResult);
// Add the results to the main result map
resultMap.putAll(peResult);
// And we get our next list of prompts (if any)
peList = promptableElementLists.apply(resultMap);
peResult = new HashMap<>();
}
}
// Restore the original state of cancellable
config.setCancellableFirstPrompt(cancellable);

return resultMap;
}

/**
* Prompt a list of choices (questions). This method takes a list of promptable elements, typically
* created with {@link PromptBuilder}. Each of the elements is processed and the user entries and
Expand Down Expand Up @@ -190,7 +263,6 @@ public void prompt(
continue;
} else {
if (config.cancellableFirstPrompt()) {
header.remove(header.size() - 1);
resultMap.clear();
return;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@

import java.io.IOError;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jline.consoleui.elements.ConfirmChoice;
import org.jline.consoleui.elements.PromptableElementIF;
import org.jline.consoleui.prompt.ConfirmResult;
import org.jline.consoleui.prompt.ConsolePrompt;
import org.jline.consoleui.prompt.PromptResultItemIF;
Expand Down Expand Up @@ -70,49 +70,53 @@ public static void main(String[] args) {
// LineReader is needed only if you are adding JLine Completers in your prompts.
// If you are not using Completers you do not need to create LineReader.
//
Map<String, PromptResultItemIF> result;
LineReader reader = LineReaderBuilder.builder().terminal(terminal).build();
Map<String, PromptResultItemIF> result1 = new HashMap<>(),
result2 = new HashMap<>(),
result3 = new HashMap<>();

try (ConsolePrompt prompt = new ConsolePrompt(reader, terminal, config)) {
while (result2.isEmpty()) {
prompt.prompt(header, pizzaOrHamburgerPrompt(prompt).build(), result1);
if (result1.isEmpty()) {
throw new Exception("User cancelled order.");
result = prompt.prompt(header, results -> {
if (results.isEmpty()) {
// No results yet, so we start with the first list of questions
return pizzaOrHamburgerPrompt(prompt);
}
while (result3.isEmpty()) {
if ("Pizza".equals(result1.get("product").getResult())) {
prompt.prompt(header, pizzaPrompt(prompt).build(), result2);
} else {
prompt.prompt(header, hamburgerPrompt(prompt).build(), result2);
// We have some results, so we know that the user chose a "product",
// so we can return the next list of questions based on that choice
if ("Pizza".equals(results.get("product").getResult())) {
// Check if the pizza questions were already answered
if (!results.containsKey("pizzatype")) {
// No, so let's return the pizza questions
return pizzaPrompt(prompt);
}
if (result2.isEmpty()) {
break;
} else {
// Check if the hamburger questions were already answered
if (!results.containsKey("hamburgertype")) {
// No, so let's return the hamburger questions
return hamburgerPrompt(prompt);
}
prompt.prompt(header, finalPrompt(prompt).build(), result3);
}
}
// Check if the final questions were already answered
if (!results.containsKey("payment")) {
return finalPrompt(prompt);
}
return null;
});
}

Map<String, PromptResultItemIF> result = new HashMap<>(result1);
result.putAll(result2);
result.putAll(result3);
System.out.println("result = " + result);

ConfirmResult delivery = (ConfirmResult) result.get("delivery");
if (delivery.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) {
System.out.println("We will deliver the order in 5 minutes");
if (result.isEmpty()) {
System.out.println("User cancelled order.");
} else {
ConfirmResult delivery = (ConfirmResult) result.get("delivery");
if (delivery.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) {
System.out.println("We will deliver the order in 5 minutes");
}
}

} catch (IOError e) {
System.out.println("<ctrl>-c pressed");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}

static PromptBuilder pizzaOrHamburgerPrompt(ConsolePrompt prompt) {
static List<PromptableElementIF> pizzaOrHamburgerPrompt(ConsolePrompt prompt) {
PromptBuilder promptBuilder = prompt.getPromptBuilder();
promptBuilder
.createInputPrompt()
Expand All @@ -132,10 +136,10 @@ static PromptBuilder pizzaOrHamburgerPrompt(ConsolePrompt prompt) {
.text("Hamburger")
.add()
.addPrompt();
return promptBuilder;
return promptBuilder.build();
}

static PromptBuilder pizzaPrompt(ConsolePrompt prompt) {
static List<PromptableElementIF> pizzaPrompt(ConsolePrompt prompt) {
PromptBuilder promptBuilder = prompt.getPromptBuilder();
promptBuilder
.createListPrompt()
Expand Down Expand Up @@ -188,10 +192,10 @@ static PromptBuilder pizzaPrompt(ConsolePrompt prompt) {
.checked(true)
.add()
.addPrompt();
return promptBuilder;
return promptBuilder.build();
}

static PromptBuilder hamburgerPrompt(ConsolePrompt prompt) {
static List<PromptableElementIF> hamburgerPrompt(ConsolePrompt prompt) {
PromptBuilder promptBuilder = prompt.getPromptBuilder();
promptBuilder
.createListPrompt()
Expand Down Expand Up @@ -232,10 +236,10 @@ static PromptBuilder hamburgerPrompt(ConsolePrompt prompt) {
.check()
.add()
.addPrompt();
return promptBuilder;
return promptBuilder.build();
}

static PromptBuilder finalPrompt(ConsolePrompt prompt) {
static List<PromptableElementIF> finalPrompt(ConsolePrompt prompt) {
PromptBuilder promptBuilder = prompt.getPromptBuilder();
promptBuilder
.createChoicePrompt()
Expand Down Expand Up @@ -268,6 +272,6 @@ static PromptBuilder finalPrompt(ConsolePrompt prompt) {
.message("Is this order for delivery?")
.defaultValue(ConfirmChoice.ConfirmationValue.YES)
.addPrompt();
return promptBuilder;
return promptBuilder.build();
}
}

0 comments on commit ffce720

Please sign in to comment.