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