leo_interpreter/
ratatui_ui.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use super::ui::{Ui, UserData};
18
19use std::{cmp, collections::VecDeque, io::Stdout, mem};
20
21use crossterm::event::{self, Event, KeyCode, KeyModifiers};
22use ratatui::{
23    Frame,
24    Terminal,
25    prelude::{
26        Buffer,
27        Constraint,
28        CrosstermBackend,
29        Direction,
30        Layout,
31        Line,
32        Modifier,
33        Rect,
34        Span,
35        Style,
36        Stylize as _,
37    },
38    text::Text,
39    widgets::{Block, Paragraph, Widget},
40};
41
42#[derive(Default)]
43struct DrawData {
44    code: String,
45    highlight: Option<(usize, usize)>,
46    result: String,
47    watchpoints: Vec<String>,
48    message: String,
49    prompt: Prompt,
50}
51
52pub struct RatatuiUi {
53    terminal: Terminal<CrosstermBackend<Stdout>>,
54    data: DrawData,
55}
56
57impl Drop for RatatuiUi {
58    fn drop(&mut self) {
59        ratatui::restore();
60    }
61}
62
63impl RatatuiUi {
64    pub fn new() -> Self {
65        RatatuiUi { terminal: ratatui::init(), data: Default::default() }
66    }
67}
68
69fn append_lines<'a>(
70    lines: &mut Vec<Line<'a>>,
71    mut last_chunk: Option<Line<'a>>,
72    string: &'a str,
73    style: Style,
74) -> Option<Line<'a>> {
75    let mut line_iter = string.lines().peekable();
76    while let Some(line) = line_iter.next() {
77        let this_span = Span::styled(line, style);
78        let mut real_last_chunk = mem::take(&mut last_chunk).unwrap_or_else(|| Line::raw(""));
79        real_last_chunk.push_span(this_span);
80        if line_iter.peek().is_some() {
81            lines.push(real_last_chunk);
82        } else if string.ends_with('\n') {
83            lines.push(real_last_chunk);
84            return None;
85        } else {
86            return Some(real_last_chunk);
87        }
88    }
89
90    last_chunk
91}
92
93fn code_text(s: &str, highlight: Option<(usize, usize)>) -> (Text, usize) {
94    let Some((lo, hi)) = highlight else {
95        return (Text::from(s), 0);
96    };
97
98    let s1 = s.get(..lo).expect("should be able to split text");
99    let s2 = s.get(lo..hi).expect("should be able to split text");
100    let s3 = s.get(hi..).expect("should be able to split text");
101
102    let mut lines = Vec::new();
103
104    let s1_chunk = append_lines(&mut lines, None, s1, Style::default());
105    let line = lines.len();
106    let s2_chunk = append_lines(&mut lines, s1_chunk, s2, Style::new().red());
107    let s3_chunk = append_lines(&mut lines, s2_chunk, s3, Style::default());
108
109    if let Some(chunk) = s3_chunk {
110        lines.push(chunk);
111    }
112
113    (Text::from(lines), line)
114}
115
116struct DebuggerLayout {
117    code: Rect,
118    result: Rect,
119    watchpoints: Rect,
120    user_input: Rect,
121    message: Rect,
122}
123
124impl DebuggerLayout {
125    fn new(total: Rect) -> Self {
126        let overall_layout = Layout::default()
127            .direction(Direction::Vertical)
128            .constraints([
129                Constraint::Fill(1),   // Code
130                Constraint::Length(6), // Result and watchpoints
131                Constraint::Length(3), // Message
132                Constraint::Length(3), // User input
133            ])
134            .split(total);
135        let code = overall_layout[0];
136        let middle = overall_layout[1];
137        let message = overall_layout[2];
138        let user_input = overall_layout[3];
139
140        let middle = Layout::default()
141            .direction(Direction::Horizontal)
142            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
143            .split(middle);
144
145        DebuggerLayout { code, result: middle[0], watchpoints: middle[1], user_input, message }
146    }
147}
148
149#[derive(Debug, Default)]
150struct Prompt {
151    history: VecDeque<String>,
152    history_index: usize,
153    current: String,
154    cursor: usize,
155}
156
157impl Widget for &Prompt {
158    fn render(self, area: Rect, buf: &mut Buffer) {
159        let mut plain = || {
160            Text::raw(&self.current).render(area, buf);
161        };
162
163        if self.cursor >= self.current.len() {
164            let span1 = Span::raw(&self.current);
165            let span2 = Span::styled(" ", Style::new().add_modifier(Modifier::REVERSED));
166            Text::from(Line::from_iter([span1, span2])).render(area, buf);
167            return;
168        }
169
170        let Some(pre) = self.current.get(..self.cursor) else {
171            plain();
172            return;
173        };
174
175        let Some(c) = self.current.get(self.cursor..self.cursor + 1) else {
176            plain();
177            return;
178        };
179
180        let Some(post) = self.current.get(self.cursor + 1..) else {
181            plain();
182            return;
183        };
184
185        Text::from(Line::from_iter([
186            Span::raw(pre),
187            Span::styled(c, Style::new().add_modifier(Modifier::REVERSED)),
188            Span::raw(post),
189        ]))
190        .render(area, buf);
191    }
192}
193
194impl Prompt {
195    fn handle_key(&mut self, key: KeyCode, control: bool) -> Option<String> {
196        match (key, control) {
197            (KeyCode::Enter, _) => {
198                self.history.push_back(mem::take(&mut self.current));
199                self.history_index = self.history.len();
200                return self.history.back().cloned();
201            }
202            (KeyCode::Backspace, _) => self.backspace(),
203            (KeyCode::Left, _) => self.left(),
204            (KeyCode::Right, _) => self.right(),
205            (KeyCode::Up, _) => self.history_prev(),
206            (KeyCode::Down, _) => self.history_next(),
207            (KeyCode::Delete, _) => self.delete(),
208            (KeyCode::Char(c), false) => self.new_character(c),
209            (KeyCode::Char('a'), true) => self.beginning_of_line(),
210            (KeyCode::Char('e'), true) => self.end_of_line(),
211            _ => {}
212        }
213
214        None
215    }
216
217    fn new_character(&mut self, c: char) {
218        if self.cursor >= self.current.len() {
219            self.current.push(c);
220            self.cursor = self.current.len();
221        } else {
222            let Some(pre) = self.current.get(..self.cursor) else {
223                return;
224            };
225            let Some(post) = self.current.get(self.cursor..) else {
226                return;
227            };
228            let mut with_char = format!("{pre}{c}");
229            self.cursor = with_char.len();
230            with_char.push_str(post);
231            self.current = with_char;
232        }
233        self.check_history();
234    }
235
236    fn right(&mut self) {
237        self.cursor = cmp::min(self.cursor + 1, self.current.len());
238    }
239
240    fn left(&mut self) {
241        self.cursor = self.cursor.saturating_sub(1);
242    }
243
244    fn backspace(&mut self) {
245        if self.cursor == 0 {
246            return;
247        }
248
249        if self.cursor >= self.current.len() {
250            self.current.pop();
251            self.cursor = self.current.len();
252            return;
253        }
254
255        let Some(pre) = self.current.get(..self.cursor - 1) else {
256            return;
257        };
258        let Some(post) = self.current.get(self.cursor..) else {
259            return;
260        };
261        self.cursor -= 1;
262
263        let s = format!("{pre}{post}");
264
265        self.current = s;
266
267        self.check_history();
268    }
269
270    fn delete(&mut self) {
271        if self.cursor + 1 >= self.current.len() {
272            return;
273        }
274
275        let Some(pre) = self.current.get(..self.cursor) else {
276            return;
277        };
278        let Some(post) = self.current.get(self.cursor + 1..) else {
279            return;
280        };
281
282        let s = format!("{pre}{post}");
283
284        self.current = s;
285
286        self.check_history();
287    }
288
289    fn beginning_of_line(&mut self) {
290        self.cursor = 0;
291    }
292
293    fn end_of_line(&mut self) {
294        self.cursor = self.current.len();
295    }
296
297    fn history_next(&mut self) {
298        self.history_index += 1;
299        if self.history_index > self.history.len() {
300            self.history_index = 0;
301        }
302        self.current = self.history.get(self.history_index).cloned().unwrap_or(String::new());
303    }
304
305    fn history_prev(&mut self) {
306        if self.history_index == 0 {
307            self.history_index = self.history.len();
308        } else {
309            self.history_index -= 1;
310        }
311        self.current = self.history.get(self.history_index).cloned().unwrap_or(String::new());
312    }
313
314    fn check_history(&mut self) {
315        const MAX_HISTORY: usize = 50;
316
317        while self.history.len() > MAX_HISTORY {
318            self.history.pop_front();
319        }
320
321        self.history_index = self.history.len();
322    }
323}
324
325fn render_titled<W: Widget>(frame: &mut Frame, widget: W, title: &str, area: Rect) {
326    let block = Block::bordered().title(title);
327    frame.render_widget(widget, block.inner(area));
328    frame.render_widget(block, area);
329}
330
331impl DrawData {
332    fn draw(&mut self, frame: &mut Frame) {
333        let layout = DebuggerLayout::new(frame.area());
334
335        let (code, line) = code_text(&self.code, self.highlight);
336        let p = Paragraph::new(code).scroll((line.saturating_sub(4) as u16, 0));
337        render_titled(frame, p, "code", layout.code);
338
339        render_titled(frame, Text::raw(&self.result), "Result", layout.result);
340
341        render_titled(frame, Text::from_iter(self.watchpoints.iter().map(|s| &**s)), "Watchpoints", layout.watchpoints);
342
343        render_titled(frame, Text::raw(&self.message), "Message", layout.message);
344
345        render_titled(frame, &self.prompt, "Command:", layout.user_input);
346    }
347}
348
349impl Ui for RatatuiUi {
350    fn display_user_data(&mut self, data: &UserData<'_>) {
351        self.data.code = data.code.to_string();
352        self.data.highlight = data.highlight;
353        self.data.result = data.result.map(|s| s.to_string()).unwrap_or_default();
354        self.data.watchpoints.clear();
355        self.data.watchpoints.extend(data.watchpoints.iter().enumerate().map(|(i, s)| format!("{i:>2} {s}")));
356        self.data.message = data.message.to_string();
357    }
358
359    fn receive_user_input(&mut self) -> String {
360        loop {
361            self.terminal.draw(|frame| self.data.draw(frame)).expect("failed to draw frame");
362            if let Event::Key(key_event) = event::read().expect("event") {
363                let control = key_event.modifiers.contains(KeyModifiers::CONTROL);
364                if let Some(string) = self.data.prompt.handle_key(key_event.code, control) {
365                    return string;
366                }
367            }
368        }
369    }
370}