1use super::context::TUIContext;
4use crate::op::OpcodeParam;
5use foundry_compilers::artifacts::sourcemap::SourceElement;
6use foundry_evm_core::buffer::{BufferKind, get_buffer_accesses};
7use foundry_evm_traces::debug::SourceData;
8use ratatui::{
9 Frame,
10 layout::{Alignment, Constraint, Direction, Layout, Rect},
11 style::{Color, Modifier, Style},
12 text::{Line, Span, Text},
13 widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
14};
15use revm_inspectors::tracing::types::CallKind;
16use std::{collections::VecDeque, fmt::Write, io};
17
18impl TUIContext<'_> {
19 pub(crate) fn draw(&self, terminal: &mut super::DebuggerTerminal) -> io::Result<()> {
21 terminal.draw(|f| self.draw_layout(f)).map(drop)
22 }
23
24 fn draw_layout(&self, f: &mut Frame<'_>) {
25 let area = f.area();
27 let min_width = 100;
28 let min_height = 16;
29 if area.width < min_width || area.height < min_height {
30 self.size_too_small(f, min_width, min_height);
31 return;
32 }
33
34 let min_column_width_for_horizontal = 200;
36 if area.width >= min_column_width_for_horizontal {
37 self.horizontal_layout(f);
38 } else {
39 self.vertical_layout(f);
40 }
41 }
42
43 fn size_too_small(&self, f: &mut Frame<'_>, min_width: u16, min_height: u16) {
44 let mut lines = Vec::with_capacity(4);
45
46 let l1 = "Terminal size too small:";
47 lines.push(Line::from(l1));
48
49 let area = f.area();
50 let width_color = if area.width >= min_width { Color::Green } else { Color::Red };
51 let height_color = if area.height >= min_height { Color::Green } else { Color::Red };
52 let l2 = vec![
53 Span::raw("Width = "),
54 Span::styled(area.width.to_string(), Style::new().fg(width_color)),
55 Span::raw(" Height = "),
56 Span::styled(area.height.to_string(), Style::new().fg(height_color)),
57 ];
58 lines.push(Line::from(l2));
59
60 let l3 = "Needed for current config:";
61 lines.push(Line::from(l3));
62 let l4 = format!("Width = {min_width} Height = {min_height}");
63 lines.push(Line::from(l4));
64
65 let paragraph =
66 Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: true });
67 f.render_widget(paragraph, area)
68 }
69
70 fn vertical_layout(&self, f: &mut Frame<'_>) {
86 let area = f.area();
87 let h_height = if self.show_shortcuts { 4 } else { 0 };
88
89 let [app, footer] = Layout::new(
94 Direction::Vertical,
95 [Constraint::Ratio(100 - h_height, 100), Constraint::Ratio(h_height, 100)],
96 )
97 .split(area)[..] else {
98 unreachable!()
99 };
100
101 let [op_pane, stack_pane, memory_pane, src_pane] = Layout::new(
103 Direction::Vertical,
104 [
105 Constraint::Ratio(1, 6),
106 Constraint::Ratio(1, 6),
107 Constraint::Ratio(1, 6),
108 Constraint::Ratio(3, 6),
109 ],
110 )
111 .split(app)[..] else {
112 unreachable!()
113 };
114
115 if self.show_shortcuts {
116 self.draw_footer(f, footer);
117 }
118 self.draw_src(f, src_pane);
119 self.draw_op_list(f, op_pane);
120 self.draw_stack(f, stack_pane);
121 self.draw_buffer(f, memory_pane);
122 }
123
124 fn horizontal_layout(&self, f: &mut Frame<'_>) {
136 let area = f.area();
137 let h_height = if self.show_shortcuts { 4 } else { 0 };
138
139 let [app, footer] = Layout::new(
141 Direction::Vertical,
142 [Constraint::Ratio(100 - h_height, 100), Constraint::Ratio(h_height, 100)],
143 )
144 .split(area)[..] else {
145 unreachable!()
146 };
147
148 let [app_left, app_right] =
150 Layout::new(Direction::Horizontal, [Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
151 .split(app)[..]
152 else {
153 unreachable!()
154 };
155
156 let [op_pane, src_pane] =
158 Layout::new(Direction::Vertical, [Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
159 .split(app_left)[..]
160 else {
161 unreachable!()
162 };
163
164 let [stack_pane, memory_pane] =
166 Layout::new(Direction::Vertical, [Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
167 .split(app_right)[..]
168 else {
169 unreachable!()
170 };
171
172 if self.show_shortcuts {
173 self.draw_footer(f, footer);
174 }
175 self.draw_src(f, src_pane);
176 self.draw_op_list(f, op_pane);
177 self.draw_stack(f, stack_pane);
178 self.draw_buffer(f, memory_pane);
179 }
180
181 fn draw_footer(&self, f: &mut Frame<'_>, area: Rect) {
182 let l1 = "[q]: quit | [k/j]: prev/next op | [a/s]: prev/next jump | [c/C]: prev/next call | [g/G]: start/end | [b]: cycle memory/calldata/returndata buffers";
183 let l2 = "[t]: stack labels | [m]: buffer decoding | [shift + j/k]: scroll stack | [ctrl + j/k]: scroll buffer | ['<char>]: goto breakpoint | [h] toggle help";
184 let dimmed = Style::new().add_modifier(Modifier::DIM);
185 let lines =
186 vec![Line::from(Span::styled(l1, dimmed)), Line::from(Span::styled(l2, dimmed))];
187 let paragraph =
188 Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: false });
189 f.render_widget(paragraph, area);
190 }
191
192 fn draw_src(&self, f: &mut Frame<'_>, area: Rect) {
193 let (text_output, source_name) = self.src_text(area);
194 let call_kind_text = match self.call_kind() {
195 CallKind::Create | CallKind::Create2 => "Contract creation",
196 CallKind::Call => "Contract call",
197 CallKind::StaticCall => "Contract staticcall",
198 CallKind::CallCode => "Contract callcode",
199 CallKind::DelegateCall => "Contract delegatecall",
200 CallKind::AuthCall => "Contract authcall",
201 };
202 let title = format!(
203 "{} {} ",
204 call_kind_text,
205 source_name.map(|s| format!("| {s}")).unwrap_or_default()
206 );
207 let block = Block::default().title(title).borders(Borders::ALL);
208 let paragraph = Paragraph::new(text_output).block(block).wrap(Wrap { trim: false });
209 f.render_widget(paragraph, area);
210 }
211
212 fn src_text(&self, area: Rect) -> (Text<'_>, Option<&str>) {
213 let (source_element, source) = match self.src_map() {
214 Ok(r) => r,
215 Err(e) => return (Text::from(e), None),
216 };
217
218 let offset = source_element.offset() as usize;
223 let len = source_element.length() as usize;
224 let max = source.source.len();
225
226 let actual_start = offset.min(max);
228 let actual_end = (offset + len).min(max);
229
230 let mut before: Vec<_> = source.source[..actual_start].split_inclusive('\n').collect();
231 let actual: Vec<_> =
232 source.source[actual_start..actual_end].split_inclusive('\n').collect();
233 let mut after: VecDeque<_> = source.source[actual_end..].split_inclusive('\n').collect();
234
235 let num_lines = before.len() + actual.len() + after.len();
236 let height = area.height as usize;
237 let needed_highlight = actual.len();
238 let mid_len = before.len() + actual.len();
239
240 let (start_line, end_line) = if needed_highlight > height {
242 let start_line = before.len().saturating_sub(1);
244 (start_line, before.len() + needed_highlight)
245 } else if height > num_lines {
246 (0, num_lines)
248 } else {
249 let remaining = height - needed_highlight;
250 let mut above = remaining / 2;
251 let mut below = remaining / 2;
252 if below > after.len() {
253 above += below - after.len();
255 } else if above > before.len() {
256 below += above - before.len();
258 } else {
259 }
261
262 if above == 0 {
266 above = 1;
267 }
268 (before.len().saturating_sub(above), mid_len + below)
269 };
270
271 let u_num = Style::new().fg(Color::Gray);
273 let u_text = Style::new().add_modifier(Modifier::DIM);
275 let h_num = Style::new().fg(Color::Cyan);
277 let h_text = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD);
279
280 let mut lines = SourceLines::new(start_line, end_line);
281
282 if let Some(last) = before.pop() {
284 let last_has_nl = last.ends_with('\n');
285
286 if last_has_nl {
287 before.push(last);
288 }
289 for line in &before[start_line..] {
290 lines.push(u_num, line, u_text);
291 }
292
293 let first = if !last_has_nl {
294 lines.push_raw(h_num, &[Span::raw(last), Span::styled(actual[0], h_text)]);
295 1
296 } else {
297 0
298 };
299
300 for line in &actual[first..] {
302 lines.push(h_num, line, h_text);
303 }
304 } else {
305 for line in &actual {
307 lines.push(h_num, line, h_text);
308 }
309 }
310
311 if let Some(last) = actual.last()
313 && !last.ends_with('\n')
314 && let Some(post) = after.pop_front()
315 && let Some(last) = lines.lines.last_mut()
316 {
317 last.spans.push(Span::raw(post));
318 }
319
320 while mid_len + after.len() > end_line {
322 after.pop_back();
323 }
324 for line in after {
325 lines.push(u_num, line, u_text);
326 }
327
328 for line in &mut lines.lines {
330 if area.width as usize > line.width() + 1 {
332 line.push_span(Span::raw(" ".repeat(area.width as usize - line.width() - 1)));
333 }
334 }
335
336 (Text::from(lines.lines), source.path.to_str())
337 }
338
339 fn src_map(&self) -> Result<(SourceElement, &SourceData), String> {
341 let address = self.address();
342 let Some(contract_name) = self.debugger_context.identified_contracts.get(address) else {
343 return Err(format!("Unknown contract at address {address}"));
344 };
345
346 self.debugger_context
347 .contracts_sources
348 .find_source_mapping(
349 contract_name,
350 self.current_step().pc as u32,
351 self.debug_call().kind.is_any_create(),
352 )
353 .ok_or_else(|| format!("No source map for contract {contract_name}"))
354 }
355
356 fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) {
357 let debug_steps = self.debug_steps();
358 let max_pc = debug_steps.iter().map(|step| step.pc).max().unwrap_or(0);
359 let max_pc_len = hex_digits(max_pc);
360
361 let items = debug_steps
362 .iter()
363 .enumerate()
364 .map(|(i, step)| {
365 let mut content = String::with_capacity(64);
366 write!(content, "{:0>max_pc_len$x}|", step.pc).unwrap();
367 if let Some(op) = self.opcode_list.get(i) {
368 content.push_str(op);
369 }
370 ListItem::new(Span::styled(content, Style::new().fg(Color::White)))
371 })
372 .collect::<Vec<_>>();
373
374 let title = format!(
375 "Address: {} | PC: {} | Gas used in call: {}",
376 self.address(),
377 self.current_step().pc,
378 self.current_step().gas_used,
379 );
380 let block = Block::default().title(title).borders(Borders::ALL);
381 let list = List::new(items)
382 .block(block)
383 .highlight_symbol("▶")
384 .highlight_style(Style::new().fg(Color::White).bg(Color::DarkGray))
385 .scroll_padding(1);
386 let mut state = ListState::default().with_selected(Some(self.current_step));
387 f.render_stateful_widget(list, area, &mut state);
388 }
389
390 fn draw_stack(&self, f: &mut Frame<'_>, area: Rect) {
391 let step = self.current_step();
392 let stack = step.stack.as_ref();
393 let stack_len = stack.map_or(0, |s| s.len());
394
395 let min_len = decimal_digits(stack_len).max(2);
396
397 let params = OpcodeParam::of(step.op.get());
398
399 let text: Vec<Line<'_>> = stack
400 .map(|stack| {
401 stack
402 .iter()
403 .rev()
404 .enumerate()
405 .skip(self.draw_memory.current_stack_startline)
406 .map(|(i, stack_item)| {
407 let param = params.iter().find(|param| param.index == i);
408 let mut spans = Vec::with_capacity(1 + 32 * 2 + 3);
409
410 spans.push(Span::styled(
412 format!("{i:0min_len$}| "),
413 Style::new().fg(Color::White),
414 ));
415
416 hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| {
418 if param.is_some() {
419 Style::new().fg(Color::Cyan)
420 } else {
421 Style::new().fg(Color::White)
422 }
423 });
424
425 if self.stack_labels
426 && let Some(param) = param
427 {
428 spans.push(Span::raw("| "));
429 spans.push(Span::raw(param.name));
430 }
431
432 spans.push(Span::raw("\n"));
433
434 Line::from(spans)
435 })
436 .collect()
437 })
438 .unwrap_or_default();
439
440 let title = format!("Stack: {stack_len}");
441 let block = Block::default().title(title).borders(Borders::ALL);
442 let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
443 f.render_widget(paragraph, area);
444 }
445
446 fn draw_buffer(&self, f: &mut Frame<'_>, area: Rect) {
447 let call = self.debug_call();
448 let step = self.current_step();
449 let buf = match self.active_buffer {
450 BufferKind::Memory => step.memory.as_ref().unwrap().as_ref(),
451 BufferKind::Calldata => call.calldata.as_ref(),
452 BufferKind::Returndata => step.returndata.as_ref(),
453 };
454
455 let min_len = hex_digits(buf.len());
456
457 let mut offset = None;
459 let mut len = None;
460 let mut write_offset = None;
461 let mut write_size = None;
462 let mut color = None;
463 let stack_len = step.stack.as_ref().map_or(0, |s| s.len());
464 if stack_len > 0
465 && let Some(stack) = step.stack.as_ref()
466 && let Some(accesses) = get_buffer_accesses(step.op.get(), stack)
467 {
468 if let Some(read_access) = accesses.read {
469 offset = Some(read_access.1.offset);
470 len = Some(read_access.1.len);
471 color = Some(Color::Cyan);
472 }
473 if let Some(write_access) = accesses.write
474 && self.active_buffer == BufferKind::Memory
475 {
476 write_offset = Some(write_access.offset);
477 write_size = Some(write_access.len);
478 }
479 }
480
481 if self.current_step > 0 {
486 let prev_step = self.current_step - 1;
487 let prev_step = &self.debug_steps()[prev_step];
488 if let Some(stack) = prev_step.stack.as_ref()
489 && let Some(write_access) =
490 get_buffer_accesses(prev_step.op.get(), stack).and_then(|a| a.write)
491 && self.active_buffer == BufferKind::Memory
492 {
493 offset = Some(write_access.offset);
494 len = Some(write_access.len);
495 color = Some(Color::Green);
496 }
497 }
498
499 let height = area.height as usize;
500 let end_line = self.draw_memory.current_buf_startline + height;
501
502 let text: Vec<Line<'_>> = buf
503 .chunks(32)
504 .enumerate()
505 .skip(self.draw_memory.current_buf_startline)
506 .take_while(|(i, _)| *i < end_line)
507 .map(|(i, buf_word)| {
508 let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1);
509
510 spans.push(Span::styled(
512 format!("{:0min_len$x}| ", i * 32),
513 Style::new().fg(Color::White),
514 ));
515
516 hex_bytes_spans(buf_word, &mut spans, |j, _| {
518 let mut byte_color = Color::White;
519 let mut end = None;
520 let idx = i * 32 + j;
521 if let (Some(offset), Some(len), Some(color)) = (offset, len, color) {
522 end = Some(offset + len);
523 if (offset..offset + len).contains(&idx) {
524 byte_color = color;
528 }
529 }
530 if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
531 let write_end = write_offset + write_size;
533 if let Some(read_end) = end {
534 let read_start = offset.unwrap();
535 if (write_offset..write_end).contains(&read_end) {
536 if (write_offset..read_end).contains(&idx) {
538 return Style::new().fg(Color::Yellow);
539 }
540 } else if (write_offset..write_end).contains(&read_start) {
541 if (read_start..write_end).contains(&idx) {
544 return Style::new().fg(Color::Yellow);
545 }
546 }
547 }
548 if (write_offset..write_end).contains(&idx) {
549 byte_color = Color::Red;
550 }
551 }
552
553 Style::new().fg(byte_color)
554 });
555
556 if self.buf_utf {
557 spans.push(Span::raw("|"));
558 for utf in buf_word.chunks(4) {
559 if let Ok(utf_str) = std::str::from_utf8(utf) {
560 spans.push(Span::raw(utf_str.replace('\0', ".")));
561 } else {
562 spans.push(Span::raw("."));
563 }
564 }
565 }
566
567 spans.push(Span::raw("\n"));
568
569 Line::from(spans)
570 })
571 .collect();
572
573 let title = self.active_buffer.title(buf.len());
574 let block = Block::default().title(title).borders(Borders::ALL);
575 let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
576 f.render_widget(paragraph, area);
577 }
578}
579
580struct SourceLines<'a> {
582 lines: Vec<Line<'a>>,
583 start_line: usize,
584 max_line_num: usize,
585}
586
587impl<'a> SourceLines<'a> {
588 fn new(start_line: usize, end_line: usize) -> Self {
589 Self { lines: Vec::new(), start_line, max_line_num: decimal_digits(end_line) }
590 }
591
592 fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) {
593 self.push_raw(line_number_style, &[Span::styled(line, line_style)]);
594 }
595
596 fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) {
597 let mut line_spans = Vec::with_capacity(4);
598
599 let line_number = format!(
600 "{number: >width$} ",
601 number = self.start_line + self.lines.len() + 1,
602 width = self.max_line_num
603 );
604 line_spans.push(Span::styled(line_number, line_number_style));
605
606 line_spans.push(Span::raw(" "));
608
609 line_spans.extend_from_slice(spans);
610
611 self.lines.push(Line::from(line_spans));
612 }
613}
614
615fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec<Span<'_>>, f: impl Fn(usize, u8) -> Style) {
616 for (i, &byte) in bytes.iter().enumerate() {
617 if i > 0 {
618 spans.push(Span::raw(" "));
619 }
620 spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte)));
621 }
622}
623
624fn decimal_digits(n: usize) -> usize {
628 n.checked_ilog10().unwrap_or(0) as usize + 1
629}
630
631fn hex_digits(n: usize) -> usize {
635 n.checked_ilog(16).unwrap_or(0) as usize + 1
636}
637
638#[cfg(test)]
639mod tests {
640 #[test]
641 fn decimal_digits() {
642 assert_eq!(super::decimal_digits(0), 1);
643 assert_eq!(super::decimal_digits(1), 1);
644 assert_eq!(super::decimal_digits(2), 1);
645 assert_eq!(super::decimal_digits(9), 1);
646 assert_eq!(super::decimal_digits(10), 2);
647 assert_eq!(super::decimal_digits(11), 2);
648 assert_eq!(super::decimal_digits(50), 2);
649 assert_eq!(super::decimal_digits(99), 2);
650 assert_eq!(super::decimal_digits(100), 3);
651 assert_eq!(super::decimal_digits(101), 3);
652 assert_eq!(super::decimal_digits(201), 3);
653 assert_eq!(super::decimal_digits(999), 3);
654 assert_eq!(super::decimal_digits(1000), 4);
655 assert_eq!(super::decimal_digits(1001), 4);
656 }
657
658 #[test]
659 fn hex_digits() {
660 assert_eq!(super::hex_digits(0), 1);
661 assert_eq!(super::hex_digits(1), 1);
662 assert_eq!(super::hex_digits(2), 1);
663 assert_eq!(super::hex_digits(9), 1);
664 assert_eq!(super::hex_digits(10), 1);
665 assert_eq!(super::hex_digits(11), 1);
666 assert_eq!(super::hex_digits(15), 1);
667 assert_eq!(super::hex_digits(16), 2);
668 assert_eq!(super::hex_digits(17), 2);
669 assert_eq!(super::hex_digits(0xff), 2);
670 assert_eq!(super::hex_digits(0x100), 3);
671 assert_eq!(super::hex_digits(0x101), 3);
672 }
673}