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