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 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 if self.should_handle_internal_resize {
103 self.should_handle_internal_resize = false;
104 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 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 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}