Lines
0 %
Functions
Branches
100 %
use std::io;
use crossterm::ExecutableCommand;
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use scripting::nomiscript::{Pair, Value};
use nms::interpreter::Interpreter;
struct App<'a> {
input: String,
cursor_pos: usize,
output: Vec<Line<'a>>,
should_quit: bool,
interpreter: Interpreter,
}
impl App<'_> {
fn new() -> anyhow::Result<Self> {
Ok(Self {
input: String::new(),
cursor_pos: 0,
output: vec![Line::from(
"Welcome to Nomiscript REPL. Type expressions and press Enter.",
)],
should_quit: false,
interpreter: Interpreter::new(false)?,
})
fn eval_input(&mut self) {
if self.input.trim().is_empty() {
return;
let input = self.input.clone();
self.output.push(Line::from(Span::styled(
format!("> {input}"),
Style::default(),
)));
match self.interpreter.eval(&input) {
Ok(values) => {
for value in values {
self.output
.push(format_value_line(&value, &self.interpreter));
Err(e) => {
for line in e.render(false).lines() {
self.output.push(format_error_line(line));
self.input.clear();
self.cursor_pos = 0;
fn insert_char(&mut self, c: char) {
self.input.insert(self.cursor_pos, c);
self.cursor_pos += 1;
fn delete_char(&mut self) {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
self.input.remove(self.cursor_pos);
fn move_cursor_left(&mut self) {
fn move_cursor_right(&mut self) {
if self.cursor_pos < self.input.len() {
fn format_value_line(value: &Value, interp: &Interpreter) -> Line<'static> {
Line::from(format_value_spans(value, interp))
fn format_error_line(line: &str) -> Line<'static> {
let red = Style::default().fg(Color::Red);
let cyan = Style::default().fg(Color::Cyan);
if let Some(rest) = line.strip_prefix("error:") {
Line::from(vec![
Span::styled("error:", red),
Span::raw(rest.to_string()),
])
} else if line.contains('|') {
let mut spans = Vec::new();
let chars = line.chars().peekable();
let mut current = String::new();
for c in chars {
if c == '|' {
if !current.is_empty() {
spans.push(Span::styled(current, cyan));
current = String::new();
spans.push(Span::styled("|", cyan));
} else if c == '^' {
spans.push(Span::raw(current));
spans.push(Span::styled("^", red));
} else {
current.push(c);
Line::from(spans)
Line::from(line.to_string())
fn format_value_spans(value: &Value, interp: &Interpreter) -> Vec<Span<'static>> {
let dim = Style::default().fg(Color::DarkGray);
match value {
Value::Nil | Value::Bool(false) => vec![Span::styled("NIL", dim)],
Value::Bool(true) => vec![Span::styled("#T", Style::default().fg(Color::Green))],
Value::Number(n) => {
let text = if *n.denom() == 1 {
n.numer().to_string()
format!("{}/{}", n.numer(), n.denom())
vec![Span::styled(text, Style::default().fg(Color::Cyan))]
Value::String(s) => vec![Span::styled(
format!("\"{s}\""),
Style::default().fg(Color::Yellow),
Value::Symbol(s) => vec![Span::styled(s.clone(), Style::default().fg(Color::Magenta))],
Value::Pair(pair) => {
let mut spans = vec![Span::styled("(", dim)];
spans.extend(format_list_spans(pair, interp));
spans.push(Span::styled(")", dim));
spans
Value::Vector(elems) => {
let mut spans = vec![Span::styled("#(", dim)];
for (i, elem) in elems.iter().enumerate() {
if i > 0 {
spans.push(Span::styled(" ", dim));
spans.extend(format_value_spans(elem, interp));
Value::Closure(_) => vec![Span::styled("<closure>", dim)],
Value::Struct { name, fields } => format_struct_spans(name, fields, interp),
fn format_list_spans(pair: &Pair, interp: &Interpreter) -> Vec<Span<'static>> {
let mut spans = format_value_spans(&pair.car, interp);
match &pair.cdr {
Value::Nil => {}
Value::Pair(next) => {
spans.extend(format_list_spans(next, interp));
other => {
spans.push(Span::styled(" . ", dim));
spans.extend(format_value_spans(other, interp));
fn format_struct_spans(name: &str, fields: &[Value], interp: &Interpreter) -> Vec<Span<'static>> {
let blue = Style::default().fg(Color::Blue);
let field_names = interp.struct_fields(name);
let mut spans = vec![
Span::styled("#S(", dim),
Span::styled(name.to_string(), blue),
];
for (i, val) in fields.iter().enumerate() {
if let Some(ref names) = field_names
&& let Some(fname) = names.get(i)
{
spans.push(Span::styled(format!(":{fname} "), dim));
spans.extend(format_value_spans(val, interp));
pub fn run() -> anyhow::Result<()> {
enable_raw_mode()?;
io::stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
let mut app = App::new()?;
while !app.should_quit {
terminal.draw(|frame| ui(frame, &app))?;
handle_events(&mut app)?;
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
Ok(())
fn ui(frame: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Length(3)])
.split(frame.area());
let output_widget = Paragraph::new(Text::from(app.output.clone()))
.block(Block::default().borders(Borders::ALL).title("Output"))
.wrap(Wrap { trim: false })
.scroll((
app.output
.len()
.saturating_sub(chunks[0].height as usize - 2) as u16,
0,
));
frame.render_widget(output_widget, chunks[0]);
let input_widget = Paragraph::new(app.input.as_str()).block(
Block::default()
.borders(Borders::ALL)
.title("Input (Ctrl+C to quit)"),
);
frame.render_widget(input_widget, chunks[1]);
frame.set_cursor_position(Position::new(
chunks[1].x + app.cursor_pos as u16 + 1,
chunks[1].y + 1,
fn handle_events(app: &mut App) -> anyhow::Result<()> {
if event::poll(std::time::Duration::from_millis(100))?
&& let Event::Key(key) = event::read()?
if key.kind != KeyEventKind::Press {
return Ok(());
match (key.modifiers, key.code) {
(KeyModifiers::CONTROL, KeyCode::Char('c')) => {
app.should_quit = true;
(_, KeyCode::Enter) => {
app.eval_input();
(_, KeyCode::Backspace) => {
app.delete_char();
(_, KeyCode::Left) => {
app.move_cursor_left();
(_, KeyCode::Right) => {
app.move_cursor_right();
(_, KeyCode::Char(c)) => {
app.insert_char(c);
_ => {}