diff --git a/Cargo.lock b/Cargo.lock
index 95a4f21ee3..247dd86f57 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -362,12 +362,27 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+[[package]]
+name = "castaway"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
+dependencies = [
+ "rustversion",
+]
+
[[package]]
name = "cc"
version = "1.0.98"
@@ -482,6 +497,20 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "compact_str"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
[[package]]
name = "console"
version = "0.15.8"
@@ -491,7 +520,7 @@ dependencies = [
"encode_unicode",
"lazy_static",
"libc",
- "unicode-width",
+ "unicode-width 0.1.12",
"windows-sys 0.52.0",
]
@@ -700,6 +729,41 @@ dependencies = [
"syn 2.0.82",
]
+[[package]]
+name = "darling"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote 1.0.36",
+ "strsim",
+ "syn 2.0.82",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+dependencies = [
+ "darling_core",
+ "quote 1.0.36",
+ "syn 2.0.82",
+]
+
[[package]]
name = "der"
version = "0.7.9"
@@ -754,6 +818,12 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -1079,7 +1149,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
- "unicode-width",
+ "unicode-width 0.1.12",
]
[[package]]
@@ -1365,6 +1435,12 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
[[package]]
name = "idna"
version = "0.5.0"
@@ -1407,7 +1483,27 @@ dependencies = [
"instant",
"number_prefix",
"portable-atomic",
- "unicode-width",
+ "unicode-width 0.1.12",
+]
+
+[[package]]
+name = "indoc"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
+
+[[package]]
+name = "instability"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e"
+dependencies = [
+ "darling",
+ "indoc",
+ "pretty_assertions",
+ "proc-macro2",
+ "quote 1.0.36",
+ "syn 2.0.82",
]
[[package]]
@@ -1566,6 +1662,7 @@ name = "leo-interpreter"
version = "2.4.0"
dependencies = [
"colored",
+ "crossterm",
"dialoguer",
"indexmap 2.6.0",
"leo-ast",
@@ -1577,12 +1674,14 @@ dependencies = [
"leo-test-framework",
"rand",
"rand_chacha",
+ "ratatui",
"serial_test",
"snarkvm",
"snarkvm-circuit",
"snarkvm-synthesizer-program",
"tempfile",
"toml 0.8.19",
+ "tui-input",
]
[[package]]
@@ -2155,11 +2254,21 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
[[package]]
name = "proc-macro2"
-version = "1.0.84"
+version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
@@ -2227,6 +2336,27 @@ dependencies = [
"rand_core",
]
+[[package]]
+name = "ratatui"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
+dependencies = [
+ "bitflags 2.5.0",
+ "cassowary",
+ "compact_str",
+ "crossterm",
+ "indoc",
+ "instability",
+ "itertools 0.13.0",
+ "lru",
+ "paste",
+ "strum",
+ "unicode-segmentation",
+ "unicode-truncate",
+ "unicode-width 0.2.0",
+]
+
[[package]]
name = "rayon"
version = "1.10.0"
@@ -2495,6 +2625,12 @@ dependencies = [
"untrusted",
]
+[[package]]
+name = "rustversion"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
+
[[package]]
name = "rusty-hook"
version = "0.11.2"
@@ -3741,12 +3877,40 @@ dependencies = [
"der",
]
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote 1.0.36",
+ "rustversion",
+ "syn 2.0.82",
+]
+
[[package]]
name = "subtle"
version = "2.5.0"
@@ -4165,6 +4329,16 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+[[package]]
+name = "tui-input"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d1733c47f1a217b7deff18730ff7ca4ecafc5771368f715ab072d679a36114"
+dependencies = [
+ "ratatui",
+ "unicode-width 0.2.0",
+]
+
[[package]]
name = "typenum"
version = "1.17.0"
@@ -4192,12 +4366,35 @@ dependencies = [
"tinyvec",
]
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-truncate"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
+dependencies = [
+ "itertools 0.13.0",
+ "unicode-segmentation",
+ "unicode-width 0.1.12",
+]
+
[[package]]
name = "unicode-width"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
+[[package]]
+name = "unicode-width"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
+
[[package]]
name = "unicode-xid"
version = "0.0.4"
@@ -4607,6 +4804,12 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
[[package]]
name = "zerocopy"
version = "0.7.34"
diff --git a/interpreter/Cargo.toml b/interpreter/Cargo.toml
index 85a9b5877a..fd58c12e7d 100644
--- a/interpreter/Cargo.toml
+++ b/interpreter/Cargo.toml
@@ -48,6 +48,9 @@ workspace = true
[dependencies.colored]
workspace = true
+[dependencies.crossterm]
+version = "0.28.1"
+
[dependencies.indexmap]
workspace = true
@@ -61,9 +64,15 @@ workspace = true
[dependencies.rand_chacha]
workspace = true
+[dependencies.ratatui]
+version = "0.29.0"
+
[dependencies.toml]
workspace = true
+[dependencies.tui-input]
+version = "0.11.1"
+
[dev-dependencies.leo-test-framework]
path = "../tests/test-framework"
diff --git a/interpreter/src/interpreter.rs b/interpreter/src/interpreter.rs
index f2b1a06267..efc7f51178 100644
--- a/interpreter/src/interpreter.rs
+++ b/interpreter/src/interpreter.rs
@@ -269,8 +269,8 @@ impl Interpreter {
s,
source_file.start_pos,
)
- .map_err(|_e| {
- LeoError::InterpreterHalt(InterpreterHalt::new("failed to parse expression".into()))
+ .map_err(|e| {
+ LeoError::InterpreterHalt(InterpreterHalt::new(format!("Failed to parse expression: {e}")))
})?;
// TODO: This leak is silly.
let expr = Box::leak(Box::new(expression));
diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs
index aee0a59586..da5a0b426a 100644
--- a/interpreter/src/lib.rs
+++ b/interpreter/src/lib.rs
@@ -49,6 +49,8 @@ use ui::Ui as _;
mod dialoguer_input;
+mod ratatui_ui;
+
const INTRO: &str = "This is the Leo Interpreter. Try the command `#help`.";
const HELP: &str = "
@@ -126,7 +128,8 @@ pub fn interpret(
) -> Result<()> {
let mut interpreter = Interpreter::new(leo_filenames.iter(), aleo_filenames.iter(), signer, block_height)?;
- let mut user_interface = dialoguer_input::DialoguerUi::new();
+ // let mut user_interface = dialoguer_input::DialoguerUi::new();
+ let mut user_interface = ratatui_ui::RatatuiUi::new();
let mut code = String::new();
let mut futures = Vec::new();
@@ -152,7 +155,7 @@ pub fn interpret(
interpreter.update_watchpoints()?;
watchpoints.extend(interpreter.watchpoints.iter().map(|watchpoint| {
- format!("{:>20} = {}", watchpoint.code, if let Some(s) = &watchpoint.last_result { &**s } else { "?" })
+ format!("{:>15} = {}", watchpoint.code, if let Some(s) = &watchpoint.last_result { &**s } else { "?" })
}));
let user_data = ui::UserData {
diff --git a/interpreter/src/ratatui_ui.rs b/interpreter/src/ratatui_ui.rs
new file mode 100644
index 0000000000..3dfd4db746
--- /dev/null
+++ b/interpreter/src/ratatui_ui.rs
@@ -0,0 +1,372 @@
+// Copyright (C) 2019-2024 Aleo Systems Inc.
+// This file is part of the Leo library.
+
+// The Leo library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// The Leo library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with the Leo library. If not, see .
+
+use super::ui::{Ui, UserData};
+
+use std::{cmp, collections::VecDeque, io::Stdout, mem};
+
+use crossterm::event::{self, Event, KeyCode, KeyModifiers};
+use ratatui::{
+ Frame,
+ Terminal,
+ prelude::{
+ Buffer,
+ Constraint,
+ CrosstermBackend,
+ Direction,
+ Layout,
+ Line,
+ Modifier,
+ Rect,
+ Span,
+ Style,
+ Stylize as _,
+ },
+ text::Text,
+ widgets::{Block, Paragraph, Widget},
+};
+
+#[derive(Default)]
+struct DrawData {
+ code: String,
+ highlight: Option<(usize, usize)>,
+ result: String,
+ watchpoints: Vec,
+ message: String,
+ prompt: Prompt,
+}
+
+pub struct RatatuiUi {
+ terminal: Terminal>,
+ data: DrawData,
+}
+
+impl Drop for RatatuiUi {
+ fn drop(&mut self) {
+ ratatui::restore();
+ }
+}
+
+impl RatatuiUi {
+ pub fn new() -> Self {
+ RatatuiUi { terminal: ratatui::init(), data: Default::default() }
+ }
+}
+
+fn append_lines<'a>(
+ lines: &mut Vec>,
+ mut last_chunk: Option>,
+ string: &'a str,
+ style: Style,
+) -> Option> {
+ let mut line_iter = string.lines().peekable();
+ while let Some(line) = line_iter.next() {
+ let this_span = Span::styled(line, style);
+ let mut real_last_chunk = mem::take(&mut last_chunk).unwrap_or_else(|| Line::raw(""));
+ real_last_chunk.push_span(this_span);
+ if line_iter.peek().is_some() {
+ lines.push(real_last_chunk);
+ } else {
+ if string.ends_with('\n') {
+ lines.push(real_last_chunk);
+ return None;
+ } else {
+ return Some(real_last_chunk);
+ }
+ }
+ }
+
+ last_chunk
+}
+
+fn code_text(s: &str, highlight: Option<(usize, usize)>) -> (Text, usize) {
+ let Some((lo, hi)) = highlight else {
+ return (Text::from(s), 0);
+ };
+
+ let s1 = s.get(..lo).expect("should be able to split text");
+ let s2 = s.get(lo..hi).expect("should be able to split text");
+ let s3 = s.get(hi..).expect("should be able to split text");
+
+ let mut lines = Vec::new();
+
+ let s1_chunk = append_lines(&mut lines, None, s1, Style::default());
+ let line = lines.len();
+ let s2_chunk = append_lines(&mut lines, s1_chunk, s2, Style::new().red());
+ let s3_chunk = append_lines(&mut lines, s2_chunk, s3, Style::default());
+
+ if let Some(chunk) = s3_chunk {
+ lines.push(chunk);
+ }
+
+ (Text::from(lines), line)
+}
+
+struct DebuggerLayout {
+ code: Rect,
+ result: Rect,
+ watchpoints: Rect,
+ user_input: Rect,
+ message: Rect,
+}
+
+impl DebuggerLayout {
+ fn new(total: Rect) -> Self {
+ let overall_layout = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints([
+ Constraint::Fill(1), // Code
+ Constraint::Length(6), // Result and watchpoints
+ Constraint::Length(3), // Message
+ Constraint::Length(3), // User input
+ ])
+ .split(total);
+ let code = overall_layout[0];
+ let middle = overall_layout[1];
+ let message = overall_layout[2];
+ let user_input = overall_layout[3];
+
+ let middle = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
+ .split(middle);
+
+ DebuggerLayout { code, result: middle[0], watchpoints: middle[1], user_input, message }
+ }
+}
+
+#[derive(Debug, Default)]
+struct Prompt {
+ history: VecDeque,
+ history_index: usize,
+ current: String,
+ cursor: usize,
+}
+
+impl<'a> Widget for &'a Prompt {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ let mut plain = || {
+ Text::raw(&self.current).render(area, buf);
+ };
+
+ if self.cursor >= self.current.len() {
+ let span1 = Span::raw(&self.current);
+ let span2 = Span::styled(" ", Style::new().add_modifier(Modifier::REVERSED));
+ Text::from(Line::from_iter([span1, span2])).render(area, buf);
+ return;
+ }
+
+ let Some(pre) = self.current.get(..self.cursor) else {
+ plain();
+ return;
+ };
+
+ let Some(c) = self.current.get(self.cursor..self.cursor + 1) else {
+ plain();
+ return;
+ };
+
+ let Some(post) = self.current.get(self.cursor + 1..) else {
+ plain();
+ return;
+ };
+
+ Text::from(Line::from_iter([
+ Span::raw(pre),
+ Span::styled(c, Style::new().add_modifier(Modifier::REVERSED)),
+ Span::raw(post),
+ ]))
+ .render(area, buf);
+ }
+}
+
+impl Prompt {
+ fn handle_key(&mut self, key: KeyCode, control: bool) -> Option {
+ match (key, control) {
+ (KeyCode::Enter, _) => {
+ self.history.push_back(mem::take(&mut self.current));
+ self.history_index = self.history.len();
+ return self.history.back().cloned();
+ }
+ (KeyCode::Backspace, _) => self.backspace(),
+ (KeyCode::Left, _) => self.left(),
+ (KeyCode::Right, _) => self.right(),
+ (KeyCode::Up, _) => self.history_prev(),
+ (KeyCode::Down, _) => self.history_next(),
+ (KeyCode::Delete, _) => self.delete(),
+ (KeyCode::Char(c), false) => self.new_character(c),
+ (KeyCode::Char('a'), true) => self.beginning_of_line(),
+ (KeyCode::Char('e'), true) => self.end_of_line(),
+ _ => {}
+ }
+
+ None
+ }
+
+ fn new_character(&mut self, c: char) {
+ if self.cursor >= self.current.len() {
+ self.current.push(c);
+ self.cursor = self.current.len();
+ } else {
+ let Some(pre) = self.current.get(..self.cursor) else {
+ return;
+ };
+ let Some(post) = self.current.get(self.cursor..) else {
+ return;
+ };
+ let mut with_char = format!("{pre}{c}");
+ self.cursor = with_char.len();
+ with_char.push_str(post);
+ self.current = with_char;
+ }
+ self.check_history();
+ }
+
+ fn right(&mut self) {
+ self.cursor = cmp::min(self.cursor + 1, self.current.len());
+ }
+
+ fn left(&mut self) {
+ self.cursor = self.cursor.saturating_sub(1);
+ }
+
+ fn backspace(&mut self) {
+ if self.cursor == 0 {
+ return;
+ }
+
+ if self.cursor >= self.current.len() {
+ self.current.pop();
+ self.cursor = self.current.len();
+ return;
+ }
+
+ let Some(pre) = self.current.get(..self.cursor - 1) else {
+ return;
+ };
+ let Some(post) = self.current.get(self.cursor..) else {
+ return;
+ };
+ self.cursor -= 1;
+
+ let s = format!("{pre}{post}");
+
+ self.current = s;
+
+ self.check_history();
+ }
+
+ fn delete(&mut self) {
+ if self.cursor + 1 >= self.current.len() {
+ return;
+ }
+
+ let Some(pre) = self.current.get(..self.cursor) else {
+ return;
+ };
+ let Some(post) = self.current.get(self.cursor + 1..) else {
+ return;
+ };
+
+ let s = format!("{pre}{post}");
+
+ self.current = s;
+
+ self.check_history();
+ }
+
+ fn beginning_of_line(&mut self) {
+ self.cursor = 0;
+ }
+
+ fn end_of_line(&mut self) {
+ self.cursor = self.current.len();
+ }
+
+ fn history_next(&mut self) {
+ self.history_index += 1;
+ if self.history_index > self.history.len() {
+ self.history_index = 0;
+ }
+ self.current = self.history.get(self.history_index).cloned().unwrap_or(String::new());
+ }
+
+ fn history_prev(&mut self) {
+ if self.history_index == 0 {
+ self.history_index = self.history.len();
+ } else {
+ self.history_index -= 1;
+ }
+ self.current = self.history.get(self.history_index).cloned().unwrap_or(String::new());
+ }
+
+ fn check_history(&mut self) {
+ const MAX_HISTORY: usize = 50;
+
+ while self.history.len() > MAX_HISTORY {
+ self.history.pop_front();
+ }
+
+ self.history_index = self.history.len();
+ }
+}
+
+fn render_titled(frame: &mut Frame, widget: W, title: &str, area: Rect) {
+ let block = Block::bordered().title(title);
+ frame.render_widget(widget, block.inner(area));
+ frame.render_widget(block, area);
+}
+
+impl DrawData {
+ fn draw(&mut self, frame: &mut Frame) {
+ let layout = DebuggerLayout::new(frame.area());
+
+ let (code, line) = code_text(&self.code, self.highlight);
+ let p = Paragraph::new(code).scroll((line.saturating_sub(4) as u16, 0));
+ render_titled(frame, p, "code", layout.code);
+
+ render_titled(frame, Text::raw(&self.result), "Result", layout.result);
+
+ render_titled(frame, Text::from_iter(self.watchpoints.iter().map(|s| &**s)), "Watchpoints", layout.watchpoints);
+
+ render_titled(frame, Text::raw(&self.message), "Message", layout.message);
+
+ render_titled(frame, &self.prompt, "Command:", layout.user_input);
+ }
+}
+
+impl Ui for RatatuiUi {
+ fn display_user_data(&mut self, data: &UserData<'_>) {
+ self.data.code = data.code.to_string();
+ self.data.highlight = data.highlight;
+ self.data.result = data.result.map(|s| s.to_string()).unwrap_or(String::new());
+ self.data.watchpoints.clear();
+ self.data.watchpoints.extend(data.watchpoints.iter().enumerate().map(|(i, s)| format!("{i:>2} {s}")));
+ self.data.message = data.message.to_string();
+ }
+
+ fn receive_user_input(&mut self) -> String {
+ loop {
+ self.terminal.draw(|frame| self.data.draw(frame)).expect("failed to draw frame");
+ if let Event::Key(key_event) = event::read().expect("event") {
+ let control = key_event.modifiers.contains(KeyModifiers::CONTROL);
+ if let Some(string) = self.data.prompt.handle_key(key_event.code, control) {
+ return string;
+ }
+ }
+ }
+ }
+}