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 }