From ac3721f7d05839d17fedd754a1561dd65908b56c Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sun, 21 Apr 2024 19:03:28 +0200 Subject: [PATCH] Migrate stdio builtin to new crate --- Cargo.lock | 7 + crates/lune-std-stdio/Cargo.toml | 9 ++ crates/lune-std-stdio/src/lib.rs | 80 ++++++++++ crates/lune-std-stdio/src/prompt.rs | 227 ++++++++++++++++++++++++++++ crates/lune-std-task/Cargo.toml | 2 +- crates/lune-std-task/src/lib.rs | 2 +- 6 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 crates/lune-std-stdio/src/prompt.rs diff --git a/Cargo.lock b/Cargo.lock index 94fddce..8e585e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1588,6 +1588,13 @@ version = "0.8.3" [[package]] name = "lune-std-stdio" version = "0.8.3" +dependencies = [ + "dialoguer", + "lune-utils", + "mlua", + "mlua-luau-scheduler 0.0.1", + "tokio", +] [[package]] name = "lune-std-task" diff --git a/crates/lune-std-stdio/Cargo.toml b/crates/lune-std-stdio/Cargo.toml index a018bd6..c51398f 100644 --- a/crates/lune-std-stdio/Cargo.toml +++ b/crates/lune-std-stdio/Cargo.toml @@ -9,3 +9,12 @@ path = "src/lib.rs" [lints] workspace = true + +[dependencies] +dialoguer = "0.11" +mlua = "0.9.7" +mlua-luau-scheduler = "0.0.1" + +tokio = { version = "1", default-features = false } + +lune-utils = { version = "0.8.3", path = "../lune-utils" } diff --git a/crates/lune-std-stdio/src/lib.rs b/crates/lune-std-stdio/src/lib.rs index 2e802e7..b1f9d7a 100644 --- a/crates/lune-std-stdio/src/lib.rs +++ b/crates/lune-std-stdio/src/lib.rs @@ -1 +1,81 @@ #![allow(clippy::cargo_common_metadata)] + +use mlua::prelude::*; +use mlua_luau_scheduler::LuaSpawnExt; + +use tokio::io::{stderr, stdin, stdout, AsyncReadExt, AsyncWriteExt}; + +use lune_utils::TableBuilder; + +mod prompt; + +use self::prompt::{prompt, PromptOptions, PromptResult}; + +/** + Creates the `stdio` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { + TableBuilder::new(lua)? + .with_function("color", stdio_color)? + .with_function("style", stdio_style)? + .with_function("format", stdio_format)? + .with_async_function("write", stdio_write)? + .with_async_function("ewrite", stdio_ewrite)? + .with_async_function("readToEnd", stdio_read_to_end)? + .with_async_function("prompt", stdio_prompt)? + .build_readonly() +} + +fn stdio_color(_: &Lua, _color: String) -> LuaResult { + // TODO: Migrate from old crate + unimplemented!() +} + +fn stdio_style(_: &Lua, _color: String) -> LuaResult { + // TODO: Migrate from old crate + unimplemented!() +} + +fn stdio_format(_: &Lua, _args: LuaMultiValue) -> LuaResult { + // TODO: Migrate from old crate + unimplemented!() +} + +async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { + let mut stdout = stdout(); + stdout.write_all(s.as_bytes()).await?; + stdout.flush().await?; + Ok(()) +} + +async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { + let mut stderr = stderr(); + stderr.write_all(s.as_bytes()).await?; + stderr.flush().await?; + Ok(()) +} + +/* + FUTURE: Figure out how to expose some kind of "readLine" function using a buffered reader. + + This is a bit tricky since we would want to be able to use **both** readLine and readToEnd + in the same script, doing something like readLine, readLine, readToEnd from lua, and + having that capture the first two lines and then read the rest of the input. +*/ + +async fn stdio_read_to_end(lua: &Lua, (): ()) -> LuaResult { + let mut input = Vec::new(); + let mut stdin = stdin(); + stdin.read_to_end(&mut input).await?; + lua.create_string(&input) +} + +async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult { + lua.spawn_blocking(move || prompt(options)) + .await + .into_lua_err() +} diff --git a/crates/lune-std-stdio/src/prompt.rs b/crates/lune-std-stdio/src/prompt.rs new file mode 100644 index 0000000..e1fcc02 --- /dev/null +++ b/crates/lune-std-stdio/src/prompt.rs @@ -0,0 +1,227 @@ +use std::{fmt, str::FromStr}; + +use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select}; +use mlua::prelude::*; + +#[derive(Debug, Clone, Copy)] +pub enum PromptKind { + Text, + Confirm, + Select, + MultiSelect, +} + +impl PromptKind { + const ALL: [PromptKind; 4] = [Self::Text, Self::Confirm, Self::Select, Self::MultiSelect]; +} + +impl Default for PromptKind { + fn default() -> Self { + Self::Text + } +} + +impl FromStr for PromptKind { + type Err = (); + fn from_str(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_str() { + "text" => Ok(Self::Text), + "confirm" => Ok(Self::Confirm), + "select" => Ok(Self::Select), + "multiselect" => Ok(Self::MultiSelect), + _ => Err(()), + } + } +} + +impl fmt::Display for PromptKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Text => "Text", + Self::Confirm => "Confirm", + Self::Select => "Select", + Self::MultiSelect => "MultiSelect", + } + ) + } +} + +impl<'lua> FromLua<'lua> for PromptKind { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + if let LuaValue::Nil = value { + Ok(Self::default()) + } else if let LuaValue::String(s) = value { + let s = s.to_str()?; + s.parse().map_err(|()| LuaError::FromLuaConversionError { + from: "string", + to: "PromptKind", + message: Some(format!( + "Invalid prompt kind '{s}', valid kinds are:\n{}", + PromptKind::ALL + .iter() + .map(ToString::to_string) + .collect::>() + .join(", ") + )), + }) + } else { + Err(LuaError::FromLuaConversionError { + from: "nil", + to: "PromptKind", + message: None, + }) + } + } +} + +pub struct PromptOptions { + pub kind: PromptKind, + pub text: Option, + pub default_string: Option, + pub default_bool: Option, + pub options: Option>, +} + +impl<'lua> FromLuaMulti<'lua> for PromptOptions { + fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult { + // Argument #1 - prompt kind (optional) + let kind = values + .pop_front() + .map(|value| PromptKind::from_lua(value, lua)) + .transpose()? + .unwrap_or_default(); + // Argument #2 - prompt text (optional) + let text = values + .pop_front() + .map(|text| String::from_lua(text, lua)) + .transpose()?; + // Argument #3 - default value / options, + // this is different per each prompt kind + let (default_bool, default_string, options) = match values.pop_front() { + None => (None, None, None), + Some(options) => match options { + LuaValue::Nil => (None, None, None), + LuaValue::Boolean(b) => (Some(b), None, None), + LuaValue::String(s) => ( + None, + Some(String::from_lua(LuaValue::String(s), lua)?), + None, + ), + LuaValue::Table(t) => ( + None, + None, + Some(Vec::::from_lua(LuaValue::Table(t), lua)?), + ), + value => { + return Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "PromptOptions", + message: Some("Argument #3 must be a boolean, table, or nil".to_string()), + }) + } + }, + }; + /* + Make sure we got the required values for the specific prompt kind: + + - "Confirm" requires a message to be present so the user knows what they are confirming + - "Select" and "MultiSelect" both require a table of options to choose from + */ + if matches!(kind, PromptKind::Confirm) && text.is_none() { + return Err(LuaError::FromLuaConversionError { + from: "nil", + to: "PromptOptions", + message: Some("Argument #2 missing or nil".to_string()), + }); + } + if matches!(kind, PromptKind::Select | PromptKind::MultiSelect) && options.is_none() { + return Err(LuaError::FromLuaConversionError { + from: "nil", + to: "PromptOptions", + message: Some("Argument #3 missing or nil".to_string()), + }); + } + // All good, return the prompt options + Ok(Self { + kind, + text, + default_string, + default_bool, + options, + }) + } +} + +#[derive(Debug, Clone)] +pub enum PromptResult { + String(String), + Boolean(bool), + Index(usize), + Indices(Vec), + None, +} + +impl<'lua> IntoLua<'lua> for PromptResult { + fn into_lua(self, lua: &'lua Lua) -> LuaResult> { + Ok(match self { + Self::String(s) => LuaValue::String(lua.create_string(&s)?), + Self::Boolean(b) => LuaValue::Boolean(b), + Self::Index(i) => LuaValue::Number(i as f64), + Self::Indices(v) => v.into_lua(lua)?, + Self::None => LuaValue::Nil, + }) + } +} + +pub fn prompt(options: PromptOptions) -> LuaResult { + let theme = ColorfulTheme::default(); + match options.kind { + PromptKind::Text => { + let input: String = Input::with_theme(&theme) + .allow_empty(true) + .with_prompt(options.text.unwrap_or_default()) + .with_initial_text(options.default_string.unwrap_or_default()) + .interact_text() + .into_lua_err()?; + Ok(PromptResult::String(input)) + } + PromptKind::Confirm => { + let mut prompt = Confirm::with_theme(&theme); + if let Some(b) = options.default_bool { + prompt = prompt.default(b); + }; + let result = prompt + .with_prompt(&options.text.expect("Missing text in prompt options")) + .interact() + .into_lua_err()?; + Ok(PromptResult::Boolean(result)) + } + PromptKind::Select => { + let chosen = Select::with_theme(&theme) + .with_prompt(&options.text.unwrap_or_default()) + .items(&options.options.expect("Missing options in prompt options")) + .interact_opt() + .into_lua_err()?; + Ok(match chosen { + Some(idx) => PromptResult::Index(idx + 1), + None => PromptResult::None, + }) + } + PromptKind::MultiSelect => { + let chosen = MultiSelect::with_theme(&theme) + .with_prompt(&options.text.unwrap_or_default()) + .items(&options.options.expect("Missing options in prompt options")) + .interact_opt() + .into_lua_err()?; + Ok(match chosen { + None => PromptResult::None, + Some(indices) => { + PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect()) + } + }) + } + } +} diff --git a/crates/lune-std-task/Cargo.toml b/crates/lune-std-task/Cargo.toml index 127a664..40c954c 100644 --- a/crates/lune-std-task/Cargo.toml +++ b/crates/lune-std-task/Cargo.toml @@ -14,6 +14,6 @@ workspace = true mlua = "0.9.7" mlua-luau-scheduler = "0.0.1" -tokio = { version = "1", features = ["time"] } +tokio = { version = "1", default-features = false, features = ["time"] } lune-utils = { version = "0.8.3", path = "../lune-utils" } diff --git a/crates/lune-std-task/src/lib.rs b/crates/lune-std-task/src/lib.rs index 9909281..47a78d5 100644 --- a/crates/lune-std-task/src/lib.rs +++ b/crates/lune-std-task/src/lib.rs @@ -16,7 +16,7 @@ use lune_utils::TableBuilder; Errors when out of memory, or if default Lua globals are missing. */ -pub fn module(lua: &Lua) -> LuaResult> { +pub fn module(lua: &Lua) -> LuaResult { let fns = Functions::new(lua)?; // Create wait & delay functions