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