mirror of
https://github.com/lune-org/lune.git
synced 2025-04-04 10:30:54 +01:00
Migrate stdio builtin to new crate
This commit is contained in:
parent
a5e349c54a
commit
ac3721f7d0
6 changed files with 325 additions and 2 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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<LuaTable> {
|
||||
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<String> {
|
||||
// TODO: Migrate from old crate
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn stdio_style(_: &Lua, _color: String) -> LuaResult<String> {
|
||||
// TODO: Migrate from old crate
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn stdio_format(_: &Lua, _args: LuaMultiValue) -> LuaResult<String> {
|
||||
// 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<LuaString> {
|
||||
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<PromptResult> {
|
||||
lua.spawn_blocking(move || prompt(options))
|
||||
.await
|
||||
.into_lua_err()
|
||||
}
|
||||
|
|
227
crates/lune-std-stdio/src/prompt.rs
Normal file
227
crates/lune-std-stdio/src/prompt.rs
Normal file
|
@ -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<Self, Self::Err> {
|
||||
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<Self> {
|
||||
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::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
})
|
||||
} else {
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: "nil",
|
||||
to: "PromptKind",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptOptions {
|
||||
pub kind: PromptKind,
|
||||
pub text: Option<String>,
|
||||
pub default_string: Option<String>,
|
||||
pub default_bool: Option<bool>,
|
||||
pub options: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl<'lua> FromLuaMulti<'lua> for PromptOptions {
|
||||
fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
|
||||
// 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::<String>::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<usize>),
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'lua> IntoLua<'lua> for PromptResult {
|
||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
||||
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<PromptResult> {
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" }
|
||||
|
|
|
@ -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<LuaTable<'_>> {
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let fns = Functions::new(lua)?;
|
||||
|
||||
// Create wait & delay functions
|
||||
|
|
Loading…
Add table
Reference in a new issue