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    #[inline]
25    fn draw_layout(&self, f: &mut Frame<'_>) {
26        // We need 100 columns to display a 32 byte word in the memory and stack panes.
27        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        // The horizontal layout draws these panes at 50% width.
36        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    /// Draws the layout in vertical mode.
72    ///
73    /// ```text
74    /// |-----------------------------|
75    /// |             op              |
76    /// |-----------------------------|
77    /// |            stack            |
78    /// |-----------------------------|
79    /// |             buf             |
80    /// |-----------------------------|
81    /// |                             |
82    /// |             src             |
83    /// |                             |
84    /// |-----------------------------|
85    /// ```
86    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        // NOTE: `Layout::split` always returns a slice of the same length as the number of
91        // constraints, so the `else` branch is unreachable.
92
93        // Split off footer.
94        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        // Split the app in 4 vertically to construct all the panes.
103        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    /// Draws the layout in horizontal mode.
126    ///
127    /// ```text
128    /// |-----------------|-----------|
129    /// |        op       |   stack   |
130    /// |-----------------|-----------|
131    /// |                 |           |
132    /// |       src       |    buf    |
133    /// |                 |           |
134    /// |-----------------|-----------|
135    /// ```
136    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        // Split off footer.
141        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        // Split app in 2 horizontally.
150        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        // Split left pane in 2 vertically to opcode list and source.
158        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        // Split right pane horizontally to construct stack and memory.
166        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        };
203        let title = format!(
204            "{} {} ",
205            call_kind_text,
206            source_name.map(|s| format!("| {s}")).unwrap_or_default()
207        );
208        let block = Block::default().title(title).borders(Borders::ALL);
209        let paragraph = Paragraph::new(text_output).block(block).wrap(Wrap { trim: false });
210        f.render_widget(paragraph, area);
211    }
212
213    fn src_text(&self, area: Rect) -> (Text<'_>, Option<&str>) {
214        let (source_element, source) = match self.src_map() {
215            Ok(r) => r,
216            Err(e) => return (Text::from(e), None),
217        };
218
219        // We are handed a vector of SourceElements that give us a span of sourcecode that is
220        // currently being executed. This includes an offset and length.
221        // This vector is in instruction pointer order, meaning the location of the instruction
222        // minus `sum(push_bytes[..pc])`.
223        let offset = source_element.offset() as usize;
224        let len = source_element.length() as usize;
225        let max = source.source.len();
226
227        // Split source into before, relevant, and after chunks, split by line, for formatting.
228        let actual_start = offset.min(max);
229        let actual_end = (offset + len).min(max);
230
231        let mut before: Vec<_> = source.source[..actual_start].split_inclusive('\n').collect();
232        let actual: Vec<_> =
233            source.source[actual_start..actual_end].split_inclusive('\n').collect();
234        let mut after: VecDeque<_> = source.source[actual_end..].split_inclusive('\n').collect();
235
236        let num_lines = before.len() + actual.len() + after.len();
237        let height = area.height as usize;
238        let needed_highlight = actual.len();
239        let mid_len = before.len() + actual.len();
240
241        // adjust what text we show of the source code
242        let (start_line, end_line) = if needed_highlight > height {
243            // highlighted section is more lines than we have available
244            let start_line = before.len().saturating_sub(1);
245            (start_line, before.len() + needed_highlight)
246        } else if height > num_lines {
247            // we can fit entire source
248            (0, num_lines)
249        } else {
250            let remaining = height - needed_highlight;
251            let mut above = remaining / 2;
252            let mut below = remaining / 2;
253            if below > after.len() {
254                // unused space below the highlight
255                above += below - after.len();
256            } else if above > before.len() {
257                // we have unused space above the highlight
258                below += above - before.len();
259            } else {
260                // no unused space
261            }
262
263            // since above is subtracted from before.len(), and the resulting
264            // start_line is used to index into before, above must be at least
265            // 1 to avoid out-of-range accesses.
266            if above == 0 {
267                above = 1;
268            }
269            (before.len().saturating_sub(above), mid_len + below)
270        };
271
272        // Unhighlighted line number: gray.
273        let u_num = Style::new().fg(Color::Gray);
274        // Unhighlighted text: default, dimmed.
275        let u_text = Style::new().add_modifier(Modifier::DIM);
276        // Highlighted line number: cyan.
277        let h_num = Style::new().fg(Color::Cyan);
278        // Highlighted text: cyan, bold.
279        let h_text = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD);
280
281        let mut lines = SourceLines::new(start_line, end_line);
282
283        // We check if there is other text on the same line before the highlight starts.
284        if let Some(last) = before.pop() {
285            let last_has_nl = last.ends_with('\n');
286
287            if last_has_nl {
288                before.push(last);
289            }
290            for line in &before[start_line..] {
291                lines.push(u_num, line, u_text);
292            }
293
294            let first = if !last_has_nl {
295                lines.push_raw(h_num, &[Span::raw(last), Span::styled(actual[0], h_text)]);
296                1
297            } else {
298                0
299            };
300
301            // Skip the first line if it has already been handled above.
302            for line in &actual[first..] {
303                lines.push(h_num, line, h_text);
304            }
305        } else {
306            // No text before the current line.
307            for line in &actual {
308                lines.push(h_num, line, h_text);
309            }
310        }
311
312        // Fill in the rest of the line as unhighlighted.
313        if let Some(last) = actual.last()
314            && !last.ends_with('\n')
315            && let Some(post) = after.pop_front()
316            && let Some(last) = lines.lines.last_mut()
317        {
318            last.spans.push(Span::raw(post));
319        }
320
321        // Add after highlighted text.
322        while mid_len + after.len() > end_line {
323            after.pop_back();
324        }
325        for line in after {
326            lines.push(u_num, line, u_text);
327        }
328
329        // pad with empty to each line to ensure the previous text is cleared
330        for line in &mut lines.lines {
331            // note that the \n is not included in the line length
332            if area.width as usize > line.width() + 1 {
333                line.push_span(Span::raw(" ".repeat(area.width as usize - line.width() - 1)));
334            }
335        }
336
337        (Text::from(lines.lines), source.path.to_str())
338    }
339
340    /// Returns source map, source code and source name of the current line.
341    fn src_map(&self) -> Result<(SourceElement, &SourceData), String> {
342        let address = self.address();
343        let Some(contract_name) = self.debugger_context.identified_contracts.get(address) else {
344            return Err(format!("Unknown contract at address {address}"));
345        };
346
347        self.debugger_context
348            .contracts_sources
349            .find_source_mapping(
350                contract_name,
351                self.current_step().pc as u32,
352                self.debug_call().kind.is_any_create(),
353            )
354            .ok_or_else(|| format!("No source map for contract {contract_name}"))
355    }
356
357    fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) {
358        let debug_steps = self.debug_steps();
359        let max_pc = debug_steps.iter().map(|step| step.pc).max().unwrap_or(0);
360        let max_pc_len = hex_digits(max_pc);
361
362        let items = debug_steps
363            .iter()
364            .enumerate()
365            .map(|(i, step)| {
366                let mut content = String::with_capacity(64);
367                write!(content, "{:0>max_pc_len$x}|", step.pc).unwrap();
368                if let Some(op) = self.opcode_list.get(i) {
369                    content.push_str(op);
370                }
371                ListItem::new(Span::styled(content, Style::new().fg(Color::White)))
372            })
373            .collect::<Vec<_>>();
374
375        let title = format!(
376            "Address: {} | PC: {} | Gas used in call: {}",
377            self.address(),
378            self.current_step().pc,
379            self.current_step().gas_used,
380        );
381        let block = Block::default().title(title).borders(Borders::ALL);
382        let list = List::new(items)
383            .block(block)
384            .highlight_symbol("▶")
385            .highlight_style(Style::new().fg(Color::White).bg(Color::DarkGray))
386            .scroll_padding(1);
387        let mut state = ListState::default().with_selected(Some(self.current_step));
388        f.render_stateful_widget(list, area, &mut state);
389    }
390
391    fn draw_stack(&self, f: &mut Frame<'_>, area: Rect) {
392        let step = self.current_step();
393        let stack = step.stack.as_ref();
394        let stack_len = stack.map_or(0, |s| s.len());
395
396        let min_len = decimal_digits(stack_len).max(2);
397
398        let params = OpcodeParam::of(step.op.get());
399
400        let text: Vec<Line<'_>> = stack
401            .map(|stack| {
402                stack
403                    .iter()
404                    .rev()
405                    .enumerate()
406                    .skip(self.draw_memory.current_stack_startline)
407                    .map(|(i, stack_item)| {
408                        let param = params.iter().find(|param| param.index == i);
409                        let mut spans = Vec::with_capacity(1 + 32 * 2 + 3);
410
411                        // Stack index.
412                        spans.push(Span::styled(
413                            format!("{i:0min_len$}| "),
414                            Style::new().fg(Color::White),
415                        ));
416
417                        // Item hex bytes.
418                        hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| {
419                            if param.is_some() {
420                                Style::new().fg(Color::Cyan)
421                            } else {
422                                Style::new().fg(Color::White)
423                            }
424                        });
425
426                        if self.stack_labels
427                            && let Some(param) = param
428                        {
429                            spans.push(Span::raw("| "));
430                            spans.push(Span::raw(param.name));
431                        }
432
433                        spans.push(Span::raw("\n"));
434
435                        Line::from(spans)
436                    })
437                    .collect()
438            })
439            .unwrap_or_default();
440
441        let title = format!("Stack: {stack_len}");
442        let block = Block::default().title(title).borders(Borders::ALL);
443        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
444        f.render_widget(paragraph, area);
445    }
446
447    fn draw_buffer(&self, f: &mut Frame<'_>, area: Rect) {
448        let call = self.debug_call();
449        let step = self.current_step();
450        let buf = match self.active_buffer {
451            BufferKind::Memory => step.memory.as_ref().unwrap().as_ref(),
452            BufferKind::Calldata => call.calldata.as_ref(),
453            BufferKind::Returndata => step.returndata.as_ref(),
454        };
455
456        let min_len = hex_digits(buf.len());
457
458        // Color memory region based on read/write.
459        let mut offset = None;
460        let mut len = None;
461        let mut write_offset = None;
462        let mut write_size = None;
463        let mut color = None;
464        let stack_len = step.stack.as_ref().map_or(0, |s| s.len());
465        if stack_len > 0
466            && let Some(stack) = step.stack.as_ref()
467            && let Some(accesses) = get_buffer_accesses(step.op.get(), stack)
468        {
469            if let Some(read_access) = accesses.read {
470                offset = Some(read_access.1.offset);
471                len = Some(read_access.1.len);
472                color = Some(Color::Cyan);
473            }
474            if let Some(write_access) = accesses.write
475                && self.active_buffer == BufferKind::Memory
476            {
477                write_offset = Some(write_access.offset);
478                write_size = Some(write_access.len);
479            }
480        }
481
482        // color word on previous write op
483        // TODO: technically it's possible for this to conflict with the current op, ie, with
484        // subsequent MCOPYs, but solc can't seem to generate that code even with high optimizer
485        // settings
486        if self.current_step > 0 {
487            let prev_step = self.current_step - 1;
488            let prev_step = &self.debug_steps()[prev_step];
489            if let Some(stack) = prev_step.stack.as_ref()
490                && let Some(write_access) =
491                    get_buffer_accesses(prev_step.op.get(), stack).and_then(|a| a.write)
492                && self.active_buffer == BufferKind::Memory
493            {
494                offset = Some(write_access.offset);
495                len = Some(write_access.len);
496                color = Some(Color::Green);
497            }
498        }
499
500        let height = area.height as usize;
501        let end_line = self.draw_memory.current_buf_startline + height;
502
503        let text: Vec<Line<'_>> = buf
504            .chunks(32)
505            .enumerate()
506            .skip(self.draw_memory.current_buf_startline)
507            .take_while(|(i, _)| *i < end_line)
508            .map(|(i, buf_word)| {
509                let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1);
510
511                // Buffer index.
512                spans.push(Span::styled(
513                    format!("{:0min_len$x}| ", i * 32),
514                    Style::new().fg(Color::White),
515                ));
516
517                // Word hex bytes.
518                hex_bytes_spans(buf_word, &mut spans, |j, _| {
519                    let mut byte_color = Color::White;
520                    let mut end = None;
521                    let idx = i * 32 + j;
522                    if let (Some(offset), Some(len), Some(color)) = (offset, len, color) {
523                        end = Some(offset + len);
524                        if (offset..offset + len).contains(&idx) {
525                            // [offset, offset + len] is the memory region to be colored.
526                            // If a byte at row i and column j in the memory panel
527                            // falls in this region, set the color.
528                            byte_color = color;
529                        }
530                    }
531                    if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
532                        // check for overlap with read region
533                        let write_end = write_offset + write_size;
534                        if let Some(read_end) = end {
535                            let read_start = offset.unwrap();
536                            if (write_offset..write_end).contains(&read_end) {
537                                // if it contains end, start from write_start up to read_end
538                                if (write_offset..read_end).contains(&idx) {
539                                    return Style::new().fg(Color::Yellow);
540                                }
541                            } else if (write_offset..write_end).contains(&read_start) {
542                                // otherwise if it contains read start, start from read_start up to
543                                // write_end
544                                if (read_start..write_end).contains(&idx) {
545                                    return Style::new().fg(Color::Yellow);
546                                }
547                            }
548                        }
549                        if (write_offset..write_end).contains(&idx) {
550                            byte_color = Color::Red;
551                        }
552                    }
553
554                    Style::new().fg(byte_color)
555                });
556
557                if self.buf_utf {
558                    spans.push(Span::raw("|"));
559                    for utf in buf_word.chunks(4) {
560                        if let Ok(utf_str) = std::str::from_utf8(utf) {
561                            spans.push(Span::raw(utf_str.replace('\0', ".")));
562                        } else {
563                            spans.push(Span::raw("."));
564                        }
565                    }
566                }
567
568                spans.push(Span::raw("\n"));
569
570                Line::from(spans)
571            })
572            .collect();
573
574        let title = self.active_buffer.title(buf.len());
575        let block = Block::default().title(title).borders(Borders::ALL);
576        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
577        f.render_widget(paragraph, area);
578    }
579}
580
581/// Wrapper around a list of [`Line`]s that prepends the line number on each new line.
582struct SourceLines<'a> {
583    lines: Vec<Line<'a>>,
584    start_line: usize,
585    max_line_num: usize,
586}
587
588impl<'a> SourceLines<'a> {
589    fn new(start_line: usize, end_line: usize) -> Self {
590        Self { lines: Vec::new(), start_line, max_line_num: decimal_digits(end_line) }
591    }
592
593    fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) {
594        self.push_raw(line_number_style, &[Span::styled(line, line_style)]);
595    }
596
597    fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) {
598        let mut line_spans = Vec::with_capacity(4);
599
600        let line_number = format!(
601            "{number: >width$} ",
602            number = self.start_line + self.lines.len() + 1,
603            width = self.max_line_num
604        );
605        line_spans.push(Span::styled(line_number, line_number_style));
606
607        // Space between line number and line text.
608        line_spans.push(Span::raw("  "));
609
610        line_spans.extend_from_slice(spans);
611
612        self.lines.push(Line::from(line_spans));
613    }
614}
615
616fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec<Span<'_>>, f: impl Fn(usize, u8) -> Style) {
617    for (i, &byte) in bytes.iter().enumerate() {
618        if i > 0 {
619            spans.push(Span::raw(" "));
620        }
621        spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte)));
622    }
623}
624
625/// Returns the number of decimal digits in the given number.
626///
627/// This is the same as `n.to_string().len()`.
628fn decimal_digits(n: usize) -> usize {
629    n.checked_ilog10().unwrap_or(0) as usize + 1
630}
631
632/// Returns the number of hexadecimal digits in the given number.
633///
634/// This is the same as `format!("{n:x}").len()`.
635fn hex_digits(n: usize) -> usize {
636    n.checked_ilog(16).unwrap_or(0) as usize + 1
637}
638
639#[cfg(test)]
640mod tests {
641    #[test]
642    fn decimal_digits() {
643        assert_eq!(super::decimal_digits(0), 1);
644        assert_eq!(super::decimal_digits(1), 1);
645        assert_eq!(super::decimal_digits(2), 1);
646        assert_eq!(super::decimal_digits(9), 1);
647        assert_eq!(super::decimal_digits(10), 2);
648        assert_eq!(super::decimal_digits(11), 2);
649        assert_eq!(super::decimal_digits(50), 2);
650        assert_eq!(super::decimal_digits(99), 2);
651        assert_eq!(super::decimal_digits(100), 3);
652        assert_eq!(super::decimal_digits(101), 3);
653        assert_eq!(super::decimal_digits(201), 3);
654        assert_eq!(super::decimal_digits(999), 3);
655        assert_eq!(super::decimal_digits(1000), 4);
656        assert_eq!(super::decimal_digits(1001), 4);
657    }
658
659    #[test]
660    fn hex_digits() {
661        assert_eq!(super::hex_digits(0), 1);
662        assert_eq!(super::hex_digits(1), 1);
663        assert_eq!(super::hex_digits(2), 1);
664        assert_eq!(super::hex_digits(9), 1);
665        assert_eq!(super::hex_digits(10), 1);
666        assert_eq!(super::hex_digits(11), 1);
667        assert_eq!(super::hex_digits(15), 1);
668        assert_eq!(super::hex_digits(16), 2);
669        assert_eq!(super::hex_digits(17), 2);
670        assert_eq!(super::hex_digits(0xff), 2);
671        assert_eq!(super::hex_digits(0x100), 3);
672        assert_eq!(super::hex_digits(0x101), 3);
673    }
674}