shelly-shell 0.5.4

A Rust based Unix style shell with a typed and structured language syntax.

use std::{any::Any, collections::HashSet, rc::Rc};

use nu_ansi_term::Style;

use reedline::Color;

use crate::{
    language::{
        data::{
            map_key::MapKey,
            native_value::{NativeData, NativeValue},
            types::{EnumValue, StructValue, TypeKind},
            value::Value,
        },
        interpreter::Interpreter,
    },
    runtime::readline::{Fault, Limits, Text},
};

use super::Result;

pub(super) fn native<T: NativeData>(value: &Value) -> Option<&T>
{
    if let Value::Native(value) = value.underlying()
    {
        value.downcast()
    }
    else
    {
        None
    }
}
pub(super) fn data(value: impl NativeData) -> Value
{
    Value::Native(NativeValue::new(value))
}
pub(super) fn field<'a>(value: &'a Value, name: &str) -> &'a Value
{
    match value.underlying()
    {
        Value::HashMap(fields) => fields
            .get(&MapKey::String(name.into()))
            .unwrap_or(&Value::None),
        Value::Struct(item) =>
        {
            let TypeKind::Struct(fields) = &item.definition.kind
            else
            {
                return &Value::None;
            };
            fields
                .iter()
                .position(|field| field.name == name)
                .map_or(&Value::None, |index| &item.fields[index])
        }
        _ => &Value::None,
    }
}
pub(super) fn string(value: &Value) -> Option<&str>
{
    if let Value::String(text, _) = value.underlying()
    {
        Some(text)
    }
    else
    {
        None
    }
}
pub(super) fn integer(value: &Value) -> Option<i64>
{
    if let Value::Integer(value) = value.underlying()
    {
        Some(*value)
    }
    else
    {
        None
    }
}
pub(super) fn optional_string(value: &Value) -> std::result::Result<Option<String>, Fault>
{
    if matches!(value.underlying(), Value::None)
    {
        Ok(None)
    }
    else
    {
        string(value)
            .map(|v| Some(v.into()))
            .ok_or_else(|| Fault::new("ConfigError", "Expected String"))
    }
}
pub(super) fn enum_name(value: &Value) -> Option<&str>
{
    if    let Value::Enum(item) = value.underlying()
       && let TypeKind::Enum(variants) = &item.definition.kind
    {
        return Some(&variants[item.variant].name);
    }
    None
}
pub(super) fn record(interpreter: &Interpreter, name: &str, fields: Vec<Value>) -> Value
{
    let registry = &interpreter.scope().types;
    Value::Struct(Rc::new(StructValue {
        definition: registry.get(registry.names[name]),
        fields,
        constructing: false,
        failure: None,
    }))
}
pub(super) fn enumeration(interpreter: &Interpreter, name: &str, member: &str) -> Value
{
    let registry = &interpreter.scope().types;
    let definition = registry.get(registry.names[name]);
    let TypeKind::Enum(variants) = &definition.kind
    else
    {
        unreachable!()
    };
    let variant = variants.iter().position(|v| v.name == member).unwrap();
    Value::Enum(Rc::new(EnumValue {
        definition,
        variant,
    }))
}
pub(super) fn bounded(
    value: &Value,
    default: i64,
    min: i64,
    max: i64,
) -> std::result::Result<i64, Fault>
{
    if matches!(value.underlying(), Value::None)
    {
        return Ok(default);
    }
    integer(value)
        .filter(|v| (min..=max).contains(v))
        .ok_or_else(|| {
            Fault::new(
                "ConfigError",
                format!("Expected Integer in {}..{}", min, max),
            )
        })
}
pub(super) fn limits(value: &Value) -> std::result::Result<Limits, Fault>
{
    Ok(Limits {
        input: bounded(field(value, "max_input_bytes"), 65536, 1, 1048576)? as usize,
        completions: bounded(field(value, "max_completions"), 1000, 1, 10000)? as usize,
        completion_bytes: bounded(field(value, "max_completion_bytes"), 1048576, 1, 4194304)?
            as usize,
        spans: bounded(field(value, "max_highlight_spans"), 4096, 1, 16384)? as usize,
        milliseconds: bounded(field(value, "callback_timeout_ms"), 50, 1, 100)? as u64,
        instructions: bounded(field(value, "callback_instructions"), 100000, 1, 250000)? as usize,
    })
}
impl NativeData for Text
{
    fn type_name(&self) -> &'static str
    {
        "__readline_Text"
    }
    fn display(&self) -> String
    {
        self.0.iter().map(|(_, text)| text.as_str()).collect()
    }
    fn as_any(&self) -> &dyn Any
    {
        self
    }
    fn retained_bytes(&self) -> Option<usize>
    {
        Some(self.bytes() + self.0.len() * 128)
    }
}
#[derive(Clone)]
struct NativeColor(Color);
impl NativeData for NativeColor
{
    fn type_name(&self) -> &'static str
    {
        "__readline_Color"
    }
    fn display(&self) -> String
    {
        format!("{:?}", self.0)
    }
    fn as_any(&self) -> &dyn Any
    {
        self
    }
    fn retained_bytes(&self) -> Option<usize>
    {
        Some(32)
    }
}
pub(super) fn text(value: &Value) -> std::result::Result<Text, Fault>
{
    if let Some(text) = native::<Text>(value)
    {
        if text.bytes() > 16384
        {
            return Err(Fault::new("LimitError", "Text exceeds 16 KiB"));
        }
        Ok(text.clone())
    }
    else if let Some(value) = string(value)
    {
        if value.len() > 16384
        {
            return Err(Fault::new("LimitError", "Text exceeds 16 KiB"));
        }
        let text = Text::plain(value);
        if text.bytes() > 16384
        {
            return Err(Fault::new("LimitError", "Escaped text exceeds 16 KiB"));
        }
        Ok(text)
    }
    else
    {
        Err(Fault::new(
            "ConfigError",
            "Prompt must return String or Text",
        ))
    }
}
pub(super) fn style(value: &Value) -> std::result::Result<Style, Fault>
{
    let color = |name| {
        let value = field(value, name);
        if matches!(value.underlying(), Value::None)
        {
            Ok(None)
        }
        else
        {
            native::<NativeColor>(value)
                .map(|v| Some(v.0))
                .ok_or_else(|| Fault::new("ConfigError", "Style color must be Color"))
        }
    };
    let boolean = |name| {
        let value = field(value, name);
        match value.underlying()
        {
            Value::None => Ok(false),
            Value::Boolean(value) => Ok(*value),
            _ => Err(Fault::new("ConfigError", "Style attribute must be Boolean")),
        }
    };
    let mut style = Style::new();
    style.foreground = color("foreground")?;
    style.background = color("background")?;
    style.is_bold = boolean("bold")?;
    style.is_dimmed = boolean("dim")?;
    style.is_underline = boolean("underline")?;
    Ok(style)
}
pub(super) fn span(value: &Value, text: &str) -> std::result::Result<reedline::Span, Fault>
{
    let start = integer(field(value, "start"))
        .ok_or_else(|| Fault::new("CallbackError", "Span start must be Integer"))?;
    let end = integer(field(value, "end"))
        .ok_or_else(|| Fault::new("CallbackError", "Span end must be Integer"))?;
    if start < 0
        || end < start
        || end as usize > text.len()
        || !text.is_char_boundary(start as usize)
        || !text.is_char_boundary(end as usize)
    {
        return Err(Fault::new(
            "CallbackError",
            "Span must be a valid UTF-8 byte range",
        ));
    }
    Ok(reedline::Span::new(start as usize, end as usize))
}

impl Interpreter
{
    pub(super) fn readline_value(
        &self,
        command: &str,
        input: &Value,
        rest: &[Value],
    ) -> Result<Option<Value>>
    {
        let value = match command
        {
            "text" => data(text(input)?),
            "styled" =>
            {
                let text =
                    string(input).ok_or_else(|| Fault::new("ConfigError", "Expected String"))?;
                if text.len() > 16384
                {
                    return Err(Fault::new("LimitError", "Styled text exceeds 16 KiB").into());
                }
                data(Text(vec![(
                    style(rest.first().unwrap_or(&Value::None))?,
                    crate::runtime::readline::visible(text),
                )]))
            }
            "concat" =>
            {
                let mut left = text(input)?;
                let right = text(rest.first().unwrap_or(&Value::None))?;
                if left.bytes() + right.bytes() > 16384 || left.0.len() + right.0.len() > 4096
                {
                    return Err(Fault::new("LimitError", "Text exceeds its limit").into());
                }
                left.0.extend(right.0);
                data(left)
            }
            "color" =>
            {
                let color = if let Some(index) = integer(input)
                {
                    Color::Fixed(
                        u8::try_from(index)
                            .map_err(|_| Fault::new("ConfigError", "Color is 0..255"))?,
                    )
                }
                else if let Value::Array(items) = input.underlying()
                {
                    let rgb = items
                        .iter()
                        .map(|v| integer(v).and_then(|v| u8::try_from(v).ok()))
                        .collect::<Option<Vec<_>>>()
                        .filter(|v| v.len() == 3)
                        .ok_or_else(|| Fault::new("ConfigError", "RGB needs three bytes"))?;
                    Color::Rgb(rgb[0], rgb[1], rgb[2])
                }
                else if let Value::Enum(value) = input.underlying()
                {
                    Color::Fixed(value.variant as u8)
                }
                else
                {
                    return Err(Fault::new("ConfigError", "Invalid Color").into());
                };
                data(NativeColor(color))
            }
            "slice" =>
            {
                let text =
                    string(input).ok_or_else(|| Fault::new("ConfigError", "Expected String"))?;
                let span = span(rest.first().unwrap_or(&Value::None), text)?;
                Value::from_string(text[span.start..span.end].into())
            }
            "words" =>
            {
                let Value::Array(items) = input.underlying()
                else
                {
                    return Err(Fault::new("ConfigError", "Expected words Array").into());
                };
                let prefix = rest.first().and_then(string).unwrap_or("");
                let mut seen = HashSet::new();
                let mut results = Vec::new();
                for item in items.iter()
                {
                    let value = string(item)
                        .ok_or_else(|| Fault::new("ConfigError", "Expected word String"))?;
                    if value.starts_with(prefix) && seen.insert(value)
                    {
                        results.push(Value::from_string(value.into()));
                    }
                }
                Value::from_array(results)
            }
            _ => return Ok(None),
        };
        Ok(Some(value))
    }
}