1use 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), Constraint::Length(6), Constraint::Length(3), Constraint::Length(3), ])
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}