mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Implement stdio prompt function
This commit is contained in:
parent
bbf1c9f4f7
commit
ee64d2ef9c
8 changed files with 306 additions and 3 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -9,9 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added a new global `stdio` which replaces `console` and adds a couple new functions:
|
- Added a new global `stdio` which replaces `console`
|
||||||
- `write` writes a string directly to stdout, without any newlines
|
- Added `stdio.write` which writes a string directly to stdout, without any newlines
|
||||||
- `ewrite` writes a string directly to stderr, 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
|
### Changed
|
||||||
|
|
||||||
|
|
66
Cargo.lock
generated
66
Cargo.lock
generated
|
@ -149,6 +149,18 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "encode_unicode"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
@ -194,6 +206,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -426,6 +447,15 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-lifetimes"
|
name = "io-lifetimes"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
@ -544,6 +574,7 @@ version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"console",
|
"console",
|
||||||
|
"dialoguer",
|
||||||
"hyper",
|
"hyper",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"mlua",
|
"mlua",
|
||||||
|
@ -787,6 +818,15 @@ version = "0.6.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
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]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.11.14"
|
version = "0.11.14"
|
||||||
|
@ -962,6 +1002,12 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-words"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -1028,6 +1074,20 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -1421,3 +1481,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"
|
||||||
|
|
9
lune.yml
9
lune.yml
|
@ -86,6 +86,15 @@ globals:
|
||||||
stdio.ewrite:
|
stdio.ewrite:
|
||||||
args:
|
args:
|
||||||
- type: string
|
- type: string
|
||||||
|
stdio.prompt:
|
||||||
|
must_use: true
|
||||||
|
args:
|
||||||
|
- required: false
|
||||||
|
type: string
|
||||||
|
- required: false
|
||||||
|
type: string
|
||||||
|
- required: false
|
||||||
|
type: string | boolean | table
|
||||||
# Task
|
# Task
|
||||||
task.cancel:
|
task.cancel:
|
||||||
args:
|
args:
|
||||||
|
|
|
@ -387,6 +387,35 @@
|
||||||
"@roblox/global/stdio.format/return/0": {
|
"@roblox/global/stdio.format/return/0": {
|
||||||
"documentation": "The formatted string"
|
"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": {
|
"@roblox/global/stdio.style": {
|
||||||
"code_sample": "",
|
"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```",
|
"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```",
|
||||||
|
|
|
@ -351,6 +351,28 @@ declare stdio: {
|
||||||
@param s The string to write to stderr
|
@param s The string to write to stderr
|
||||||
]=]
|
]=]
|
||||||
ewrite: (s: string) -> (),
|
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 }?)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
|
|
|
@ -21,6 +21,7 @@ tokio.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
|
||||||
console = "0.15.5"
|
console = "0.15.5"
|
||||||
|
dialoguer = "0.10.3"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
os_str_bytes = "6.4.1"
|
os_str_bytes = "6.4.1"
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
|
@ -30,6 +31,125 @@ pub fn create(lua: &Lua) -> LuaResult<()> {
|
||||||
eprint!("{s}");
|
eprint!("{s}");
|
||||||
Ok(())
|
Ok(())
|
||||||
})?
|
})?
|
||||||
|
.with_function("prompt", prompt)?
|
||||||
.build_readonly()?,
|
.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
37
tests/stdio/prompt.luau
Normal 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")
|
Loading…
Reference in a new issue