1use super::context::{StatusKind, TUIContext};
4use crate::{debugger::DebuggerStats, op::OpcodeParam};
5use alloy_primitives::Address;
6use foundry_compilers::artifacts::sourcemap::SourceElement;
7use foundry_evm_core::buffer::{BufferKind, get_buffer_accesses};
8use foundry_evm_traces::debug::SourceData;
9use ratatui::{
10 Frame,
11 layout::{Alignment, Constraint, Direction, Layout, Rect},
12 style::{Color, Modifier, Style},
13 text::{Line, Span, Text},
14 widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
15};
16use revm_inspectors::tracing::types::CallKind;
17use std::{collections::VecDeque, fmt::Write};
18
19impl TUIContext<'_> {
20 pub(crate) fn draw_layout(&self, f: &mut Frame<'_>) {
21 let area = f.area();
23 let min_width = 100;
24 let min_height = 16;
25 if area.width < min_width || area.height < min_height {
26 self.size_too_small(f, min_width, min_height);
27 return;
28 }
29
30 let min_column_width_for_horizontal = 200;
32 if area.width >= min_column_width_for_horizontal {
33 self.horizontal_layout(f);
34 } else {
35 self.vertical_layout(f);
36 }
37 }
38
39 fn size_too_small(&self, f: &mut Frame<'_>, min_width: u16, min_height: u16) {
40 let mut lines = Vec::with_capacity(4);
41
42 let l1 = "Terminal size too small:";
43 lines.push(Line::from(l1));
44
45 let area = f.area();
46 let width_color = if area.width >= min_width { Color::Green } else { Color::Red };
47 let height_color = if area.height >= min_height { Color::Green } else { Color::Red };
48 let l2 = vec![
49 Span::raw("Width = "),
50 Span::styled(area.width.to_string(), Style::new().fg(width_color)),
51 Span::raw(" Height = "),
52 Span::styled(area.height.to_string(), Style::new().fg(height_color)),
53 ];
54 lines.push(Line::from(l2));
55
56 let l3 = "Needed for current config:";
57 lines.push(Line::from(l3));
58 let l4 = format!("Width = {min_width} Height = {min_height}");
59 lines.push(Line::from(l4));
60
61 let paragraph =
62 Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: true });
63 f.render_widget(paragraph, area)
64 }
65
66 fn vertical_layout(&self, f: &mut Frame<'_>) {
82 let area = f.area();
83 let footer_height = self.footer_height();
84
85 let [app, footer] = Layout::new(
90 Direction::Vertical,
91 [Constraint::Min(0), Constraint::Length(footer_height)],
92 )
93 .split(area)[..] else {
94 unreachable!()
95 };
96
97 let [op_pane, stack_pane, memory_pane, src_pane] = Layout::new(
99 Direction::Vertical,
100 [
101 Constraint::Ratio(1, 6),
102 Constraint::Ratio(1, 6),
103 Constraint::Ratio(1, 6),
104 Constraint::Ratio(3, 6),
105 ],
106 )
107 .split(app)[..] else {
108 unreachable!()
109 };
110
111 if footer_height > 0 {
112 self.draw_footer(f, footer);
113 }
114 self.draw_src(f, src_pane);
115 self.draw_op_list(f, op_pane);
116 self.draw_stack(f, stack_pane);
117 self.draw_buffer(f, memory_pane);
118 }
119
120 fn horizontal_layout(&self, f: &mut Frame<'_>) {
132 let area = f.area();
133 let footer_height = self.footer_height();
134
135 let [app, footer] = Layout::new(
137 Direction::Vertical,
138 [Constraint::Min(0), Constraint::Length(footer_height)],
139 )
140 .split(area)[..] else {
141 unreachable!()
142 };
143
144 let [app_left, app_right] =
146 Layout::new(Direction::Horizontal, [Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
147 .split(app)[..]
148 else {
149 unreachable!()
150 };
151
152 let [op_pane, src_pane] =
154 Layout::new(Direction::Vertical, [Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
155 .split(app_left)[..]
156 else {
157 unreachable!()
158 };
159
160 let [stack_pane, memory_pane] =
162 Layout::new(Direction::Vertical, [Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
163 .split(app_right)[..]
164 else {
165 unreachable!()
166 };
167
168 if footer_height > 0 {
169 self.draw_footer(f, footer);
170 }
171 self.draw_src(f, src_pane);
172 self.draw_op_list(f, op_pane);
173 self.draw_stack(f, stack_pane);
174 self.draw_buffer(f, memory_pane);
175 }
176
177 fn footer_height(&self) -> u16 {
178 let status_or_input = u16::from(self.pc_input.is_some() || self.status.is_some());
179 let shortcuts = if self.show_shortcuts { 2 } else { 0 };
180 status_or_input + shortcuts
181 }
182
183 fn draw_footer(&self, f: &mut Frame<'_>, area: Rect) {
184 let mut lines = Vec::with_capacity(self.footer_height() as usize);
185
186 if let Some(input) = &self.pc_input {
187 lines.push(Line::from(vec![
188 Span::styled(
189 "Goto PC: ",
190 Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
191 ),
192 Span::raw(input.as_str()),
193 Span::styled("█", Style::new().fg(Color::Cyan)),
194 Span::styled(
195 " Enter: jump | Esc: cancel | hex: 0x2a/2a | decimal: d:42",
196 Style::new().add_modifier(Modifier::DIM),
197 ),
198 ]));
199 } else if let Some(status) = &self.status {
200 let style = match status.kind {
201 StatusKind::Info => Style::new().fg(Color::Green),
202 StatusKind::Error => Style::new().fg(Color::Red).add_modifier(Modifier::BOLD),
203 };
204 lines.push(Line::from(Span::styled(status.text.as_str(), style)));
205 }
206
207 let l1 = "[q]: quit | [k/j]: prev/next op | [a/s]: prev/next jump | [c/C]: prev/next call | [g/G]: start/end | [p]: goto PC | [b]: cycle memory/calldata/returndata buffers";
208 let l2 = "[t]: stack labels | [m]: buffer decoding | [shift + j/k]: scroll stack | [ctrl + j/k]: scroll buffer | ['<char>]: goto breakpoint | [h] toggle help";
209 let dimmed = Style::new().add_modifier(Modifier::DIM);
210 if self.show_shortcuts {
211 lines.push(Line::from(Span::styled(l1, dimmed)));
212 lines.push(Line::from(Span::styled(l2, dimmed)));
213 }
214
215 let paragraph =
216 Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: false });
217 f.render_widget(paragraph, area);
218 }
219
220 fn draw_src(&self, f: &mut Frame<'_>, area: Rect) {
221 let (text_output, source_name) = self.src_text(area);
222 let call_kind_text = match self.call_kind() {
223 CallKind::Create | CallKind::Create2 => "Contract creation",
224 CallKind::Call => "Contract call",
225 CallKind::StaticCall => "Contract staticcall",
226 CallKind::CallCode => "Contract callcode",
227 CallKind::DelegateCall => "Contract delegatecall",
228 CallKind::AuthCall => "Contract authcall",
229 };
230 let title = format!(
231 "{} {} ",
232 call_kind_text,
233 source_name.map(|s| format!("| {s}")).unwrap_or_default()
234 );
235 let block = Block::default().title(title).borders(Borders::ALL);
236 let paragraph = Paragraph::new(text_output).block(block).wrap(Wrap { trim: false });
237 f.render_widget(paragraph, area);
238 }
239
240 fn src_text(&self, area: Rect) -> (Text<'_>, Option<&str>) {
241 let (source_element, source) = match self.src_map() {
242 Ok(r) => r,
243 Err(e) => return (Text::from(e), None),
244 };
245
246 let offset = source_element.offset() as usize;
251 let len = source_element.length() as usize;
252 let max = source.source.len();
253
254 let actual_start = offset.min(max);
256 let actual_end = (offset + len).min(max);
257
258 let mut before: Vec<_> = source.source[..actual_start].split_inclusive('\n').collect();
259 let actual: Vec<_> =
260 source.source[actual_start..actual_end].split_inclusive('\n').collect();
261 let mut after: VecDeque<_> = source.source[actual_end..].split_inclusive('\n').collect();
262
263 let num_lines = before.len() + actual.len() + after.len();
264 let height = area.height as usize;
265 let needed_highlight = actual.len();
266 let mid_len = before.len() + actual.len();
267
268 let (start_line, end_line) = if needed_highlight > height {
270 let start_line = before.len().saturating_sub(1);
272 (start_line, before.len() + needed_highlight)
273 } else if height > num_lines {
274 (0, num_lines)
276 } else {
277 let remaining = height - needed_highlight;
278 let mut above = remaining / 2;
279 let mut below = remaining / 2;
280 if below > after.len() {
281 above += below - after.len();
283 } else if above > before.len() {
284 below += above - before.len();
286 } else {
287 }
289
290 if above == 0 {
294 above = 1;
295 }
296 (before.len().saturating_sub(above), mid_len + below)
297 };
298
299 let u_num = Style::new().fg(Color::Gray);
301 let u_text = Style::new().add_modifier(Modifier::DIM);
303 let h_num = Style::new().fg(Color::Cyan);
305 let h_text = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD);
307
308 let mut lines = SourceLines::new(start_line, end_line);
309
310 if let Some(last) = before.pop() {
312 let last_has_nl = last.ends_with('\n');
313
314 if last_has_nl {
315 before.push(last);
316 }
317 for line in &before[start_line..] {
318 lines.push(u_num, line, u_text);
319 }
320
321 let first = if last_has_nl {
322 0
323 } else {
324 lines.push_raw(h_num, &[Span::raw(last), Span::styled(actual[0], h_text)]);
325 1
326 };
327
328 for line in &actual[first..] {
330 lines.push(h_num, line, h_text);
331 }
332 } else {
333 for line in &actual {
335 lines.push(h_num, line, h_text);
336 }
337 }
338
339 if let Some(last) = actual.last()
341 && !last.ends_with('\n')
342 && let Some(post) = after.pop_front()
343 && let Some(last) = lines.lines.last_mut()
344 {
345 last.spans.push(Span::raw(post));
346 }
347
348 while mid_len + after.len() > end_line {
350 after.pop_back();
351 }
352 for line in after {
353 lines.push(u_num, line, u_text);
354 }
355
356 for line in &mut lines.lines {
358 if area.width as usize > line.width() + 1 {
360 line.push_span(Span::raw(" ".repeat(area.width as usize - line.width() - 1)));
361 }
362 }
363
364 (Text::from(lines.lines), source.path.to_str())
365 }
366
367 fn src_map(&self) -> Result<(SourceElement, &SourceData), String> {
369 let address = self.address();
370 let Some(contract_name) = self.debugger_context.identified_contracts.get(address) else {
371 return Err(format!("Unknown contract at address {address}"));
372 };
373
374 self.debugger_context
375 .contracts_sources
376 .find_source_mapping(
377 contract_name,
378 self.current_step().pc as u32,
379 self.debug_call().kind.is_any_create(),
380 )
381 .ok_or_else(|| format!("No source map for contract {contract_name}"))
382 }
383
384 fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) {
385 let debug_steps = self.debug_steps();
386 let max_pc = debug_steps.iter().map(|step| step.pc).max().unwrap_or(0);
387 let max_pc_len = hex_digits(max_pc);
388
389 let items = debug_steps
390 .iter()
391 .enumerate()
392 .map(|(i, step)| {
393 let mut content = String::with_capacity(64);
394 write!(content, "{:0>max_pc_len$x}|", step.pc).unwrap();
395 if let Some(op) = self.opcode_list.get(i) {
396 content.push_str(op);
397 }
398 ListItem::new(Span::styled(content, Style::new().fg(Color::White)))
399 })
400 .collect::<Vec<_>>();
401
402 let step = self.current_step();
403 let call_gas_used = self.debug_call().gas_limit.saturating_sub(step.gas_remaining);
404 let title = op_list_title(
405 self.address(),
406 step.pc,
407 step.gas_remaining,
408 call_gas_used,
409 step.gas_refund_counter,
410 self.debugger_context.stats,
411 );
412 let block = Block::default().title(title).borders(Borders::ALL);
413 let list = List::new(items)
414 .block(block)
415 .highlight_symbol("▶")
416 .highlight_style(Style::new().fg(Color::White).bg(Color::DarkGray))
417 .scroll_padding(1);
418 let mut state = ListState::default().with_selected(Some(self.current_step));
419 f.render_stateful_widget(list, area, &mut state);
420 }
421
422 fn draw_stack(&self, f: &mut Frame<'_>, area: Rect) {
423 let step = self.current_step();
424 let stack = step.stack.as_ref();
425 let stack_len = stack.map_or(0, |s| s.len());
426
427 let min_len = decimal_digits(stack_len).max(2);
428
429 let params = OpcodeParam::of(step.op.get());
430
431 let text: Vec<Line<'_>> = stack
432 .map(|stack| {
433 stack
434 .iter()
435 .rev()
436 .enumerate()
437 .skip(self.draw_memory.current_stack_startline)
438 .map(|(i, stack_item)| {
439 let param = params.iter().find(|param| param.index == i);
440 let mut spans = Vec::with_capacity(1 + 32 * 2 + 3);
441
442 spans.push(Span::styled(
444 format!("{i:0min_len$}| "),
445 Style::new().fg(Color::White),
446 ));
447
448 hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| {
450 if param.is_some() {
451 Style::new().fg(Color::Cyan)
452 } else {
453 Style::new().fg(Color::White)
454 }
455 });
456
457 if self.stack_labels
458 && let Some(param) = param
459 {
460 spans.push(Span::raw("| "));
461 spans.push(Span::raw(param.name));
462 }
463
464 spans.push(Span::raw("\n"));
465
466 Line::from(spans)
467 })
468 .collect()
469 })
470 .unwrap_or_default();
471
472 let title = format!("Stack: {stack_len}");
473 let block = Block::default().title(title).borders(Borders::ALL);
474 let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
475 f.render_widget(paragraph, area);
476 }
477
478 fn draw_buffer(&self, f: &mut Frame<'_>, area: Rect) {
479 let call = self.debug_call();
480 let step = self.current_step();
481 let buf = match self.active_buffer {
482 BufferKind::Memory => step.memory.as_ref().unwrap().as_ref(),
483 BufferKind::Calldata => call.calldata.as_ref(),
484 BufferKind::Returndata => step.returndata.as_ref(),
485 };
486
487 let min_len = hex_digits(buf.len());
488
489 let mut offset = None;
491 let mut len = None;
492 let mut write_offset = None;
493 let mut write_size = None;
494 let mut color = None;
495 let stack_len = step.stack.as_ref().map_or(0, |s| s.len());
496 if stack_len > 0
497 && let Some(stack) = step.stack.as_ref()
498 && let Some(accesses) = get_buffer_accesses(step.op.get(), stack)
499 {
500 if let Some(read_access) = accesses.read {
501 offset = Some(read_access.1.offset);
502 len = Some(read_access.1.len);
503 color = Some(Color::Cyan);
504 }
505 if let Some(write_access) = accesses.write
506 && self.active_buffer == BufferKind::Memory
507 {
508 write_offset = Some(write_access.offset);
509 write_size = Some(write_access.len);
510 }
511 }
512
513 if self.current_step > 0 {
518 let prev_step = self.current_step - 1;
519 let prev_step = &self.debug_steps()[prev_step];
520 if let Some(stack) = prev_step.stack.as_ref()
521 && let Some(write_access) =
522 get_buffer_accesses(prev_step.op.get(), stack).and_then(|a| a.write)
523 && self.active_buffer == BufferKind::Memory
524 {
525 offset = Some(write_access.offset);
526 len = Some(write_access.len);
527 color = Some(Color::Green);
528 }
529 }
530
531 let height = area.height as usize;
532 let end_line = self.draw_memory.current_buf_startline + height;
533
534 let text: Vec<Line<'_>> = buf
535 .chunks(32)
536 .enumerate()
537 .skip(self.draw_memory.current_buf_startline)
538 .take_while(|(i, _)| *i < end_line)
539 .map(|(i, buf_word)| {
540 let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1);
541
542 spans.push(Span::styled(
544 format!("{:0min_len$x}| ", i * 32),
545 Style::new().fg(Color::White),
546 ));
547
548 hex_bytes_spans(buf_word, &mut spans, |j, _| {
550 let mut byte_color = Color::White;
551 let mut end = None;
552 let idx = i * 32 + j;
553 if let (Some(offset), Some(len), Some(color)) = (offset, len, color) {
554 end = Some(offset + len);
555 if (offset..offset + len).contains(&idx) {
556 byte_color = color;
560 }
561 }
562 if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
563 let write_end = write_offset + write_size;
565 if let Some(read_end) = end {
566 let read_start = offset.unwrap();
567 if (write_offset..write_end).contains(&read_end) {
568 if (write_offset..read_end).contains(&idx) {
570 return Style::new().fg(Color::Yellow);
571 }
572 } else if (write_offset..write_end).contains(&read_start) {
573 if (read_start..write_end).contains(&idx) {
576 return Style::new().fg(Color::Yellow);
577 }
578 }
579 }
580 if (write_offset..write_end).contains(&idx) {
581 byte_color = Color::Red;
582 }
583 }
584
585 Style::new().fg(byte_color)
586 });
587
588 if self.buf_utf {
589 spans.push(Span::raw("|"));
590 for utf in buf_word.chunks(4) {
591 if let Ok(utf_str) = std::str::from_utf8(utf) {
592 spans.push(Span::raw(utf_str.replace('\0', ".")));
593 } else {
594 spans.push(Span::raw("."));
595 }
596 }
597 }
598
599 spans.push(Span::raw("\n"));
600
601 Line::from(spans)
602 })
603 .collect();
604
605 let title = self.active_buffer.title(buf.len());
606 let block = Block::default().title(title).borders(Borders::ALL);
607 let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
608 f.render_widget(paragraph, area);
609 }
610}
611
612fn op_list_title(
613 address: &Address,
614 pc: usize,
615 gas_remaining: u64,
616 call_gas_used: u64,
617 gas_refund_counter: u64,
618 stats: Option<DebuggerStats>,
619) -> String {
620 let address = full_checksum_address(address);
621 let mut title = format!(
622 "address: {address} | pc: 0x{pc:x} ({pc}) | gasLeft: {gas_remaining} | \
623 callGasUsed: {call_gas_used} | gasRefund: {gas_refund_counter}"
624 );
625
626 if let Some(stats) = stats {
627 write!(
628 title,
629 " | sessionTraceGasUsed: {} | sessionSubcalls: {}",
630 stats.session_trace_gas_used, stats.session_subcalls
631 )
632 .unwrap();
633 }
634
635 title
636}
637
638fn full_checksum_address(address: &Address) -> String {
639 address.to_string()
640}
641
642struct SourceLines<'a> {
644 lines: Vec<Line<'a>>,
645 start_line: usize,
646 max_line_num: usize,
647}
648
649impl<'a> SourceLines<'a> {
650 fn new(start_line: usize, end_line: usize) -> Self {
651 Self { lines: Vec::new(), start_line, max_line_num: decimal_digits(end_line) }
652 }
653
654 fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) {
655 self.push_raw(line_number_style, &[Span::styled(line, line_style)]);
656 }
657
658 fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) {
659 let mut line_spans = Vec::with_capacity(4);
660
661 let line_number = format!(
662 "{number: >width$} ",
663 number = self.start_line + self.lines.len() + 1,
664 width = self.max_line_num
665 );
666 line_spans.push(Span::styled(line_number, line_number_style));
667
668 line_spans.push(Span::raw(" "));
670
671 line_spans.extend_from_slice(spans);
672
673 self.lines.push(Line::from(line_spans));
674 }
675}
676
677fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec<Span<'_>>, f: impl Fn(usize, u8) -> Style) {
678 for (i, &byte) in bytes.iter().enumerate() {
679 if i > 0 {
680 spans.push(Span::raw(" "));
681 }
682 spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte)));
683 }
684}
685
686fn decimal_digits(n: usize) -> usize {
690 n.checked_ilog10().unwrap_or(0) as usize + 1
691}
692
693fn hex_digits(n: usize) -> usize {
697 n.checked_ilog(16).unwrap_or(0) as usize + 1
698}
699
700#[cfg(test)]
701mod tests {
702 use crate::debugger::DebuggerStats;
703 use alloy_primitives::{Address, address};
704
705 #[test]
706 fn op_list_title_includes_gas_and_subcall_stats() {
707 let stats = DebuggerStats { session_trace_gas_used: 789_012, session_subcalls: 3 };
708 let address = Address::from([0x42; 20]);
709 let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, Some(stats));
710
711 assert!(title.contains("pc: 0x2a (42)"));
712 assert!(title.contains(&format!("address: {}", super::full_checksum_address(&address))));
713 assert!(title.contains("gasLeft: 123456"));
714 assert!(title.contains("sessionTraceGasUsed: 789012"));
715 assert!(title.contains("sessionSubcalls: 3"));
716 assert!(title.contains("callGasUsed: 42"));
717 assert!(title.contains("gasRefund: 7"));
718 }
719
720 #[test]
721 fn op_list_title_omits_aggregate_stats_when_unavailable() {
722 let title = super::op_list_title(&Address::from([0x42; 20]), 0x2a, 123_456, 42, 7, None);
723
724 assert!(!title.contains("sessionTraceGasUsed"));
725 assert!(!title.contains("sessionSubcalls"));
726 }
727
728 #[test]
729 fn op_list_title_uses_full_checksum_address() {
730 let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045");
731 let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, None);
732
733 assert!(title.contains("address: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
734 assert!(!title.contains('…'));
735 }
736
737 #[test]
738 fn decimal_digits() {
739 assert_eq!(super::decimal_digits(0), 1);
740 assert_eq!(super::decimal_digits(1), 1);
741 assert_eq!(super::decimal_digits(2), 1);
742 assert_eq!(super::decimal_digits(9), 1);
743 assert_eq!(super::decimal_digits(10), 2);
744 assert_eq!(super::decimal_digits(11), 2);
745 assert_eq!(super::decimal_digits(50), 2);
746 assert_eq!(super::decimal_digits(99), 2);
747 assert_eq!(super::decimal_digits(100), 3);
748 assert_eq!(super::decimal_digits(101), 3);
749 assert_eq!(super::decimal_digits(201), 3);
750 assert_eq!(super::decimal_digits(999), 3);
751 assert_eq!(super::decimal_digits(1000), 4);
752 assert_eq!(super::decimal_digits(1001), 4);
753 }
754
755 #[test]
756 fn hex_digits() {
757 assert_eq!(super::hex_digits(0), 1);
758 assert_eq!(super::hex_digits(1), 1);
759 assert_eq!(super::hex_digits(2), 1);
760 assert_eq!(super::hex_digits(9), 1);
761 assert_eq!(super::hex_digits(10), 1);
762 assert_eq!(super::hex_digits(11), 1);
763 assert_eq!(super::hex_digits(15), 1);
764 assert_eq!(super::hex_digits(16), 2);
765 assert_eq!(super::hex_digits(17), 2);
766 assert_eq!(super::hex_digits(0xff), 2);
767 assert_eq!(super::hex_digits(0x100), 3);
768 assert_eq!(super::hex_digits(0x101), 3);
769 }
770}