1
use std::io;
2

            
3
use crossterm::ExecutableCommand;
4
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
5
use crossterm::terminal::{
6
    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
7
};
8
use ratatui::prelude::*;
9
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
10
use scripting::nomiscript::{Pair, Value};
11

            
12
use nms::interpreter::Interpreter;
13

            
14
struct App<'a> {
15
    input: String,
16
    cursor_pos: usize,
17
    output: Vec<Line<'a>>,
18
    should_quit: bool,
19
    interpreter: Interpreter,
20
}
21

            
22
impl App<'_> {
23
    fn new() -> anyhow::Result<Self> {
24
        Ok(Self {
25
            input: String::new(),
26
            cursor_pos: 0,
27
            output: vec![Line::from(
28
                "Welcome to Nomiscript REPL. Type expressions and press Enter.",
29
            )],
30
            should_quit: false,
31
            interpreter: Interpreter::new(false)?,
32
        })
33
    }
34

            
35
    fn eval_input(&mut self) {
36
        if self.input.trim().is_empty() {
37
            return;
38
        }
39

            
40
        let input = self.input.clone();
41
        self.output.push(Line::from(Span::styled(
42
            format!("> {input}"),
43
            Style::default(),
44
        )));
45

            
46
        match self.interpreter.eval(&input) {
47
            Ok(values) => {
48
                for value in values {
49
                    self.output
50
                        .push(format_value_line(&value, &self.interpreter));
51
                }
52
            }
53
            Err(e) => {
54
                for line in e.render(false).lines() {
55
                    self.output.push(format_error_line(line));
56
                }
57
            }
58
        }
59

            
60
        self.input.clear();
61
        self.cursor_pos = 0;
62
    }
63

            
64
    fn insert_char(&mut self, c: char) {
65
        self.input.insert(self.cursor_pos, c);
66
        self.cursor_pos += 1;
67
    }
68

            
69
    fn delete_char(&mut self) {
70
        if self.cursor_pos > 0 {
71
            self.cursor_pos -= 1;
72
            self.input.remove(self.cursor_pos);
73
        }
74
    }
75

            
76
    fn move_cursor_left(&mut self) {
77
        if self.cursor_pos > 0 {
78
            self.cursor_pos -= 1;
79
        }
80
    }
81

            
82
    fn move_cursor_right(&mut self) {
83
        if self.cursor_pos < self.input.len() {
84
            self.cursor_pos += 1;
85
        }
86
    }
87
}
88

            
89
fn format_value_line(value: &Value, interp: &Interpreter) -> Line<'static> {
90
    Line::from(format_value_spans(value, interp))
91
}
92

            
93
fn format_error_line(line: &str) -> Line<'static> {
94
    let red = Style::default().fg(Color::Red);
95
    let cyan = Style::default().fg(Color::Cyan);
96

            
97
    if let Some(rest) = line.strip_prefix("error:") {
98
        Line::from(vec![
99
            Span::styled("error:", red),
100
            Span::raw(rest.to_string()),
101
        ])
102
    } else if line.contains('|') {
103
        let mut spans = Vec::new();
104
        let chars = line.chars().peekable();
105
        let mut current = String::new();
106

            
107
        for c in chars {
108
            if c == '|' {
109
                if !current.is_empty() {
110
                    spans.push(Span::styled(current, cyan));
111
                    current = String::new();
112
                }
113
                spans.push(Span::styled("|", cyan));
114
            } else if c == '^' {
115
                if !current.is_empty() {
116
                    spans.push(Span::raw(current));
117
                    current = String::new();
118
                }
119
                spans.push(Span::styled("^", red));
120
            } else {
121
                current.push(c);
122
            }
123
        }
124
        if !current.is_empty() {
125
            spans.push(Span::raw(current));
126
        }
127
        Line::from(spans)
128
    } else {
129
        Line::from(line.to_string())
130
    }
131
}
132

            
133
fn format_value_spans(value: &Value, interp: &Interpreter) -> Vec<Span<'static>> {
134
    let dim = Style::default().fg(Color::DarkGray);
135
    match value {
136
        Value::Nil | Value::Bool(false) => vec![Span::styled("NIL", dim)],
137
        Value::Bool(true) => vec![Span::styled("#T", Style::default().fg(Color::Green))],
138
        Value::Number(n) => {
139
            let text = if *n.denom() == 1 {
140
                n.numer().to_string()
141
            } else {
142
                format!("{}/{}", n.numer(), n.denom())
143
            };
144
            vec![Span::styled(text, Style::default().fg(Color::Cyan))]
145
        }
146
        Value::String(s) => vec![Span::styled(
147
            format!("\"{s}\""),
148
            Style::default().fg(Color::Yellow),
149
        )],
150
        Value::Symbol(s) => vec![Span::styled(s.clone(), Style::default().fg(Color::Magenta))],
151
        Value::Pair(pair) => {
152
            let mut spans = vec![Span::styled("(", dim)];
153
            spans.extend(format_list_spans(pair, interp));
154
            spans.push(Span::styled(")", dim));
155
            spans
156
        }
157
        Value::Vector(elems) => {
158
            let mut spans = vec![Span::styled("#(", dim)];
159
            for (i, elem) in elems.iter().enumerate() {
160
                if i > 0 {
161
                    spans.push(Span::styled(" ", dim));
162
                }
163
                spans.extend(format_value_spans(elem, interp));
164
            }
165
            spans.push(Span::styled(")", dim));
166
            spans
167
        }
168
        Value::Closure(_) => vec![Span::styled("<closure>", dim)],
169
        Value::Struct { name, fields } => format_struct_spans(name, fields, interp),
170
    }
171
}
172

            
173
fn format_list_spans(pair: &Pair, interp: &Interpreter) -> Vec<Span<'static>> {
174
    let dim = Style::default().fg(Color::DarkGray);
175
    let mut spans = format_value_spans(&pair.car, interp);
176
    match &pair.cdr {
177
        Value::Nil => {}
178
        Value::Pair(next) => {
179
            spans.push(Span::styled(" ", dim));
180
            spans.extend(format_list_spans(next, interp));
181
        }
182
        other => {
183
            spans.push(Span::styled(" . ", dim));
184
            spans.extend(format_value_spans(other, interp));
185
        }
186
    }
187
    spans
188
}
189

            
190
fn format_struct_spans(name: &str, fields: &[Value], interp: &Interpreter) -> Vec<Span<'static>> {
191
    let dim = Style::default().fg(Color::DarkGray);
192
    let blue = Style::default().fg(Color::Blue);
193
    let field_names = interp.struct_fields(name);
194
    let mut spans = vec![
195
        Span::styled("#S(", dim),
196
        Span::styled(name.to_string(), blue),
197
    ];
198
    for (i, val) in fields.iter().enumerate() {
199
        spans.push(Span::styled(" ", dim));
200
        if let Some(ref names) = field_names
201
            && let Some(fname) = names.get(i)
202
        {
203
            spans.push(Span::styled(format!(":{fname} "), dim));
204
        }
205
        spans.extend(format_value_spans(val, interp));
206
    }
207
    spans.push(Span::styled(")", dim));
208
    spans
209
}
210

            
211
pub fn run() -> anyhow::Result<()> {
212
    enable_raw_mode()?;
213
    io::stdout().execute(EnterAlternateScreen)?;
214

            
215
    let backend = CrosstermBackend::new(io::stdout());
216
    let mut terminal = Terminal::new(backend)?;
217
    let mut app = App::new()?;
218

            
219
    while !app.should_quit {
220
        terminal.draw(|frame| ui(frame, &app))?;
221
        handle_events(&mut app)?;
222
    }
223

            
224
    disable_raw_mode()?;
225
    io::stdout().execute(LeaveAlternateScreen)?;
226
    Ok(())
227
}
228

            
229
fn ui(frame: &mut Frame, app: &App) {
230
    let chunks = Layout::default()
231
        .direction(Direction::Vertical)
232
        .constraints([Constraint::Min(3), Constraint::Length(3)])
233
        .split(frame.area());
234

            
235
    let output_widget = Paragraph::new(Text::from(app.output.clone()))
236
        .block(Block::default().borders(Borders::ALL).title("Output"))
237
        .wrap(Wrap { trim: false })
238
        .scroll((
239
            app.output
240
                .len()
241
                .saturating_sub(chunks[0].height as usize - 2) as u16,
242
            0,
243
        ));
244
    frame.render_widget(output_widget, chunks[0]);
245

            
246
    let input_widget = Paragraph::new(app.input.as_str()).block(
247
        Block::default()
248
            .borders(Borders::ALL)
249
            .title("Input (Ctrl+C to quit)"),
250
    );
251
    frame.render_widget(input_widget, chunks[1]);
252

            
253
    frame.set_cursor_position(Position::new(
254
        chunks[1].x + app.cursor_pos as u16 + 1,
255
        chunks[1].y + 1,
256
    ));
257
}
258

            
259
fn handle_events(app: &mut App) -> anyhow::Result<()> {
260
    if event::poll(std::time::Duration::from_millis(100))?
261
        && let Event::Key(key) = event::read()?
262
    {
263
        if key.kind != KeyEventKind::Press {
264
            return Ok(());
265
        }
266

            
267
        match (key.modifiers, key.code) {
268
            (KeyModifiers::CONTROL, KeyCode::Char('c')) => {
269
                app.should_quit = true;
270
            }
271
            (_, KeyCode::Enter) => {
272
                app.eval_input();
273
            }
274
            (_, KeyCode::Backspace) => {
275
                app.delete_char();
276
            }
277
            (_, KeyCode::Left) => {
278
                app.move_cursor_left();
279
            }
280
            (_, KeyCode::Right) => {
281
                app.move_cursor_right();
282
            }
283
            (_, KeyCode::Char(c)) => {
284
                app.insert_char(c);
285
            }
286
            _ => {}
287
        }
288
    }
289
    Ok(())
290
}