Skip to main content

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};
17
18impl TUIContext<'_> {
19    pub(crate) fn draw_layout(&self, f: &mut Frame<'_>) {
20        // We need 100 columns to display a 32 byte word in the memory and stack panes.
21        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        // The horizontal layout draws these panes at 50% width.
30        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    /// Draws the layout in vertical mode.
66    ///
67    /// ```text
68    /// |-----------------------------|
69    /// |             op              |
70    /// |-----------------------------|
71    /// |            stack            |
72    /// |-----------------------------|
73    /// |             buf             |
74    /// |-----------------------------|
75    /// |                             |
76    /// |             src             |
77    /// |                             |
78    /// |-----------------------------|
79    /// ```
80    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        // NOTE: `Layout::split` always returns a slice of the same length as the number of
85        // constraints, so the `else` branch is unreachable.
86
87        // Split off footer.
88        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        // Split the app in 4 vertically to construct all the panes.
97        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    /// Draws the layout in horizontal mode.
120    ///
121    /// ```text
122    /// |-----------------|-----------|
123    /// |        op       |   stack   |
124    /// |-----------------|-----------|
125    /// |                 |           |
126    /// |       src       |    buf    |
127    /// |                 |           |
128    /// |-----------------|-----------|
129    /// ```
130    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        // Split off footer.
135        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        // Split app in 2 horizontally.
144        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        // Split left pane in 2 vertically to opcode list and source.
152        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        // Split right pane horizontally to construct stack and memory.
160        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        // We are handed a vector of SourceElements that give us a span of sourcecode that is
214        // currently being executed. This includes an offset and length.
215        // This vector is in instruction pointer order, meaning the location of the instruction
216        // minus `sum(push_bytes[..pc])`.
217        let offset = source_element.offset() as usize;
218        let len = source_element.length() as usize;
219        let max = source.source.len();
220
221        // Split source into before, relevant, and after chunks, split by line, for formatting.
222        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        // adjust what text we show of the source code
236        let (start_line, end_line) = if needed_highlight > height {
237            // highlighted section is more lines than we have available
238            let start_line = before.len().saturating_sub(1);
239            (start_line, before.len() + needed_highlight)
240        } else if height > num_lines {
241            // we can fit entire source
242            (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                // unused space below the highlight
249                above += below - after.len();
250            } else if above > before.len() {
251                // we have unused space above the highlight
252                below += above - before.len();
253            } else {
254                // no unused space
255            }
256
257            // since above is subtracted from before.len(), and the resulting
258            // start_line is used to index into before, above must be at least
259            // 1 to avoid out-of-range accesses.
260            if above == 0 {
261                above = 1;
262            }
263            (before.len().saturating_sub(above), mid_len + below)
264        };
265
266        // Unhighlighted line number: gray.
267        let u_num = Style::new().fg(Color::Gray);
268        // Unhighlighted text: default, dimmed.
269        let u_text = Style::new().add_modifier(Modifier::DIM);
270        // Highlighted line number: cyan.
271        let h_num = Style::new().fg(Color::Cyan);
272        // Highlighted text: cyan, bold.
273        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        // We check if there is other text on the same line before the highlight starts.
278        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            // Skip the first line if it has already been handled above.
296            for line in &actual[first..] {
297                lines.push(h_num, line, h_text);
298            }
299        } else {
300            // No text before the current line.
301            for line in &actual {
302                lines.push(h_num, line, h_text);
303            }
304        }
305
306        // Fill in the rest of the line as unhighlighted.
307        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        // Add after highlighted text.
316        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        // pad with empty to each line to ensure the previous text is cleared
324        for line in &mut lines.lines {
325            // note that the \n is not included in the line length
326            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    /// Returns source map, source code and source name of the current line.
335    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                        // Stack index.
407                        spans.push(Span::styled(
408                            format!("{i:0min_len$}| "),
409                            Style::new().fg(Color::White),
410                        ));
411
412                        // Item hex bytes.
413                        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        // Color memory region based on read/write.
454        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        // color word on previous write op
478        // TODO: technically it's possible for this to conflict with the current op, ie, with
479        // subsequent MCOPYs, but solc can't seem to generate that code even with high optimizer
480        // settings
481        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                // Buffer index.
507                spans.push(Span::styled(
508                    format!("{:0min_len$x}| ", i * 32),
509                    Style::new().fg(Color::White),
510                ));
511
512                // Word hex bytes.
513                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                            // [offset, offset + len] is the memory region to be colored.
521                            // If a byte at row i and column j in the memory panel
522                            // falls in this region, set the color.
523                            byte_color = color;
524                        }
525                    }
526                    if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
527                        // check for overlap with read region
528                        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 it contains end, start from write_start up to read_end
533                                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                                // otherwise if it contains read start, start from read_start up to
538                                // write_end
539                                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
576/// Wrapper around a list of [`Line`]s that prepends the line number on each new line.
577struct 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        // Space between line number and line text.
603        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
620/// Returns the number of decimal digits in the given number.
621///
622/// This is the same as `n.to_string().len()`.
623fn decimal_digits(n: usize) -> usize {
624    n.checked_ilog10().unwrap_or(0) as usize + 1
625}
626
627/// Returns the number of hexadecimal digits in the given number.
628///
629/// This is the same as `format!("{n:x}").len()`.
630fn 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}