From 73361d5a52a5938a6da301756f282945f829c600 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 19 Aug 2023 19:13:50 -0500 Subject: [PATCH] Add back stdio builtin and pretty error formatting --- src/lune/builtins/mod.rs | 7 +- src/lune/builtins/stdio/mod.rs | 108 +++++++ src/lune/builtins/stdio/prompt.rs | 192 ++++++++++++ src/lune/builtins/task/mod.rs | 2 +- src/lune/error.rs | 8 +- src/lune/scheduler/impl_runner.rs | 2 +- src/lune/util/formatting.rs | 466 ++++++++++++++++++++++++++++++ src/lune/util/mod.rs | 2 + 8 files changed, 783 insertions(+), 4 deletions(-) create mode 100644 src/lune/builtins/stdio/mod.rs create mode 100644 src/lune/builtins/stdio/prompt.rs create mode 100644 src/lune/util/formatting.rs diff --git a/src/lune/builtins/mod.rs b/src/lune/builtins/mod.rs index 666f71d..51cdb55 100644 --- a/src/lune/builtins/mod.rs +++ b/src/lune/builtins/mod.rs @@ -2,11 +2,13 @@ use std::str::FromStr; use mlua::prelude::*; +mod stdio; mod task; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum LuneBuiltin { Task, + Stdio, } impl<'lua> LuneBuiltin @@ -16,15 +18,17 @@ where pub fn name(&self) -> &'static str { match self { Self::Task => "task", + Self::Stdio => "stdio", } } pub fn create(&self, lua: &'lua Lua) -> LuaResult> { let res = match self { Self::Task => task::create(lua), + Self::Stdio => stdio::create(lua), }; match res { - Ok(v) => Ok(v.into_lua_multi(lua)?), + Ok(v) => v.into_lua_multi(lua), Err(e) => Err(e.context(format!( "Failed to create builtin library '{}'", self.name() @@ -38,6 +42,7 @@ impl FromStr for LuneBuiltin { fn from_str(s: &str) -> Result { match s.trim().to_ascii_lowercase().as_str() { "task" => Ok(Self::Task), + "stdio" => Ok(Self::Stdio), _ => Err(format!("Unknown builtin library '{s}'")), } } diff --git a/src/lune/builtins/stdio/mod.rs b/src/lune/builtins/stdio/mod.rs new file mode 100644 index 0000000..139a643 --- /dev/null +++ b/src/lune/builtins/stdio/mod.rs @@ -0,0 +1,108 @@ +use mlua::prelude::*; + +use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select}; +use tokio::{ + io::{self, AsyncWriteExt}, + task, +}; + +use crate::lune::util::{ + formatting::{ + format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str, + }, + TableBuilder, +}; + +mod prompt; +use prompt::{PromptKind, PromptOptions, PromptResult}; + +pub fn create(lua: &'static 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("prompt", stdio_prompt)? + .build_readonly() +} + +fn stdio_color(_: &Lua, color: String) -> LuaResult { + let ansi_string = format_style(style_from_color_str(&color)?); + Ok(ansi_string) +} + +fn stdio_style(_: &Lua, color: String) -> LuaResult { + let ansi_string = format_style(style_from_style_str(&color)?); + Ok(ansi_string) +} + +fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult { + pretty_format_multi_value(&args) +} + +async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { + let mut stdout = io::stdout(); + stdout.write_all(s.as_bytes()).await?; + stdout.flush().await?; + Ok(()) +} + +async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { + let mut stderr = io::stderr(); + stderr.write_all(s.as_bytes()).await?; + stderr.flush().await?; + Ok(()) +} + +async fn stdio_prompt(_: &Lua, options: PromptOptions) -> LuaResult { + task::spawn_blocking(move || prompt(options)) + .await + .into_lua_err()? +} + +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()?; + Ok(PromptResult::String(input)) + } + PromptKind::Confirm => { + let mut prompt = Confirm::with_theme(&theme); + if let Some(b) = options.default_bool { + prompt.default(b); + }; + let result = prompt + .with_prompt(&options.text.expect("Missing text in prompt options")) + .interact()?; + 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()?; + 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()?; + Ok(match chosen { + None => PromptResult::None, + Some(indices) => { + PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect()) + } + }) + } + } +} diff --git a/src/lune/builtins/stdio/prompt.rs b/src/lune/builtins/stdio/prompt.rs new file mode 100644 index 0000000..9cdd899 --- /dev/null +++ b/src/lune/builtins/stdio/prompt.rs @@ -0,0 +1,192 @@ +use std::fmt; + +use mlua::prelude::*; + +#[derive(Debug, Clone, Copy)] +pub enum PromptKind { + Text, + Confirm, + Select, + MultiSelect, +} + +impl PromptKind { + fn get_all() -> Vec { + vec![Self::Text, Self::Confirm, Self::Select, Self::MultiSelect] + } +} + +impl Default for PromptKind { + fn default() -> Self { + Self::Text + } +} + +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()?; + /* + If the user only typed the prompt kind slightly wrong, meaning + it has some kind of space in it, a weird character, or an uppercase + character, we should try to be permissive as possible and still work + + Not everyone is using an IDE with proper Luau type definitions + installed, and Luau is still a permissive scripting language + even though it has a strict (but optional) type system + */ + let s = s + .chars() + .filter_map(|c| { + if c.is_ascii_alphabetic() { + Some(c.to_ascii_lowercase()) + } else { + None + } + }) + .collect::(); + // If the prompt kind is still invalid we will + // show the user a descriptive error message + match s.as_ref() { + "text" => Ok(Self::Text), + "confirm" => Ok(Self::Confirm), + "select" => Ok(Self::Select), + "multiselect" => Ok(Self::MultiSelect), + s => Err(LuaError::FromLuaConversionError { + from: "string", + to: "PromptKind", + message: Some(format!( + "Invalid prompt kind '{s}', valid kinds are:\n{}", + PromptKind::get_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_bool, + default_string, + 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, + }) + } +} diff --git a/src/lune/builtins/task/mod.rs b/src/lune/builtins/task/mod.rs index 1c2ff96..2bba0f8 100644 --- a/src/lune/builtins/task/mod.rs +++ b/src/lune/builtins/task/mod.rs @@ -9,7 +9,7 @@ use crate::lune::{scheduler::Scheduler, util::TableBuilder}; mod tof; use tof::LuaThreadOrFunction; -pub fn create(lua: &'static Lua) -> LuaResult> { +pub fn create(lua: &'static Lua) -> LuaResult> { TableBuilder::new(lua)? .with_function("cancel", task_cancel)? .with_function("defer", task_defer)? diff --git a/src/lune/error.rs b/src/lune/error.rs index 1999877..c83fad1 100644 --- a/src/lune/error.rs +++ b/src/lune/error.rs @@ -5,6 +5,8 @@ use std::{ use mlua::prelude::*; +use crate::lune::util::formatting::pretty_format_luau_error; + /** An opaque error type for formatted lua errors. */ @@ -73,7 +75,11 @@ impl From<&LuaError> for LuneError { impl Display for LuneError { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{}", self.error) // TODO: Pretty formatting + write!( + f, + "{}", + pretty_format_luau_error(&self.error, !self.disable_colors) + ) } } diff --git a/src/lune/scheduler/impl_runner.rs b/src/lune/scheduler/impl_runner.rs index 918cb6d..f013c9a 100644 --- a/src/lune/scheduler/impl_runner.rs +++ b/src/lune/scheduler/impl_runner.rs @@ -54,7 +54,7 @@ where if let Err(err) = &res { self.state.increment_error_count(); // NOTE: LuneError will pretty-format this error - eprint!("{}", LuneError::from(err)); + eprintln!("{}", LuneError::from(err)); } // Send results of resuming this thread to any listeners diff --git a/src/lune/util/formatting.rs b/src/lune/util/formatting.rs new file mode 100644 index 0000000..165ceb4 --- /dev/null +++ b/src/lune/util/formatting.rs @@ -0,0 +1,466 @@ +use std::fmt::Write; + +use console::{colors_enabled, set_colors_enabled, style, Style}; +use mlua::prelude::*; +use once_cell::sync::Lazy; + +const MAX_FORMAT_DEPTH: usize = 4; + +const INDENT: &str = " "; + +pub const STYLE_RESET_STR: &str = "\x1b[0m"; + +// Colors +pub static COLOR_BLACK: Lazy