/ interpreter / src / ratatui_ui.rs
ratatui_ui.rs
  1  // Copyright (C) 2019-2025 ADnet Contributors
  2  // This file is part of the ADL library.
  3  
  4  // The ADL 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 ADL 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 ADL library. If not, see <https://www.gnu.org/licenses/>.
 16  
 17  use super::ui::{Ui, UserData};
 18  
 19  use std::{cmp, collections::VecDeque, io::Stdout, mem};
 20  
 21  use crossterm::event::{self, Event, KeyCode, KeyModifiers};
 22  use 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)]
 43  struct DrawData {
 44      code: String,
 45      highlight: Option<(usize, usize)>,
 46      result: String,
 47      watchpoints: Vec<String>,
 48      message: String,
 49      prompt: Prompt,
 50  }
 51  
 52  pub struct RatatuiUi {
 53      terminal: Terminal<CrosstermBackend<Stdout>>,
 54      data: DrawData,
 55  }
 56  
 57  impl Drop for RatatuiUi {
 58      fn drop(&mut self) {
 59          ratatui::restore();
 60      }
 61  }
 62  
 63  impl RatatuiUi {
 64      pub fn new() -> Self {
 65          RatatuiUi { terminal: ratatui::init(), data: Default::default() }
 66      }
 67  }
 68  
 69  fn 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  
 93  fn 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  
116  struct DebuggerLayout {
117      code: Rect,
118      result: Rect,
119      watchpoints: Rect,
120      user_input: Rect,
121      message: Rect,
122  }
123  
124  impl 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)]
150  struct Prompt {
151      history: VecDeque<String>,
152      history_index: usize,
153      current: String,
154      cursor: usize,
155  }
156  
157  impl 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  
194  impl 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  
325  fn 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  
331  impl 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  
349  impl 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  }