crashdump_viewer_lib/
app.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::config::CommonColors;
16use crate::parser::*;
17use ratatui::{
18    buffer::Buffer,
19    layout::{Alignment, Constraint, Direction, Layout, Rect, Size},
20    style::{palette::tailwind, Color, Style, Stylize},
21    text::{Line, Span, Text},
22    widgets::{
23        Block, Cell, Clear, HighlightSpacing, Paragraph, Row, StatefulWidget, Table, TableState,
24        Tabs, Widget, Wrap,
25    },
26};
27use rayon::prelude::*;
28use std::collections::HashMap;
29use std::error;
30use std::io;
31use std::time::Instant;
32use tui_scrollview::{ScrollView, ScrollViewState};
33
34use strum::IntoEnumIterator;
35use strum_macros::{Display, EnumIter, FromRepr};
36
37/// Application result type.
38pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
39
40/// Application.
41pub struct App<'a> {
42    /// header
43    pub header: String,
44    pub state: AppState,
45    pub selected_tab: SelectedTab,
46
47    /// parser
48    pub parser: parser::CDParser,
49    pub filepath: String,
50    pub crash_dump: types::CrashDump,
51    pub index_map: IndexMap,
52    pub ancestor_map: HashMap<String, Vec<String>>,
53
54    /// process information list
55    pub tab_lists: HashMap<SelectedTab, Vec<String>>,
56    pub tab_rows: HashMap<SelectedTab, Vec<Row<'a>>>,
57
58    pub inspecting_pid: String,
59    pub inspect_scroll_state: ScrollViewState,
60
61    pub table_states: HashMap<SelectedTab, TableState>,
62
63    pub process_group_table: Table<'a>,
64    pub process_group_sort_column: ProcessGroupSortColumn,
65    pub process_group_sort_direction: SortDirection,
66
67    pub process_view_table: Table<'a>,
68    pub process_view_state: ProcessViewState,
69    pub process_sort_column: ProcessSortColumn,
70    pub process_sort_direction: SortDirection,
71    pub process_readonly_view: Option<dashmap::ReadOnlyView<String, InfoOrIndex<ProcInfo>>>,
72
73    pub footer_text: HashMap<SelectedTab, String>,
74
75    pub colors: CommonColors,
76    pub show_help: bool,
77}
78
79#[derive(Default, Clone, Copy, PartialEq, Eq)]
80pub enum AppState {
81    #[default]
82    Running,
83    Quitting,
84}
85
86#[derive(Default, Clone, Copy, PartialEq, Eq)]
87pub enum ProcessViewState {
88    Heap,
89    #[default]
90    Stack,
91    MessageQueue,
92}
93
94#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter, PartialEq, Eq, Hash)]
95pub enum SelectedTab {
96    #[default]
97    #[strum(to_string = "General Information")]
98    General,
99    // #[strum(to_string = "Index")]
100    // Index,
101    #[strum(to_string = "Process Group Info")]
102    ProcessGroup,
103    #[strum(to_string = "Process Info")]
104    Process,
105    #[strum(to_string = "Inspector")]
106    Inspect,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum ProcessSortColumn {
111    Pid = 0,
112    Name = 1,
113    MessageQueueLength = 2,
114    Memory = 3,
115    TotalBinVHeap = 4,
116    BinVHeap = 5,
117    BinVHeapUnused = 6,
118    OldBinVHeap = 7,
119    OldBinVHeapUnused = 8,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum ProcessGroupSortColumn {
124    Pid = 0,
125    Name = 1,
126    TotalMemorySize = 2,
127    ChildrenCount = 3,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum SortDirection {
132    Ascending,
133    Descending,
134}
135
136impl Default for App<'_> {
137    fn default() -> Self {
138        Self {
139            state: AppState::Running,
140            selected_tab: SelectedTab::General,
141            parser: parser::CDParser::default(),
142            filepath: "".to_string(),
143            crash_dump: types::CrashDump::new(),
144            index_map: IndexMap::new(),
145            ancestor_map: HashMap::new(),
146            header: "ERL CRASH DUMP VIEWER".to_string(),
147            tab_lists: HashMap::from_iter(SelectedTab::iter().map(|tab| (tab, vec![]))),
148            tab_rows: HashMap::from_iter(SelectedTab::iter().map(|tab| (tab, vec![]))),
149            table_states: HashMap::from_iter(
150                SelectedTab::iter().map(|tab| (tab, TableState::default())),
151            ),
152            process_group_table: Table::default(),
153            process_group_sort_column: ProcessGroupSortColumn::TotalMemorySize,
154            process_group_sort_direction: SortDirection::Descending,
155
156            process_view_state: ProcessViewState::default(),
157            process_view_table: Table::default(),
158            process_sort_column: ProcessSortColumn::BinVHeap,
159            process_sort_direction: SortDirection::Descending,
160            process_readonly_view: None,
161            footer_text: HashMap::new(),
162            inspecting_pid: "".to_string(),
163            inspect_scroll_state: ScrollViewState::default(),
164            colors: CommonColors::default(),
165            show_help: false,
166        }
167    }
168}
169
170impl App<'_> {
171    /// Constructs a new instance of [`App`].
172    pub fn new(filepath: String) -> Self {
173        let now = Instant::now();
174        let parser = parser::CDParser::new(&filepath).unwrap();
175        let mut ret = Self::default();
176        ret.filepath = filepath.clone();
177
178        ret.index_map = parser.build_index().unwrap();
179        ret.crash_dump = parser.parse(&ret.index_map).unwrap();
180
181        ret.ancestor_map = parser::CDParser::create_descendants_table(&ret.crash_dump.processes);
182        ret.crash_dump.group_info_map =
183            parser::CDParser::calculate_group_info(&ret.ancestor_map, &ret.crash_dump.processes);
184
185        let read_only_processes = ret.crash_dump.processes.clone().into_read_only();
186        ret.process_readonly_view = Some(read_only_processes);
187
188        ret.sort_and_update_process_table();
189        ret.sort_and_update_process_group_table();
190
191        ///////// Process Group Info
192
193        ret.footer_text.insert(SelectedTab::Process, "Press S for Stack, H for Heap, M for Message Queue | I to inspect contents |  < > to change tabs | Press q to quit".to_string());
194        ret.footer_text.insert(
195            SelectedTab::Inspect,
196            " <-, -> to change tabs | q to quit | ? for help".to_string(),
197        );
198
199        if let Some(state) = ret.table_states.get_mut(&SelectedTab::Process) {
200            if !ret.tab_lists[&SelectedTab::Process].is_empty() {
201                state.select(Some(0));
202            }
203        }
204
205        if let Some(state) = ret.table_states.get_mut(&SelectedTab::ProcessGroup) {
206            if !ret.tab_lists[&SelectedTab::ProcessGroup].is_empty() {
207                state.select(Some(0));
208            }
209        }
210
211        ret.inspect_scroll_state = ScrollViewState::default();
212
213        let elapsed = now.elapsed();
214        println!("Building everything took: {:.2?}", elapsed);
215
216        ret
217    }
218
219    pub fn sort_and_update_process_group_table(&mut self) {
220        let column = self.process_group_sort_column;
221        let direction = self.process_group_sort_direction;
222
223        let mut sorted_keys: Vec<(&String, &GroupInfo)> = self
224            .crash_dump
225            .group_info_map
226            .par_iter() // Use parallel iterator
227            .collect();
228        sorted_keys.par_sort_by(|a, b| match (a.1, b.1) {
229            (grp_a, grp_b) => match direction {
230                SortDirection::Ascending => match column {
231                    ProcessGroupSortColumn::Pid => grp_a.pid.cmp(&grp_b.pid),
232                    ProcessGroupSortColumn::Name => grp_a.name.cmp(&grp_b.name),
233                    ProcessGroupSortColumn::ChildrenCount => grp_a.children.len().cmp(&grp_b.children.len()),
234                    ProcessGroupSortColumn::TotalMemorySize => {
235                        grp_a.total_memory_size.cmp(&grp_b.total_memory_size)
236                    }
237                },
238                SortDirection::Descending => match column {
239                    ProcessGroupSortColumn::Pid => grp_b.pid.cmp(&grp_a.pid),
240                    ProcessGroupSortColumn::Name => grp_b.name.cmp(&grp_a.name),
241                    ProcessGroupSortColumn::ChildrenCount => grp_b.children.len().cmp(&grp_a.children.len()),
242                    ProcessGroupSortColumn::TotalMemorySize => {
243                        grp_b.total_memory_size.cmp(&grp_a.total_memory_size)
244                    }
245                },
246            },
247        });
248
249        let selected_row_style = Style::default()
250            .fg(self.colors.highlight_text)
251            .bg(self.colors.highlight_background);
252        let header_style = Style::default()
253            .fg(self.colors.header_text)
254            .bg(self.colors.header_background);
255
256        let sorted_key_list: Vec<String> = sorted_keys
257            .into_par_iter() // Use parallel iterator
258            .map(|(key, _)| key.clone())
259            .collect();
260        self.tab_lists
261            .get_mut(&SelectedTab::ProcessGroup)
262            .map(|val| {
263                *val = sorted_key_list;
264            });
265        let process_group_rows: Vec<Row> = self.tab_lists[&SelectedTab::ProcessGroup]
266            .par_iter()
267            .map(|group| {
268                let group_info = self.crash_dump.group_info_map.get(group).unwrap();
269                let item = group_info.ref_array();
270                Row::new(item)
271            })
272            .collect();
273
274        let header_cells = GroupInfo::headers()
275            .iter()
276            .enumerate()
277            .map(|(i, &h)| {
278                let indicator = if i == self.process_group_sort_column as usize {
279                    match self.process_group_sort_direction {
280                        SortDirection::Ascending => " ▲",
281                        SortDirection::Descending => " ▼",
282                    }
283                } else {
284                    ""
285                };
286                Cell::from(format!("{}{}", h, indicator))
287            })
288            .collect::<Vec<_>>();
289
290        let process_group_header = Row::new(header_cells).style(header_style).height(1);
291
292        self.process_group_table = Table::new(
293            process_group_rows,
294            [
295                Constraint::Length(30),
296                Constraint::Length(30),
297                Constraint::Length(30),
298                Constraint::Length(30),
299            ],
300        )
301        .header(process_group_header)
302        .row_highlight_style(selected_row_style)
303        .highlight_spacing(HighlightSpacing::Always)
304        .block(Block::bordered().title(SelectedTab::Process.to_string()));
305    }
306
307    pub fn sort_and_update_process_table(&mut self) {
308        let column = self.process_sort_column;
309        let direction = self.process_sort_direction;
310        let processes_map = &self.crash_dump.processes;
311
312        let mut pids_to_sort: Vec<(&String, &InfoOrIndex<ProcInfo>)> = self
313            .process_readonly_view
314            .as_ref()
315            .unwrap()
316            .iter()
317            .collect();
318
319        pids_to_sort.par_sort_by(|a, b| match (a.1, b.1) {
320            (InfoOrIndex::Info(proc_a), InfoOrIndex::Info(proc_b)) => match direction {
321                SortDirection::Ascending => match column {
322                    ProcessSortColumn::Pid => proc_a.pid.cmp(&proc_b.pid),
323                    ProcessSortColumn::Name => proc_a.name.cmp(&proc_b.name),
324                    ProcessSortColumn::Memory => proc_a.memory.cmp(&proc_b.memory),
325                    ProcessSortColumn::TotalBinVHeap => {
326                        proc_a.total_bin_vheap.cmp(&proc_b.total_bin_vheap)
327                    }
328                    ProcessSortColumn::BinVHeap => proc_a.bin_vheap.cmp(&proc_b.bin_vheap),
329                    ProcessSortColumn::BinVHeapUnused => {
330                        proc_a.bin_vheap_unused.cmp(&proc_b.bin_vheap_unused)
331                    }
332                    ProcessSortColumn::OldBinVHeap => {
333                        proc_a.old_bin_vheap.cmp(&proc_b.old_bin_vheap)
334                    }
335                    ProcessSortColumn::OldBinVHeapUnused => proc_a
336                        .old_bin_vheap_unused
337                        .cmp(&proc_b.old_bin_vheap_unused),
338                    ProcessSortColumn::MessageQueueLength => proc_a
339                        .message_queue_length
340                        .cmp(&proc_b.message_queue_length),
341                },
342                // This is very gross, but it's very fast
343                SortDirection::Descending => match column {
344                    ProcessSortColumn::Pid => proc_b.pid.cmp(&proc_a.pid),
345                    ProcessSortColumn::Name => proc_b.name.cmp(&proc_a.name),
346                    ProcessSortColumn::Memory => proc_b.memory.cmp(&proc_a.memory),
347                    ProcessSortColumn::TotalBinVHeap => {
348                        proc_b.total_bin_vheap.cmp(&proc_a.total_bin_vheap)
349                    }
350                    ProcessSortColumn::BinVHeap => proc_b.bin_vheap.cmp(&proc_a.bin_vheap),
351                    ProcessSortColumn::BinVHeapUnused => {
352                        proc_b.bin_vheap_unused.cmp(&proc_a.bin_vheap_unused)
353                    }
354                    ProcessSortColumn::OldBinVHeap => {
355                        proc_b.old_bin_vheap.cmp(&proc_a.old_bin_vheap)
356                    }
357                    ProcessSortColumn::OldBinVHeapUnused => proc_b
358                        .old_bin_vheap_unused
359                        .cmp(&proc_a.old_bin_vheap_unused),
360                    ProcessSortColumn::MessageQueueLength => proc_b
361                        .message_queue_length
362                        .cmp(&proc_a.message_queue_length),
363                },
364            },
365            _ => unreachable!(),
366        });
367
368        let sorted_key_list: Vec<String> = pids_to_sort
369            .into_par_iter()
370            .map(|(key, _)| key.clone())
371            .collect();
372        self.tab_lists.get_mut(&SelectedTab::Process).map(|val| {
373            *val = sorted_key_list;
374        });
375
376        let process_rows: Vec<Row> = self.tab_lists[&SelectedTab::Process]
377            .par_iter()
378            .map(|pid| {
379                match processes_map.get(pid) {
380                    Some(process_ref) => match *process_ref.value() {
381                        InfoOrIndex::Info(ref proc_info) => {
382                            // Use the updated ref_array which has 8 elements
383                            Row::new(proc_info.ref_array())
384                        }
385                        _ => Row::new(vec![format!("Index: {}", pid)]),
386                    },
387                    None => Row::new(vec![format!("Not Found: {}", pid)]),
388                }
389            })
390            .collect();
391
392        let selected_row_style = Style::default()
393            .fg(self.colors.highlight_text)
394            .bg(self.colors.highlight_background);
395        let header_style = Style::default()
396            .fg(self.colors.header_text)
397            .bg(self.colors.header_background);
398
399        let header_cells = ProcInfo::headers()
400            .iter()
401            .enumerate()
402            .map(|(i, &h)| {
403                let indicator = if i == self.process_sort_column as usize {
404                    match self.process_sort_direction {
405                        SortDirection::Ascending => " ▲",
406                        SortDirection::Descending => " ▼",
407                    }
408                } else {
409                    ""
410                };
411                Cell::from(format!("{}{}", h, indicator))
412            })
413            .collect::<Vec<_>>();
414
415        let process_header = Row::new(header_cells).style(header_style).height(1);
416
417        let constraints = vec![
418            Constraint::Length(25),
419            Constraint::Length(25),
420            Constraint::Length(25),
421            Constraint::Length(25),
422            Constraint::Length(25),
423            Constraint::Length(25),
424            Constraint::Length(25),
425            Constraint::Length(25),
426            Constraint::Length(25),
427        ];
428
429        self.process_view_table = Table::new(process_rows, constraints) // Use updated constraints
430            .header(process_header)
431            .row_highlight_style(selected_row_style)
432            .highlight_spacing(HighlightSpacing::Always)
433            .block(Block::bordered().title(SelectedTab::Process.to_string()));
434
435        if let Some(state) = self.table_states.get_mut(&SelectedTab::Process) {
436            state.select(Some(0));
437        }
438    }
439
440    /// Handles the tick event of the terminal.
441    pub fn tick(&self) {}
442
443    /// Set running to false to quit the application.
444    pub fn quit(&mut self) {
445        self.state = AppState::Quitting;
446    }
447
448    pub fn next_tab(&mut self) {
449        self.selected_tab = self.selected_tab.next()
450    }
451
452    pub fn prev_tab(&mut self) {
453        self.selected_tab = self.selected_tab.previous()
454    }
455
456    pub fn get_heap_info(&self, pid: &str) -> io::Result<Text> {
457        self.parser
458            .get_heap_info(&self.crash_dump, &self.filepath, pid)
459    }
460
461    pub fn get_stack_info(&self, pid: &str) -> io::Result<Text> {
462        self.parser
463            .get_stack_info(&self.crash_dump, &self.filepath, pid)
464    }
465
466    pub fn get_message_queue_info(&self, pid: &str) -> io::Result<Text> {
467        self.parser
468            .get_message_queue_info(&self.crash_dump, &self.filepath, pid)
469    }
470}
471
472// Separated because this is the UI code. We need this here in order to render stuff *within* App state
473impl App<'_> {
474    pub fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
475        let titles = SelectedTab::iter().map(SelectedTab::title);
476        let highlight_style = (Color::default(), self.selected_tab.palette().c700);
477        let selected_tab_index = self.selected_tab as usize;
478        Tabs::new(titles)
479            .highlight_style(highlight_style)
480            .select(selected_tab_index)
481            .padding("", "")
482            .divider(" ")
483            .render(area, buf);
484    }
485
486    pub fn get_selected_pid(&self) -> String {
487        if self.selected_tab == SelectedTab::Process {
488            let process_table_state = self.table_states.get(&SelectedTab::Process).unwrap();
489            let selected_item = process_table_state.selected().unwrap_or(0);
490            self.tab_lists[&SelectedTab::Process][selected_item].clone()
491        } else {
492            String::new()
493        }
494    }
495}
496
497impl Widget for &mut App<'_> {
498    fn render(self, area: Rect, buf: &mut Buffer) {
499        use Constraint::{Length, Min};
500        let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
501        let [header_area, inner_area, footer_area] = vertical.areas(area);
502
503        let horizontal = Layout::horizontal([Min(0), Length(20)]);
504        let [tabs_area, title_area] = horizontal.areas(header_area);
505
506        render_title(title_area, buf);
507        self.render_tabs(tabs_area, buf);
508        match self.selected_tab {
509            SelectedTab::General => self.selected_tab.render_general(inner_area, buf, self),
510            SelectedTab::Process => self.selected_tab.render_process(inner_area, buf, self),
511            SelectedTab::ProcessGroup => self
512                .selected_tab
513                .render_process_group(inner_area, buf, self),
514            SelectedTab::Inspect => self.selected_tab.render_inspect(inner_area, buf, self),
515        }
516        let footer_text = "<-, -> to change tabs | Press q to quit | ? for help";
517        // let footer_text = self
518        //     .footer_text
519        //     .get(&self.selected_tab)
520        //     .map_or("< > to change tabs | Press q to quit | ", |v| v);
521        render_footer(footer_text, footer_area, buf);
522
523        if self.show_help {
524            // Define the updated and reordered help text
525            let help_text = Text::from(vec![
526                Line::from(Span::styled("Keyboard Shortcuts", Style::default().bold())),
527                Line::from(""),
528                Line::from("General Navigation:"),
529                Line::from("  q / Esc       : Quit Application"),
530                Line::from("  Ctrl+C        : Quit Application"),
531                Line::from("  ?             : Toggle this Help Popup"),
532                Line::from("  Left / Right  : Change Tabs"),
533                Line::from(""),
534                // --- Process Group Tab Section (Moved Up) ---
535                Line::from("Process Group Tab ('Process Group Info'):"),
536                Line::from("  Up / k        : Move Selection Up"),
537                Line::from("  Down / j      : Move Selection Down"),
538                Line::from("  Shift+T       : Sort by Total Memory Size"),
539                Line::from("  Shift+P       : Sort by Pid"),
540                Line::from("  Shift+N       : Sort by Name"),
541                Line::from("  Shift+C       : Sort by Children Count"),
542                Line::from("  (Press same sort key again to toggle Asc/Desc direction)"),
543                Line::from(""),
544                // --- Process List Tab Section (Moved Down) ---
545                Line::from("Process List Tab ('Process Info'):"),
546                Line::from("  Up / k        : Move Selection Up"),
547                Line::from("  Down / j      : Move Selection Down"),
548                Line::from("  S             : View Process Stack (in lower pane)"),
549                Line::from("  H             : View Process Heap (in lower pane)"),
550                Line::from("  M             : View Process Message Queue (in lower pane)"),
551                Line::from("  I             : Inspect Selected View (Stack/Heap/MsgQ) fullscreen"),
552                Line::from("  Shift+P       : Sort by Pid"),
553                Line::from("  Shift+N       : Sort by Name"),
554                Line::from("  Shift+Q       : Sort by Msg Queue Len"),
555                Line::from("  Shift+M       : Sort by Memory"),
556                Line::from("  Shift+T       : Sort by TotalBinVHeap"),
557                Line::from("  Shift+B       : Sort by BinVHeap"),
558                Line::from("  Shift+U       : Sort by BinVHeap Unused"),
559                Line::from("  Shift+O       : Sort by OldBinVHeap"),
560                Line::from("  Shift+V       : Sort by OldBinVHeap Unused"),
561                Line::from("  (Press same sort key again to toggle Asc/Desc direction)"),
562                Line::from(""),
563                // --- Inspect View Section ---
564                Line::from("Inspect View:"),
565                Line::from("  Up / k        : Scroll Up"),
566                Line::from("  Down / j      : Scroll Down"),
567                Line::from("  PgUp / b      : Page Up"),
568                Line::from("  PgDown / f    : Page Down"),
569                Line::from("  Home / g      : Go to Top"),
570                Line::from("  End / G       : Go to Bottom"),
571                Line::from("  I             : Return to Process List"),
572            ]);
573
574            let block = Block::bordered()
575                .title(Line::from(vec![
576                    Span::styled(
577                        " Help ",
578                        Style::default().fg(self.colors.header_text).bold(),
579                    ),
580                    Span::styled(
581                        "[?/Esc/q to close]",
582                        Style::default().fg(self.colors.default_text),
583                    ),
584                ]))
585                .border_style(Style::default().fg(self.colors.border_color))
586                .style(Style::default().bg(Color::Black));
587
588            let paragraph = Paragraph::new(help_text)
589                .block(block)
590                .wrap(Wrap { trim: true });
591
592            let area = centered_rect(70, 90, area);
593
594            Clear.render(area, buf);
595            paragraph.render(area, buf);
596        }
597    }
598}
599
600impl SelectedTab {
601    /// Get the previous tab, if there is no previous tab return the current tab.
602    fn previous(self) -> Self {
603        let current_index: usize = self as usize;
604        let previous_index = current_index.saturating_sub(1);
605        Self::from_repr(previous_index).unwrap_or(self)
606    }
607
608    /// Get the next tab, if there is no next tab return the current tab.
609    fn next(self) -> Self {
610        let current_index = self as usize;
611        let next_index = current_index.saturating_add(1);
612        Self::from_repr(next_index).unwrap_or(self)
613    }
614}
615
616impl SelectedTab {
617    /// Return tab's name as a styled `Line`
618    fn title(self) -> Line<'static> {
619        format!("  {self}  ")
620            .fg(tailwind::SLATE.c200)
621            .bg(self.palette().c900)
622            .into()
623    }
624
625    fn render_general(self, area: Rect, buf: &mut Buffer, app: &mut App) {
626        let preamble_text = app.crash_dump.preamble.format();
627        let process_count = app.index_map[&Tag::Proc].len();
628        let ets_count = app.index_map[&Tag::Ets].len();
629        let fn_count = app.index_map[&Tag::Fun].len();
630
631        let memory_info_text = app.crash_dump.memory.format();
632
633        // Split the preamble text into lines
634        let preamble_lines: Vec<Line> = preamble_text
635            .lines()
636            .map(|line| {
637                Line::from(Span::styled(
638                    line,
639                    Style::default().fg(app.colors.default_text),
640                ))
641            })
642            .collect();
643
644        // Split the memory information text into lines
645        let memory_information_lines: Vec<Line> = memory_info_text
646            .lines()
647            .map(|line| {
648                Line::from(Span::styled(
649                    line,
650                    Style::default().fg(app.colors.default_text),
651                ))
652            })
653            .collect();
654
655        // Add a header for memory information
656        let memory_information_header = Line::from(vec![
657            Span::styled(
658                "Memory Information:",
659                Style::default().fg(app.colors.header_text),
660            ),
661            Span::raw("\n"),
662        ]);
663
664        let process_count = Line::from(vec![
665            Span::styled("Process Count: ", Style::default().fg(app.colors.info_text)),
666            Span::styled(
667                process_count.to_string(),
668                Style::default().fg(app.colors.default_text),
669            ),
670        ]);
671
672        let ets_count = Line::from(vec![
673            Span::styled("ETS Tables: ", Style::default().fg(app.colors.info_text)),
674            Span::styled(
675                ets_count.to_string(),
676                Style::default().fg(app.colors.default_text),
677            ),
678        ]);
679
680        let fn_count = Line::from(vec![
681            Span::styled("Funs: ", Style::default().fg(app.colors.info_text)),
682            Span::styled(
683                fn_count.to_string(),
684                Style::default().fg(app.colors.default_text),
685            ),
686        ]);
687
688        // Combine all lines into a single Text object
689        let mut general_info_text = Text::from(preamble_lines);
690        general_info_text.extend(vec![memory_information_header]);
691        general_info_text.extend(memory_information_lines);
692        general_info_text.extend(process_count);
693        general_info_text.extend(ets_count);
694        general_info_text.extend(fn_count);
695
696        let paragraph = Paragraph::new(general_info_text)
697            .block(Block::bordered().title("General Information"))
698            .style(Style::default().fg(app.colors.default_text))
699            .alignment(Alignment::Left);
700
701        Widget::render(&paragraph, area, buf);
702    }
703
704    // fn render_index(self, area: Rect, buf: &mut Buffer, app: &mut App) {
705    //     let index_list_state = app.list_states.get_mut(&SelectedTab::Index).unwrap();
706    //     let list_items: Vec<ListItem> = app.tab_lists[&SelectedTab::Index]
707    //         .iter()
708    //         .map(|i| ListItem::new::<&str>(i.as_ref()))
709    //         .collect();
710
711    //     let binding = SelectedTab::Index.to_string();
712    //     let list = List::new(list_items)
713    //         .block(Block::bordered().title(binding.as_str()))
714    //         .highlight_symbol(">>")
715    //         .repeat_highlight_symbol(true)
716    //         .highlight_style(Style::default().bg(Color::Blue));
717
718    //     StatefulWidget::render(list, area, buf, index_list_state);
719    // }
720
721    fn render_process(self, area: Rect, buf: &mut Buffer, app: &mut App) {
722        let outer_layout = Layout::default()
723            .direction(Direction::Vertical)
724            .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
725            .split(area);
726
727        // split the second side into the info side
728        let inner_layout = Layout::default()
729            .direction(Direction::Horizontal)
730            .constraints(vec![Constraint::Percentage(25), Constraint::Percentage(75)])
731            .split(outer_layout[1]);
732
733        let selected_item;
734        {
735            let process_table_state = app.table_states.get_mut(&SelectedTab::Process).unwrap();
736            selected_item = process_table_state.selected().unwrap_or(0);
737            StatefulWidget::render(
738                &app.process_view_table,
739                outer_layout[0],
740                buf,
741                process_table_state,
742            );
743        }
744
745        let selected_pid = &app.tab_lists[&SelectedTab::Process][selected_item];
746        let selected_process_result = app.crash_dump.processes.get(selected_pid);
747
748        let active_proc_info: types::ProcInfo;
749        let process_info_text: Text;
750        match selected_process_result {
751            Some(process_ref) => {
752                let text = match *process_ref.value() {
753                    InfoOrIndex::Info(ref proc_info) => {
754                        let proc_info: &types::ProcInfo = proc_info;
755                        active_proc_info = proc_info.clone();
756                        active_proc_info.format_as_ratatui_text()
757                    }
758                    InfoOrIndex::Index(_) => {
759                        Text::raw(format!("Index for pid: {:?}", selected_pid).to_string())
760                    }
761                };
762                process_info_text = text;
763            }
764            None => {
765                process_info_text =
766                    Text::raw(format!("Process not found: {:?}", selected_pid).to_string());
767            }
768        };
769
770        let (inspect_info_title, inspect_info_text) = match app.process_view_state {
771            ProcessViewState::Stack => {
772                app.inspecting_pid = selected_pid.clone();
773                ("Decoded Stack", app.get_stack_info(selected_pid).unwrap())
774            }
775            ProcessViewState::Heap => {
776                app.inspecting_pid = selected_pid.clone();
777
778                ("Decoded Heap", app.get_heap_info(selected_pid).unwrap())
779            }
780            ProcessViewState::MessageQueue => {
781                app.inspecting_pid = selected_pid.clone();
782                (
783                    "Decoded Message Queue",
784                    app.get_message_queue_info(selected_pid).unwrap(),
785                )
786            }
787        };
788
789        //println!("heap info text: {}", heap_info_text);
790
791        let detail_block = Paragraph::new(process_info_text)
792            .block(Block::bordered().title("Process Details"))
793            .style(Style::default().fg(app.colors.default_text))
794            .wrap(Wrap { trim: false })
795            .alignment(Alignment::Left);
796
797        let proc_heap = Paragraph::new(inspect_info_text)
798            .block(Block::bordered().title(inspect_info_title))
799            .style(Style::default().fg(app.colors.default_text))
800            .alignment(Alignment::Left);
801
802        Widget::render(&detail_block, inner_layout[0], buf);
803        Widget::render(&proc_heap, inner_layout[1], buf);
804    }
805
806    fn render_process_group(self, area: Rect, buf: &mut Buffer, app: &mut App) {
807        let outer_layout = Layout::default()
808            .direction(Direction::Horizontal)
809            .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
810            .split(area);
811
812        // split the second side into the info side
813        let inner_layout = Layout::default()
814            .direction(Direction::Vertical)
815            .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
816            .split(outer_layout[1]);
817
818        let group_table_state = app
819            .table_states
820            .get_mut(&SelectedTab::ProcessGroup)
821            .unwrap();
822
823        let selected_item = group_table_state.selected().unwrap_or(0);
824        let selected_pid = &app.tab_lists[&SelectedTab::ProcessGroup][selected_item];
825        let selected_process_result = app.crash_dump.processes.get(selected_pid);
826
827        let active_proc_info: types::ProcInfo;
828        let process_info_text: Text;
829        match selected_process_result {
830            Some(process_ref) => {
831                let text = match *process_ref.value() {
832                    InfoOrIndex::Info(ref proc_info) => {
833                        let proc_info: &types::ProcInfo = proc_info;
834                        active_proc_info = proc_info.clone();
835                        active_proc_info.format_as_ratatui_text()
836                    }
837                    InfoOrIndex::Index(_) => {
838                        Text::raw(format!("Index for pid: {:?}", selected_pid).to_string())
839                    }
840                };
841                process_info_text = text;
842            }
843            None => {
844                process_info_text =
845                    Text::raw(format!("Process not found: {:?}", selected_pid).to_string());
846            }
847        };
848
849        let children: Vec<Row> = match app.ancestor_map.get(selected_pid) {
850            Some(child_pids) => {
851                child_pids
852                    .iter() // Use iter() here as we are just borrowing the child_pids
853                    .map(|child_pid| {
854                        match app.crash_dump.processes.get(child_pid) {
855                            Some(child_info_ref) => {
856                                match *child_info_ref.value() {
857                                    // Dereference the Ref
858                                    InfoOrIndex::Info(ref proc_info) => {
859                                        Row::new(proc_info.summary_ref_array())
860                                    }
861                                    InfoOrIndex::Index(_) => {
862                                        Row::new(vec![format!("{:?}", child_pid)])
863                                    } // Format the pid
864                                }
865                            }
866                            None => {
867                                // Handle the case where child_pid is not found in processes
868                                Row::new(vec![format!("Info not found: {:?}", child_pid)])
869                            }
870                        }
871                    })
872                    .collect()
873            }
874            None => vec![Row::new(vec!["No data".to_string()])],
875        };
876
877        // needs Pid, Name, Reductions, Memory, MsgQ Length,
878        let children_block = Table::new(
879            children,
880            [
881                Constraint::Length(15),
882                Constraint::Length(60),
883                Constraint::Length(10),
884                Constraint::Length(20),
885                Constraint::Length(25),
886            ],
887        )
888        .header(
889            ["Pid", "Name", "Memory", "Reductions", "MsgQ Length"]
890                .iter()
891                .map(|&h| Cell::from(h))
892                .collect::<Row>()
893                .style(
894                    Style::default()
895                        .fg(app.colors.default_text)
896                        .bg(app.colors.header_background),
897                ),
898        )
899        .highlight_spacing(HighlightSpacing::Always)
900        .block(Block::bordered().title("Group Children"));
901
902        let detail_block = Paragraph::new(process_info_text)
903            .block(Block::bordered().title("Ancestor Details"))
904            .style(Style::default().fg(app.colors.default_text))
905            .wrap(Wrap { trim: false })
906            .alignment(Alignment::Left);
907
908        Widget::render(&children_block, inner_layout[0], buf);
909        Widget::render(&detail_block, inner_layout[1], buf);
910        StatefulWidget::render(
911            &app.process_group_table,
912            outer_layout[0],
913            buf,
914            group_table_state,
915        );
916    }
917
918    fn render_inspect(self, area: Rect, buf: &mut Buffer, app: &mut App) {
919        let width = if buf.area.height < 70 {
920            buf.area.width - 1
921        } else {
922            buf.area.width
923        };
924        let mut scroll_view = ScrollView::new(Size::new(width, 70));
925
926        let inspect_info_text;
927        let inspect_info_title;
928        {
929            let (t1, t2) = match app.process_view_state {
930                ProcessViewState::Stack => (
931                    "Decoded Stack",
932                    app.get_stack_info(&app.inspecting_pid).unwrap(),
933                ),
934                ProcessViewState::Heap => (
935                    "Decoded Heap",
936                    app.get_heap_info(&app.inspecting_pid).unwrap(),
937                ),
938                ProcessViewState::MessageQueue => (
939                    "Decoded Message Queue",
940                    app.get_message_queue_info(&app.inspecting_pid).unwrap(),
941                ),
942            };
943            inspect_info_title = t1;
944            inspect_info_text = t2.clone();
945        }
946
947        let proc_info = Paragraph::new(inspect_info_text)
948            .block(Block::bordered().title(inspect_info_title))
949            .style(Style::default().fg(app.colors.default_text))
950            .wrap(Wrap { trim: false })
951            .alignment(Alignment::Left);
952
953        proc_info.render(area, &mut scroll_view.buf_mut());
954        scroll_view.render(area, buf, &mut app.inspect_scroll_state);
955    }
956
957    const fn palette(self) -> tailwind::Palette {
958        match self {
959            Self::General => tailwind::BLUE,
960            Self::Process => tailwind::EMERALD,
961            Self::ProcessGroup => tailwind::INDIGO,
962            Self::Inspect => tailwind::PURPLE,
963        }
964    }
965}
966
967fn render_title(area: Rect, buf: &mut Buffer) {
968    "ERL Crash Dump".render(area, buf);
969}
970
971fn render_footer(footer_text: &str, area: Rect, buf: &mut Buffer) {
972    Line::raw(footer_text).centered().render(area, buf);
973}
974
975fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
976    let popup_layout = Layout::vertical([
977        Constraint::Percentage((100 - percent_y) / 2),
978        Constraint::Percentage(percent_y),
979        Constraint::Percentage((100 - percent_y) / 2),
980    ])
981    .split(r);
982
983    Layout::horizontal([
984        Constraint::Percentage((100 - percent_x) / 2),
985        Constraint::Percentage(percent_x),
986        Constraint::Percentage((100 - percent_x) / 2),
987    ])
988    .split(popup_layout[1])[1]
989}