foundry_debugger/tui/
draw.rs

1//! TUI draw implementation.
2
3use 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    /// Draws the TUI layout and subcomponents to the given terminal.
20    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        // We need 100 columns to display a 32 byte word in the memory and stack panes.
26        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        // The horizontal layout draws these panes at 50% width.
35        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    /// Draws the layout in vertical mode.
71    ///
72    /// ```text
73    /// |-----------------------------|
74    /// |             op              |
75    /// |-----------------------------|
76    /// |            stack            |
77    /// |-----------------------------|
78    /// |             buf             |
79    /// |-----------------------------|
80    /// |                             |
81    /// |             src             |
82    /// |                             |
83    /// |-----------------------------|
84    /// ```
85    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        // NOTE: `Layout::split` always returns a slice of the same length as the number of
90        // constraints, so the `else` branch is unreachable.
91
92        // Split off footer.
93        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        // Split the app in 4 vertically to construct all the panes.
102        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    /// Draws the layout in horizontal mode.
125    ///
126    /// ```text
127    /// |-----------------|-----------|
128    /// |        op       |   stack   |
129    /// |-----------------|-----------|
130    /// |                 |           |
131    /// |       src       |    buf    |
132    /// |                 |           |
133    /// |-----------------|-----------|
134    /// ```
135    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        // Split off footer.
140        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        // Split app in 2 horizontally.
149        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        // Split left pane in 2 vertically to opcode list and source.
157        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        // Split right pane horizontally to construct stack and memory.
165        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        // We are handed a vector of SourceElements that give us a span of sourcecode that is
219        // currently being executed. This includes an offset and length.
220        // This vector is in instruction pointer order, meaning the location of the instruction
221        // minus `sum(push_bytes[..pc])`.
222        let offset = source_element.offset() as usize;
223        let len = source_element.length() as usize;
224        let max = source.source.len();
225
226        // Split source into before, relevant, and after chunks, split by line, for formatting.
227        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        // adjust what text we show of the source code
241        let (start_line, end_line) = if needed_highlight > height {
242            // highlighted section is more lines than we have available
243            let start_line = before.len().saturating_sub(1);
244            (start_line, before.len() + needed_highlight)
245        } else if height > num_lines {
246            // we can fit entire source
247            (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                // unused space below the highlight
254                above += below - after.len();
255            } else if above > before.len() {
256                // we have unused space above the highlight
257                below += above - before.len();
258            } else {
259                // no unused space
260            }
261
262            // since above is subtracted from before.len(), and the resulting
263            // start_line is used to index into before, above must be at least
264            // 1 to avoid out-of-range accesses.
265            if above == 0 {
266                above = 1;
267            }
268            (before.len().saturating_sub(above), mid_len + below)
269        };
270
271        // Unhighlighted line number: gray.
272        let u_num = Style::new().fg(Color::Gray);
273        // Unhighlighted text: default, dimmed.
274        let u_text = Style::new().add_modifier(Modifier::DIM);
275        // Highlighted line number: cyan.
276        let h_num = Style::new().fg(Color::Cyan);
277        // Highlighted text: cyan, bold.
278        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        // We check if there is other text on the same line before the highlight starts.
283        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            // Skip the first line if it has already been handled above.
301            for line in &actual[first..] {
302                lines.push(h_num, line, h_text);
303            }
304        } else {
305            // No text before the current line.
306            for line in &actual {
307                lines.push(h_num, line, h_text);
308            }
309        }
310
311        // Fill in the rest of the line as unhighlighted.
312        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        // Add after highlighted text.
321        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        // pad with empty to each line to ensure the previous text is cleared
329        for line in &mut lines.lines {
330            // note that the \n is not included in the line length
331            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    /// Returns source map, source code and source name of the current line.
340    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                        // Stack index.
411                        spans.push(Span::styled(
412                            format!("{i:0min_len$}| "),
413                            Style::new().fg(Color::White),
414                        ));
415
416                        // Item hex bytes.
417                        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        // Color memory region based on read/write.
458        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        // color word on previous write op
482        // TODO: technically it's possible for this to conflict with the current op, ie, with
483        // subsequent MCOPYs, but solc can't seem to generate that code even with high optimizer
484        // settings
485        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                // Buffer index.
511                spans.push(Span::styled(
512                    format!("{:0min_len$x}| ", i * 32),
513                    Style::new().fg(Color::White),
514                ));
515
516                // Word hex bytes.
517                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                            // [offset, offset + len] is the memory region to be colored.
525                            // If a byte at row i and column j in the memory panel
526                            // falls in this region, set the color.
527                            byte_color = color;
528                        }
529                    }
530                    if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
531                        // check for overlap with read region
532                        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 it contains end, start from write_start up to read_end
537                                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                                // otherwise if it contains read start, start from read_start up to
542                                // write_end
543                                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
580/// Wrapper around a list of [`Line`]s that prepends the line number on each new line.
581struct 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        // Space between line number and line text.
607        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
624/// Returns the number of decimal digits in the given number.
625///
626/// This is the same as `n.to_string().len()`.
627fn decimal_digits(n: usize) -> usize {
628    n.checked_ilog10().unwrap_or(0) as usize + 1
629}
630
631/// Returns the number of hexadecimal digits in the given number.
632///
633/// This is the same as `format!("{n:x}").len()`.
634fn 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}