tracexec_tui/app/
ui.rs

1use ratatui::{
2  buffer::Buffer,
3  layout::{
4    Constraint,
5    Layout,
6    Rect,
7  },
8  style::Stylize,
9  text::{
10    Line,
11    Span,
12  },
13  widgets::{
14    Block,
15    Paragraph,
16    StatefulWidget,
17    StatefulWidgetRef,
18    Widget,
19    Wrap,
20  },
21};
22use tracexec_core::{
23  cli::options::ActivePane,
24  pty::PtySize,
25};
26use tui_popup::Popup;
27
28use super::{
29  super::{
30    breakpoint_manager::BreakPointManager,
31    copy_popup::CopyPopup,
32    details_popup::DetailsPopup,
33    error_popup::InfoPopup,
34    help::{
35      fancy_help_desc,
36      help,
37      help_item,
38      help_key,
39    },
40    hit_manager::HitManager,
41    theme::THEME,
42    ui::render_title,
43  },
44  App,
45  AppLayout,
46};
47use crate::{
48  action::ActivePopup,
49  backtrace_popup::BacktracePopup,
50};
51
52impl Widget for &mut App {
53  fn render(self, area: Rect, buf: &mut Buffer) {
54    // Create a space for header, todo list and the footer.
55    let vertical = Layout::vertical([
56      Constraint::Length(1),
57      Constraint::Length(1),
58      Constraint::Min(0),
59      Constraint::Length(2),
60    ]);
61    let [header_area, search_bar_area, rest_area, footer_area] = vertical.areas(area);
62    let horizontal_constraints = [
63      Constraint::Percentage(self.split_percentage),
64      Constraint::Percentage(100 - self.split_percentage),
65    ];
66    let [event_area, term_area] = (if self.layout == AppLayout::Horizontal {
67      Layout::horizontal
68    } else {
69      Layout::vertical
70    })(horizontal_constraints)
71    .areas(rest_area);
72    let mut title = vec![Span::from(" tracexec "), env!("CARGO_PKG_VERSION").into()];
73    if !self.active_experiments.is_empty() {
74      title.push(Span::from(" with "));
75      title.push(Span::from("experimental ").yellow());
76      for (i, &f) in self.active_experiments.iter().enumerate() {
77        title.push(Span::from(f).yellow());
78        if i != self.active_experiments.len() - 1 {
79          title.push(Span::from(", "));
80        }
81      }
82      title.push(Span::from(" feature(s) active"));
83    }
84    render_title(header_area, buf, Line::from(title));
85    if let Some(query_builder) = self.query_builder.as_mut() {
86      query_builder.render(search_bar_area, buf);
87    }
88
89    self.render_help(footer_area, buf);
90
91    if event_area.width < 4 || (self.term.is_some() && term_area.width < 4) {
92      Paragraph::new("Terminal\nor\npane\ntoo\nsmall").render(rest_area, buf);
93      return;
94    }
95
96    if event_area.height < 4 || (self.term.is_some() && term_area.height < 4) {
97      Paragraph::new("Terminal or pane too small").render(rest_area, buf);
98      return;
99    }
100
101    // resize
102    if self.should_handle_internal_resize {
103      self.should_handle_internal_resize = false;
104      // Set the window size of the event list
105      self.event_list.max_window_len = event_area.height as usize - 2;
106      self.event_list.set_window((
107        self.event_list.get_window().0,
108        self.event_list.get_window().0 + self.event_list.max_window_len,
109      ));
110      if let Some(term) = self.term.as_mut() {
111        term
112          .resize(PtySize {
113            rows: term_area.height - 2,
114            cols: term_area.width - 2,
115            pixel_width: 0,
116            pixel_height: 0,
117          })
118          .unwrap();
119      }
120    }
121
122    let block = Block::default()
123      .title("Events")
124      .borders(ratatui::widgets::Borders::ALL)
125      .border_style(if self.active_pane == ActivePane::Events {
126        THEME.active_border
127      } else {
128        THEME.inactive_border
129      })
130      .title(self.event_list.statistics());
131    let inner = block.inner(event_area);
132    block.render(event_area, buf);
133    self.event_list.render(inner, buf);
134    if let Some(term) = self.term.as_mut() {
135      let block = Block::default()
136        .title("Terminal")
137        .borders(ratatui::widgets::Borders::ALL)
138        .border_style(if self.active_pane == ActivePane::Terminal {
139          THEME.active_border
140        } else {
141          THEME.inactive_border
142        });
143      term.render(block.inner(term_area), buf);
144      block.render(term_area, buf);
145    }
146
147    if let Some(breakpoint_mgr_state) = self.breakpoint_manager.as_mut() {
148      BreakPointManager.render_ref(rest_area, buf, breakpoint_mgr_state);
149    }
150
151    if let Some(h) = self.hit_manager_state.as_mut()
152      && h.visible
153    {
154      HitManager.render(rest_area, buf, h);
155    }
156
157    // popups
158    for popup in self.popup.iter_mut() {
159      match popup {
160        ActivePopup::Help => {
161          let popup = Popup::new(help(rest_area))
162            .title("Help")
163            .style(THEME.help_popup);
164          Widget::render(popup, area, buf);
165        }
166        ActivePopup::CopyTargetSelection(state) => {
167          CopyPopup.render_ref(area, buf, state);
168        }
169        ActivePopup::InfoPopup(state) => {
170          InfoPopup.render(area, buf, state);
171        }
172        ActivePopup::Backtrace(state) => {
173          BacktracePopup.render_ref(rest_area, buf, state);
174        }
175        ActivePopup::ViewDetails(state) => {
176          DetailsPopup::new(self.clipboard.is_some()).render_ref(rest_area, buf, state)
177        }
178      }
179    }
180  }
181}
182
183impl App {
184  fn render_help(&self, area: Rect, buf: &mut Buffer) {
185    let mut items = Vec::from_iter(
186      Some(help_item!("Ctrl+S", "Switch\u{00a0}Pane"))
187        .filter(|_| self.term.is_some())
188        .into_iter()
189        .flatten(),
190    );
191
192    if let Some(popup) = &self.popup.last() {
193      items.extend(help_item!("Q", "Close\u{00a0}Popup"));
194      match popup {
195        ActivePopup::ViewDetails(state) => {
196          state.update_help(&mut items);
197        }
198        ActivePopup::CopyTargetSelection(state) => {
199          items.extend(help_item!("Enter", "Choose"));
200          items.extend(state.help_items())
201        }
202        ActivePopup::Backtrace(state) => {
203          state.list.update_help(&mut items);
204        }
205        _ => {}
206      }
207    } else if let Some(breakpoint_manager) = self.breakpoint_manager.as_ref() {
208      items.extend(breakpoint_manager.help());
209    } else if self.hit_manager_state.as_ref().is_some_and(|x| x.visible) {
210      items.extend(self.hit_manager_state.as_ref().unwrap().help());
211    } else if let Some(query_builder) = self.query_builder.as_ref().filter(|q| q.editing()) {
212      items.extend(query_builder.help());
213    } else if self.active_pane == ActivePane::Events {
214      items.extend(help_item!("F1", "Help"));
215      self.event_list.update_help(&mut items);
216      if self.term.is_some() {
217        items.extend(help_item!("G/S", "Grow/Shrink\u{00a0}Pane"));
218        items.extend(help_item!("Alt+L", "Layout"));
219      }
220      if let Some(h) = self.hit_manager_state.as_ref() {
221        items.extend(help_item!("B", "Breakpoints"));
222        if h.count() > 0 {
223          items.extend([
224            help_key("Z"),
225            fancy_help_desc(format!("Hits({})", h.count())),
226            "\u{200b}".into(),
227          ])
228        } else {
229          items.extend(help_item!("Z", "Hits"));
230        }
231      }
232      if let Some(query_builder) = self.query_builder.as_ref() {
233        items.extend(query_builder.help());
234      }
235      items.extend(help_item!("Q", "Quit"));
236    } else {
237      // Terminal
238      if let Some(h) = self.hit_manager_state.as_ref()
239        && h.count() > 0
240      {
241        items.extend([
242          help_key("Ctrl+S,\u{00a0}Z"),
243          fancy_help_desc(format!("Hits({})", h.count())),
244          "\u{200b}".into(),
245        ]);
246      }
247    };
248
249    let line = Line::default().spans(items);
250    Paragraph::new(line)
251      .wrap(Wrap { trim: false })
252      .centered()
253      .render(area, buf);
254  }
255}