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::Bytes(b) => vec![Span::styled(
169
            format_bytes_inline(b),
170
            Style::default().fg(Color::Yellow),
171
        )],
172
        Value::Closure(_) => vec![Span::styled("<closure>", dim)],
173
        Value::Struct { name, fields } => format_struct_spans(name, fields, interp),
174
        Value::Commodity {
175
            amount,
176
            commodity_id,
177
        } => {
178
            let amt = if *amount.denom() == 1 {
179
                amount.numer().to_string()
180
            } else {
181
                format!("{}/{}", amount.numer(), amount.denom())
182
            };
183
            vec![Span::styled(
184
                format!("(:commodity {amt} :id \"{commodity_id}\")"),
185
                Style::default().fg(Color::Cyan),
186
            )]
187
        }
188
    }
189
}
190

            
191
fn format_bytes_inline(bytes: &[u8]) -> String {
192
    let parts: Vec<String> = bytes.iter().map(u8::to_string).collect();
193
    format!("#u8({})", parts.join(" "))
194
}
195

            
196
fn format_list_spans(pair: &Pair, interp: &Interpreter) -> Vec<Span<'static>> {
197
    let dim = Style::default().fg(Color::DarkGray);
198
    let mut spans = format_value_spans(&pair.car, interp);
199
    match &pair.cdr {
200
        Value::Nil => {}
201
        Value::Pair(next) => {
202
            spans.push(Span::styled(" ", dim));
203
            spans.extend(format_list_spans(next, interp));
204
        }
205
        other => {
206
            spans.push(Span::styled(" . ", dim));
207
            spans.extend(format_value_spans(other, interp));
208
        }
209
    }
210
    spans
211
}
212

            
213
fn format_struct_spans(name: &str, fields: &[Value], interp: &Interpreter) -> Vec<Span<'static>> {
214
    let dim = Style::default().fg(Color::DarkGray);
215
    let blue = Style::default().fg(Color::Blue);
216
    let field_names = interp.struct_fields(name);
217
    let mut spans = vec![
218
        Span::styled("#S(", dim),
219
        Span::styled(name.to_string(), blue),
220
    ];
221
    for (i, val) in fields.iter().enumerate() {
222
        spans.push(Span::styled(" ", dim));
223
        if let Some(ref names) = field_names
224
            && let Some(fname) = names.get(i)
225
        {
226
            spans.push(Span::styled(format!(":{fname} "), dim));
227
        }
228
        spans.extend(format_value_spans(val, interp));
229
    }
230
    spans.push(Span::styled(")", dim));
231
    spans
232
}
233

            
234
pub fn run() -> anyhow::Result<()> {
235
    enable_raw_mode()?;
236
    io::stdout().execute(EnterAlternateScreen)?;
237

            
238
    let backend = CrosstermBackend::new(io::stdout());
239
    let mut terminal = Terminal::new(backend)?;
240
    let mut app = App::new()?;
241

            
242
    while !app.should_quit {
243
        terminal.draw(|frame| ui(frame, &app))?;
244
        handle_events(&mut app)?;
245
    }
246

            
247
    disable_raw_mode()?;
248
    io::stdout().execute(LeaveAlternateScreen)?;
249
    Ok(())
250
}
251

            
252
fn ui(frame: &mut Frame, app: &App) {
253
    let chunks = Layout::default()
254
        .direction(Direction::Vertical)
255
        .constraints([Constraint::Min(3), Constraint::Length(3)])
256
        .split(frame.area());
257

            
258
    let output_widget = Paragraph::new(Text::from(app.output.clone()))
259
        .block(Block::default().borders(Borders::ALL).title("Output"))
260
        .wrap(Wrap { trim: false })
261
        .scroll((
262
            app.output
263
                .len()
264
                .saturating_sub(chunks[0].height as usize - 2) as u16,
265
            0,
266
        ));
267
    frame.render_widget(output_widget, chunks[0]);
268

            
269
    let input_widget = Paragraph::new(app.input.as_str()).block(
270
        Block::default()
271
            .borders(Borders::ALL)
272
            .title("Input (Ctrl+C to quit)"),
273
    );
274
    frame.render_widget(input_widget, chunks[1]);
275

            
276
    frame.set_cursor_position(Position::new(
277
        chunks[1].x + app.cursor_pos as u16 + 1,
278
        chunks[1].y + 1,
279
    ));
280
}
281

            
282
fn handle_events(app: &mut App) -> anyhow::Result<()> {
283
    if event::poll(std::time::Duration::from_millis(100))?
284
        && let Event::Key(key) = event::read()?
285
    {
286
        if key.kind != KeyEventKind::Press {
287
            return Ok(());
288
        }
289

            
290
        match (key.modifiers, key.code) {
291
            (KeyModifiers::CONTROL, KeyCode::Char('c')) => {
292
                app.should_quit = true;
293
            }
294
            (_, KeyCode::Enter) => {
295
                app.eval_input();
296
            }
297
            (_, KeyCode::Backspace) => {
298
                app.delete_char();
299
            }
300
            (_, KeyCode::Left) => {
301
                app.move_cursor_left();
302
            }
303
            (_, KeyCode::Right) => {
304
                app.move_cursor_right();
305
            }
306
            (_, KeyCode::Char(c)) => {
307
                app.insert_char(c);
308
            }
309
            _ => {}
310
        }
311
    }
312
    Ok(())
313
}