///|
/// Interactive prompt implementation using async I/O.
///
/// This module provides the core prompt function and helper utilities.
/// Note: In MoonBit, async functions are called like regular functions -
/// they automatically wait for completion when called from another async function.
pub async fn prompt(questions : Array[Question]) -> Answer {
let answers : Answer = {}
for question in questions {
let answer_value = ask_question(question)
answers[question.question_name] = answer_value
}
answers
}
///|
async fn ask_question(question : Question) -> AnswerValue {
match question.question_type {
QuestionType::Input =>
read_input(question.question_message, question.question_default)
QuestionType::Confirm =>
read_confirm(question.question_message, question.question_default)
QuestionType::List =>
read_list(
question.question_message,
question.question_choices,
question.question_default,
)
QuestionType::RawList =>
read_rawlist(
question.question_message,
question.question_choices,
question.question_default,
)
QuestionType::Expand =>
read_expand(question.question_message, question.question_choices)
QuestionType::Checkbox =>
read_checkbox(
question.question_message,
question.question_choices,
question.question_default,
)
QuestionType::Password => read_password(question.question_message)
QuestionType::Editor => read_editor(question.question_message)
}
}
///|
fn string_view_to_string(sv : StringView) -> String {
sv.to_owned()
}
///|
async fn read_input(message : String, default : AnswerValue?) -> AnswerValue {
let default_str = match default {
Some(AnswerValue::StringValue(s)) => s
_ => ""
}
let prompt_str = if default_str.length() > 0 {
"\{message} (\{default_str}): "
} else {
"\{message}: "
}
@stdio.stdout.write(prompt_str)
let input = @stdio.stdin.read_until("\n")
let input = match input {
Some(sv) => string_view_to_string(sv.trim())
None => ""
}
if input.length() == 0 {
AnswerValue::StringValue(default_str)
} else {
AnswerValue::StringValue(input)
}
}
///|
async fn read_confirm(message : String, default : AnswerValue?) -> AnswerValue {
let default_bool = match default {
Some(AnswerValue::BoolValue(b)) => b
_ => true
}
let default_str = if default_bool { "[Y/n]" } else { "[y/N]" }
@stdio.stdout.write("\{message} \{default_str}: ")
let input = @stdio.stdin.read_until("\n")
let input = match input {
Some(sv) => string_view_to_string(sv.trim().to_upper())
None => ""
}
match input {
"Y" | "YES" | "TRUE" | "1" => AnswerValue::BoolValue(true)
"N" | "NO" | "FALSE" | "0" => AnswerValue::BoolValue(false)
_ => AnswerValue::BoolValue(default_bool)
}
}
///|
async fn read_list(
message : String,
choices : Array[String]?,
default : AnswerValue?,
) -> AnswerValue {
match choices {
Some(choices) if choices.length() > 0 => {
@stdio.stdout.write("\{message}\n")
for i, choice in choices {
let default_marker = match default {
Some(AnswerValue::IntValue(idx)) if idx == i => " (default)"
_ => ""
}
@stdio.stdout.write(" \{i + 1}. \{choice}\{default_marker}\n")
}
let red = "\u001b[31m"
let reset = "\u001b[0m"
let n = choices.length()
let mut result = AnswerValue::StringValue(choices[0])
let mut done = false
while !done {
@stdio.stdout.write("Choose an option: ")
let input = @stdio.stdin.read_until("\n")
let input = match input {
Some(sv) => string_view_to_string(sv.trim())
None => ""
}
match safe_parse_int(input) {
Some(num) if num >= 1 && num <= n => {
result = AnswerValue::StringValue(choices[num - 1])
done = true
}
Some(num) =>
@stdio.stdout.write(
red +
"Option " +
num.to_string() +
" does not exist. Please enter a number between 1 and " +
n.to_string() +
".\n" +
reset,
)
None if input.length() == 0 => {
let default_idx = match default {
Some(AnswerValue::IntValue(idx)) if idx >= 0 && idx < n => idx
_ => 0
}
result = AnswerValue::StringValue(choices[default_idx])
done = true
}
_ =>
@stdio.stdout.write(
red +
"'" +
input +
"' is not a valid number. Please enter a number between 1 and " +
n.to_string() +
".\n" +
reset,
)
}
}
result
}
_ => AnswerValue::StringValue("")
}
}
///|
async fn read_rawlist(
message : String,
choices : Array[String]?,
default : AnswerValue?,
) -> AnswerValue {
match choices {
Some(choices) if choices.length() > 0 => {
@stdio.stdout.write("\{message}\n")
for i, choice in choices {
let default_marker = match default {
Some(AnswerValue::IntValue(idx)) if idx == i => " (default)"
_ => ""
}
@stdio.stdout.write(" \{i + 1}. \{choice}\{default_marker}\n")
}
@stdio.stdout.write("Choose an option: ")
let input = @stdio.stdin.read_until("\n")
let input = match input {
Some(sv) => string_view_to_string(sv.trim())
None => ""
}
match safe_parse_int(input) {
Some(num) if num >= 1 && num <= choices.length() =>
AnswerValue::StringValue(choices[num - 1])
_ => {
let default_idx = match default {
Some(AnswerValue::IntValue(idx)) if idx >= 0 &&
idx < choices.length() => idx
_ => 0
}
AnswerValue::StringValue(choices[default_idx])
}
}
}
_ => AnswerValue::StringValue("")
}
}
///|
async fn read_expand(message : String, choices : Array[String]?) -> AnswerValue {
match choices {
Some(choices) if choices.length() > 0 => {
@stdio.stdout.write("\{message}\n")
for i, choice in choices {
let key = Int::unsafe_to_char(97 + i).to_string() // a, b, c, ...
@stdio.stdout.write(" [\{key}] \{choice}\n")
}
@stdio.stdout.write("Choose an option: ")
let input = @stdio.stdin.read_until("\n")
let input = match input {
Some(sv) => string_view_to_string(sv.trim().to_upper())
None => ""
}
// Find matching key
for i, _ in choices {
let key = Int::unsafe_to_char(97 + i).to_string().to_upper()
if input == key {
return AnswerValue::StringValue(choices[i])
}
}
// Default to first option
AnswerValue::StringValue(choices[0])
}
_ => AnswerValue::StringValue("")
}
}
///|
async fn read_checkbox(
message : String,
choices : Array[String]?,
default : AnswerValue?,
) -> AnswerValue {
match choices {
Some(choices) if choices.length() > 0 => {
@stdio.stdout.write("\{message}\n")
let default_arr = match default {
Some(AnswerValue::ArrayValue(arr)) => arr
_ => []
}
for i, choice in choices {
let marker = if default_arr.contains(choice) { "[x]" } else { "[ ]" }
@stdio.stdout.write(" \{marker} \{i + 1}. \{choice}\n")
}
let red = "\u001b[31m"
let reset = "\u001b[0m"
let n = choices.length()
let mut result = AnswerValue::ArrayValue([])
let mut done = false
while !done {
@stdio.stdout.write(
"\nEnter numbers separated by commas (e.g., 1,3,5), 0 for all, or press Enter for default: ",
)
let input = @stdio.stdin.read_until("\n")
let input = match input {
Some(sv) => string_view_to_string(sv.trim())
None => ""
}
if input.length() == 0 {
if default_arr.length() > 0 {
result = AnswerValue::ArrayValue(default_arr)
} else {
result = AnswerValue::ArrayValue(choices)
}
done = true
} else if input == "0" {
result = AnswerValue::ArrayValue(choices)
done = true
} else {
let selected : Array[String] = []
let invalid_nums : Array[Int] = []
let parts = input.split(",")
for part_sv in parts {
let part = string_view_to_string(part_sv.trim())
match safe_parse_int(part) {
Some(num) if num >= 1 && num <= n =>
selected.push(choices[num - 1])
Some(num) => invalid_nums.push(num)
_ => ignore(())
}
}
if invalid_nums.length() > 0 {
let invalid_strs : Array[String] = []
for n in invalid_nums {
invalid_strs.push(n.to_string())
}
@stdio.stdout.write(
red +
"Options do not exist: " +
invalid_strs.join(", ") +
". Please enter numbers between 1 and " +
n.to_string() +
".\n" +
reset,
)
} else if selected.length() == 0 {
@stdio.stdout.write(
red +
"No valid options selected. Please enter numbers between 1 and " +
n.to_string() +
".\n" +
reset,
)
} else {
result = AnswerValue::ArrayValue(selected)
done = true
}
}
}
result
}
_ => AnswerValue::ArrayValue([])
}
}
///|
async fn read_password(message : String) -> AnswerValue {
@stdio.stdout.write("\{message}: ")
let input = @stdio.stdin.read_until("\n")
let input = match input {
Some(sv) => string_view_to_string(sv.trim())
None => ""
}
AnswerValue::StringValue(input)
}
///|
async fn read_editor(message : String) -> AnswerValue {
@stdio.stdout.write(
"\{message} (Enter your text, press Enter once to finish):\n",
)
// Read single line for editor
let input = @stdio.stdin.read_until("\n")
let input = match input {
Some(sv) => string_view_to_string(sv.trim())
None => ""
}
AnswerValue::StringValue(input)
}
///|
/// Safe wrapper for @string.parse_int that returns Int? instead of raising
pub fn safe_parse_int(s : String) -> Int? {
Some(@string.parse_int(s)) catch {
_ => None
}
}
///|
/// Alias for backwards compatibility
pub fn parse_int(s : String) -> Int? {
safe_parse_int(s)
}