Implement stdio prompt function

This commit is contained in:
Filip Tibell 2023-02-06 12:59:48 -05:00
parent bbf1c9f4f7
commit ee64d2ef9c
No known key found for this signature in database
8 changed files with 306 additions and 3 deletions

View file

@ -9,9 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a new global `stdio` which replaces `console` and adds a couple new functions:
- `write` writes a string directly to stdout, without any newlines
- `ewrite` writes a string directly to stderr, without any newlines
- Added a new global `stdio` which replaces `console`
- Added `stdio.write` which writes a string directly to stdout, without any newlines
- Added `stdio.ewrite` which writes a string directly to stderr, without any newlines
- Added `stdio.prompt` which will prompt the user for different kinds of input
Example usage:
```lua
local text = stdio.prompt()
local text2 = stdio.prompt("text", "Please write some text")
local didConfirm = stdio.prompt("confirm", "Please confirm this action")
local optionIndex = stdio.prompt("select", "Please select an option", { "one", "two", "three" })
local optionIndices = stdio.prompt(
"multiselect",
"Please select one or more options",
{ "one", "two", "three", "four", "five" }
)
```
### Changed

66
Cargo.lock generated
View file

@ -149,6 +149,18 @@ dependencies = [
"syn",
]
[[package]]
name = "dialoguer"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3c796f3b0b408d9fd581611b47fa850821fcb84aa640b83a3c1a5be2d691f2"
dependencies = [
"console",
"shell-words",
"tempfile",
"zeroize",
]
[[package]]
name = "encode_unicode"
version = "0.3.6"
@ -194,6 +206,15 @@ dependencies = [
"libc",
]
[[package]]
name = "fastrand"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
dependencies = [
"instant",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -426,6 +447,15 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "io-lifetimes"
version = "1.0.5"
@ -544,6 +574,7 @@ version = "0.2.2"
dependencies = [
"anyhow",
"console",
"dialoguer",
"hyper",
"lazy_static",
"mlua",
@ -787,6 +818,15 @@ version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "reqwest"
version = "0.11.14"
@ -962,6 +1002,12 @@ dependencies = [
"serde",
]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
@ -1028,6 +1074,20 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [
"cfg-if",
"fastrand",
"libc",
"redox_syscall",
"remove_dir_all",
"winapi",
]
[[package]]
name = "termcolor"
version = "1.2.0"
@ -1421,3 +1481,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "zeroize"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"

View file

@ -86,6 +86,15 @@ globals:
stdio.ewrite:
args:
- type: string
stdio.prompt:
must_use: true
args:
- required: false
type: string
- required: false
type: string
- required: false
type: string | boolean | table
# Task
task.cancel:
args:

View file

@ -387,6 +387,35 @@
"@roblox/global/stdio.format/return/0": {
"documentation": "The formatted string"
},
"@roblox/global/stdio.prompt": {
"code_sample": "",
"documentation": "Prompts for user input using the wanted kind of prompt:\n\n* `\"text\"` - Prompts for a plain text string from the user\n* `\"confirm\"` - Prompts the user to confirm with y / n\n* `\"select\"` - Prompts the user to select *one* value from a list\n* `\"multiselect\"` - Prompts the user to select *one or more* values from a list\n* `nil` - Equivalent to `\"text\"` with no extra arguments",
"learn_more_link": "",
"params": [
{
"documentation": "@roblox/global/stdio.prompt/param/0",
"name": "kind"
},
{
"documentation": "@roblox/global/stdio.prompt/param/1",
"name": "message"
},
{
"documentation": "@roblox/global/stdio.prompt/param/2",
"name": "defaultOrOptions"
}
],
"returns": []
},
"@roblox/global/stdio.prompt/param/0": {
"documentation": "The kind of prompt to use"
},
"@roblox/global/stdio.prompt/param/1": {
"documentation": "The message to show the user"
},
"@roblox/global/stdio.prompt/param/2": {
"documentation": "The default value for the prompt, or options to choose from for selection prompts"
},
"@roblox/global/stdio.style": {
"code_sample": "",
"documentation": "Return an ANSI string that can be used to modify the persistent output style.\n\nPass `\"reset\"` to get a string that can reset the persistent output style.\n\n### Example usage\n\n```lua\nstdio.write(stdio.style(\"bold\"))\nprint(\"This text will be bold\")\nstdio.write(stdio.style(\"reset\"))\nprint(\"This text will be normal\")\n```",

View file

@ -351,6 +351,28 @@ declare stdio: {
@param s The string to write to stderr
]=]
ewrite: (s: string) -> (),
--[=[
@within stdio
Prompts for user input using the wanted kind of prompt:
* `"text"` - Prompts for a plain text string from the user
* `"confirm"` - Prompts the user to confirm with y / n
* `"select"` - Prompts the user to select *one* value from a list
* `"multiselect"` - Prompts the user to select *one or more* values from a list
* `nil` - Equivalent to `"text"` with no extra arguments
@param kind The kind of prompt to use
@param message The message to show the user
@param defaultOrOptions The default value for the prompt, or options to choose from for selection prompts
]=]
prompt: (
(() -> string)
& ((kind: "text", message: string?, defaultOrOptions: string?) -> string)
& ((kind: "confirm", message: string, defaultOrOptions: boolean?) -> boolean)
& ((kind: "select", message: string?, defaultOrOptions: { string }) -> number?)
& ((kind: "multiselect", message: string?, defaultOrOptions: { string }) -> { number }?)
),
}
--[=[

View file

@ -21,6 +21,7 @@ tokio.workspace = true
reqwest.workspace = true
console = "0.15.5"
dialoguer = "0.10.3"
lazy_static = "1.4.0"
os_str_bytes = "6.4.1"

View file

@ -1,3 +1,4 @@
use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
use mlua::prelude::*;
use crate::utils::{
@ -30,6 +31,125 @@ pub fn create(lua: &Lua) -> LuaResult<()> {
eprint!("{s}");
Ok(())
})?
.with_function("prompt", prompt)?
.build_readonly()?,
)
}
fn prompt_theme() -> ColorfulTheme {
ColorfulTheme::default()
}
fn prompt<'a>(
lua: &'a Lua,
(kind, message, options): (Option<String>, Option<String>, LuaValue<'a>),
) -> LuaResult<LuaValue<'a>> {
match kind.map(|k| k.trim().to_ascii_lowercase()).as_deref() {
None | Some("text") => {
let theme = prompt_theme();
let mut prompt = Input::with_theme(&theme);
if let Some(message) = message {
prompt.with_prompt(message);
};
if let LuaValue::String(s) = options {
let txt = String::from_lua(LuaValue::String(s), lua)?;
prompt.with_initial_text(&txt);
};
let input: String = prompt.allow_empty(true).interact_text()?;
Ok(LuaValue::String(lua.create_string(&input)?))
}
Some("confirm") => {
if let Some(message) = message {
let theme = prompt_theme();
let mut prompt = Confirm::with_theme(&theme);
if let LuaValue::Boolean(b) = options {
prompt.default(b);
};
let result = prompt.with_prompt(&message).interact()?;
Ok(LuaValue::Boolean(result))
} else {
Err(LuaError::RuntimeError(
"Argument #2 missing or nil".to_string(),
))
}
}
Some(s) if matches!(s, "select" | "multiselect") => {
let options = match options {
LuaValue::Table(t) => {
let v: Vec<String> = Vec::from_lua(LuaValue::Table(t), lua)?;
if v.len() < 2 {
return Err(LuaError::RuntimeError(
"Options table must contain at least 2 options".to_string(),
));
}
v
}
LuaValue::Nil => {
return Err(LuaError::RuntimeError(
"Argument #3 missing or nil".to_string(),
))
}
value => {
return Err(LuaError::RuntimeError(format!(
"Argument #3 must be a table, got '{}'",
value.type_name()
)))
}
};
if let Some(message) = message {
match s {
"select" => {
let chosen = Select::with_theme(&prompt_theme())
.with_prompt(&message)
.items(&options)
.interact_opt()?;
Ok(match chosen {
Some(idx) => LuaValue::Number((idx + 1) as f64),
None => LuaValue::Nil,
})
}
"multiselect" => {
let chosen = MultiSelect::with_theme(&prompt_theme())
.with_prompt(&message)
.items(&options)
.interact_opt()?;
Ok(match chosen {
Some(indices) => indices
.iter()
.map(|idx| (*idx + 1) as f64)
.collect::<Vec<_>>()
.to_lua(lua)?,
None => LuaValue::Nil,
})
}
_ => unreachable!(),
}
} else {
match s {
"select" => {
let chosen = Select::new().items(&options).interact_opt()?;
Ok(match chosen {
Some(idx) => LuaValue::Number((idx + 1) as f64),
None => LuaValue::Nil,
})
}
"multiselect" => {
let chosen = MultiSelect::new().items(&options).interact_opt()?;
Ok(match chosen {
Some(indices) => indices
.iter()
.map(|idx| (*idx + 1) as f64)
.collect::<Vec<_>>()
.to_lua(lua)?,
None => LuaValue::Nil,
})
}
_ => unreachable!(),
}
}
}
Some(s) => Err(LuaError::RuntimeError(format!(
"Invalid stdio prompt kind: '{s}'"
))),
}
}

37
tests/stdio/prompt.luau Normal file
View file

@ -0,0 +1,37 @@
-- NOTE: This test is intentionally not included in the
-- automated tests suite since it requires user input
-- Text prompt
local text = stdio.prompt("text", "Type some text")
assert(#text > 0, "Did not get any text")
print(`Got text '{text}'\n`)
-- Confirmation prompt
local confirmed = stdio.prompt("confirm", "Please confirm", true)
assert(confirmed == true, "Did not get true as result")
print(if confirmed then "Confirmed\n" else "Did not confirm\n")
-- Selection prompt
local option = stdio.prompt(
"select",
"Please select the first option from the list",
{ "one", "two", "three", "four" }
)
assert(option == 1, "Did not get the first option as result")
print(if option then `Got option #{option}\n` else "Got no option\n")
-- Multi-selection prompt
local options = stdio.prompt(
"multiselect",
"Please select options two and four",
{ "one", "two", "three", "four", "five" }
)
assert(
options ~= nil and table.find(options, 2) and table.find(options, 4),
"Did not get options 2 and 4 as result"
)
print(if options then `Got option(s) {stdio.format(options)}\n` else "Got no option\n")