mirror of
https://github.com/lune-org/lune.git
synced 2025-04-19 03:13: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]]
|
[[package]]
|
||||||
name = "lune-std-stdio"
|
name = "lune-std-stdio"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
dependencies = [
|
||||||
|
"dialoguer",
|
||||||
|
"lune-utils",
|
||||||
|
"mlua",
|
||||||
|
"mlua-luau-scheduler 0.0.1",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lune-std-task"
|
name = "lune-std-task"
|
||||||
|
|
|
@ -9,3 +9,12 @@ path = "src/lib.rs"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
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)]
|
#![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 = "0.9.7"
|
||||||
mlua-luau-scheduler = "0.0.1"
|
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" }
|
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.
|
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)?;
|
let fns = Functions::new(lua)?;
|
||||||
|
|
||||||
// Create wait & delay functions
|
// Create wait & delay functions
|
||||||
|
|
Loading…
Add table
Reference in a new issue