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