diff --git a/Cargo.toml b/Cargo.toml
index b7b7b5fe..fc529251 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,10 +25,10 @@ doc = false
 
 [profile.release]
 debug = 0
+strip = "symbols"
 lto = true
 opt-level = 3
 codegen-units = 1
-strip = "symbols"
 
 [features]
 default = ["fern", "log", "battery", "gpu"]
@@ -40,11 +40,12 @@ nvidia = ["nvml-wrapper"]
 [dependencies]
 anyhow = "1.0.57"
 backtrace = "0.3.65"
+cfg-if = "1.0.0"
 crossterm = "0.18.2"
 ctrlc = { version = "3.1.9", features = ["termination"] }
 clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] }
-cfg-if = "1.0.0"
 concat-string = "1.0.1"
+# const_format = "0.2.23"
 dirs = "4.0.0"
 futures = "0.3.21"
 futures-timer = "3.0.2"
diff --git a/src/app.rs b/src/app.rs
index 5f9ac413..d139f60f 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,7 +1,6 @@
 use std::{
     cmp::{max, min},
     collections::HashMap,
-    convert::TryInto,
     path::PathBuf,
     time::Instant,
 };
@@ -12,12 +11,13 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
 use typed_builder::*;
 
 use data_farmer::*;
-use data_harvester::{processes, temperature};
+use data_harvester::temperature;
 use layout_manager::*;
 pub use states::*;
 
 use crate::{
-    canvas, constants,
+    constants,
+    data_conversion::ConvertedData,
     options::Config,
     options::ConfigFlags,
     options::WidgetIdEnabled,
@@ -26,12 +26,15 @@ use crate::{
     Pid,
 };
 
+use self::widgets::{ProcWidget, ProcWidgetMode};
+
 pub mod data_farmer;
 pub mod data_harvester;
 pub mod layout_manager;
 mod process_killer;
 pub mod query;
 pub mod states;
+pub mod widgets;
 
 const MAX_SEARCH_LENGTH: usize = 200;
 
@@ -104,7 +107,7 @@ pub struct App {
     last_key_press: Instant,
 
     #[builder(default, setter(skip))]
-    pub canvas_data: canvas::DisplayableData,
+    pub converted_data: ConvertedData,
 
     #[builder(default, setter(skip))]
     pub data_collection: DataCollection,
@@ -127,12 +130,9 @@ pub struct App {
     #[builder(default = false, setter(skip))]
     pub basic_mode_use_percent: bool,
 
-    #[builder(default = false, setter(skip))]
-    pub did_config_fail_to_save: bool,
-
     #[cfg(target_family = "unix")]
     #[builder(default, setter(skip))]
-    pub user_table: processes::UserTable,
+    pub user_table: data_harvester::processes::UserTable,
 
     pub cpu_state: CpuState,
     pub mem_state: MemState,
@@ -147,8 +147,8 @@ pub struct App {
     pub current_widget: BottomWidget,
     pub used_widgets: UsedWidgets,
     pub filters: DataFilters,
-    pub config: Config,
-    pub config_path: Option<PathBuf>,
+    pub config: Config,               //  TODO: Is this even used...?
+    pub config_path: Option<PathBuf>, //  TODO: Is this even used...?
 }
 
 #[cfg(target_os = "windows")]
@@ -172,9 +172,8 @@ impl App {
             .widget_states
             .values_mut()
             .for_each(|state| {
-                state.process_search_state.search_state.reset();
+                state.proc_search.search_state.reset();
             });
-        self.proc_state.force_update_all = true;
 
         // Clear current delete list
         self.to_delete_process_list = None;
@@ -218,32 +217,25 @@ impl App {
         } else {
             match self.current_widget.widget_type {
                 BottomWidgetType::Proc => {
-                    if let Some(current_proc_state) = self
+                    if let Some(pws) = self
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        if current_proc_state.is_search_enabled() || current_proc_state.is_sort_open
-                        {
-                            current_proc_state
-                                .process_search_state
-                                .search_state
-                                .is_enabled = false;
-                            current_proc_state.is_sort_open = false;
+                        if pws.is_search_enabled() || pws.is_sort_open {
+                            pws.proc_search.search_state.is_enabled = false;
+                            pws.is_sort_open = false;
                             self.is_force_redraw = true;
                             return;
                         }
                     }
                 }
                 BottomWidgetType::ProcSearch => {
-                    if let Some(current_proc_state) = self
+                    if let Some(pws) = self
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id - 1)
                     {
-                        if current_proc_state.is_search_enabled() {
-                            current_proc_state
-                                .process_search_state
-                                .search_state
-                                .is_enabled = false;
+                        if pws.is_search_enabled() {
+                            pws.proc_search.search_state.is_enabled = false;
                             self.move_widget_selection(&WidgetDirection::Up);
                             self.is_force_redraw = true;
                             return;
@@ -251,14 +243,12 @@ impl App {
                     }
                 }
                 BottomWidgetType::ProcSort => {
-                    if let Some(current_proc_state) = self
+                    if let Some(pws) = self
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id - 2)
                     {
-                        if current_proc_state.is_sort_open {
-                            current_proc_state.columns.current_scroll_position =
-                                current_proc_state.columns.backup_prev_scroll_position;
-                            current_proc_state.is_sort_open = false;
+                        if pws.is_sort_open {
+                            pws.is_sort_open = false;
                             self.move_widget_selection(&WidgetDirection::Right);
                             self.is_force_redraw = true;
                             return;
@@ -314,53 +304,7 @@ impl App {
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        // Do NOT allow when in tree mode!
-                        if !proc_widget_state.is_tree_mode {
-                            // Toggles process widget grouping state
-                            proc_widget_state.is_grouped = !(proc_widget_state.is_grouped);
-
-                            // Forcefully switch off column if we were on it...
-                            if (proc_widget_state.is_grouped
-                                && (proc_widget_state.process_sorting_type
-                                    == processes::ProcessSorting::Pid
-                                    || proc_widget_state.process_sorting_type
-                                        == processes::ProcessSorting::User
-                                    || proc_widget_state.process_sorting_type
-                                        == processes::ProcessSorting::State))
-                                || (!proc_widget_state.is_grouped
-                                    && proc_widget_state.process_sorting_type
-                                        == processes::ProcessSorting::Count)
-                            {
-                                proc_widget_state.process_sorting_type =
-                                    processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group
-                                proc_widget_state.is_process_sort_descending = true;
-                            }
-
-                            proc_widget_state.columns.set_to_sorted_index_from_type(
-                                &proc_widget_state.process_sorting_type,
-                            );
-
-                            proc_widget_state.columns.try_set(
-                                &processes::ProcessSorting::State,
-                                !(proc_widget_state.is_grouped),
-                            );
-
-                            #[cfg(target_family = "unix")]
-                            proc_widget_state.columns.try_set(
-                                &processes::ProcessSorting::User,
-                                !(proc_widget_state.is_grouped),
-                            );
-
-                            proc_widget_state
-                                .columns
-                                .toggle(&processes::ProcessSorting::Count);
-                            proc_widget_state
-                                .columns
-                                .toggle(&processes::ProcessSorting::Pid);
-
-                            proc_widget_state.requires_redraw = true;
-                            self.proc_state.force_update = Some(self.current_widget.widget_id);
-                        }
+                        proc_widget_state.toggle_tab();
                     }
                 }
                 _ => {}
@@ -368,16 +312,6 @@ impl App {
         }
     }
 
-    /// I don't like this, but removing it causes a bunch of breakage.
-    /// Use ``proc_widget_state.is_grouped`` if possible!
-    pub fn is_grouped(&self, widget_id: u64) -> bool {
-        if let Some(proc_widget_state) = self.proc_state.widget_states.get(&widget_id) {
-            proc_widget_state.is_grouped
-        } else {
-            false
-        }
-    }
-
     pub fn on_slash(&mut self) {
         if !self.ignore_normal_keybinds() {
             match &self.current_widget.widget_type {
@@ -390,10 +324,7 @@ impl App {
                                 _ => 0,
                             },
                     ) {
-                        proc_widget_state
-                            .process_search_state
-                            .search_state
-                            .is_enabled = true;
+                        proc_widget_state.proc_search.search_state.is_enabled = true;
                         self.move_widget_selection(&WidgetDirection::Down);
                         self.is_force_redraw = true;
                     }
@@ -404,37 +335,30 @@ impl App {
     }
 
     pub fn toggle_sort(&mut self) {
-        match &self.current_widget.widget_type {
-            BottomWidgetType::Proc | BottomWidgetType::ProcSort => {
-                let widget_id = self.current_widget.widget_id
-                    - match &self.current_widget.widget_type {
-                        BottomWidgetType::Proc => 0,
-                        BottomWidgetType::ProcSort => 2,
-                        _ => 0,
-                    };
+        let widget_id = self.current_widget.widget_id
+            - match &self.current_widget.widget_type {
+                BottomWidgetType::Proc => 0,
+                BottomWidgetType::ProcSort => 2,
+                _ => 0,
+            };
 
-                if let Some(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) {
-                    // Open up sorting dialog for that specific proc widget.
-                    // TODO: It might be a decent idea to allow sorting ALL?  I dunno.
+        if let Some(pws) = self.proc_state.get_mut_widget_state(widget_id) {
+            pws.is_sort_open = !pws.is_sort_open;
+            pws.force_rerender = true;
 
-                    proc_widget_state.is_sort_open = !proc_widget_state.is_sort_open;
-                    if proc_widget_state.is_sort_open {
-                        // If it just opened, move left
-                        proc_widget_state
-                            .columns
-                            .set_to_sorted_index_from_type(&proc_widget_state.process_sorting_type);
-                        self.move_widget_selection(&WidgetDirection::Left);
-                    } else {
-                        // Otherwise, move right if currently on the sort widget
-                        if let BottomWidgetType::ProcSort = self.current_widget.widget_type {
-                            self.move_widget_selection(&WidgetDirection::Right);
-                        }
-                    }
+            // If the sort is now open, move left. Otherwise, if the proc sort was selected, force move right.
+            if pws.is_sort_open {
+                if let SortState::Sortable(st) = &pws.table_state.sort_state {
+                    pws.sort_table_state.scroll_bar = 0;
+                    pws.sort_table_state.current_scroll_position = st
+                        .current_index
+                        .clamp(0, pws.num_enabled_columns().saturating_sub(1));
                 }
-
-                self.is_force_redraw = true;
+                self.move_widget_selection(&WidgetDirection::Left);
+            } else if let BottomWidgetType::ProcSort = self.current_widget.widget_type {
+                self.move_widget_selection(&WidgetDirection::Right);
             }
-            _ => {}
+            self.is_force_redraw = true;
         }
     }
 
@@ -449,10 +373,12 @@ impl App {
                     };
 
                 if let Some(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) {
-                    proc_widget_state.is_process_sort_descending =
-                        !proc_widget_state.is_process_sort_descending;
-
-                    self.proc_state.force_update = Some(widget_id);
+                    if let SortState::Sortable(state) =
+                        &mut proc_widget_state.table_state.sort_state
+                    {
+                        state.toggle_order();
+                        proc_widget_state.force_data_update();
+                    }
                 }
             }
             _ => {}
@@ -470,30 +396,8 @@ impl App {
                     .widget_states
                     .get_mut(&self.current_widget.widget_id)
                 {
-                    proc_widget_state
-                        .columns
-                        .toggle(&processes::ProcessSorting::Mem);
-                    if let Some(mem_percent_state) = proc_widget_state
-                        .columns
-                        .toggle(&processes::ProcessSorting::MemPercent)
-                    {
-                        if proc_widget_state.process_sorting_type
-                            == processes::ProcessSorting::MemPercent
-                            || proc_widget_state.process_sorting_type
-                                == processes::ProcessSorting::Mem
-                        {
-                            if mem_percent_state {
-                                proc_widget_state.process_sorting_type =
-                                    processes::ProcessSorting::MemPercent;
-                            } else {
-                                proc_widget_state.process_sorting_type =
-                                    processes::ProcessSorting::Mem;
-                            }
-                        }
-                    }
-
-                    proc_widget_state.requires_redraw = true;
-                    self.proc_state.force_update = Some(self.current_widget.widget_id);
+                    proc_widget_state.toggle_mem_percentage();
+                    proc_widget_state.force_data_update();
                 }
             }
             _ => {}
@@ -509,14 +413,11 @@ impl App {
             .get_mut(&(self.current_widget.widget_id - 1))
         {
             if is_in_search_widget && proc_widget_state.is_search_enabled() {
-                proc_widget_state
-                    .process_search_state
-                    .search_toggle_ignore_case();
+                proc_widget_state.proc_search.search_toggle_ignore_case();
                 proc_widget_state.update_query();
-                self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
 
                 // Remember, it's the opposite (ignoring case is case "in"sensitive)
-                is_case_sensitive = Some(!proc_widget_state.process_search_state.is_ignoring_case);
+                is_case_sensitive = Some(!proc_widget_state.proc_search.is_ignoring_case);
             }
         }
 
@@ -550,8 +451,6 @@ impl App {
                         .build(),
                 );
             }
-
-            // self.did_config_fail_to_save = self.update_config_file().is_err();
         }
     }
 
@@ -564,17 +463,11 @@ impl App {
             .get_mut(&(self.current_widget.widget_id - 1))
         {
             if is_in_search_widget && proc_widget_state.is_search_enabled() {
-                proc_widget_state
-                    .process_search_state
-                    .search_toggle_whole_word();
+                proc_widget_state.proc_search.search_toggle_whole_word();
                 proc_widget_state.update_query();
-                self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
 
-                is_searching_whole_word = Some(
-                    proc_widget_state
-                        .process_search_state
-                        .is_searching_whole_word,
-                );
+                is_searching_whole_word =
+                    Some(proc_widget_state.proc_search.is_searching_whole_word);
             }
         }
 
@@ -624,15 +517,11 @@ impl App {
             .get_mut(&(self.current_widget.widget_id - 1))
         {
             if is_in_search_widget && proc_widget_state.is_search_enabled() {
-                proc_widget_state.process_search_state.search_toggle_regex();
+                proc_widget_state.proc_search.search_toggle_regex();
                 proc_widget_state.update_query();
-                self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
 
-                is_searching_with_regex = Some(
-                    proc_widget_state
-                        .process_search_state
-                        .is_searching_with_regex,
-                );
+                is_searching_with_regex =
+                    Some(proc_widget_state.proc_search.is_searching_with_regex);
             }
         }
 
@@ -666,8 +555,6 @@ impl App {
                         .build(),
                 );
             }
-
-            // self.did_config_fail_to_save = self.update_config_file().is_err();
         }
     }
 
@@ -677,37 +564,19 @@ impl App {
             .widget_states
             .get_mut(&(self.current_widget.widget_id))
         {
-            proc_widget_state.is_tree_mode = !proc_widget_state.is_tree_mode;
-
-            // FIXME: For consistency, either disable tree mode if grouped, or allow grouped mode if in tree mode.
-            if proc_widget_state.is_tree_mode {
-                // Disable grouping if so!
-                proc_widget_state.is_grouped = false;
-
-                proc_widget_state
-                    .columns
-                    .try_enable(&processes::ProcessSorting::State);
-
-                #[cfg(target_family = "unix")]
-                proc_widget_state
-                    .columns
-                    .try_enable(&processes::ProcessSorting::User);
-
-                proc_widget_state
-                    .columns
-                    .try_disable(&processes::ProcessSorting::Count);
-
-                proc_widget_state
-                    .columns
-                    .try_enable(&processes::ProcessSorting::Pid);
-
-                // We enabled... set PID sort type to ascending.
-                proc_widget_state.process_sorting_type = processes::ProcessSorting::Pid;
-                proc_widget_state.is_process_sort_descending = false;
+            match proc_widget_state.mode {
+                ProcWidgetMode::Tree { .. } => {
+                    proc_widget_state.mode = ProcWidgetMode::Normal;
+                    proc_widget_state.force_rerender_and_update();
+                }
+                ProcWidgetMode::Normal => {
+                    proc_widget_state.mode = ProcWidgetMode::Tree {
+                        collapsed_pids: Default::default(),
+                    };
+                    proc_widget_state.force_rerender_and_update();
+                }
+                ProcWidgetMode::Grouped => {}
             }
-
-            self.proc_state.force_update = Some(self.current_widget.widget_id);
-            proc_widget_state.requires_redraw = true;
         }
     }
 
@@ -744,9 +613,9 @@ impl App {
                     .widget_states
                     .get_mut(&(self.current_widget.widget_id - 2))
                 {
-                    proc_widget_state.update_sorting_with_columns();
-                    self.proc_state.force_update = Some(self.current_widget.widget_id - 2);
-                    self.toggle_sort();
+                    proc_widget_state.use_sort_table_value();
+                    self.move_widget_selection(&WidgetDirection::Right);
+                    self.is_force_redraw = true;
                 }
             }
         }
@@ -761,13 +630,10 @@ impl App {
                 .get_mut(&(self.current_widget.widget_id - 1))
             {
                 if is_in_search_widget {
-                    if proc_widget_state
-                        .process_search_state
-                        .search_state
-                        .is_enabled
+                    if proc_widget_state.proc_search.search_state.is_enabled
                         && proc_widget_state.get_search_cursor_position()
                             < proc_widget_state
-                                .process_search_state
+                                .proc_search
                                 .search_state
                                 .current_search_query
                                 .len()
@@ -777,27 +643,24 @@ impl App {
                             .search_walk_forward(proc_widget_state.get_search_cursor_position());
 
                         let _removed_chars: String = proc_widget_state
-                            .process_search_state
+                            .proc_search
                             .search_state
                             .current_search_query
                             .drain(current_cursor..proc_widget_state.get_search_cursor_position())
                             .collect();
 
-                        proc_widget_state
-                            .process_search_state
-                            .search_state
-                            .grapheme_cursor = GraphemeCursor::new(
-                            current_cursor,
-                            proc_widget_state
-                                .process_search_state
-                                .search_state
-                                .current_search_query
-                                .len(),
-                            true,
-                        );
+                        proc_widget_state.proc_search.search_state.grapheme_cursor =
+                            GraphemeCursor::new(
+                                current_cursor,
+                                proc_widget_state
+                                    .proc_search
+                                    .search_state
+                                    .current_search_query
+                                    .len(),
+                                true,
+                            );
 
                         proc_widget_state.update_query();
-                        self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
                     }
                 } else {
                     self.start_killing_process()
@@ -815,10 +678,7 @@ impl App {
                 .get_mut(&(self.current_widget.widget_id - 1))
             {
                 if is_in_search_widget
-                    && proc_widget_state
-                        .process_search_state
-                        .search_state
-                        .is_enabled
+                    && proc_widget_state.proc_search.search_state.is_enabled
                     && proc_widget_state.get_search_cursor_position() > 0
                 {
                     let current_cursor = proc_widget_state.get_search_cursor_position();
@@ -826,37 +686,32 @@ impl App {
                         .search_walk_back(proc_widget_state.get_search_cursor_position());
 
                     let removed_chars: String = proc_widget_state
-                        .process_search_state
+                        .proc_search
                         .search_state
                         .current_search_query
                         .drain(proc_widget_state.get_search_cursor_position()..current_cursor)
                         .collect();
 
-                    proc_widget_state
-                        .process_search_state
-                        .search_state
-                        .grapheme_cursor = GraphemeCursor::new(
-                        proc_widget_state.get_search_cursor_position(),
-                        proc_widget_state
-                            .process_search_state
-                            .search_state
-                            .current_search_query
-                            .len(),
-                        true,
-                    );
+                    proc_widget_state.proc_search.search_state.grapheme_cursor =
+                        GraphemeCursor::new(
+                            proc_widget_state.get_search_cursor_position(),
+                            proc_widget_state
+                                .proc_search
+                                .search_state
+                                .current_search_query
+                                .len(),
+                            true,
+                        );
 
                     proc_widget_state
-                        .process_search_state
+                        .proc_search
                         .search_state
                         .char_cursor_position -= UnicodeWidthStr::width(removed_chars.as_str());
 
-                    proc_widget_state
-                        .process_search_state
-                        .search_state
-                        .cursor_direction = CursorDirection::Left;
+                    proc_widget_state.proc_search.search_state.cursor_direction =
+                        CursorDirection::Left;
 
                     proc_widget_state.update_query();
-                    self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
                 }
             }
         }
@@ -864,7 +719,7 @@ impl App {
 
     pub fn get_process_filter(&self, widget_id: u64) -> &Option<query::Query> {
         if let Some(process_widget_state) = self.proc_state.widget_states.get(&widget_id) {
-            &process_widget_state.process_search_state.search_state.query
+            &process_widget_state.proc_search.search_state.query
         } else {
             &None
         }
@@ -961,24 +816,22 @@ impl App {
                                 .search_walk_back(proc_widget_state.get_search_cursor_position());
                             if proc_widget_state.get_search_cursor_position() < prev_cursor {
                                 let str_slice = &proc_widget_state
-                                    .process_search_state
+                                    .proc_search
                                     .search_state
                                     .current_search_query
                                     [proc_widget_state.get_search_cursor_position()..prev_cursor];
                                 proc_widget_state
-                                    .process_search_state
+                                    .proc_search
                                     .search_state
                                     .char_cursor_position -= UnicodeWidthStr::width(str_slice);
-                                proc_widget_state
-                                    .process_search_state
-                                    .search_state
-                                    .cursor_direction = CursorDirection::Left;
+                                proc_widget_state.proc_search.search_state.cursor_direction =
+                                    CursorDirection::Left;
                             }
                         }
                     }
                 }
                 BottomWidgetType::Battery => {
-                    if !self.canvas_data.battery_data.is_empty() {
+                    if !self.converted_data.battery_data.is_empty() {
                         if let Some(battery_widget_state) = self
                             .battery_state
                             .get_mut_widget_state(self.current_widget.widget_id)
@@ -1033,25 +886,23 @@ impl App {
                             );
                             if proc_widget_state.get_search_cursor_position() > prev_cursor {
                                 let str_slice = &proc_widget_state
-                                    .process_search_state
+                                    .proc_search
                                     .search_state
                                     .current_search_query
                                     [prev_cursor..proc_widget_state.get_search_cursor_position()];
                                 proc_widget_state
-                                    .process_search_state
+                                    .proc_search
                                     .search_state
                                     .char_cursor_position += UnicodeWidthStr::width(str_slice);
-                                proc_widget_state
-                                    .process_search_state
-                                    .search_state
-                                    .cursor_direction = CursorDirection::Right;
+                                proc_widget_state.proc_search.search_state.cursor_direction =
+                                    CursorDirection::Right;
                             }
                         }
                     }
                 }
                 BottomWidgetType::Battery => {
-                    if !self.canvas_data.battery_data.is_empty() {
-                        let battery_count = self.canvas_data.battery_data.len();
+                    if !self.converted_data.battery_data.is_empty() {
+                        let battery_count = self.converted_data.battery_data.len();
                         if let Some(battery_widget_state) = self
                             .battery_state
                             .get_mut_widget_state(self.current_widget.widget_id)
@@ -1111,12 +962,8 @@ impl App {
                 &self.current_widget.bottom_right_corner,
             ) {
                 let border_offset = if self.is_drawing_border() { 1 } else { 0 };
-                let header_gap_offset = 1 + if self.is_drawing_gap(&self.current_widget) {
-                    self.app_config_fields.table_gap
-                } else {
-                    0
-                };
-                let height = brc_y - tlc_y - 2 * border_offset - header_gap_offset;
+                let header_offset = self.header_offset(&self.current_widget);
+                let height = brc_y - tlc_y - 2 * border_offset - header_offset;
                 self.change_position_count(-(height as i64));
             }
         }
@@ -1138,12 +985,8 @@ impl App {
                 &self.current_widget.bottom_right_corner,
             ) {
                 let border_offset = if self.is_drawing_border() { 1 } else { 0 };
-                let header_gap_offset = 1 + if self.is_drawing_gap(&self.current_widget) {
-                    self.app_config_fields.table_gap
-                } else {
-                    0
-                };
-                let height = brc_y - tlc_y - 2 * border_offset - header_gap_offset;
+                let header_offset = self.header_offset(&self.current_widget);
+                let height = brc_y - tlc_y - 2 * border_offset - header_offset;
                 self.change_position_count(height as i64);
             }
         }
@@ -1159,26 +1002,22 @@ impl App {
                     .get_mut(&(self.current_widget.widget_id - 1))
                 {
                     if is_in_search_widget {
+                        proc_widget_state.proc_search.search_state.grapheme_cursor =
+                            GraphemeCursor::new(
+                                0,
+                                proc_widget_state
+                                    .proc_search
+                                    .search_state
+                                    .current_search_query
+                                    .len(),
+                                true,
+                            );
                         proc_widget_state
-                            .process_search_state
-                            .search_state
-                            .grapheme_cursor = GraphemeCursor::new(
-                            0,
-                            proc_widget_state
-                                .process_search_state
-                                .search_state
-                                .current_search_query
-                                .len(),
-                            true,
-                        );
-                        proc_widget_state
-                            .process_search_state
+                            .proc_search
                             .search_state
                             .char_cursor_position = 0;
-                        proc_widget_state
-                            .process_search_state
-                            .search_state
-                            .cursor_direction = CursorDirection::Left;
+                        proc_widget_state.proc_search.search_state.cursor_direction =
+                            CursorDirection::Left;
                     }
                 }
             }
@@ -1195,36 +1034,32 @@ impl App {
                     .get_mut(&(self.current_widget.widget_id - 1))
                 {
                     if is_in_search_widget {
+                        proc_widget_state.proc_search.search_state.grapheme_cursor =
+                            GraphemeCursor::new(
+                                proc_widget_state
+                                    .proc_search
+                                    .search_state
+                                    .current_search_query
+                                    .len(),
+                                proc_widget_state
+                                    .proc_search
+                                    .search_state
+                                    .current_search_query
+                                    .len(),
+                                true,
+                            );
                         proc_widget_state
-                            .process_search_state
-                            .search_state
-                            .grapheme_cursor = GraphemeCursor::new(
-                            proc_widget_state
-                                .process_search_state
-                                .search_state
-                                .current_search_query
-                                .len(),
-                            proc_widget_state
-                                .process_search_state
-                                .search_state
-                                .current_search_query
-                                .len(),
-                            true,
-                        );
-                        proc_widget_state
-                            .process_search_state
+                            .proc_search
                             .search_state
                             .char_cursor_position = UnicodeWidthStr::width(
                             proc_widget_state
-                                .process_search_state
+                                .proc_search
                                 .search_state
                                 .current_search_query
                                 .as_str(),
                         );
-                        proc_widget_state
-                            .process_search_state
-                            .search_state
-                            .cursor_direction = CursorDirection::Right;
+                        proc_widget_state.proc_search.search_state.cursor_direction =
+                            CursorDirection::Right;
                     }
                 }
             }
@@ -1239,7 +1074,6 @@ impl App {
                 .get_mut(&(self.current_widget.widget_id - 1))
             {
                 proc_widget_state.clear_search();
-                self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
             }
         }
     }
@@ -1279,19 +1113,16 @@ impl App {
                 }
 
                 let removed_chars: String = proc_widget_state
-                    .process_search_state
+                    .proc_search
                     .search_state
                     .current_search_query
                     .drain(start_index..end_index)
                     .collect();
 
-                proc_widget_state
-                    .process_search_state
-                    .search_state
-                    .grapheme_cursor = GraphemeCursor::new(
+                proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new(
                     start_index,
                     proc_widget_state
-                        .process_search_state
+                        .proc_search
                         .search_state
                         .current_search_query
                         .len(),
@@ -1299,23 +1130,13 @@ impl App {
                 );
 
                 proc_widget_state
-                    .process_search_state
+                    .proc_search
                     .search_state
                     .char_cursor_position -= UnicodeWidthStr::width(removed_chars.as_str());
 
-                proc_widget_state
-                    .process_search_state
-                    .search_state
-                    .cursor_direction = CursorDirection::Left;
+                proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Left;
 
                 proc_widget_state.update_query();
-                self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
-
-                // Now, convert this range into a String-friendly range and remove it all at once!
-
-                // Now make sure to also update our current cursor positions...
-
-                self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
             }
         }
     }
@@ -1323,38 +1144,36 @@ impl App {
     pub fn start_killing_process(&mut self) {
         self.reset_multi_tap_keys();
 
-        if let Some(proc_widget_state) = self
+        if let Some(pws) = self
             .proc_state
             .widget_states
             .get(&self.current_widget.widget_id)
         {
-            if let Some(corresponding_filtered_process_list) = self
-                .canvas_data
-                .finalized_process_data_map
-                .get(&self.current_widget.widget_id)
+            if let Some(table_row) = pws
+                .table_data
+                .data
+                .get(pws.table_state.current_scroll_position)
             {
-                if proc_widget_state.scroll_state.current_scroll_position
-                    < corresponding_filtered_process_list.len()
-                {
-                    let current_process: (String, Vec<Pid>);
-                    if self.is_grouped(self.current_widget.widget_id) {
-                        if let Some(process) = &corresponding_filtered_process_list
-                            .get(proc_widget_state.scroll_state.current_scroll_position)
+                if let Some(col_value) = table_row.row().get(ProcWidget::PROC_NAME_OR_CMD) {
+                    let val = col_value.main_text().to_string();
+                    if pws.is_using_command() {
+                        if let Some(pids) = self.data_collection.process_data.cmd_pid_map.get(&val)
                         {
-                            current_process = (process.name.to_string(), process.group_pids.clone())
-                        } else {
-                            return;
-                        }
-                    } else {
-                        let process = corresponding_filtered_process_list
-                            [proc_widget_state.scroll_state.current_scroll_position]
-                            .clone();
-                        current_process = (process.name.clone(), vec![process.pid])
-                    };
+                            let current_process = (val, pids.clone());
 
-                    self.to_delete_process_list = Some(current_process);
-                    self.delete_dialog_state.is_showing_dd = true;
-                    self.is_determining_widget_boundary = true;
+                            self.to_delete_process_list = Some(current_process);
+                            self.delete_dialog_state.is_showing_dd = true;
+                            self.is_determining_widget_boundary = true;
+                        }
+                    } else if let Some(pids) =
+                        self.data_collection.process_data.name_pid_map.get(&val)
+                    {
+                        let current_process = (val, pids.clone());
+
+                        self.to_delete_process_list = Some(current_process);
+                        self.delete_dialog_state.is_showing_dd = true;
+                        self.is_determining_widget_boundary = true;
+                    }
                 }
             }
         }
@@ -1389,45 +1208,40 @@ impl App {
                         && proc_widget_state.is_search_enabled()
                         && UnicodeWidthStr::width(
                             proc_widget_state
-                                .process_search_state
+                                .proc_search
                                 .search_state
                                 .current_search_query
                                 .as_str(),
                         ) <= MAX_SEARCH_LENGTH
                     {
                         proc_widget_state
-                            .process_search_state
+                            .proc_search
                             .search_state
                             .current_search_query
                             .insert(proc_widget_state.get_search_cursor_position(), caught_char);
 
-                        proc_widget_state
-                            .process_search_state
-                            .search_state
-                            .grapheme_cursor = GraphemeCursor::new(
-                            proc_widget_state.get_search_cursor_position(),
-                            proc_widget_state
-                                .process_search_state
-                                .search_state
-                                .current_search_query
-                                .len(),
-                            true,
-                        );
+                        proc_widget_state.proc_search.search_state.grapheme_cursor =
+                            GraphemeCursor::new(
+                                proc_widget_state.get_search_cursor_position(),
+                                proc_widget_state
+                                    .proc_search
+                                    .search_state
+                                    .current_search_query
+                                    .len(),
+                                true,
+                            );
                         proc_widget_state
                             .search_walk_forward(proc_widget_state.get_search_cursor_position());
 
                         proc_widget_state
-                            .process_search_state
+                            .proc_search
                             .search_state
                             .char_cursor_position +=
                             UnicodeWidthChar::width(caught_char).unwrap_or(0);
 
                         proc_widget_state.update_query();
-                        self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
-                        proc_widget_state
-                            .process_search_state
-                            .search_state
-                            .cursor_direction = CursorDirection::Right;
+                        proc_widget_state.proc_search.search_state.cursor_direction =
+                            CursorDirection::Right;
 
                         return;
                     }
@@ -1528,23 +1342,18 @@ impl App {
             'f' => {
                 self.is_frozen = !self.is_frozen;
                 if self.is_frozen {
-                    self.data_collection.set_frozen_time();
+                    self.data_collection.freeze();
+                } else {
+                    self.data_collection.thaw();
                 }
             }
-            'C' => {
-                // self.open_config(),
-            }
             'c' => {
                 if let BottomWidgetType::Proc = self.current_widget.widget_type {
                     if let Some(proc_widget_state) = self
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        proc_widget_state
-                            .columns
-                            .set_to_sorted_index_from_type(&processes::ProcessSorting::CpuPercent);
-                        proc_widget_state.update_sorting_with_columns();
-                        self.proc_state.force_update = Some(self.current_widget.widget_id);
+                        proc_widget_state.select_column(ProcWidget::CPU);
                     }
                 }
             }
@@ -1554,18 +1363,7 @@ impl App {
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        proc_widget_state.columns.set_to_sorted_index_from_type(
-                            &(if proc_widget_state
-                                .columns
-                                .is_enabled(&processes::ProcessSorting::MemPercent)
-                            {
-                                processes::ProcessSorting::MemPercent
-                            } else {
-                                processes::ProcessSorting::Mem
-                            }),
-                        );
-                        proc_widget_state.update_sorting_with_columns();
-                        self.proc_state.force_update = Some(self.current_widget.widget_id);
+                        proc_widget_state.select_column(ProcWidget::MEM);
                     }
                 }
             }
@@ -1575,14 +1373,7 @@ impl App {
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        // Skip if grouped
-                        if !proc_widget_state.is_grouped {
-                            proc_widget_state
-                                .columns
-                                .set_to_sorted_index_from_type(&processes::ProcessSorting::Pid);
-                            proc_widget_state.update_sorting_with_columns();
-                            self.proc_state.force_update = Some(self.current_widget.widget_id);
-                        }
+                        proc_widget_state.select_column(ProcWidget::PID_OR_COUNT);
                     }
                 }
             }
@@ -1592,25 +1383,7 @@ impl App {
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        proc_widget_state.is_using_command = !proc_widget_state.is_using_command;
-                        proc_widget_state
-                            .toggle_command_and_name(proc_widget_state.is_using_command);
-
-                        match &proc_widget_state.process_sorting_type {
-                            processes::ProcessSorting::Command
-                            | processes::ProcessSorting::ProcessName => {
-                                if proc_widget_state.is_using_command {
-                                    proc_widget_state.process_sorting_type =
-                                        processes::ProcessSorting::Command;
-                                } else {
-                                    proc_widget_state.process_sorting_type =
-                                        processes::ProcessSorting::ProcessName;
-                                }
-                            }
-                            _ => {}
-                        }
-                        proc_widget_state.requires_redraw = true;
-                        self.proc_state.force_update = Some(self.current_widget.widget_id);
+                        proc_widget_state.toggle_command();
                     }
                 }
             }
@@ -1620,15 +1393,7 @@ impl App {
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        proc_widget_state.columns.set_to_sorted_index_from_type(
-                            &(if proc_widget_state.is_using_command {
-                                processes::ProcessSorting::Command
-                            } else {
-                                processes::ProcessSorting::ProcessName
-                            }),
-                        );
-                        proc_widget_state.update_sorting_with_columns();
-                        self.proc_state.force_update = Some(self.current_widget.widget_id);
+                        proc_widget_state.select_column(ProcWidget::PROC_NAME_OR_CMD);
                     }
                 }
             }
@@ -1648,7 +1413,6 @@ impl App {
             's' => self.toggle_sort(),
             'I' => self.invert_sort(),
             '%' => self.toggle_percentages(),
-            ' ' => self.on_space(),
             _ => {}
         }
 
@@ -1659,31 +1423,6 @@ impl App {
         }
     }
 
-    pub fn on_space(&mut self) {}
-
-    /// TODO: Disabled.
-    /// Call this whenever the config value is updated!
-    // fn update_config_file(&mut self) -> anyhow::Result<()> {
-    //     if self.app_config_fields.no_write {
-    //         // debug!("No write enabled.  Config will not be written.");
-    //         // Don't write!
-    //         // FIXME: [CONFIG] This should be made VERY clear to the user... make a thing saying "it will not write due to no_write option"
-    //         Ok(())
-    //     } else if let Some(config_path) = &self.config_path {
-    //         // Update
-    //         // debug!("Updating config file - writing to: {:?}", config_path);
-    //         std::fs::File::create(config_path)?
-    //             .write_all(self.config.get_config_as_bytes()?.as_ref())?;
-    //         Ok(())
-    //     } else {
-    //         // FIXME: [CONFIG] Put an actual error message?
-    //         Err(anyhow::anyhow!(
-    //             "Config path was missing, please try restarting bottom..."
-    //         ))
-    //     }
-    //     Ok(())
-    // }
-
     pub fn kill_highlighted_process(&mut self) -> Result<()> {
         if let BottomWidgetType::Proc = self.current_widget.widget_type {
             if let Some(current_selected_processes) = &self.to_delete_process_list {
@@ -2198,8 +1937,8 @@ impl App {
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        proc_widget_state.scroll_state.current_scroll_position = 0;
-                        proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Up;
+                        proc_widget_state.table_state.current_scroll_position = 0;
+                        proc_widget_state.table_state.scroll_direction = ScrollDirection::Up;
                     }
                 }
                 BottomWidgetType::ProcSort => {
@@ -2207,8 +1946,8 @@ impl App {
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id - 2)
                     {
-                        proc_widget_state.columns.current_scroll_position = 0;
-                        proc_widget_state.columns.scroll_direction = ScrollDirection::Up;
+                        proc_widget_state.sort_table_state.current_scroll_position = 0;
+                        proc_widget_state.sort_table_state.scroll_direction = ScrollDirection::Up;
                     }
                 }
                 BottomWidgetType::Temp => {
@@ -2216,8 +1955,8 @@ impl App {
                         .temp_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        temp_widget_state.scroll_state.current_scroll_position = 0;
-                        temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Up;
+                        temp_widget_state.table_state.current_scroll_position = 0;
+                        temp_widget_state.table_state.scroll_direction = ScrollDirection::Up;
                     }
                 }
                 BottomWidgetType::Disk => {
@@ -2225,8 +1964,8 @@ impl App {
                         .disk_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        disk_widget_state.scroll_state.current_scroll_position = 0;
-                        disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Up;
+                        disk_widget_state.table_state.current_scroll_position = 0;
+                        disk_widget_state.table_state.scroll_direction = ScrollDirection::Up;
                     }
                 }
                 BottomWidgetType::CpuLegend => {
@@ -2234,8 +1973,8 @@ impl App {
                         .cpu_state
                         .get_mut_widget_state(self.current_widget.widget_id - 1)
                     {
-                        cpu_widget_state.scroll_state.current_scroll_position = 0;
-                        cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Up;
+                        cpu_widget_state.table_state.current_scroll_position = 0;
+                        cpu_widget_state.table_state.scroll_direction = ScrollDirection::Up;
                     }
                 }
 
@@ -2257,18 +1996,9 @@ impl App {
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        if let Some(finalized_process_data) = self
-                            .canvas_data
-                            .finalized_process_data_map
-                            .get(&self.current_widget.widget_id)
-                        {
-                            if !self.canvas_data.finalized_process_data_map.is_empty() {
-                                proc_widget_state.scroll_state.current_scroll_position =
-                                    finalized_process_data.len() - 1;
-                                proc_widget_state.scroll_state.scroll_direction =
-                                    ScrollDirection::Down;
-                            }
-                        }
+                        proc_widget_state.table_state.current_scroll_position =
+                            proc_widget_state.table_data.data.len().saturating_sub(1);
+                        proc_widget_state.table_state.scroll_direction = ScrollDirection::Down;
                     }
                 }
                 BottomWidgetType::ProcSort => {
@@ -2276,9 +2006,9 @@ impl App {
                         .proc_state
                         .get_mut_widget_state(self.current_widget.widget_id - 2)
                     {
-                        proc_widget_state.columns.current_scroll_position =
-                            proc_widget_state.columns.get_enabled_columns_len() - 1;
-                        proc_widget_state.columns.scroll_direction = ScrollDirection::Down;
+                        proc_widget_state.sort_table_state.current_scroll_position =
+                            proc_widget_state.num_enabled_columns() - 1;
+                        proc_widget_state.sort_table_state.scroll_direction = ScrollDirection::Down;
                     }
                 }
                 BottomWidgetType::Temp => {
@@ -2286,10 +2016,10 @@ impl App {
                         .temp_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        if !self.canvas_data.temp_sensor_data.is_empty() {
-                            temp_widget_state.scroll_state.current_scroll_position =
-                                self.canvas_data.temp_sensor_data.len() - 1;
-                            temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Down;
+                        if !self.converted_data.temp_sensor_data.data.is_empty() {
+                            temp_widget_state.table_state.current_scroll_position =
+                                self.converted_data.temp_sensor_data.data.len() - 1;
+                            temp_widget_state.table_state.scroll_direction = ScrollDirection::Down;
                         }
                     }
                 }
@@ -2298,10 +2028,10 @@ impl App {
                         .disk_state
                         .get_mut_widget_state(self.current_widget.widget_id)
                     {
-                        if !self.canvas_data.disk_data.is_empty() {
-                            disk_widget_state.scroll_state.current_scroll_position =
-                                self.canvas_data.disk_data.len() - 1;
-                            disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Down;
+                        if !self.converted_data.disk_data.data.is_empty() {
+                            disk_widget_state.table_state.current_scroll_position =
+                                self.converted_data.disk_data.data.len() - 1;
+                            disk_widget_state.table_state.scroll_direction = ScrollDirection::Down;
                         }
                     }
                 }
@@ -2310,10 +2040,10 @@ impl App {
                         .cpu_state
                         .get_mut_widget_state(self.current_widget.widget_id - 1)
                     {
-                        let cap = self.canvas_data.cpu_data.len();
+                        let cap = self.converted_data.cpu_data.len();
                         if cap > 0 {
-                            cpu_widget_state.scroll_state.current_scroll_position = cap - 1;
-                            cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Down;
+                            cpu_widget_state.table_state.current_scroll_position = cap - 1;
+                            cpu_widget_state.table_state.scroll_direction = ScrollDirection::Down;
                         }
                     }
                 }
@@ -2359,23 +2089,10 @@ impl App {
             .proc_state
             .get_mut_widget_state(self.current_widget.widget_id - 2)
         {
-            let current_posn = proc_widget_state.columns.current_scroll_position;
-            let num_columns = proc_widget_state.columns.get_enabled_columns_len();
-            let prop: core::result::Result<usize, _> =
-                (current_posn as i64 + num_to_change_by).try_into();
-
-            if let Ok(prop) = prop {
-                if prop < num_columns {
-                    proc_widget_state.columns.current_scroll_position =
-                        (current_posn as i64 + num_to_change_by) as usize;
-                }
-
-                if num_to_change_by < 0 {
-                    proc_widget_state.columns.scroll_direction = ScrollDirection::Up;
-                } else {
-                    proc_widget_state.columns.scroll_direction = ScrollDirection::Down;
-                }
-            }
+            let num_entries = proc_widget_state.num_enabled_columns();
+            proc_widget_state
+                .sort_table_state
+                .update_position(num_to_change_by, num_entries);
         }
     }
 
@@ -2386,8 +2103,8 @@ impl App {
             .get_mut(&(self.current_widget.widget_id - 1))
         {
             cpu_widget_state
-                .scroll_state
-                .update_position(num_to_change_by, self.canvas_data.cpu_data.len());
+                .table_state
+                .update_position(num_to_change_by, self.converted_data.cpu_data.len());
         }
     }
 
@@ -2397,17 +2114,9 @@ impl App {
             .proc_state
             .get_mut_widget_state(self.current_widget.widget_id)
         {
-            if let Some(finalized_process_data) = self
-                .canvas_data
-                .finalized_process_data_map
-                .get(&self.current_widget.widget_id)
-            {
-                proc_widget_state
-                    .scroll_state
-                    .update_position(num_to_change_by, finalized_process_data.len())
-            } else {
-                None
-            }
+            proc_widget_state
+                .table_state
+                .update_position(num_to_change_by, proc_widget_state.table_data.data.len())
         } else {
             None
         }
@@ -2419,9 +2128,10 @@ impl App {
             .widget_states
             .get_mut(&self.current_widget.widget_id)
         {
-            temp_widget_state
-                .scroll_state
-                .update_position(num_to_change_by, self.canvas_data.temp_sensor_data.len());
+            temp_widget_state.table_state.update_position(
+                num_to_change_by,
+                self.converted_data.temp_sensor_data.data.len(),
+            );
         }
     }
 
@@ -2432,8 +2142,8 @@ impl App {
             .get_mut(&self.current_widget.widget_id)
         {
             disk_widget_state
-                .scroll_state
-                .update_position(num_to_change_by, self.canvas_data.disk_data.len());
+                .table_state
+                .update_position(num_to_change_by, self.converted_data.disk_data.data.len());
         }
     }
 
@@ -2513,31 +2223,12 @@ impl App {
     }
 
     fn toggle_collapsing_process_branch(&mut self) {
-        if let Some(proc_widget_state) = self
+        if let Some(pws) = self
             .proc_state
             .widget_states
             .get_mut(&self.current_widget.widget_id)
         {
-            let current_posn = proc_widget_state.scroll_state.current_scroll_position;
-
-            if let Some(displayed_process_list) = self
-                .canvas_data
-                .finalized_process_data_map
-                .get(&self.current_widget.widget_id)
-            {
-                if let Some(corresponding_process) = displayed_process_list.get(current_posn) {
-                    let corresponding_pid = corresponding_process.pid;
-
-                    if let Some(process_data) = self
-                        .canvas_data
-                        .single_process_data
-                        .get_mut(&corresponding_pid)
-                    {
-                        process_data.is_collapsed_entry = !process_data.is_collapsed_entry;
-                        self.proc_state.force_update = Some(self.current_widget.widget_id);
-                    }
-                }
-            }
+            pws.toggle_tree_branch();
         }
     }
 
@@ -2858,7 +2549,6 @@ impl App {
                 if (x >= tlc_x && y >= tlc_y) && (x < brc_x && y < brc_y) {
                     if let Some(new_widget) = self.widget_map.get(new_widget_id) {
                         self.current_widget = new_widget.clone();
-
                         match &self.current_widget.widget_type {
                             BottomWidgetType::Temp
                             | BottomWidgetType::Proc
@@ -2905,13 +2595,8 @@ impl App {
                     | BottomWidgetType::Disk => {
                         // Get our index...
                         let clicked_entry = y - *tlc_y;
-                        // + 1 so we start at 0.
-                        let header_gap_offset = 1 + if self.is_drawing_gap(&self.current_widget) {
-                            self.app_config_fields.table_gap
-                        } else {
-                            0
-                        };
-                        let offset = border_offset + header_gap_offset;
+                        let header_offset = self.header_offset(&self.current_widget);
+                        let offset = border_offset + header_offset;
                         if clicked_entry >= offset {
                             let offset_clicked_entry = clicked_entry - offset;
                             match &self.current_widget.widget_type {
@@ -2921,16 +2606,20 @@ impl App {
                                         .get_widget_state(self.current_widget.widget_id)
                                     {
                                         if let Some(visual_index) =
-                                            proc_widget_state.scroll_state.table_state.selected()
+                                            proc_widget_state.table_state.table_state.selected()
                                         {
                                             // If in tree mode, also check to see if this click is on
                                             // the same entry as the already selected one - if it is,
                                             // then we minimize.
 
+                                            let is_tree_mode = matches!(
+                                                proc_widget_state.mode,
+                                                ProcWidgetMode::Tree { .. }
+                                            );
+
                                             let previous_scroll_position = proc_widget_state
-                                                .scroll_state
+                                                .table_state
                                                 .current_scroll_position;
-                                            let is_tree_mode = proc_widget_state.is_tree_mode;
 
                                             let new_position = self.change_process_position(
                                                 offset_clicked_entry as i64 - visual_index as i64,
@@ -2947,13 +2636,15 @@ impl App {
                                     }
                                 }
                                 BottomWidgetType::ProcSort => {
-                                    // TODO: This should sort if you double click!
+                                    // TODO: [Feature] This could sort if you double click!
                                     if let Some(proc_widget_state) = self
                                         .proc_state
                                         .get_widget_state(self.current_widget.widget_id - 2)
                                     {
-                                        if let Some(visual_index) =
-                                            proc_widget_state.columns.column_state.selected()
+                                        if let Some(visual_index) = proc_widget_state
+                                            .sort_table_state
+                                            .table_state
+                                            .selected()
                                         {
                                             self.change_process_sort_position(
                                                 offset_clicked_entry as i64 - visual_index as i64,
@@ -2967,7 +2658,7 @@ impl App {
                                         .get_widget_state(self.current_widget.widget_id - 1)
                                     {
                                         if let Some(visual_index) =
-                                            cpu_widget_state.scroll_state.table_state.selected()
+                                            cpu_widget_state.table_state.table_state.selected()
                                         {
                                             self.change_cpu_legend_position(
                                                 offset_clicked_entry as i64 - visual_index as i64,
@@ -2981,7 +2672,7 @@ impl App {
                                         .get_widget_state(self.current_widget.widget_id)
                                     {
                                         if let Some(visual_index) =
-                                            temp_widget_state.scroll_state.table_state.selected()
+                                            temp_widget_state.table_state.table_state.selected()
                                         {
                                             self.change_temp_position(
                                                 offset_clicked_entry as i64 - visual_index as i64,
@@ -2995,7 +2686,7 @@ impl App {
                                         .get_widget_state(self.current_widget.widget_id)
                                     {
                                         if let Some(visual_index) =
-                                            disk_widget_state.scroll_state.table_state.selected()
+                                            disk_widget_state.table_state.table_state.selected()
                                         {
                                             self.change_disk_position(
                                                 offset_clicked_entry as i64 - visual_index as i64,
@@ -3009,45 +2700,19 @@ impl App {
                             // We might have clicked on a header!  Check if we only exceeded the table + border offset, and
                             // it's implied we exceeded the gap offset.
                             if clicked_entry == border_offset {
-                                #[allow(clippy::single_match)]
-                                match &self.current_widget.widget_type {
-                                    BottomWidgetType::Proc => {
-                                        if let Some(proc_widget_state) = self
-                                            .proc_state
-                                            .get_mut_widget_state(self.current_widget.widget_id)
+                                if let BottomWidgetType::Proc = &self.current_widget.widget_type {
+                                    if let Some(proc_widget_state) = self
+                                        .proc_state
+                                        .get_mut_widget_state(self.current_widget.widget_id)
+                                    {
+                                        if let SortState::Sortable(st) =
+                                            &mut proc_widget_state.table_state.sort_state
                                         {
-                                            // Let's now check if it's a column header.
-                                            if let (Some(y_loc), Some(x_locs)) = (
-                                                &proc_widget_state.columns.column_header_y_loc,
-                                                &proc_widget_state.columns.column_header_x_locs,
-                                            ) {
-                                                // debug!("x, y: {}, {}", x, y);
-                                                // debug!("y_loc: {}", y_loc);
-                                                // debug!("x_locs: {:?}", x_locs);
-
-                                                if y == *y_loc {
-                                                    for (itx, (x_left, x_right)) in
-                                                        x_locs.iter().enumerate()
-                                                    {
-                                                        if x >= *x_left && x <= *x_right {
-                                                            // Found our column!
-                                                            proc_widget_state
-                                                            .columns
-                                                            .set_to_sorted_index_from_visual_index(
-                                                                itx,
-                                                            );
-                                                            proc_widget_state
-                                                                .update_sorting_with_columns();
-                                                            self.proc_state.force_update =
-                                                                Some(self.current_widget.widget_id);
-                                                            break;
-                                                        }
-                                                    }
-                                                }
+                                            if st.try_select_location(x, y).is_some() {
+                                                proc_widget_state.force_data_update();
                                             }
                                         }
                                     }
-                                    _ => {}
                                 }
                             }
                         }
@@ -3080,13 +2745,23 @@ impl App {
         self.is_expanded || !self.app_config_fields.use_basic_mode
     }
 
-    fn is_drawing_gap(&self, widget: &BottomWidget) -> bool {
+    fn header_offset(&self, widget: &BottomWidget) -> u16 {
         if let (Some((_tlc_x, tlc_y)), Some((_brc_x, brc_y))) =
             (widget.top_left_corner, widget.bottom_right_corner)
         {
-            brc_y - tlc_y >= constants::TABLE_GAP_HEIGHT_LIMIT
+            let height_diff = brc_y - tlc_y;
+            if height_diff >= constants::TABLE_GAP_HEIGHT_LIMIT {
+                1 + self.app_config_fields.table_gap
+            } else {
+                let min_height_for_header = if self.is_drawing_border() { 3 } else { 1 };
+                if height_diff > min_height_for_header {
+                    1
+                } else {
+                    0
+                }
+            }
         } else {
-            self.app_config_fields.table_gap == 0
+            1 + self.app_config_fields.table_gap
         }
     }
 }
diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs
index 1860c91b..0f9d65d7 100644
--- a/src/app/data_farmer.rs
+++ b/src/app/data_farmer.rs
@@ -1,27 +1,32 @@
-/// In charge of cleaning, processing, and managing data.  I couldn't think of
-/// a better name for the file.  Since I called data collection "harvesting",
-/// then this is the farmer I guess.
-///
-/// Essentially the main goal is to shift the initial calculation and distribution
-/// of joiner points and data to one central location that will only do it
-/// *once* upon receiving the data --- as opposed to doing it on canvas draw,
-/// which will be a costly process.
-///
-/// This will also handle the *cleaning* of stale data.  That should be done
-/// in some manner (timer on another thread, some loop) that will occasionally
-/// call the purging function.  Failure to do so *will* result in a growing
-/// memory usage and higher CPU usage - you will be trying to process more and
-/// more points as this is used!
+//! In charge of cleaning, processing, and managing data.  I couldn't think of
+//! a better name for the file.  Since I called data collection "harvesting",
+//! then this is the farmer I guess.
+//!
+//! Essentially the main goal is to shift the initial calculation and distribution
+//! of joiner points and data to one central location that will only do it
+//! *once* upon receiving the data --- as opposed to doing it on canvas draw,
+//! which will be a costly process.
+//!
+//! This will also handle the *cleaning* of stale data.  That should be done
+//! in some manner (timer on another thread, some loop) that will occasionally
+//! call the purging function.  Failure to do so *will* result in a growing
+//! memory usage and higher CPU usage - you will be trying to process more and
+//! more points as this is used!
+
 use once_cell::sync::Lazy;
 
+use fxhash::FxHashMap;
+use itertools::Itertools;
+
 use std::{time::Instant, vec::Vec};
 
 #[cfg(feature = "battery")]
 use crate::data_harvester::batteries;
 
 use crate::{
-    data_harvester::{cpu, disks, memory, network, processes, temperature, Data},
+    data_harvester::{cpu, disks, memory, network, processes::ProcessHarvest, temperature, Data},
     utils::gen_util::{get_decimal_bytes, GIGA_LIMIT},
+    Pid,
 };
 use regex::Regex;
 
@@ -38,6 +43,97 @@ pub struct TimedData {
     pub swap_data: Option<Value>,
 }
 
+pub type StringPidMap = FxHashMap<String, Vec<Pid>>;
+
+#[derive(Clone, Debug, Default)]
+pub struct ProcessData {
+    /// A PID to process data map.
+    pub process_harvest: FxHashMap<Pid, ProcessHarvest>,
+
+    /// A mapping from a process name to any PID with that name.
+    pub name_pid_map: StringPidMap,
+
+    /// A mapping from a process command to any PID with that name.
+    pub cmd_pid_map: StringPidMap,
+
+    /// A mapping between a process PID to any children process PIDs.
+    pub process_parent_mapping: FxHashMap<Pid, Vec<Pid>>,
+
+    /// PIDs corresponding to processes that have no parents.
+    pub orphan_pids: Vec<Pid>,
+}
+
+impl ProcessData {
+    fn ingest(&mut self, list_of_processes: Vec<ProcessHarvest>) {
+        // TODO: [Optimization] Probably more efficient to all of this in the data collection step, but it's fine for now.
+        self.name_pid_map.clear();
+        self.cmd_pid_map.clear();
+        self.process_parent_mapping.clear();
+
+        // Reverse as otherwise the pid mappings are in the wrong order.
+        list_of_processes.iter().rev().for_each(|process_harvest| {
+            if let Some(entry) = self.name_pid_map.get_mut(&process_harvest.name) {
+                entry.push(process_harvest.pid);
+            } else {
+                self.name_pid_map
+                    .insert(process_harvest.name.to_string(), vec![process_harvest.pid]);
+            }
+
+            if let Some(entry) = self.cmd_pid_map.get_mut(&process_harvest.command) {
+                entry.push(process_harvest.pid);
+            } else {
+                self.cmd_pid_map.insert(
+                    process_harvest.command.to_string(),
+                    vec![process_harvest.pid],
+                );
+            }
+
+            if let Some(parent_pid) = process_harvest.parent_pid {
+                if let Some(entry) = self.process_parent_mapping.get_mut(&parent_pid) {
+                    entry.push(process_harvest.pid);
+                } else {
+                    self.process_parent_mapping
+                        .insert(parent_pid, vec![process_harvest.pid]);
+                }
+            }
+        });
+
+        self.name_pid_map.shrink_to_fit();
+        self.cmd_pid_map.shrink_to_fit();
+        self.process_parent_mapping.shrink_to_fit();
+
+        let process_pid_map = list_of_processes
+            .into_iter()
+            .map(|process| (process.pid, process))
+            .collect();
+        self.process_harvest = process_pid_map;
+
+        // This also needs a quick sort + reverse to be in the correct order.
+        self.orphan_pids = {
+            let mut res: Vec<Pid> = self
+                .process_harvest
+                .iter()
+                .filter_map(|(pid, process_harvest)| {
+                    if let Some(parent_pid) = process_harvest.parent_pid {
+                        if self.process_harvest.contains_key(&parent_pid) {
+                            None
+                        } else {
+                            Some(*pid)
+                        }
+                    } else {
+                        Some(*pid)
+                    }
+                })
+                .sorted()
+                .collect();
+
+            res.reverse();
+
+            res
+        }
+    }
+}
+
 /// AppCollection represents the pooled data stored within the main app
 /// thread.  Basically stores a (occasionally cleaned) record of the data
 /// collected, and what is needed to convert into a displayable form.
@@ -57,7 +153,7 @@ pub struct DataCollection {
     pub swap_harvest: memory::MemHarvest,
     pub cpu_harvest: cpu::CpuHarvest,
     pub load_avg_harvest: cpu::LoadAvgHarvest,
-    pub process_harvest: Vec<processes::ProcessHarvest>,
+    pub process_data: ProcessData,
     pub disk_harvest: Vec<disks::DiskHarvest>,
     pub io_harvest: disks::IoHarvest,
     pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>,
@@ -78,7 +174,7 @@ impl Default for DataCollection {
             swap_harvest: memory::MemHarvest::default(),
             cpu_harvest: cpu::CpuHarvest::default(),
             load_avg_harvest: cpu::LoadAvgHarvest::default(),
-            process_harvest: Vec::default(),
+            process_data: Default::default(),
             disk_harvest: Vec::default(),
             io_harvest: disks::IoHarvest::default(),
             io_labels_and_prev: Vec::default(),
@@ -97,7 +193,7 @@ impl DataCollection {
         self.memory_harvest = memory::MemHarvest::default();
         self.swap_harvest = memory::MemHarvest::default();
         self.cpu_harvest = cpu::CpuHarvest::default();
-        self.process_harvest = Vec::default();
+        self.process_data = Default::default();
         self.disk_harvest = Vec::default();
         self.io_harvest = disks::IoHarvest::default();
         self.io_labels_and_prev = Vec::default();
@@ -108,10 +204,14 @@ impl DataCollection {
         }
     }
 
-    pub fn set_frozen_time(&mut self) {
+    pub fn freeze(&mut self) {
         self.frozen_instant = Some(self.current_instant);
     }
 
+    pub fn thaw(&mut self) {
+        self.frozen_instant = None;
+    }
+
     pub fn clean_data(&mut self, max_time_millis: u64) {
         let current_time = Instant::now();
 
@@ -319,8 +419,8 @@ impl DataCollection {
         self.io_harvest = io;
     }
 
-    fn eat_proc(&mut self, list_of_processes: Vec<processes::ProcessHarvest>) {
-        self.process_harvest = list_of_processes;
+    fn eat_proc(&mut self, list_of_processes: Vec<ProcessHarvest>) {
+        self.process_data.ingest(list_of_processes);
     }
 
     #[cfg(feature = "battery")]
diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs
index 9dcc377c..979bdadb 100644
--- a/src/app/data_harvester.rs
+++ b/src/app/data_harvester.rs
@@ -104,6 +104,9 @@ pub struct DataCollector {
     #[cfg(feature = "battery")]
     battery_list: Option<Vec<Battery>>,
     filters: DataFilters,
+
+    #[cfg(target_family = "unix")]
+    user_table: self::processes::UserTable,
 }
 
 impl DataCollector {
@@ -133,6 +136,8 @@ impl DataCollector {
             #[cfg(feature = "battery")]
             battery_list: None,
             filters,
+            #[cfg(target_family = "unix")]
+            user_table: Default::default(),
         }
     }
 
@@ -191,7 +196,7 @@ impl DataCollector {
         };
     }
 
-    pub fn set_collected_data(&mut self, used_widgets: UsedWidgets) {
+    pub fn set_data_collection(&mut self, used_widgets: UsedWidgets) {
         self.widgets_to_harvest = used_widgets;
     }
 
@@ -270,15 +275,28 @@ impl DataCollector {
                             .duration_since(self.last_collection_time)
                             .as_secs(),
                         self.mem_total_kb,
+                        &mut self.user_table,
                     )
                 }
                 #[cfg(not(target_os = "linux"))]
                 {
-                    processes::get_process_data(
-                        &self.sys,
-                        self.use_current_cpu_total,
-                        self.mem_total_kb,
-                    )
+                    #[cfg(target_family = "unix")]
+                    {
+                        processes::get_process_data(
+                            &self.sys,
+                            self.use_current_cpu_total,
+                            self.mem_total_kb,
+                            &mut self.user_table,
+                        )
+                    }
+                    #[cfg(not(target_family = "unix"))]
+                    {
+                        processes::get_process_data(
+                            &self.sys,
+                            self.use_current_cpu_total,
+                            self.mem_total_kb,
+                        )
+                    }
                 }
             } {
                 self.data.list_of_processes = Some(process_list);
diff --git a/src/app/data_harvester/network/heim.rs b/src/app/data_harvester/network/heim.rs
index 3c12fd73..b7980ddb 100644
--- a/src/app/data_harvester/network/heim.rs
+++ b/src/app/data_harvester/network/heim.rs
@@ -3,7 +3,7 @@
 use super::NetworkHarvest;
 use std::time::Instant;
 
-// FIXME: Eventually make it so that this thing also takes individual usage into account, so we can allow for showing per-interface!
+// TODO: Eventually make it so that this thing also takes individual usage into account, so we can show per-interface!
 pub async fn get_network_data(
     prev_net_access_time: Instant, prev_net_rx: &mut u64, prev_net_tx: &mut u64,
     curr_time: Instant, actually_get: bool, filter: &Option<crate::app::Filter>,
diff --git a/src/app/data_harvester/processes/linux.rs b/src/app/data_harvester/processes/linux.rs
index 081af802..1e91f93a 100644
--- a/src/app/data_harvester/processes/linux.rs
+++ b/src/app/data_harvester/processes/linux.rs
@@ -5,7 +5,7 @@ use std::collections::hash_map::Entry;
 use crate::utils::error::{self, BottomError};
 use crate::Pid;
 
-use super::ProcessHarvest;
+use super::{ProcessHarvest, UserTable};
 
 use sysinfo::ProcessStatus;
 
@@ -120,6 +120,7 @@ fn get_linux_cpu_usage(
 fn read_proc(
     prev_proc: &PrevProcDetails, stat: &Stat, cpu_usage: f64, cpu_fraction: f64,
     use_current_cpu_total: bool, time_difference_in_secs: u64, mem_total_kb: u64,
+    user_table: &mut UserTable,
 ) -> error::Result<(ProcessHarvest, u64)> {
     use std::convert::TryFrom;
 
@@ -156,7 +157,10 @@ fn read_proc(
     };
 
     let process_state_char = stat.state;
-    let process_state = ProcessStatus::from(process_state_char).to_string();
+    let process_state = (
+        ProcessStatus::from(process_state_char).to_string(),
+        process_state_char,
+    );
     let (cpu_usage_percent, new_process_times) = get_linux_cpu_usage(
         stat,
         cpu_usage,
@@ -199,7 +203,7 @@ fn read_proc(
             (0, 0, 0, 0)
         };
 
-    let uid = Some(process.owner);
+    let uid = process.owner;
 
     Ok((
         ProcessHarvest {
@@ -215,8 +219,11 @@ fn read_proc(
             total_read_bytes,
             total_write_bytes,
             process_state,
-            process_state_char,
             uid,
+            user: user_table
+                .get_uid_to_username_mapping(uid)
+                .map(Into::into)
+                .unwrap_or_else(|_| "N/A".into()),
         },
         new_process_times,
     ))
@@ -225,7 +232,7 @@ fn read_proc(
 pub fn get_process_data(
     prev_idle: &mut f64, prev_non_idle: &mut f64,
     pid_mapping: &mut FxHashMap<Pid, PrevProcDetails>, use_current_cpu_total: bool,
-    time_difference_in_secs: u64, mem_total_kb: u64,
+    time_difference_in_secs: u64, mem_total_kb: u64, user_table: &mut UserTable,
 ) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
     // TODO: [PROC THREADS] Add threads
 
@@ -268,6 +275,7 @@ pub fn get_process_data(
                                 use_current_cpu_total,
                                 time_difference_in_secs,
                                 mem_total_kb,
+                                user_table,
                             ) {
                                 prev_proc_details.cpu_time = new_process_times;
                                 prev_proc_details.total_read_bytes =
diff --git a/src/app/data_harvester/processes/macos.rs b/src/app/data_harvester/processes/macos.rs
index f08e17ca..0c401b0a 100644
--- a/src/app/data_harvester/processes/macos.rs
+++ b/src/app/data_harvester/processes/macos.rs
@@ -3,6 +3,8 @@
 use super::ProcessHarvest;
 use sysinfo::{PidExt, ProcessExt, ProcessStatus, ProcessorExt, System, SystemExt};
 
+use crate::data_harvester::processes::UserTable;
+
 fn get_macos_process_cpu_usage(
     pids: &[i32],
 ) -> std::io::Result<std::collections::HashMap<i32, f64>> {
@@ -35,7 +37,7 @@ fn get_macos_process_cpu_usage(
 }
 
 pub fn get_process_data(
-    sys: &System, use_current_cpu_total: bool, mem_total_kb: u64,
+    sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable,
 ) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
     let mut process_vector: Vec<ProcessHarvest> = Vec::new();
     let process_hashmap = sys.processes();
@@ -86,6 +88,11 @@ pub fn get_process_data(
         };
 
         let disk_usage = process_val.disk_usage();
+        let process_state = {
+            let ps = process_val.status();
+            (ps.to_string(), convert_process_status_to_char(ps))
+        };
+        let uid = process_val.uid;
         process_vector.push(ProcessHarvest {
             pid: process_val.pid().as_u32() as _,
             parent_pid: process_val.parent().map(|p| p.as_u32() as _),
@@ -102,16 +109,19 @@ pub fn get_process_data(
             write_bytes_per_sec: disk_usage.written_bytes,
             total_read_bytes: disk_usage.total_read_bytes,
             total_write_bytes: disk_usage.total_written_bytes,
-            process_state: process_val.status().to_string(),
-            process_state_char: convert_process_status_to_char(process_val.status()),
-            uid: Some(process_val.uid),
+            process_state,
+            uid,
+            user: user_table
+                .get_uid_to_username_mapping(uid)
+                .map(Into::into)
+                .unwrap_or_else(|_| "N/A".into()),
         });
     }
 
     let unknown_state = ProcessStatus::Unknown(0).to_string();
     let cpu_usage_unknown_pids: Vec<i32> = process_vector
         .iter()
-        .filter(|process| process.process_state == unknown_state)
+        .filter(|process| process.process_state.0 == unknown_state)
         .map(|process| process.pid)
         .collect();
     let cpu_usages = get_macos_process_cpu_usage(&cpu_usage_unknown_pids)?;
diff --git a/src/app/data_harvester/processes/mod.rs b/src/app/data_harvester/processes/mod.rs
index 283080b3..40662d7a 100644
--- a/src/app/data_harvester/processes/mod.rs
+++ b/src/app/data_harvester/processes/mod.rs
@@ -25,73 +25,64 @@ cfg_if::cfg_if! {
 
 use crate::Pid;
 
-// TODO: Add value so we know if it's sorted ascending or descending by default?
-#[derive(Clone, PartialEq, Eq, Hash, Debug)]
-pub enum ProcessSorting {
-    CpuPercent,
-    Mem,
-    MemPercent,
-    Pid,
-    ProcessName,
-    Command,
-    ReadPerSecond,
-    WritePerSecond,
-    TotalRead,
-    TotalWrite,
-    State,
-    User,
-    Count,
-}
-
-impl std::fmt::Display for ProcessSorting {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "{}",
-            match &self {
-                ProcessSorting::CpuPercent => "CPU%",
-                ProcessSorting::MemPercent => "Mem%",
-                ProcessSorting::Mem => "Mem",
-                ProcessSorting::ReadPerSecond => "R/s",
-                ProcessSorting::WritePerSecond => "W/s",
-                ProcessSorting::TotalRead => "T.Read",
-                ProcessSorting::TotalWrite => "T.Write",
-                ProcessSorting::State => "State",
-                ProcessSorting::ProcessName => "Name",
-                ProcessSorting::Command => "Command",
-                ProcessSorting::Pid => "PID",
-                ProcessSorting::Count => "Count",
-                ProcessSorting::User => "User",
-            }
-        )
-    }
-}
-
-impl Default for ProcessSorting {
-    fn default() -> Self {
-        ProcessSorting::CpuPercent
-    }
-}
-
 #[derive(Debug, Clone, Default)]
 pub struct ProcessHarvest {
+    /// The pid of the process.
     pub pid: Pid,
-    pub parent_pid: Option<Pid>, // Remember, parent_pid 0 is root...
+
+    /// The parent PID of the process. Remember, parent_pid 0 is root.
+    pub parent_pid: Option<Pid>,
+
+    /// CPU usage as a percentage.
     pub cpu_usage_percent: f64,
+
+    /// Memory usage as a percentage.
     pub mem_usage_percent: f64,
+
+    /// Memory usage as bytes.
     pub mem_usage_bytes: u64,
+
+    /// The name of the process.
+    pub name: String,
+
+    /// The exact command for the process.
+    pub command: String,
+
+    /// Bytes read per second.
+    pub read_bytes_per_sec: u64,
+
+    /// Bytes written per second.
+    pub write_bytes_per_sec: u64,
+
+    /// The total number of bytes read by the process.
+    pub total_read_bytes: u64,
+
+    /// The total number of bytes written by the process.
+    pub total_write_bytes: u64,
+
+    /// The current state of the process (e.g. zombie, asleep)
+    pub process_state: (String, char),
+
+    /// This is the *effective* user ID of the process. This is only used on Unix platforms.
+    #[cfg(target_family = "unix")]
+    pub uid: libc::uid_t,
+
+    /// This is the process' user. This is only used on Unix platforms.
+    #[cfg(target_family = "unix")]
+    pub user: std::borrow::Cow<'static, str>,
+    // TODO: Additional fields
     // pub rss_kb: u64,
     // pub virt_kb: u64,
-    pub name: String,
-    pub command: String,
-    pub read_bytes_per_sec: u64,
-    pub write_bytes_per_sec: u64,
-    pub total_read_bytes: u64,
-    pub total_write_bytes: u64,
-    pub process_state: String,
-    pub process_state_char: char,
-
-    /// This is the *effective* user ID.
-    #[cfg(target_family = "unix")]
-    pub uid: Option<libc::uid_t>,
+}
+
+impl ProcessHarvest {
+    pub(crate) fn add(&mut self, rhs: &ProcessHarvest) {
+        self.cpu_usage_percent += rhs.cpu_usage_percent;
+        self.mem_usage_bytes += rhs.mem_usage_bytes;
+        self.mem_usage_percent += rhs.mem_usage_percent;
+        self.read_bytes_per_sec += rhs.read_bytes_per_sec;
+        self.write_bytes_per_sec += rhs.write_bytes_per_sec;
+        self.total_read_bytes += rhs.total_read_bytes;
+        self.total_write_bytes += rhs.total_write_bytes;
+    }
 }
diff --git a/src/app/data_harvester/processes/unix.rs b/src/app/data_harvester/processes/unix.rs
index 8fadc590..75f70bec 100644
--- a/src/app/data_harvester/processes/unix.rs
+++ b/src/app/data_harvester/processes/unix.rs
@@ -1,10 +1,12 @@
 //! Unix-specific parts of process collection.
 
+use fxhash::FxHashMap;
+
 use crate::utils::error;
 
 #[derive(Debug, Default)]
 pub struct UserTable {
-    pub uid_user_mapping: std::collections::HashMap<libc::uid_t, String>,
+    pub uid_user_mapping: FxHashMap<libc::uid_t, String>,
 }
 
 impl UserTable {
diff --git a/src/app/data_harvester/processes/windows.rs b/src/app/data_harvester/processes/windows.rs
index af9ead5f..a3564874 100644
--- a/src/app/data_harvester/processes/windows.rs
+++ b/src/app/data_harvester/processes/windows.rs
@@ -55,6 +55,7 @@ pub fn get_process_data(
         };
 
         let disk_usage = process_val.disk_usage();
+        let process_state = (process_val.status().to_string(), 'R');
         process_vector.push(ProcessHarvest {
             pid: process_val.pid().as_u32() as _,
             parent_pid: process_val.parent().map(|p| p.as_u32() as _),
@@ -71,8 +72,7 @@ pub fn get_process_data(
             write_bytes_per_sec: disk_usage.written_bytes,
             total_read_bytes: disk_usage.total_read_bytes,
             total_write_bytes: disk_usage.total_written_bytes,
-            process_state: process_val.status().to_string(),
-            process_state_char: 'R',
+            process_state,
         });
     }
 
diff --git a/src/app/query.rs b/src/app/query.rs
index 0f18c901..46c40022 100644
--- a/src/app/query.rs
+++ b/src/app/query.rs
@@ -1,449 +1,434 @@
-use super::ProcWidgetState;
-use crate::{
-    data_conversion::ConvertedProcessData,
-    utils::error::{
-        BottomError::{self, QueryError},
-        Result,
-    },
+use crate::utils::error::{
+    BottomError::{self, QueryError},
+    Result,
 };
 use std::fmt::Debug;
 use std::{borrow::Cow, collections::VecDeque};
 
+use super::data_harvester::processes::ProcessHarvest;
+
 const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
 const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
 const OR_LIST: [&str; 2] = ["or", "||"];
 const AND_LIST: [&str; 2] = ["and", "&&"];
 
-/// I only separated this as otherwise, the states.rs file gets huge... and this should
-/// belong in another file anyways, IMO.
-pub trait ProcessQuery {
-    /// In charge of parsing the given query.
-    /// We are defining the following language for a query (case-insensitive prefixes):
-    ///
-    /// - Process names: No prefix required, can use regex, match word, or case.
-    ///   Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
-    ///   rather than a prefix.
-    /// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
-    /// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case).  Can compare.
-    /// - MEM: Use prefix `mem`, cannot use r/m/c.  Can compare.
-    /// - STATE: Use prefix `state`, can use regex, match word, or case.
-    /// - USER: Use prefix `user`, can use regex, match word, or case.
-    /// - Read/s: Use prefix `r`.  Can compare.
-    /// - Write/s: Use prefix `w`.  Can compare.
-    /// - Total read: Use prefix `read`.  Can compare.
-    /// - Total write: Use prefix `write`.  Can compare.
-    ///
-    /// For queries, whitespaces are our delimiters.  We will merge together any adjacent non-prefixed
-    /// or quoted elements after splitting to treat as process names.
-    /// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
-    fn parse_query(&self) -> Result<Query>;
-}
+/// In charge of parsing the given query.
+/// We are defining the following language for a query (case-insensitive prefixes):
+///
+/// - Process names: No prefix required, can use regex, match word, or case.
+///   Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
+///   rather than a prefix.
+/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
+/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case).  Can compare.
+/// - MEM: Use prefix `mem`, cannot use r/m/c.  Can compare.
+/// - STATE: Use prefix `state`, can use regex, match word, or case.
+/// - USER: Use prefix `user`, can use regex, match word, or case.
+/// - Read/s: Use prefix `r`.  Can compare.
+/// - Write/s: Use prefix `w`.  Can compare.
+/// - Total read: Use prefix `read`.  Can compare.
+/// - Total write: Use prefix `write`.  Can compare.
+///
+/// For queries, whitespaces are our delimiters.  We will merge together any adjacent non-prefixed
+/// or quoted elements after splitting to treat as process names.
+/// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
+pub fn parse_query(
+    search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool,
+    is_searching_with_regex: bool,
+) -> Result<Query> {
+    fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
+        let lhs = process_or(query)?;
+        let mut list_of_ors = vec![lhs];
 
-impl ProcessQuery for ProcWidgetState {
-    fn parse_query(&self) -> Result<Query> {
-        fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
-            let lhs = process_or(query)?;
-            let mut list_of_ors = vec![lhs];
-
-            while query.front().is_some() {
-                list_of_ors.push(process_or(query)?);
-            }
-
-            Ok(Query { query: list_of_ors })
+        while query.front().is_some() {
+            list_of_ors.push(process_or(query)?);
         }
 
-        fn process_or(query: &mut VecDeque<String>) -> Result<Or> {
-            let mut lhs = process_and(query)?;
-            let mut rhs: Option<Box<And>> = None;
+        Ok(Query { query: list_of_ors })
+    }
 
-            while let Some(queue_top) = query.front() {
-                // debug!("OR QT: {:?}", queue_top);
-                if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
-                    query.pop_front();
-                    rhs = Some(Box::new(process_and(query)?));
+    fn process_or(query: &mut VecDeque<String>) -> Result<Or> {
+        let mut lhs = process_and(query)?;
+        let mut rhs: Option<Box<And>> = None;
 
-                    if let Some(queue_next) = query.front() {
-                        if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
-                            // Must merge LHS and RHS
-                            lhs = And {
-                                lhs: Prefix {
-                                    or: Some(Box::new(Or { lhs, rhs })),
-                                    regex_prefix: None,
-                                    compare_prefix: None,
-                                },
-                                rhs: None,
-                            };
-                            rhs = None;
-                        }
-                    } else {
-                        break;
-                    }
-                } else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
-                    return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
-                } else {
-                    break;
-                }
-            }
+        while let Some(queue_top) = query.front() {
+            // debug!("OR QT: {:?}", queue_top);
+            if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
+                query.pop_front();
+                rhs = Some(Box::new(process_and(query)?));
 
-            Ok(Or { lhs, rhs })
-        }
-
-        fn process_and(query: &mut VecDeque<String>) -> Result<And> {
-            let mut lhs = process_prefix(query, false)?;
-            let mut rhs: Option<Box<Prefix>> = None;
-
-            while let Some(queue_top) = query.front() {
-                // debug!("AND QT: {:?}", queue_top);
-                if AND_LIST.contains(&queue_top.to_lowercase().as_str()) {
-                    query.pop_front();
-
-                    rhs = Some(Box::new(process_prefix(query, false)?));
-
-                    if let Some(next_queue_top) = query.front() {
-                        if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) {
-                            // Must merge LHS and RHS
-                            lhs = Prefix {
-                                or: Some(Box::new(Or {
-                                    lhs: And { lhs, rhs },
-                                    rhs: None,
-                                })),
-                                regex_prefix: None,
-                                compare_prefix: None,
-                            };
-                            rhs = None;
-                        } else {
-                            break;
-                        }
-                    } else {
-                        break;
-                    }
-                } else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
-                    return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
-                } else {
-                    break;
-                }
-            }
-
-            Ok(And { lhs, rhs })
-        }
-
-        fn process_prefix(query: &mut VecDeque<String>, inside_quotation: bool) -> Result<Prefix> {
-            if let Some(queue_top) = query.pop_front() {
-                if inside_quotation {
-                    if queue_top == "\"" {
-                        // This means we hit something like "".  Return an empty prefix, and to deal with
-                        // the close quote checker, add one to the top of the stack.  Ugly fix but whatever.
-                        query.push_front("\"".to_string());
-                        return Ok(Prefix {
-                            or: None,
-                            regex_prefix: Some((
-                                PrefixType::Name,
-                                StringQuery::Value(String::default()),
-                            )),
-                            compare_prefix: None,
-                        });
-                    } else {
-                        let mut quoted_string = queue_top;
-                        while let Some(next_str) = query.front() {
-                            if next_str == "\"" {
-                                // Stop!
-                                break;
-                            } else {
-                                quoted_string.push_str(next_str);
-                                query.pop_front();
-                            }
-                        }
-                        return Ok(Prefix {
-                            or: None,
-                            regex_prefix: Some((
-                                PrefixType::Name,
-                                StringQuery::Value(quoted_string),
-                            )),
-                            compare_prefix: None,
-                        });
-                    }
-                } else if queue_top == "(" {
-                    if query.is_empty() {
-                        return Err(QueryError(Cow::Borrowed("Missing closing parentheses")));
-                    }
-
-                    let mut list_of_ors = VecDeque::new();
-
-                    while let Some(in_paren_query_top) = query.front() {
-                        if in_paren_query_top != ")" {
-                            list_of_ors.push_back(process_or(query)?);
-                        } else {
-                            break;
-                        }
-                    }
-
-                    // Ensure not empty
-                    if list_of_ors.is_empty() {
-                        return Err(QueryError("No values within parentheses group".into()));
-                    }
-
-                    // Now convert this back to a OR...
-                    let initial_or = Or {
-                        lhs: And {
+                if let Some(queue_next) = query.front() {
+                    if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
+                        // Must merge LHS and RHS
+                        lhs = And {
                             lhs: Prefix {
-                                or: list_of_ors.pop_front().map(Box::new),
-                                compare_prefix: None,
+                                or: Some(Box::new(Or { lhs, rhs })),
                                 regex_prefix: None,
+                                compare_prefix: None,
                             },
                             rhs: None,
-                        },
-                        rhs: None,
-                    };
-                    let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {
-                        lhs: And {
-                            lhs: Prefix {
-                                or: Some(Box::new(lhs)),
-                                compare_prefix: None,
-                                regex_prefix: None,
-                            },
-                            rhs: Some(Box::new(Prefix {
-                                or: Some(Box::new(rhs)),
-                                compare_prefix: None,
-                                regex_prefix: None,
-                            })),
-                        },
-                        rhs: None,
-                    });
+                        };
+                        rhs = None;
+                    }
+                } else {
+                    break;
+                }
+            } else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
+                return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
+            } else {
+                break;
+            }
+        }
 
-                    if let Some(close_paren) = query.pop_front() {
-                        if close_paren == ")" {
-                            return Ok(Prefix {
-                                or: Some(Box::new(returned_or)),
-                                regex_prefix: None,
-                                compare_prefix: None,
-                            });
+        Ok(Or { lhs, rhs })
+    }
+
+    fn process_and(query: &mut VecDeque<String>) -> Result<And> {
+        let mut lhs = process_prefix(query, false)?;
+        let mut rhs: Option<Box<Prefix>> = None;
+
+        while let Some(queue_top) = query.front() {
+            // debug!("AND QT: {:?}", queue_top);
+            if AND_LIST.contains(&queue_top.to_lowercase().as_str()) {
+                query.pop_front();
+
+                rhs = Some(Box::new(process_prefix(query, false)?));
+
+                if let Some(next_queue_top) = query.front() {
+                    if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) {
+                        // Must merge LHS and RHS
+                        lhs = Prefix {
+                            or: Some(Box::new(Or {
+                                lhs: And { lhs, rhs },
+                                rhs: None,
+                            })),
+                            regex_prefix: None,
+                            compare_prefix: None,
+                        };
+                        rhs = None;
+                    } else {
+                        break;
+                    }
+                } else {
+                    break;
+                }
+            } else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
+                return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
+            } else {
+                break;
+            }
+        }
+
+        Ok(And { lhs, rhs })
+    }
+
+    fn process_prefix(query: &mut VecDeque<String>, inside_quotation: bool) -> Result<Prefix> {
+        if let Some(queue_top) = query.pop_front() {
+            if inside_quotation {
+                if queue_top == "\"" {
+                    // This means we hit something like "".  Return an empty prefix, and to deal with
+                    // the close quote checker, add one to the top of the stack.  Ugly fix but whatever.
+                    query.push_front("\"".to_string());
+                    return Ok(Prefix {
+                        or: None,
+                        regex_prefix: Some((
+                            PrefixType::Name,
+                            StringQuery::Value(String::default()),
+                        )),
+                        compare_prefix: None,
+                    });
+                } else {
+                    let mut quoted_string = queue_top;
+                    while let Some(next_str) = query.front() {
+                        if next_str == "\"" {
+                            // Stop!
+                            break;
                         } else {
-                            return Err(QueryError("Missing closing parentheses".into()));
+                            quoted_string.push_str(next_str);
+                            query.pop_front();
                         }
+                    }
+                    return Ok(Prefix {
+                        or: None,
+                        regex_prefix: Some((PrefixType::Name, StringQuery::Value(quoted_string))),
+                        compare_prefix: None,
+                    });
+                }
+            } else if queue_top == "(" {
+                if query.is_empty() {
+                    return Err(QueryError(Cow::Borrowed("Missing closing parentheses")));
+                }
+
+                let mut list_of_ors = VecDeque::new();
+
+                while let Some(in_paren_query_top) = query.front() {
+                    if in_paren_query_top != ")" {
+                        list_of_ors.push_back(process_or(query)?);
+                    } else {
+                        break;
+                    }
+                }
+
+                // Ensure not empty
+                if list_of_ors.is_empty() {
+                    return Err(QueryError("No values within parentheses group".into()));
+                }
+
+                // Now convert this back to a OR...
+                let initial_or = Or {
+                    lhs: And {
+                        lhs: Prefix {
+                            or: list_of_ors.pop_front().map(Box::new),
+                            compare_prefix: None,
+                            regex_prefix: None,
+                        },
+                        rhs: None,
+                    },
+                    rhs: None,
+                };
+                let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {
+                    lhs: And {
+                        lhs: Prefix {
+                            or: Some(Box::new(lhs)),
+                            compare_prefix: None,
+                            regex_prefix: None,
+                        },
+                        rhs: Some(Box::new(Prefix {
+                            or: Some(Box::new(rhs)),
+                            compare_prefix: None,
+                            regex_prefix: None,
+                        })),
+                    },
+                    rhs: None,
+                });
+
+                if let Some(close_paren) = query.pop_front() {
+                    if close_paren == ")" {
+                        return Ok(Prefix {
+                            or: Some(Box::new(returned_or)),
+                            regex_prefix: None,
+                            compare_prefix: None,
+                        });
                     } else {
                         return Err(QueryError("Missing closing parentheses".into()));
                     }
-                } else if queue_top == ")" {
-                    return Err(QueryError("Missing opening parentheses".into()));
-                } else if queue_top == "\"" {
-                    // Similar to parentheses, trap and check for missing closing quotes.  Note, however, that we
-                    // will DIRECTLY call another process_prefix call...
+                } else {
+                    return Err(QueryError("Missing closing parentheses".into()));
+                }
+            } else if queue_top == ")" {
+                return Err(QueryError("Missing opening parentheses".into()));
+            } else if queue_top == "\"" {
+                // Similar to parentheses, trap and check for missing closing quotes.  Note, however, that we
+                // will DIRECTLY call another process_prefix call...
 
-                    let prefix = process_prefix(query, true)?;
-                    if let Some(close_paren) = query.pop_front() {
-                        if close_paren == "\"" {
-                            return Ok(prefix);
-                        } else {
-                            return Err(QueryError("Missing closing quotation".into()));
-                        }
+                let prefix = process_prefix(query, true)?;
+                if let Some(close_paren) = query.pop_front() {
+                    if close_paren == "\"" {
+                        return Ok(prefix);
                     } else {
                         return Err(QueryError("Missing closing quotation".into()));
                     }
                 } else {
-                    //  Get prefix type...
-                    let prefix_type = queue_top.parse::<PrefixType>()?;
-                    let content = if let PrefixType::Name = prefix_type {
-                        Some(queue_top)
-                    } else {
-                        query.pop_front()
-                    };
+                    return Err(QueryError("Missing closing quotation".into()));
+                }
+            } else {
+                //  Get prefix type...
+                let prefix_type = queue_top.parse::<PrefixType>()?;
+                let content = if let PrefixType::Name = prefix_type {
+                    Some(queue_top)
+                } else {
+                    query.pop_front()
+                };
 
-                    if let Some(content) = content {
-                        match &prefix_type {
-                            PrefixType::Name => {
-                                return Ok(Prefix {
-                                    or: None,
-                                    regex_prefix: Some((prefix_type, StringQuery::Value(content))),
-                                    compare_prefix: None,
-                                })
-                            }
-                            PrefixType::Pid | PrefixType::State | PrefixType::User => {
-                                // We have to check if someone put an "="...
-                                if content == "=" {
-                                    // Check next string if possible
-                                    if let Some(queue_next) = query.pop_front() {
-                                        // TODO: Need to consider the following cases:
-                                        // - (test)
-                                        // - (test
-                                        // - test)
-                                        // These are split into 2 to 3 different strings due to parentheses being
-                                        // delimiters in our query system.
-                                        //
-                                        // Do we want these to be valid?  They should, as a string, right?
+                if let Some(content) = content {
+                    match &prefix_type {
+                        PrefixType::Name => {
+                            return Ok(Prefix {
+                                or: None,
+                                regex_prefix: Some((prefix_type, StringQuery::Value(content))),
+                                compare_prefix: None,
+                            })
+                        }
+                        PrefixType::Pid | PrefixType::State | PrefixType::User => {
+                            // We have to check if someone put an "="...
+                            if content == "=" {
+                                // Check next string if possible
+                                if let Some(queue_next) = query.pop_front() {
+                                    // TODO: [Query, ???] Need to consider the following cases:
+                                    // - (test)
+                                    // - (test
+                                    // - test)
+                                    // These are split into 2 to 3 different strings due to parentheses being
+                                    // delimiters in our query system.
+                                    //
+                                    // Do we want these to be valid?  They should, as a string, right?
 
-                                        return Ok(Prefix {
-                                            or: None,
-                                            regex_prefix: Some((
-                                                prefix_type,
-                                                StringQuery::Value(queue_next),
-                                            )),
-                                            compare_prefix: None,
-                                        });
-                                    }
-                                } else {
                                     return Ok(Prefix {
                                         or: None,
                                         regex_prefix: Some((
                                             prefix_type,
-                                            StringQuery::Value(content),
+                                            StringQuery::Value(queue_next),
                                         )),
                                         compare_prefix: None,
                                     });
                                 }
+                            } else {
+                                return Ok(Prefix {
+                                    or: None,
+                                    regex_prefix: Some((prefix_type, StringQuery::Value(content))),
+                                    compare_prefix: None,
+                                });
                             }
-                            _ => {
-                                // Now we gotta parse the content... yay.
+                        }
+                        _ => {
+                            // Now we gotta parse the content... yay.
 
-                                let mut condition: Option<QueryComparison> = None;
-                                let mut value: Option<f64> = None;
+                            let mut condition: Option<QueryComparison> = None;
+                            let mut value: Option<f64> = None;
 
-                                if content == "=" {
-                                    condition = Some(QueryComparison::Equal);
-                                    if let Some(queue_next) = query.pop_front() {
-                                        value = queue_next.parse::<f64>().ok();
-                                    } else {
-                                        return Err(QueryError("Missing value".into()));
-                                    }
-                                } else if content == ">" || content == "<" {
-                                    // We also have to check if the next string is an "="...
-                                    if let Some(queue_next) = query.pop_front() {
-                                        if queue_next == "=" {
-                                            condition = Some(if content == ">" {
-                                                QueryComparison::GreaterOrEqual
-                                            } else {
-                                                QueryComparison::LessOrEqual
-                                            });
-                                            if let Some(queue_next_next) = query.pop_front() {
-                                                value = queue_next_next.parse::<f64>().ok();
-                                            } else {
-                                                return Err(QueryError("Missing value".into()));
-                                            }
+                            if content == "=" {
+                                condition = Some(QueryComparison::Equal);
+                                if let Some(queue_next) = query.pop_front() {
+                                    value = queue_next.parse::<f64>().ok();
+                                } else {
+                                    return Err(QueryError("Missing value".into()));
+                                }
+                            } else if content == ">" || content == "<" {
+                                // We also have to check if the next string is an "="...
+                                if let Some(queue_next) = query.pop_front() {
+                                    if queue_next == "=" {
+                                        condition = Some(if content == ">" {
+                                            QueryComparison::GreaterOrEqual
                                         } else {
-                                            condition = Some(if content == ">" {
-                                                QueryComparison::Greater
-                                            } else {
-                                                QueryComparison::Less
-                                            });
-                                            value = queue_next.parse::<f64>().ok();
+                                            QueryComparison::LessOrEqual
+                                        });
+                                        if let Some(queue_next_next) = query.pop_front() {
+                                            value = queue_next_next.parse::<f64>().ok();
+                                        } else {
+                                            return Err(QueryError("Missing value".into()));
                                         }
                                     } else {
-                                        return Err(QueryError("Missing value".into()));
+                                        condition = Some(if content == ">" {
+                                            QueryComparison::Greater
+                                        } else {
+                                            QueryComparison::Less
+                                        });
+                                        value = queue_next.parse::<f64>().ok();
                                     }
+                                } else {
+                                    return Err(QueryError("Missing value".into()));
                                 }
+                            }
 
-                                if let Some(condition) = condition {
-                                    if let Some(read_value) = value {
-                                        // Now we want to check one last thing - is there a unit?
-                                        // If no unit, assume base.
-                                        // Furthermore, base must be PEEKED at initially, and will
-                                        // require (likely) prefix_type specific checks
-                                        // Lastly, if it *is* a unit, remember to POP!
+                            if let Some(condition) = condition {
+                                if let Some(read_value) = value {
+                                    // Now we want to check one last thing - is there a unit?
+                                    // If no unit, assume base.
+                                    // Furthermore, base must be PEEKED at initially, and will
+                                    // require (likely) prefix_type specific checks
+                                    // Lastly, if it *is* a unit, remember to POP!
 
-                                        let mut value = read_value;
+                                    let mut value = read_value;
 
-                                        match prefix_type {
-                                            PrefixType::MemBytes
-                                            | PrefixType::Rps
-                                            | PrefixType::Wps
-                                            | PrefixType::TRead
-                                            | PrefixType::TWrite => {
-                                                if let Some(potential_unit) = query.front() {
-                                                    match potential_unit.to_lowercase().as_str() {
-                                                        "tb" => {
-                                                            value *= 1_000_000_000_000.0;
-                                                            query.pop_front();
-                                                        }
-                                                        "tib" => {
-                                                            value *= 1_099_511_627_776.0;
-                                                            query.pop_front();
-                                                        }
-                                                        "gb" => {
-                                                            value *= 1_000_000_000.0;
-                                                            query.pop_front();
-                                                        }
-                                                        "gib" => {
-                                                            value *= 1_073_741_824.0;
-                                                            query.pop_front();
-                                                        }
-                                                        "mb" => {
-                                                            value *= 1_000_000.0;
-                                                            query.pop_front();
-                                                        }
-                                                        "mib" => {
-                                                            value *= 1_048_576.0;
-                                                            query.pop_front();
-                                                        }
-                                                        "kb" => {
-                                                            value *= 1000.0;
-                                                            query.pop_front();
-                                                        }
-                                                        "kib" => {
-                                                            value *= 1024.0;
-                                                            query.pop_front();
-                                                        }
-                                                        "b" => {
-                                                            // Just gotta pop.
-                                                            query.pop_front();
-                                                        }
-                                                        _ => {}
+                                    match prefix_type {
+                                        PrefixType::MemBytes
+                                        | PrefixType::Rps
+                                        | PrefixType::Wps
+                                        | PrefixType::TRead
+                                        | PrefixType::TWrite => {
+                                            if let Some(potential_unit) = query.front() {
+                                                match potential_unit.to_lowercase().as_str() {
+                                                    "tb" => {
+                                                        value *= 1_000_000_000_000.0;
+                                                        query.pop_front();
                                                     }
+                                                    "tib" => {
+                                                        value *= 1_099_511_627_776.0;
+                                                        query.pop_front();
+                                                    }
+                                                    "gb" => {
+                                                        value *= 1_000_000_000.0;
+                                                        query.pop_front();
+                                                    }
+                                                    "gib" => {
+                                                        value *= 1_073_741_824.0;
+                                                        query.pop_front();
+                                                    }
+                                                    "mb" => {
+                                                        value *= 1_000_000.0;
+                                                        query.pop_front();
+                                                    }
+                                                    "mib" => {
+                                                        value *= 1_048_576.0;
+                                                        query.pop_front();
+                                                    }
+                                                    "kb" => {
+                                                        value *= 1000.0;
+                                                        query.pop_front();
+                                                    }
+                                                    "kib" => {
+                                                        value *= 1024.0;
+                                                        query.pop_front();
+                                                    }
+                                                    "b" => {
+                                                        // Just gotta pop.
+                                                        query.pop_front();
+                                                    }
+                                                    _ => {}
                                                 }
                                             }
-                                            _ => {}
                                         }
-
-                                        return Ok(Prefix {
-                                            or: None,
-                                            regex_prefix: None,
-                                            compare_prefix: Some((
-                                                prefix_type,
-                                                NumericalQuery { condition, value },
-                                            )),
-                                        });
+                                        _ => {}
                                     }
+
+                                    return Ok(Prefix {
+                                        or: None,
+                                        regex_prefix: None,
+                                        compare_prefix: Some((
+                                            prefix_type,
+                                            NumericalQuery { condition, value },
+                                        )),
+                                    });
                                 }
                             }
                         }
-                    } else {
-                        return Err(QueryError("Missing argument for search prefix".into()));
                     }
+                } else {
+                    return Err(QueryError("Missing argument for search prefix".into()));
                 }
-            } else if inside_quotation {
-                // Uh oh, it's empty with quotes!
-                return Err(QueryError("Missing closing quotation".into()));
             }
-
-            Err(QueryError("Invalid query".into()))
+        } else if inside_quotation {
+            // Uh oh, it's empty with quotes!
+            return Err(QueryError("Missing closing quotation".into()));
         }
 
-        let mut split_query = VecDeque::new();
-
-        self.get_current_search_query()
-            .split_whitespace()
-            .for_each(|s| {
-                // From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses
-                let mut last = 0;
-                for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
-                    if last != index {
-                        split_query.push_back(s[last..index].to_owned());
-                    }
-                    split_query.push_back(matched.to_owned());
-                    last = index + matched.len();
-                }
-                if last < s.len() {
-                    split_query.push_back(s[last..].to_owned());
-                }
-            });
-
-        let mut process_filter = process_string_to_filter(&mut split_query)?;
-        process_filter.process_regexes(
-            self.process_search_state.is_searching_whole_word,
-            self.process_search_state.is_ignoring_case,
-            self.process_search_state.is_searching_with_regex,
-        )?;
-
-        Ok(process_filter)
+        Err(QueryError("Invalid query".into()))
     }
+
+    let mut split_query = VecDeque::new();
+
+    search_query.split_whitespace().for_each(|s| {
+        // From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses
+        let mut last = 0;
+        for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
+            if last != index {
+                split_query.push_back(s[last..index].to_owned());
+            }
+            split_query.push_back(matched.to_owned());
+            last = index + matched.len();
+        }
+        if last < s.len() {
+            split_query.push_back(s[last..].to_owned());
+        }
+    });
+
+    let mut process_filter = process_string_to_filter(&mut split_query)?;
+    process_filter.process_regexes(
+        is_searching_whole_word,
+        is_ignoring_case,
+        is_searching_with_regex,
+    )?;
+
+    Ok(process_filter)
 }
 
 pub struct Query {
@@ -467,7 +452,7 @@ impl Query {
         Ok(())
     }
 
-    pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool {
+    pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
         self.query
             .iter()
             .all(|ok| ok.check(process, is_using_command))
@@ -507,7 +492,7 @@ impl Or {
         Ok(())
     }
 
-    pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool {
+    pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
         if let Some(rhs) = &self.rhs {
             self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command)
         } else {
@@ -552,7 +537,7 @@ impl And {
         Ok(())
     }
 
-    pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool {
+    pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
         if let Some(rhs) = &self.rhs {
             self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command)
         } else {
@@ -662,7 +647,7 @@ impl Prefix {
         Ok(())
     }
 
-    pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool {
+    pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
         fn matches_condition(condition: &QueryComparison, lhs: f64, rhs: f64) -> bool {
             match condition {
                 QueryComparison::Equal => (lhs - rhs).abs() < std::f64::EPSILON,
@@ -684,11 +669,14 @@ impl Prefix {
                         process.name.as_str()
                     }),
                     PrefixType::Pid => r.is_match(process.pid.to_string().as_str()),
-                    PrefixType::State => r.is_match(process.process_state.as_str()),
+                    PrefixType::State => r.is_match(process.process_state.0.as_str()),
                     PrefixType::User => {
-                        if let Some(user) = &process.user {
-                            r.is_match(user.as_str())
-                        } else {
+                        #[cfg(target_family = "unix")]
+                        {
+                            r.is_match(process.user.as_ref())
+                        }
+                        #[cfg(not(target_family = "unix"))]
+                        {
                             false
                         }
                     }
@@ -701,12 +689,12 @@ impl Prefix {
             match prefix_type {
                 PrefixType::PCpu => matches_condition(
                     &numerical_query.condition,
-                    process.cpu_percent_usage,
+                    process.cpu_usage_percent,
                     numerical_query.value,
                 ),
                 PrefixType::PMem => matches_condition(
                     &numerical_query.condition,
-                    process.mem_percent_usage,
+                    process.mem_usage_percent,
                     numerical_query.value,
                 ),
                 PrefixType::MemBytes => matches_condition(
@@ -716,22 +704,22 @@ impl Prefix {
                 ),
                 PrefixType::Rps => matches_condition(
                     &numerical_query.condition,
-                    process.rps_f64,
+                    process.read_bytes_per_sec as f64,
                     numerical_query.value,
                 ),
                 PrefixType::Wps => matches_condition(
                     &numerical_query.condition,
-                    process.wps_f64,
+                    process.write_bytes_per_sec as f64,
                     numerical_query.value,
                 ),
                 PrefixType::TRead => matches_condition(
                     &numerical_query.condition,
-                    process.tr_f64,
+                    process.total_read_bytes as f64,
                     numerical_query.value,
                 ),
                 PrefixType::TWrite => matches_condition(
                     &numerical_query.condition,
-                    process.tw_f64,
+                    process.total_write_bytes as f64,
                     numerical_query.value,
                 ),
                 _ => true,
diff --git a/src/app/states.rs b/src/app/states.rs
index 8cbb8ae1..3dff4644 100644
--- a/src/app/states.rs
+++ b/src/app/states.rs
@@ -1,15 +1,16 @@
-use std::{collections::HashMap, convert::TryInto, time::Instant};
+use std::{collections::HashMap, time::Instant};
 
 use unicode_segmentation::GraphemeCursor;
 
-use tui::widgets::TableState;
-
 use crate::{
     app::{layout_manager::BottomWidgetType, query::*},
     constants,
-    data_harvester::processes::{self, ProcessSorting},
 };
-use ProcessSorting::*;
+
+pub mod table_state;
+pub use table_state::*;
+
+use super::widgets::ProcWidget;
 
 #[derive(Debug)]
 pub enum ScrollDirection {
@@ -31,41 +32,11 @@ pub enum CursorDirection {
     Right,
 }
 
-/// AppScrollWidgetState deals with fields for a scrollable app's current state.
+/// Meant for canvas operations involving table column widths.
 #[derive(Default)]
-pub struct AppScrollWidgetState {
-    pub current_scroll_position: usize,
-    pub scroll_bar: usize,
-    pub scroll_direction: ScrollDirection,
-    pub table_state: TableState,
-}
-
-impl AppScrollWidgetState {
-    /// Updates the position if possible, and if there is a valid change, returns the new position.
-    pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option<usize> {
-        if change == 0 {
-            return None;
-        }
-
-        let csp: Result<i64, _> = self.current_scroll_position.try_into();
-        if let Ok(csp) = csp {
-            let proposed: Result<usize, _> = (csp + change).try_into();
-            if let Ok(proposed) = proposed {
-                if proposed < num_entries {
-                    self.current_scroll_position = proposed;
-                    if change < 0 {
-                        self.scroll_direction = ScrollDirection::Up;
-                    } else {
-                        self.scroll_direction = ScrollDirection::Down;
-                    }
-
-                    return Some(self.current_scroll_position);
-                }
-            }
-        }
-
-        None
-    }
+pub struct CanvasTableWidthState {
+    pub desired_column_widths: Vec<u16>,
+    pub calculated_column_widths: Vec<u16>,
 }
 
 #[derive(PartialEq)]
@@ -159,561 +130,20 @@ impl AppSearchState {
     }
 }
 
-/// Meant for canvas operations involving table column widths.
-#[derive(Default)]
-pub struct CanvasTableWidthState {
-    pub desired_column_widths: Vec<u16>,
-    pub calculated_column_widths: Vec<u16>,
-}
-
-/// ProcessSearchState only deals with process' search's current settings and state.
-pub struct ProcessSearchState {
-    pub search_state: AppSearchState,
-    pub is_ignoring_case: bool,
-    pub is_searching_whole_word: bool,
-    pub is_searching_with_regex: bool,
-}
-
-impl Default for ProcessSearchState {
-    fn default() -> Self {
-        ProcessSearchState {
-            search_state: AppSearchState::default(),
-            is_ignoring_case: true,
-            is_searching_whole_word: false,
-            is_searching_with_regex: false,
-        }
-    }
-}
-
-impl ProcessSearchState {
-    pub fn search_toggle_ignore_case(&mut self) {
-        self.is_ignoring_case = !self.is_ignoring_case;
-    }
-
-    pub fn search_toggle_whole_word(&mut self) {
-        self.is_searching_whole_word = !self.is_searching_whole_word;
-    }
-
-    pub fn search_toggle_regex(&mut self) {
-        self.is_searching_with_regex = !self.is_searching_with_regex;
-    }
-}
-
-pub struct ColumnInfo {
-    pub enabled: bool,
-    pub shortcut: Option<&'static str>,
-    // FIXME: Move column width logic here!
-    // pub hard_width: Option<u16>,
-    // pub max_soft_width: Option<f64>,
-}
-
-pub struct ProcColumn {
-    pub ordered_columns: Vec<ProcessSorting>,
-    /// The y location of headers.  Since they're all aligned, it's just one value.
-    pub column_header_y_loc: Option<u16>,
-    /// The x start and end bounds for each header.
-    pub column_header_x_locs: Option<Vec<(u16, u16)>>,
-    pub column_mapping: HashMap<ProcessSorting, ColumnInfo>,
-    pub longest_header_len: u16,
-    pub column_state: TableState,
-    pub scroll_direction: ScrollDirection,
-    pub current_scroll_position: usize,
-    pub previous_scroll_position: usize,
-    pub backup_prev_scroll_position: usize,
-}
-
-impl Default for ProcColumn {
-    fn default() -> Self {
-        let ordered_columns = vec![
-            Count,
-            Pid,
-            ProcessName,
-            Command,
-            CpuPercent,
-            Mem,
-            MemPercent,
-            ReadPerSecond,
-            WritePerSecond,
-            TotalRead,
-            TotalWrite,
-            User,
-            State,
-        ];
-
-        let mut column_mapping = HashMap::new();
-        let mut longest_header_len = 0;
-        for column in ordered_columns.clone() {
-            longest_header_len = std::cmp::max(longest_header_len, column.to_string().len());
-            match column {
-                CpuPercent => {
-                    column_mapping.insert(
-                        column,
-                        ColumnInfo {
-                            enabled: true,
-                            shortcut: Some("c"),
-                            // hard_width: None,
-                            // max_soft_width: None,
-                        },
-                    );
-                }
-                MemPercent => {
-                    column_mapping.insert(
-                        column,
-                        ColumnInfo {
-                            enabled: true,
-                            shortcut: Some("m"),
-                            // hard_width: None,
-                            // max_soft_width: None,
-                        },
-                    );
-                }
-                Mem => {
-                    column_mapping.insert(
-                        column,
-                        ColumnInfo {
-                            enabled: false,
-                            shortcut: Some("m"),
-                            // hard_width: None,
-                            // max_soft_width: None,
-                        },
-                    );
-                }
-                ProcessName => {
-                    column_mapping.insert(
-                        column,
-                        ColumnInfo {
-                            enabled: true,
-                            shortcut: Some("n"),
-                            // hard_width: None,
-                            // max_soft_width: None,
-                        },
-                    );
-                }
-                Command => {
-                    column_mapping.insert(
-                        column,
-                        ColumnInfo {
-                            enabled: false,
-                            shortcut: Some("n"),
-                            // hard_width: None,
-                            // max_soft_width: None,
-                        },
-                    );
-                }
-                Pid => {
-                    column_mapping.insert(
-                        column,
-                        ColumnInfo {
-                            enabled: true,
-                            shortcut: Some("p"),
-                            // hard_width: None,
-                            // max_soft_width: None,
-                        },
-                    );
-                }
-                Count => {
-                    column_mapping.insert(
-                        column,
-                        ColumnInfo {
-                            enabled: false,
-                            shortcut: None,
-                            // hard_width: None,
-                            // max_soft_width: None,
-                        },
-                    );
-                }
-                User => {
-                    column_mapping.insert(
-                        column,
-                        ColumnInfo {
-                            enabled: cfg!(target_family = "unix"),
-                            shortcut: None,
-                        },
-                    );
-                }
-                _ => {
-                    column_mapping.insert(
-                        column,
-                        ColumnInfo {
-                            enabled: true,
-                            shortcut: None,
-                            // hard_width: None,
-                            // max_soft_width: None,
-                        },
-                    );
-                }
-            }
-        }
-        let longest_header_len = longest_header_len as u16;
-
-        ProcColumn {
-            ordered_columns,
-            column_mapping,
-            longest_header_len,
-            column_state: TableState::default(),
-            scroll_direction: ScrollDirection::default(),
-            current_scroll_position: 0,
-            previous_scroll_position: 0,
-            backup_prev_scroll_position: 0,
-            column_header_y_loc: None,
-            column_header_x_locs: None,
-        }
-    }
-}
-
-impl ProcColumn {
-    /// Returns its new status.
-    pub fn toggle(&mut self, column: &ProcessSorting) -> Option<bool> {
-        if let Some(mapping) = self.column_mapping.get_mut(column) {
-            mapping.enabled = !(mapping.enabled);
-            Some(mapping.enabled)
-        } else {
-            None
-        }
-    }
-
-    pub fn try_set(&mut self, column: &ProcessSorting, setting: bool) -> Option<bool> {
-        if let Some(mapping) = self.column_mapping.get_mut(column) {
-            mapping.enabled = setting;
-            Some(mapping.enabled)
-        } else {
-            None
-        }
-    }
-
-    pub fn try_enable(&mut self, column: &ProcessSorting) -> Option<bool> {
-        if let Some(mapping) = self.column_mapping.get_mut(column) {
-            mapping.enabled = true;
-            Some(mapping.enabled)
-        } else {
-            None
-        }
-    }
-
-    pub fn try_disable(&mut self, column: &ProcessSorting) -> Option<bool> {
-        if let Some(mapping) = self.column_mapping.get_mut(column) {
-            mapping.enabled = false;
-            Some(mapping.enabled)
-        } else {
-            None
-        }
-    }
-
-    pub fn is_enabled(&self, column: &ProcessSorting) -> bool {
-        if let Some(mapping) = self.column_mapping.get(column) {
-            mapping.enabled
-        } else {
-            false
-        }
-    }
-
-    pub fn get_enabled_columns_len(&self) -> usize {
-        self.ordered_columns
-            .iter()
-            .filter_map(|column_type| {
-                if let Some(col_map) = self.column_mapping.get(column_type) {
-                    if col_map.enabled {
-                        Some(1)
-                    } else {
-                        None
-                    }
-                } else {
-                    None
-                }
-            })
-            .sum()
-    }
-
-    /// NOTE: ALWAYS call this when opening the sorted window.
-    pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) {
-        // TODO [Custom Columns]: If we add custom columns, this may be needed!  Since column indices will change, this runs the risk of OOB.  So, when you change columns, CALL THIS AND ADAPT!
-        let mut true_index = 0;
-        for column in &self.ordered_columns {
-            if *column == *proc_sorting_type {
-                break;
-            }
-            if self.column_mapping.get(column).unwrap().enabled {
-                true_index += 1;
-            }
-        }
-
-        self.current_scroll_position = true_index;
-        self.backup_prev_scroll_position = self.previous_scroll_position;
-    }
-
-    /// This function sets the scroll position based on the index.
-    pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) {
-        self.current_scroll_position = visual_index;
-        self.backup_prev_scroll_position = self.previous_scroll_position;
-    }
-
-    pub fn get_column_headers(
-        &self, proc_sorting_type: &ProcessSorting, sort_reverse: bool,
-    ) -> Vec<String> {
-        const DOWN_ARROW: char = '▼';
-        const UP_ARROW: char = '▲';
-
-        // TODO: Gonna have to figure out how to do left/right GUI notation if we add it.
-        self.ordered_columns
-            .iter()
-            .filter_map(|column_type| {
-                let mapping = self.column_mapping.get(column_type).unwrap();
-                let mut command_str = String::default();
-                if let Some(command) = mapping.shortcut {
-                    command_str = format!("({})", command);
-                }
-
-                if mapping.enabled {
-                    Some(format!(
-                        "{}{}{}",
-                        column_type,
-                        command_str,
-                        if proc_sorting_type == column_type {
-                            if sort_reverse {
-                                DOWN_ARROW
-                            } else {
-                                UP_ARROW
-                            }
-                        } else {
-                            ' '
-                        }
-                    ))
-                } else {
-                    None
-                }
-            })
-            .collect()
-    }
-}
-
-pub struct ProcWidgetState {
-    pub process_search_state: ProcessSearchState,
-    pub is_grouped: bool,
-    pub scroll_state: AppScrollWidgetState,
-    pub process_sorting_type: processes::ProcessSorting,
-    pub is_process_sort_descending: bool,
-    pub is_using_command: bool,
-    pub current_column_index: usize,
-    pub is_sort_open: bool,
-    pub columns: ProcColumn,
-    pub is_tree_mode: bool,
-    pub table_width_state: CanvasTableWidthState,
-    pub requires_redraw: bool,
-}
-
-impl ProcWidgetState {
-    pub fn init(
-        is_case_sensitive: bool, is_match_whole_word: bool, is_use_regex: bool, is_grouped: bool,
-        show_memory_as_values: bool, is_tree_mode: bool, is_using_command: bool,
-    ) -> Self {
-        let mut process_search_state = ProcessSearchState::default();
-
-        if is_case_sensitive {
-            // By default it's off
-            process_search_state.search_toggle_ignore_case();
-        }
-        if is_match_whole_word {
-            process_search_state.search_toggle_whole_word();
-        }
-        if is_use_regex {
-            process_search_state.search_toggle_regex();
-        }
-
-        let (process_sorting_type, is_process_sort_descending) = if is_tree_mode {
-            (processes::ProcessSorting::Pid, false)
-        } else {
-            (processes::ProcessSorting::CpuPercent, true)
-        };
-
-        // TODO: If we add customizable columns, this should pull from config
-        let mut columns = ProcColumn::default();
-        columns.set_to_sorted_index_from_type(&process_sorting_type);
-        if is_grouped {
-            // Normally defaults to showing by PID, toggle count on instead.
-            columns.toggle(&ProcessSorting::Count);
-            columns.toggle(&ProcessSorting::Pid);
-        }
-        if show_memory_as_values {
-            // Normally defaults to showing by percent, toggle value on instead.
-            columns.toggle(&ProcessSorting::Mem);
-            columns.toggle(&ProcessSorting::MemPercent);
-        }
-        if is_using_command {
-            columns.toggle(&ProcessSorting::ProcessName);
-            columns.toggle(&ProcessSorting::Command);
-        }
-
-        ProcWidgetState {
-            process_search_state,
-            is_grouped,
-            scroll_state: AppScrollWidgetState::default(),
-            process_sorting_type,
-            is_process_sort_descending,
-            is_using_command,
-            current_column_index: 0,
-            is_sort_open: false,
-            columns,
-            is_tree_mode,
-            table_width_state: CanvasTableWidthState::default(),
-            requires_redraw: false,
-        }
-    }
-
-    /// Updates sorting when using the column list.
-    /// ...this really should be part of the ProcColumn struct (along with the sorting fields),
-    /// but I'm too lazy.
-    ///
-    /// Sorry, future me, you're gonna have to refactor this later.  Too busy getting
-    /// the feature to work in the first place!  :)
-    pub fn update_sorting_with_columns(&mut self) {
-        let mut true_index = 0;
-        let mut enabled_index = 0;
-        let target_itx = self.columns.current_scroll_position;
-        for column in &self.columns.ordered_columns {
-            let enabled = self.columns.column_mapping.get(column).unwrap().enabled;
-            if enabled_index == target_itx && enabled {
-                break;
-            }
-            if enabled {
-                enabled_index += 1;
-            }
-            true_index += 1;
-        }
-
-        if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) {
-            if *new_sort_type == self.process_sorting_type {
-                // Just reverse the search if we're reselecting!
-                self.is_process_sort_descending = !(self.is_process_sort_descending);
-            } else {
-                self.process_sorting_type = new_sort_type.clone();
-                match self.process_sorting_type {
-                    ProcessSorting::State
-                    | ProcessSorting::Pid
-                    | ProcessSorting::ProcessName
-                    | ProcessSorting::Command => {
-                        // Also invert anything that uses alphabetical sorting by default.
-                        self.is_process_sort_descending = false;
-                    }
-                    _ => {
-                        self.is_process_sort_descending = true;
-                    }
-                }
-            }
-        }
-    }
-
-    pub fn toggle_command_and_name(&mut self, is_using_command: bool) {
-        if let Some(pn) = self
-            .columns
-            .column_mapping
-            .get_mut(&ProcessSorting::ProcessName)
-        {
-            pn.enabled = !is_using_command;
-        }
-        if let Some(c) = self
-            .columns
-            .column_mapping
-            .get_mut(&ProcessSorting::Command)
-        {
-            c.enabled = is_using_command;
-        }
-    }
-
-    pub fn get_search_cursor_position(&self) -> usize {
-        self.process_search_state
-            .search_state
-            .grapheme_cursor
-            .cur_cursor()
-    }
-
-    pub fn get_char_cursor_position(&self) -> usize {
-        self.process_search_state.search_state.char_cursor_position
-    }
-
-    pub fn is_search_enabled(&self) -> bool {
-        self.process_search_state.search_state.is_enabled
-    }
-
-    pub fn get_current_search_query(&self) -> &String {
-        &self.process_search_state.search_state.current_search_query
-    }
-
-    pub fn update_query(&mut self) {
-        if self
-            .process_search_state
-            .search_state
-            .current_search_query
-            .is_empty()
-        {
-            self.process_search_state.search_state.is_blank_search = true;
-            self.process_search_state.search_state.is_invalid_search = false;
-            self.process_search_state.search_state.error_message = None;
-        } else {
-            let parsed_query = self.parse_query();
-            // debug!("Parsed query: {:#?}", parsed_query);
-
-            if let Ok(parsed_query) = parsed_query {
-                self.process_search_state.search_state.query = Some(parsed_query);
-                self.process_search_state.search_state.is_blank_search = false;
-                self.process_search_state.search_state.is_invalid_search = false;
-                self.process_search_state.search_state.error_message = None;
-            } else if let Err(err) = parsed_query {
-                self.process_search_state.search_state.is_blank_search = false;
-                self.process_search_state.search_state.is_invalid_search = true;
-                self.process_search_state.search_state.error_message = Some(err.to_string());
-            }
-        }
-        self.scroll_state.scroll_bar = 0;
-        self.scroll_state.current_scroll_position = 0;
-    }
-
-    pub fn clear_search(&mut self) {
-        self.process_search_state.search_state.reset();
-    }
-
-    pub fn search_walk_forward(&mut self, start_position: usize) {
-        self.process_search_state
-            .search_state
-            .grapheme_cursor
-            .next_boundary(
-                &self.process_search_state.search_state.current_search_query[start_position..],
-                start_position,
-            )
-            .unwrap();
-    }
-
-    pub fn search_walk_back(&mut self, start_position: usize) {
-        self.process_search_state
-            .search_state
-            .grapheme_cursor
-            .prev_boundary(
-                &self.process_search_state.search_state.current_search_query[..start_position],
-                0,
-            )
-            .unwrap();
-    }
-}
-
 pub struct ProcState {
-    pub widget_states: HashMap<u64, ProcWidgetState>,
-    pub force_update: Option<u64>,
-    pub force_update_all: bool,
+    pub widget_states: HashMap<u64, ProcWidget>,
 }
 
 impl ProcState {
-    pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self {
-        ProcState {
-            widget_states,
-            force_update: None,
-            force_update_all: false,
-        }
+    pub fn init(widget_states: HashMap<u64, ProcWidget>) -> Self {
+        ProcState { widget_states }
     }
 
-    pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> {
+    pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidget> {
         self.widget_states.get_mut(&widget_id)
     }
 
-    pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> {
+    pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidget> {
         self.widget_states.get(&widget_id)
     }
 }
@@ -721,29 +151,13 @@ impl ProcState {
 pub struct NetWidgetState {
     pub current_display_time: u64,
     pub autohide_timer: Option<Instant>,
-    // pub draw_max_range_cache: f64,
-    // pub draw_labels_cache: Vec<String>,
-    // pub draw_time_start_cache: f64,
-    // TODO: Re-enable these when we move net details state-side!
-    // pub unit_type: DataUnitTypes,
-    // pub scale_type: AxisScaling,
 }
 
 impl NetWidgetState {
-    pub fn init(
-        current_display_time: u64,
-        autohide_timer: Option<Instant>,
-        // unit_type: DataUnitTypes,
-        // scale_type: AxisScaling,
-    ) -> Self {
+    pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
         NetWidgetState {
             current_display_time,
             autohide_timer,
-            // draw_max_range_cache: 0.0,
-            // draw_labels_cache: vec![],
-            // draw_time_start_cache: 0.0,
-            // unit_type,
-            // scale_type,
         }
     }
 }
@@ -774,20 +188,34 @@ pub struct CpuWidgetState {
     pub current_display_time: u64,
     pub is_legend_hidden: bool,
     pub autohide_timer: Option<Instant>,
-    pub scroll_state: AppScrollWidgetState,
+    pub table_state: TableComponentState,
     pub is_multi_graph_mode: bool,
-    pub table_width_state: CanvasTableWidthState,
 }
 
 impl CpuWidgetState {
     pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
+        const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
+        const WIDTHS: [WidthBounds; CPU_LEGEND_HEADER.len()] = [
+            WidthBounds::soft_from_str("CPU", Some(0.5)),
+            WidthBounds::soft_from_str("Use%", Some(0.5)),
+        ];
+
+        let table_state = TableComponentState::new(
+            CPU_LEGEND_HEADER
+                .iter()
+                .zip(WIDTHS)
+                .map(|(c, width)| {
+                    TableComponentColumn::new_custom(CellContent::new(*c, None), width)
+                })
+                .collect(),
+        );
+
         CpuWidgetState {
             current_display_time,
             is_legend_hidden: false,
             autohide_timer,
-            scroll_state: AppScrollWidgetState::default(),
+            table_state,
             is_multi_graph_mode: false,
-            table_width_state: CanvasTableWidthState::default(),
         }
     }
 }
@@ -850,15 +278,27 @@ impl MemState {
 }
 
 pub struct TempWidgetState {
-    pub scroll_state: AppScrollWidgetState,
-    pub table_width_state: CanvasTableWidthState,
+    pub table_state: TableComponentState,
 }
 
-impl TempWidgetState {
-    pub fn init() -> Self {
+impl Default for TempWidgetState {
+    fn default() -> Self {
+        const TEMP_HEADERS: [&str; 2] = ["Sensor", "Temp"];
+        const WIDTHS: [WidthBounds; TEMP_HEADERS.len()] = [
+            WidthBounds::soft_from_str(TEMP_HEADERS[0], Some(0.8)),
+            WidthBounds::soft_from_str(TEMP_HEADERS[1], None),
+        ];
+
         TempWidgetState {
-            scroll_state: AppScrollWidgetState::default(),
-            table_width_state: CanvasTableWidthState::default(),
+            table_state: TableComponentState::new(
+                TEMP_HEADERS
+                    .iter()
+                    .zip(WIDTHS)
+                    .map(|(header, width)| {
+                        TableComponentColumn::new_custom(CellContent::new(*header, None), width)
+                    })
+                    .collect(),
+            ),
         }
     }
 }
@@ -882,15 +322,32 @@ impl TempState {
 }
 
 pub struct DiskWidgetState {
-    pub scroll_state: AppScrollWidgetState,
-    pub table_width_state: CanvasTableWidthState,
+    pub table_state: TableComponentState,
 }
 
-impl DiskWidgetState {
-    pub fn init() -> Self {
+impl Default for DiskWidgetState {
+    fn default() -> Self {
+        const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"];
+        const WIDTHS: [WidthBounds; DISK_HEADERS.len()] = [
+            WidthBounds::soft_from_str(DISK_HEADERS[0], Some(0.2)),
+            WidthBounds::soft_from_str(DISK_HEADERS[1], Some(0.2)),
+            WidthBounds::Hard(4),
+            WidthBounds::Hard(6),
+            WidthBounds::Hard(6),
+            WidthBounds::Hard(7),
+            WidthBounds::Hard(7),
+        ];
+
         DiskWidgetState {
-            scroll_state: AppScrollWidgetState::default(),
-            table_width_state: CanvasTableWidthState::default(),
+            table_state: TableComponentState::new(
+                DISK_HEADERS
+                    .iter()
+                    .zip(WIDTHS)
+                    .map(|(header, width)| {
+                        TableComponentColumn::new_custom(CellContent::new(*header, None), width)
+                    })
+                    .collect(),
+            ),
         }
     }
 }
@@ -954,76 +411,3 @@ pub struct ParagraphScrollState {
     pub current_scroll_index: u16,
     pub max_scroll_index: u16,
 }
-
-#[derive(Default)]
-pub struct ConfigState {
-    pub current_category_index: usize,
-    pub category_list: Vec<ConfigCategory>,
-}
-
-#[derive(Default)]
-pub struct ConfigCategory {
-    pub category_name: &'static str,
-    pub options_list: Vec<ConfigOption>,
-}
-
-pub struct ConfigOption {
-    pub set_function: Box<dyn Fn() -> anyhow::Result<()>>,
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    #[test]
-    fn test_scroll_update_position() {
-        fn check_scroll_update(
-            scroll: &mut AppScrollWidgetState, change: i64, max: usize, ret: Option<usize>,
-            new_position: usize,
-        ) {
-            assert_eq!(scroll.update_position(change, max), ret);
-            assert_eq!(scroll.current_scroll_position, new_position);
-        }
-
-        let mut scroll = AppScrollWidgetState {
-            current_scroll_position: 5,
-            scroll_bar: 0,
-            scroll_direction: ScrollDirection::Down,
-            table_state: Default::default(),
-        };
-        let s = &mut scroll;
-
-        // Update by 0. Should not change.
-        check_scroll_update(s, 0, 15, None, 5);
-
-        // Update by 5. Should increment to index 10.
-        check_scroll_update(s, 5, 15, Some(10), 10);
-
-        // Update by 5. Should not change.
-        check_scroll_update(s, 5, 15, None, 10);
-
-        // Update by 4. Should increment to index 14 (supposed max).
-        check_scroll_update(s, 4, 15, Some(14), 14);
-
-        // Update by 1. Should do nothing.
-        check_scroll_update(s, 1, 15, None, 14);
-
-        // Update by -15. Should do nothing.
-        check_scroll_update(s, -15, 15, None, 14);
-
-        // Update by -14. Should land on position 0.
-        check_scroll_update(s, -14, 15, Some(0), 0);
-
-        // Update by -1. Should do nothing.
-        check_scroll_update(s, -15, 15, None, 0);
-
-        // Update by 0. Should do nothing.
-        check_scroll_update(s, 0, 15, None, 0);
-
-        // Update by 15. Should do nothing.
-        check_scroll_update(s, 15, 15, None, 0);
-
-        // Update by 15 but with a larger bound. Should increment to 15.
-        check_scroll_update(s, 15, 16, Some(15), 15);
-    }
-}
diff --git a/src/app/states/table_state.rs b/src/app/states/table_state.rs
new file mode 100644
index 00000000..c6ce3b46
--- /dev/null
+++ b/src/app/states/table_state.rs
@@ -0,0 +1,681 @@
+use std::{borrow::Cow, convert::TryInto, ops::Range};
+
+use itertools::Itertools;
+use tui::{layout::Rect, widgets::TableState};
+
+use super::ScrollDirection;
+
+/// A bound on the width of a column.
+#[derive(Clone, Copy, Debug)]
+pub enum WidthBounds {
+    /// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point.
+    Soft {
+        /// The minimum amount before giving up and hiding.
+        min_width: u16,
+
+        /// The desired, calculated width. Take this if possible as the base starting width.
+        desired: u16,
+
+        /// The max width, as a percentage of the total width available. If [`None`],
+        /// then it can grow as desired.
+        max_percentage: Option<f32>,
+    },
+
+    /// A width of this type is either as long as specified, or does not appear at all.
+    Hard(u16),
+
+    /// Always uses the width of the [`CellContent`].
+    CellWidth,
+}
+
+impl WidthBounds {
+    pub const fn soft_from_str(name: &'static str, max_percentage: Option<f32>) -> WidthBounds {
+        let len = name.len() as u16;
+        WidthBounds::Soft {
+            min_width: len,
+            desired: len,
+            max_percentage,
+        }
+    }
+
+    pub const fn soft_from_str_with_alt(
+        name: &'static str, alt: &'static str, max_percentage: Option<f32>,
+    ) -> WidthBounds {
+        WidthBounds::Soft {
+            min_width: alt.len() as u16,
+            desired: name.len() as u16,
+            max_percentage,
+        }
+    }
+}
+
+/// A [`CellContent`] contains text information for display in a table.
+#[derive(Clone, Debug)]
+pub enum CellContent {
+    Simple(Cow<'static, str>),
+    HasAlt {
+        alt: Cow<'static, str>,
+        main: Cow<'static, str>,
+    },
+}
+
+impl CellContent {
+    /// Creates a new [`CellContent`].
+    pub fn new<I>(name: I, alt: Option<I>) -> Self
+    where
+        I: Into<Cow<'static, str>>,
+    {
+        if let Some(alt) = alt {
+            CellContent::HasAlt {
+                alt: alt.into(),
+                main: name.into(),
+            }
+        } else {
+            CellContent::Simple(name.into())
+        }
+    }
+
+    /// Returns the length of the [`CellContent`]. Note that for a [`CellContent::HasAlt`], it will return
+    /// the length of the "main" field.
+    pub fn len(&self) -> usize {
+        match self {
+            CellContent::Simple(s) => s.len(),
+            CellContent::HasAlt { alt: _, main: long } => long.len(),
+        }
+    }
+
+    /// Whether the [`CellContent`]'s text is empty.
+    pub fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+
+    pub fn main_text(&self) -> &Cow<'static, str> {
+        match self {
+            CellContent::Simple(main) => main,
+            CellContent::HasAlt { alt: _, main } => main,
+        }
+    }
+}
+
+pub trait TableComponentHeader {
+    fn header_text(&self) -> &CellContent;
+}
+
+impl TableComponentHeader for CellContent {
+    fn header_text(&self) -> &CellContent {
+        self
+    }
+}
+impl From<Cow<'static, str>> for CellContent {
+    fn from(c: Cow<'static, str>) -> Self {
+        CellContent::Simple(c)
+    }
+}
+
+impl From<&'static str> for CellContent {
+    fn from(s: &'static str) -> Self {
+        CellContent::Simple(s.into())
+    }
+}
+
+impl From<String> for CellContent {
+    fn from(s: String) -> Self {
+        CellContent::Simple(s.into())
+    }
+}
+
+pub struct TableComponentColumn<H: TableComponentHeader> {
+    /// The header of the column.
+    pub header: H,
+
+    /// A restriction on this column's width, if desired.
+    pub width_bounds: WidthBounds,
+
+    /// The calculated width of the column.
+    pub calculated_width: u16,
+
+    /// Marks that this column is currently "hidden", and should *always* be skipped.
+    pub is_hidden: bool,
+}
+
+impl<H: TableComponentHeader> TableComponentColumn<H> {
+    pub fn new_custom(header: H, width_bounds: WidthBounds) -> Self {
+        Self {
+            header,
+            width_bounds,
+            calculated_width: 0,
+            is_hidden: false,
+        }
+    }
+
+    pub fn new(header: H) -> Self {
+        Self {
+            header,
+            width_bounds: WidthBounds::CellWidth,
+            calculated_width: 0,
+            is_hidden: false,
+        }
+    }
+
+    pub fn new_hard(header: H, width: u16) -> Self {
+        Self {
+            header,
+            width_bounds: WidthBounds::Hard(width),
+            calculated_width: 0,
+            is_hidden: false,
+        }
+    }
+
+    pub fn new_soft(header: H, max_percentage: Option<f32>) -> Self {
+        let min_width = header.header_text().len() as u16;
+        Self {
+            header,
+            width_bounds: WidthBounds::Soft {
+                min_width,
+                desired: min_width,
+                max_percentage,
+            },
+            calculated_width: 0,
+            is_hidden: false,
+        }
+    }
+
+    pub fn is_zero_width(&self) -> bool {
+        self.calculated_width == 0
+    }
+
+    pub fn is_skipped(&self) -> bool {
+        self.is_zero_width() || self.is_hidden
+    }
+}
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub enum SortOrder {
+    Ascending,
+    Descending,
+}
+
+impl SortOrder {
+    pub fn is_descending(&self) -> bool {
+        matches!(self, SortOrder::Descending)
+    }
+}
+
+/// Represents the current table's sorting state.
+#[derive(Debug)]
+pub enum SortState {
+    Unsortable,
+    Sortable(SortableState),
+}
+
+#[derive(Debug)]
+pub struct SortableState {
+    /// The "x locations" of the headers.
+    visual_mappings: Vec<Range<u16>>,
+
+    /// The "y location" of the header row. Since all headers share the same y-location we just set it once here.
+    y_loc: u16,
+
+    /// This is a bit of a lazy hack to handle this for now - ideally the entire [`SortableState`]
+    /// is instead handled by a separate table struct that also can access the columns and their default sort orderings.
+    default_sort_orderings: Vec<SortOrder>,
+
+    /// The currently selected sort index.
+    pub current_index: usize,
+
+    /// The current sorting order.
+    pub order: SortOrder,
+}
+
+impl SortableState {
+    /// Creates a new [`SortableState`].
+    pub fn new(
+        default_index: usize, default_order: SortOrder, default_sort_orderings: Vec<SortOrder>,
+    ) -> Self {
+        Self {
+            visual_mappings: Default::default(),
+            y_loc: 0,
+            default_sort_orderings,
+            current_index: default_index,
+            order: default_order,
+        }
+    }
+
+    /// Toggles the current sort order.
+    pub fn toggle_order(&mut self) {
+        self.order = match self.order {
+            SortOrder::Ascending => SortOrder::Descending,
+            SortOrder::Descending => SortOrder::Ascending,
+        }
+    }
+
+    /// Updates the visual index.
+    ///
+    /// This function will create a *sorted* range list - in debug mode,
+    /// the program will assert this, but it will not do so in release mode!
+    pub fn update_visual_index(&mut self, draw_loc: Rect, row_widths: &[u16]) {
+        let mut start = draw_loc.x;
+        let visual_index = row_widths
+            .iter()
+            .map(|width| {
+                let range_start = start;
+                let range_end = start + width + 1; // +1 for the gap b/w cols.
+                start = range_end;
+                range_start..range_end
+            })
+            .collect_vec();
+
+        debug_assert!(visual_index.iter().all(|a| { a.start <= a.end }));
+
+        debug_assert!(visual_index
+            .iter()
+            .tuple_windows()
+            .all(|(a, b)| { b.start >= a.end }));
+
+        self.visual_mappings = visual_index;
+        self.y_loc = draw_loc.y;
+    }
+
+    /// Given some `x` and `y`, if possible, select the corresponding column or toggle the column if already selected,
+    /// and otherwise do nothing.
+    ///
+    /// If there was some update, the corresponding column type will be returned. If nothing happens, [`None`] is
+    /// returned.
+    pub fn try_select_location(&mut self, x: u16, y: u16) -> Option<usize> {
+        if self.y_loc == y {
+            if let Some(index) = self.get_range(x) {
+                self.update_sort_index(index);
+                Some(self.current_index)
+            } else {
+                None
+            }
+        } else {
+            None
+        }
+    }
+
+    /// Updates the sort index, and sets the sort order as appropriate.
+    ///
+    /// If the index is different from the previous one, it will move to the new index and set the sort order
+    /// to the prescribed default sort order.
+    ///
+    /// If the index is the same as the previous one, it will simply toggle the current sort order.
+    pub fn update_sort_index(&mut self, index: usize) {
+        if self.current_index == index {
+            self.toggle_order();
+        } else {
+            self.current_index = index;
+            self.order = self.default_sort_orderings[index];
+        }
+    }
+
+    /// Given a `needle` coordinate, select the corresponding index and value.
+    fn get_range(&self, needle: u16) -> Option<usize> {
+        match self
+            .visual_mappings
+            .binary_search_by_key(&needle, |range| range.start)
+        {
+            Ok(index) => Some(index),
+            Err(index) => index.checked_sub(1),
+        }
+        .and_then(|index| {
+            if needle < self.visual_mappings[index].end {
+                Some(index)
+            } else {
+                None
+            }
+        })
+    }
+}
+
+/// [`TableComponentState`] deals with fields for a scrollable's current state.
+pub struct TableComponentState<H: TableComponentHeader = CellContent> {
+    pub current_scroll_position: usize,
+    pub scroll_bar: usize,
+    pub scroll_direction: ScrollDirection,
+    pub table_state: TableState,
+    pub columns: Vec<TableComponentColumn<H>>,
+    pub sort_state: SortState,
+}
+
+impl<H: TableComponentHeader> TableComponentState<H> {
+    pub fn new(columns: Vec<TableComponentColumn<H>>) -> Self {
+        Self {
+            current_scroll_position: 0,
+            scroll_bar: 0,
+            scroll_direction: ScrollDirection::Down,
+            table_state: Default::default(),
+            columns,
+            sort_state: SortState::Unsortable,
+        }
+    }
+
+    pub fn sort_state(mut self, sort_state: SortState) -> Self {
+        self.sort_state = sort_state;
+        self
+    }
+
+    /// Calculates widths for the columns for this table.
+    ///
+    /// * `total_width` is the, well, total width available.
+    /// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
+    ///   false.
+    ///
+    /// **NOTE:** Trailing 0's may break tui-rs, remember to filter them out later!
+    pub fn calculate_column_widths(&mut self, total_width: u16, left_to_right: bool) {
+        use itertools::Either;
+        use std::cmp::{max, min};
+
+        let mut total_width_left = total_width;
+
+        let columns = if left_to_right {
+            Either::Left(self.columns.iter_mut())
+        } else {
+            Either::Right(self.columns.iter_mut().rev())
+        };
+
+        let arrow_offset = match self.sort_state {
+            SortState::Unsortable => 0,
+            SortState::Sortable { .. } => 1,
+        };
+
+        let mut num_columns = 0;
+        let mut skip_iter = false;
+        for column in columns {
+            column.calculated_width = 0;
+
+            if column.is_hidden || skip_iter {
+                continue;
+            }
+
+            match &column.width_bounds {
+                WidthBounds::Soft {
+                    min_width,
+                    desired,
+                    max_percentage,
+                } => {
+                    let min_width = *min_width + arrow_offset;
+                    if min_width > total_width_left {
+                        skip_iter = true;
+                        continue;
+                    }
+
+                    let soft_limit = max(
+                        if let Some(max_percentage) = max_percentage {
+                            // TODO: Rust doesn't have an `into()` or `try_into()` for floats to integers.
+                            ((*max_percentage * f32::from(total_width)).ceil()) as u16
+                        } else {
+                            *desired
+                        },
+                        min_width,
+                    );
+                    let space_taken = min(min(soft_limit, *desired), total_width_left);
+
+                    if min_width > space_taken || min_width == 0 {
+                        skip_iter = true;
+                    } else if space_taken > 0 {
+                        total_width_left = total_width_left.saturating_sub(space_taken + 1);
+                        column.calculated_width = space_taken;
+                        num_columns += 1;
+                    }
+                }
+                WidthBounds::CellWidth => {
+                    let width = column.header.header_text().len() as u16;
+                    let min_width = width + arrow_offset;
+
+                    if min_width > total_width_left || min_width == 0 {
+                        skip_iter = true;
+                    } else if min_width > 0 {
+                        total_width_left = total_width_left.saturating_sub(min_width + 1);
+                        column.calculated_width = min_width;
+                        num_columns += 1;
+                    }
+                }
+                WidthBounds::Hard(width) => {
+                    let min_width = *width + arrow_offset;
+
+                    if min_width > total_width_left || min_width == 0 {
+                        skip_iter = true;
+                    } else if min_width > 0 {
+                        total_width_left = total_width_left.saturating_sub(min_width + 1);
+                        column.calculated_width = min_width;
+                        num_columns += 1;
+                    }
+                }
+            }
+        }
+
+        if num_columns > 0 {
+            // Redistribute remaining.
+            let mut num_dist = num_columns;
+            let amount_per_slot = total_width_left / num_dist;
+            total_width_left %= num_dist;
+
+            for column in self.columns.iter_mut() {
+                if num_dist == 0 {
+                    break;
+                }
+
+                if column.calculated_width > 0 {
+                    if total_width_left > 0 {
+                        column.calculated_width += amount_per_slot + 1;
+                        total_width_left -= 1;
+                    } else {
+                        column.calculated_width += amount_per_slot;
+                    }
+
+                    num_dist -= 1;
+                }
+            }
+        }
+    }
+
+    /// Updates the position if possible, and if there is a valid change, returns the new position.
+    pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option<usize> {
+        if change == 0 {
+            return None;
+        }
+
+        let csp: Result<i64, _> = self.current_scroll_position.try_into();
+        if let Ok(csp) = csp {
+            let proposed: Result<usize, _> = (csp + change).try_into();
+            if let Ok(proposed) = proposed {
+                if proposed < num_entries {
+                    self.current_scroll_position = proposed;
+                    if change < 0 {
+                        self.scroll_direction = ScrollDirection::Up;
+                    } else {
+                        self.scroll_direction = ScrollDirection::Down;
+                    }
+
+                    return Some(self.current_scroll_position);
+                }
+            }
+        }
+
+        None
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_scroll_update_position() {
+        #[track_caller]
+        fn check_scroll_update(
+            scroll: &mut TableComponentState, change: i64, max: usize, ret: Option<usize>,
+            new_position: usize,
+        ) {
+            assert_eq!(scroll.update_position(change, max), ret);
+            assert_eq!(scroll.current_scroll_position, new_position);
+        }
+
+        let mut scroll = TableComponentState {
+            current_scroll_position: 5,
+            scroll_bar: 0,
+            scroll_direction: ScrollDirection::Down,
+            table_state: Default::default(),
+            columns: vec![],
+            sort_state: SortState::Unsortable,
+        };
+        let s = &mut scroll;
+
+        // Update by 0. Should not change.
+        check_scroll_update(s, 0, 15, None, 5);
+
+        // Update by 5. Should increment to index 10.
+        check_scroll_update(s, 5, 15, Some(10), 10);
+
+        // Update by 5. Should not change.
+        check_scroll_update(s, 5, 15, None, 10);
+
+        // Update by 4. Should increment to index 14 (supposed max).
+        check_scroll_update(s, 4, 15, Some(14), 14);
+
+        // Update by 1. Should do nothing.
+        check_scroll_update(s, 1, 15, None, 14);
+
+        // Update by -15. Should do nothing.
+        check_scroll_update(s, -15, 15, None, 14);
+
+        // Update by -14. Should land on position 0.
+        check_scroll_update(s, -14, 15, Some(0), 0);
+
+        // Update by -1. Should do nothing.
+        check_scroll_update(s, -15, 15, None, 0);
+
+        // Update by 0. Should do nothing.
+        check_scroll_update(s, 0, 15, None, 0);
+
+        // Update by 15. Should do nothing.
+        check_scroll_update(s, 15, 15, None, 0);
+
+        // Update by 15 but with a larger bound. Should increment to 15.
+        check_scroll_update(s, 15, 16, Some(15), 15);
+    }
+
+    #[test]
+    fn test_table_width_calculation() {
+        #[track_caller]
+        fn test_calculation(state: &mut TableComponentState, width: u16, expected: Vec<u16>) {
+            state.calculate_column_widths(width, true);
+            assert_eq!(
+                state
+                    .columns
+                    .iter()
+                    .filter_map(|c| if c.calculated_width == 0 {
+                        None
+                    } else {
+                        Some(c.calculated_width)
+                    })
+                    .collect::<Vec<_>>(),
+                expected
+            )
+        }
+
+        let mut state = TableComponentState::new(vec![
+            TableComponentColumn::new(CellContent::from("a")),
+            TableComponentColumn::new_custom(
+                "a".into(),
+                WidthBounds::Soft {
+                    min_width: 1,
+                    desired: 10,
+                    max_percentage: Some(0.125),
+                },
+            ),
+            TableComponentColumn::new_custom(
+                "a".into(),
+                WidthBounds::Soft {
+                    min_width: 2,
+                    desired: 10,
+                    max_percentage: Some(0.5),
+                },
+            ),
+        ]);
+
+        test_calculation(&mut state, 0, vec![]);
+        test_calculation(&mut state, 1, vec![1]);
+        test_calculation(&mut state, 2, vec![1]);
+        test_calculation(&mut state, 3, vec![1, 1]);
+        test_calculation(&mut state, 4, vec![1, 1]);
+        test_calculation(&mut state, 5, vec![2, 1]);
+        test_calculation(&mut state, 6, vec![1, 1, 2]);
+        test_calculation(&mut state, 7, vec![1, 1, 3]);
+        test_calculation(&mut state, 8, vec![1, 1, 4]);
+        test_calculation(&mut state, 14, vec![2, 2, 7]);
+        test_calculation(&mut state, 20, vec![2, 4, 11]);
+        test_calculation(&mut state, 100, vec![27, 35, 35]);
+
+        state.sort_state = SortState::Sortable(SortableState::new(1, SortOrder::Ascending, vec![]));
+
+        test_calculation(&mut state, 0, vec![]);
+        test_calculation(&mut state, 1, vec![]);
+        test_calculation(&mut state, 2, vec![2]);
+        test_calculation(&mut state, 3, vec![2]);
+        test_calculation(&mut state, 4, vec![3]);
+        test_calculation(&mut state, 5, vec![2, 2]);
+        test_calculation(&mut state, 6, vec![2, 2]);
+        test_calculation(&mut state, 7, vec![3, 2]);
+        test_calculation(&mut state, 8, vec![3, 3]);
+        test_calculation(&mut state, 14, vec![2, 2, 7]);
+        test_calculation(&mut state, 20, vec![3, 4, 10]);
+        test_calculation(&mut state, 100, vec![27, 35, 35]);
+    }
+
+    #[test]
+    fn test_visual_index_selection() {
+        let mut state = SortableState::new(
+            0,
+            SortOrder::Ascending,
+            vec![SortOrder::Ascending, SortOrder::Descending],
+        );
+
+        const X_OFFSET: u16 = 10;
+        const Y_OFFSET: u16 = 15;
+        state.update_visual_index(Rect::new(X_OFFSET, Y_OFFSET, 20, 15), &[4, 14]);
+
+        #[track_caller]
+        fn test_selection(
+            state: &mut SortableState, from_x_offset: u16, from_y_offset: u16,
+            result: (Option<usize>, SortOrder),
+        ) {
+            assert_eq!(
+                state.try_select_location(X_OFFSET + from_x_offset, Y_OFFSET + from_y_offset),
+                result.0
+            );
+            assert_eq!(state.order, result.1);
+        }
+
+        use SortOrder::*;
+
+        // Clicking on these don't do anything, so don't show any change.
+        test_selection(&mut state, 5, 1, (None, Ascending));
+        test_selection(&mut state, 21, 0, (None, Ascending));
+
+        // Clicking on the first column should toggle it as it is already selected.
+        test_selection(&mut state, 3, 0, (Some(0), Descending));
+
+        // Clicking on the first column should toggle it again as it is already selected.
+        test_selection(&mut state, 4, 0, (Some(0), Ascending));
+
+        // Clicking on second column should select and switch to the descending ordering as that is its default.
+        test_selection(&mut state, 5, 0, (Some(1), Descending));
+
+        // Clicking on second column should toggle it.
+        test_selection(&mut state, 19, 0, (Some(1), Ascending));
+
+        // Overshoot, should not do anything.
+        test_selection(&mut state, 20, 0, (None, Ascending));
+
+        // Further overshoot, should not do anything.
+        test_selection(&mut state, 25, 0, (None, Ascending));
+
+        // Go back to first column, should be ascending to match default for index 0.
+        test_selection(&mut state, 3, 0, (Some(0), Ascending));
+
+        // Click on first column should then go to descending as it is already selected and ascending.
+        test_selection(&mut state, 3, 0, (Some(0), Descending));
+    }
+}
diff --git a/src/app/widgets.rs b/src/app/widgets.rs
new file mode 100644
index 00000000..9d3a54ba
--- /dev/null
+++ b/src/app/widgets.rs
@@ -0,0 +1,2 @@
+pub mod process_widget;
+pub use process_widget::*;
diff --git a/src/app/widgets/process_widget.rs b/src/app/widgets/process_widget.rs
new file mode 100644
index 00000000..f49ab40a
--- /dev/null
+++ b/src/app/widgets/process_widget.rs
@@ -0,0 +1,1164 @@
+use crate::{
+    app::{
+        data_farmer::{DataCollection, ProcessData, StringPidMap},
+        data_harvester::processes::ProcessHarvest,
+        query::*,
+        AppSearchState, CellContent, ScrollDirection, SortOrder, SortState, SortableState,
+        TableComponentColumn, TableComponentHeader, TableComponentState, WidthBounds,
+    },
+    data_conversion::{binary_byte_string, dec_bytes_per_second_string, TableData, TableRow},
+    utils::gen_util::sort_partial_fn,
+    Pid,
+};
+
+use concat_string::concat_string;
+use fxhash::{FxHashMap, FxHashSet};
+use itertools::Itertools;
+use std::{
+    borrow::Cow,
+    cmp::{max, Reverse},
+};
+
+/// ProcessSearchState only deals with process' search's current settings and state.
+pub struct ProcessSearchState {
+    pub search_state: AppSearchState,
+    pub is_ignoring_case: bool,
+    pub is_searching_whole_word: bool,
+    pub is_searching_with_regex: bool,
+}
+
+impl Default for ProcessSearchState {
+    fn default() -> Self {
+        ProcessSearchState {
+            search_state: AppSearchState::default(),
+            is_ignoring_case: true,
+            is_searching_whole_word: false,
+            is_searching_with_regex: false,
+        }
+    }
+}
+
+impl ProcessSearchState {
+    pub fn search_toggle_ignore_case(&mut self) {
+        self.is_ignoring_case = !self.is_ignoring_case;
+    }
+
+    pub fn search_toggle_whole_word(&mut self) {
+        self.is_searching_whole_word = !self.is_searching_whole_word;
+    }
+
+    pub fn search_toggle_regex(&mut self) {
+        self.is_searching_with_regex = !self.is_searching_with_regex;
+    }
+}
+
+#[derive(Clone, Debug)]
+pub enum ProcWidgetMode {
+    Tree { collapsed_pids: FxHashSet<Pid> },
+    Grouped,
+    Normal,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum ProcWidgetColumn {
+    CpuPercent,
+    Memory { show_percentage: bool },
+    PidOrCount { is_count: bool },
+    ProcNameOrCommand { is_command: bool },
+    ReadPerSecond,
+    WritePerSecond,
+    TotalRead,
+    TotalWrite,
+    State,
+    User,
+}
+
+impl ProcWidgetColumn {
+    const CPU_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("CPU%"));
+    const MEM_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("Mem%"));
+    const MEM: CellContent = CellContent::Simple(Cow::Borrowed("Mem"));
+    const READS_PER_SECOND: CellContent = CellContent::Simple(Cow::Borrowed("R/s"));
+    const WRITES_PER_SECOND: CellContent = CellContent::Simple(Cow::Borrowed("W/s"));
+    const TOTAL_READ: CellContent = CellContent::Simple(Cow::Borrowed("T.Read"));
+    const TOTAL_WRITE: CellContent = CellContent::Simple(Cow::Borrowed("T.Write"));
+    const STATE: CellContent = CellContent::Simple(Cow::Borrowed("State"));
+    const PROCESS_NAME: CellContent = CellContent::Simple(Cow::Borrowed("Name"));
+    const COMMAND: CellContent = CellContent::Simple(Cow::Borrowed("Command"));
+    const PID: CellContent = CellContent::Simple(Cow::Borrowed("PID"));
+    const COUNT: CellContent = CellContent::Simple(Cow::Borrowed("Count"));
+    const USER: CellContent = CellContent::Simple(Cow::Borrowed("User"));
+
+    const SHORTCUT_CPU_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("CPU%(c)"));
+    const SHORTCUT_MEM_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("Mem%(m)"));
+    const SHORTCUT_MEM: CellContent = CellContent::Simple(Cow::Borrowed("Mem(m)"));
+    const SHORTCUT_PROCESS_NAME: CellContent = CellContent::Simple(Cow::Borrowed("Name(n)"));
+    const SHORTCUT_COMMAND: CellContent = CellContent::Simple(Cow::Borrowed("Command(n)"));
+    const SHORTCUT_PID: CellContent = CellContent::Simple(Cow::Borrowed("PID(p)"));
+
+    pub fn text(&self) -> &CellContent {
+        match self {
+            ProcWidgetColumn::CpuPercent => &Self::CPU_PERCENT,
+            ProcWidgetColumn::Memory { show_percentage } => {
+                if *show_percentage {
+                    &Self::MEM_PERCENT
+                } else {
+                    &Self::MEM
+                }
+            }
+            ProcWidgetColumn::PidOrCount { is_count } => {
+                if *is_count {
+                    &Self::COUNT
+                } else {
+                    &Self::PID
+                }
+            }
+            ProcWidgetColumn::ProcNameOrCommand { is_command } => {
+                if *is_command {
+                    &Self::COMMAND
+                } else {
+                    &Self::PROCESS_NAME
+                }
+            }
+            ProcWidgetColumn::ReadPerSecond => &Self::READS_PER_SECOND,
+            ProcWidgetColumn::WritePerSecond => &Self::WRITES_PER_SECOND,
+            ProcWidgetColumn::TotalRead => &Self::TOTAL_READ,
+            ProcWidgetColumn::TotalWrite => &Self::TOTAL_WRITE,
+            ProcWidgetColumn::State => &Self::STATE,
+            ProcWidgetColumn::User => &Self::USER,
+        }
+    }
+
+    /// Sorts the given data in-place.
+    pub fn sort(
+        &self, sort_descending: bool, data: &mut [&ProcessHarvest], is_using_command: bool,
+        cmd_pid_map: &StringPidMap, name_pid_map: &StringPidMap,
+    ) {
+        match self {
+            ProcWidgetColumn::CpuPercent => {
+                data.sort_by_cached_key(|p| p.name.to_lowercase());
+                data.sort_by(|a, b| {
+                    sort_partial_fn(sort_descending)(a.cpu_usage_percent, b.cpu_usage_percent)
+                });
+            }
+            ProcWidgetColumn::Memory { show_percentage } => {
+                data.sort_by_cached_key(|p| p.name.to_lowercase());
+                if *show_percentage {
+                    data.sort_by(|a, b| {
+                        sort_partial_fn(sort_descending)(a.mem_usage_percent, b.mem_usage_percent)
+                    });
+                } else {
+                    data.sort_by(|a, b| {
+                        sort_partial_fn(sort_descending)(a.mem_usage_bytes, b.mem_usage_bytes)
+                    });
+                }
+            }
+            ProcWidgetColumn::PidOrCount { is_count } => {
+                data.sort_by_cached_key(|c| c.name.to_lowercase());
+                if *is_count {
+                    if is_using_command {
+                        if sort_descending {
+                            data.sort_by_cached_key(|p| {
+                                Reverse(cmd_pid_map.get(&p.command).map(|v| v.len()).unwrap_or(0))
+                            })
+                        } else {
+                            data.sort_by_cached_key(|p| {
+                                cmd_pid_map.get(&p.command).map(|v| v.len()).unwrap_or(0)
+                            })
+                        }
+                    } else {
+                        #[allow(clippy::collapsible-else-if)]
+                        if sort_descending {
+                            data.sort_by_cached_key(|p| {
+                                Reverse(name_pid_map.get(&p.name).map(|v| v.len()).unwrap_or(0))
+                            })
+                        } else {
+                            data.sort_by_cached_key(|p| {
+                                name_pid_map.get(&p.name).map(|v| v.len()).unwrap_or(0)
+                            })
+                        }
+                    }
+                } else {
+                    data.sort_by(|a, b| sort_partial_fn(sort_descending)(a.pid, b.pid));
+                }
+            }
+            ProcWidgetColumn::ProcNameOrCommand { is_command } => {
+                if *is_command {
+                    if sort_descending {
+                        data.sort_by_cached_key(|p| Reverse(p.command.to_lowercase()));
+                    } else {
+                        data.sort_by_cached_key(|p| p.command.to_lowercase());
+                    }
+                } else if sort_descending {
+                    data.sort_by_cached_key(|p| Reverse(p.name.to_lowercase()));
+                } else {
+                    data.sort_by_cached_key(|p| p.name.to_lowercase());
+                }
+            }
+            ProcWidgetColumn::ReadPerSecond => {
+                data.sort_by_cached_key(|p| p.name.to_lowercase());
+                if sort_descending {
+                    data.sort_by_key(|a| Reverse(a.read_bytes_per_sec));
+                } else {
+                    data.sort_by_key(|a| a.read_bytes_per_sec);
+                }
+            }
+            ProcWidgetColumn::WritePerSecond => {
+                data.sort_by_cached_key(|p| p.name.to_lowercase());
+                if sort_descending {
+                    data.sort_by_key(|a| Reverse(a.write_bytes_per_sec));
+                } else {
+                    data.sort_by_key(|a| a.write_bytes_per_sec);
+                }
+            }
+            ProcWidgetColumn::TotalRead => {
+                data.sort_by_cached_key(|p| p.name.to_lowercase());
+                if sort_descending {
+                    data.sort_by_key(|a| Reverse(a.total_read_bytes));
+                } else {
+                    data.sort_by_key(|a| a.total_read_bytes);
+                }
+            }
+            ProcWidgetColumn::TotalWrite => {
+                data.sort_by_cached_key(|p| p.name.to_lowercase());
+                if sort_descending {
+                    data.sort_by_key(|a| Reverse(a.total_write_bytes));
+                } else {
+                    data.sort_by_key(|a| a.total_write_bytes);
+                }
+            }
+            ProcWidgetColumn::State => {
+                data.sort_by_cached_key(|p| p.name.to_lowercase());
+                if sort_descending {
+                    data.sort_by_cached_key(|p| Reverse(p.process_state.0.to_lowercase()));
+                } else {
+                    data.sort_by_cached_key(|p| p.process_state.0.to_lowercase());
+                }
+            }
+            ProcWidgetColumn::User => {
+                #[cfg(target_family = "unix")]
+                {
+                    data.sort_by_cached_key(|p| p.name.to_lowercase());
+                    if sort_descending {
+                        data.sort_by_cached_key(|p| Reverse(p.user.to_lowercase()));
+                    } else {
+                        data.sort_by_cached_key(|p| p.user.to_lowercase());
+                    }
+                }
+            }
+        }
+    }
+
+    /// Basically, anything "alphabetical" should sort in ascending order by default. This also includes something like
+    /// PID, as one would probably want PID to sort by default starting from 0 or 1.
+    fn default_sort_order(&self) -> SortOrder {
+        match self {
+            ProcWidgetColumn::PidOrCount { is_count: true }
+            | ProcWidgetColumn::CpuPercent
+            | ProcWidgetColumn::ReadPerSecond
+            | ProcWidgetColumn::WritePerSecond
+            | ProcWidgetColumn::TotalRead
+            | ProcWidgetColumn::TotalWrite
+            | ProcWidgetColumn::Memory { .. } => SortOrder::Descending,
+
+            ProcWidgetColumn::PidOrCount { is_count: false }
+            | ProcWidgetColumn::ProcNameOrCommand { .. }
+            | ProcWidgetColumn::State
+            | ProcWidgetColumn::User => SortOrder::Ascending,
+        }
+    }
+}
+
+impl TableComponentHeader for ProcWidgetColumn {
+    fn header_text(&self) -> &CellContent {
+        match self {
+            ProcWidgetColumn::CpuPercent => &Self::SHORTCUT_CPU_PERCENT,
+            ProcWidgetColumn::Memory { show_percentage } => {
+                if *show_percentage {
+                    &Self::SHORTCUT_MEM_PERCENT
+                } else {
+                    &Self::SHORTCUT_MEM
+                }
+            }
+            ProcWidgetColumn::PidOrCount { is_count } => {
+                if *is_count {
+                    &Self::COUNT
+                } else {
+                    &Self::SHORTCUT_PID
+                }
+            }
+            ProcWidgetColumn::ProcNameOrCommand { is_command } => {
+                if *is_command {
+                    &Self::SHORTCUT_COMMAND
+                } else {
+                    &Self::SHORTCUT_PROCESS_NAME
+                }
+            }
+            ProcWidgetColumn::ReadPerSecond => &Self::READS_PER_SECOND,
+            ProcWidgetColumn::WritePerSecond => &Self::WRITES_PER_SECOND,
+            ProcWidgetColumn::TotalRead => &Self::TOTAL_READ,
+            ProcWidgetColumn::TotalWrite => &Self::TOTAL_WRITE,
+            ProcWidgetColumn::State => &Self::STATE,
+            ProcWidgetColumn::User => &Self::USER,
+        }
+    }
+}
+
+pub struct ProcWidget {
+    pub mode: ProcWidgetMode,
+
+    pub proc_search: ProcessSearchState,
+    pub table_state: TableComponentState<ProcWidgetColumn>,
+    pub sort_table_state: TableComponentState,
+
+    pub is_sort_open: bool,
+    pub force_rerender: bool,
+    pub force_update_data: bool,
+
+    pub table_data: TableData,
+}
+
+impl ProcWidget {
+    pub const PID_OR_COUNT: usize = 0;
+    pub const PROC_NAME_OR_CMD: usize = 1;
+    pub const CPU: usize = 2;
+    pub const MEM: usize = 3;
+    pub const RPS: usize = 4;
+    pub const WPS: usize = 5;
+    pub const T_READ: usize = 6;
+    pub const T_WRITE: usize = 7;
+    #[cfg(target_family = "unix")]
+    pub const USER: usize = 8;
+    #[cfg(target_family = "unix")]
+    pub const STATE: usize = 9;
+    #[cfg(not(target_family = "unix"))]
+    pub const STATE: usize = 8;
+
+    pub fn init(
+        mode: ProcWidgetMode, is_case_sensitive: bool, is_match_whole_word: bool,
+        is_use_regex: bool, show_memory_as_values: bool, is_command: bool,
+    ) -> Self {
+        let mut process_search_state = ProcessSearchState::default();
+
+        if is_case_sensitive {
+            // By default it's off
+            process_search_state.search_toggle_ignore_case();
+        }
+        if is_match_whole_word {
+            process_search_state.search_toggle_whole_word();
+        }
+        if is_use_regex {
+            process_search_state.search_toggle_regex();
+        }
+
+        let is_count = matches!(mode, ProcWidgetMode::Grouped);
+
+        let mut sort_table_state = TableComponentState::new(vec![TableComponentColumn::new_hard(
+            CellContent::Simple("Sort By".into()),
+            7,
+        )]);
+        sort_table_state.columns[0].calculated_width = 7;
+
+        let table_state = {
+            let (default_index, default_order) = if matches!(mode, ProcWidgetMode::Tree { .. }) {
+                (Self::PID_OR_COUNT, SortOrder::Ascending)
+            } else {
+                (Self::CPU, SortOrder::Descending)
+            };
+
+            let columns = vec![
+                TableComponentColumn::new(ProcWidgetColumn::PidOrCount { is_count }),
+                TableComponentColumn::new_soft(
+                    ProcWidgetColumn::ProcNameOrCommand { is_command },
+                    Some(0.3),
+                ),
+                TableComponentColumn::new(ProcWidgetColumn::CpuPercent),
+                TableComponentColumn::new(ProcWidgetColumn::Memory {
+                    show_percentage: !show_memory_as_values,
+                }),
+                TableComponentColumn::new_hard(ProcWidgetColumn::ReadPerSecond, 8),
+                TableComponentColumn::new_hard(ProcWidgetColumn::WritePerSecond, 8),
+                TableComponentColumn::new_hard(ProcWidgetColumn::TotalRead, 8),
+                TableComponentColumn::new_hard(ProcWidgetColumn::TotalWrite, 8),
+                #[cfg(target_family = "unix")]
+                TableComponentColumn::new_soft(ProcWidgetColumn::User, Some(0.05)),
+                TableComponentColumn::new_hard(ProcWidgetColumn::State, 7),
+            ];
+
+            let default_sort_orderings = columns
+                .iter()
+                .map(|column| column.header.default_sort_order())
+                .collect();
+
+            TableComponentState::new(columns).sort_state(SortState::Sortable(SortableState::new(
+                default_index,
+                default_order,
+                default_sort_orderings,
+            )))
+        };
+
+        ProcWidget {
+            proc_search: process_search_state,
+            table_state,
+            sort_table_state,
+            is_sort_open: false,
+            mode,
+            force_rerender: true,
+            force_update_data: false,
+            table_data: TableData::default(),
+        }
+    }
+
+    pub fn is_using_command(&self) -> bool {
+        if let Some(ProcWidgetColumn::ProcNameOrCommand { is_command }) = self
+            .table_state
+            .columns
+            .get(ProcWidget::PROC_NAME_OR_CMD)
+            .map(|col| &col.header)
+        {
+            *is_command
+        } else {
+            // Technically impossible.
+            false
+        }
+    }
+
+    /// This function *only* updates the displayed process data. If there is a need to update the actual *stored* data,
+    /// call it before this function.
+    pub fn update_displayed_process_data(&mut self, data_collection: &DataCollection) {
+        let search_query = if self.proc_search.search_state.is_invalid_or_blank_search() {
+            &None
+        } else {
+            &self.proc_search.search_state.query
+        };
+        let table_data = match &self.mode {
+            ProcWidgetMode::Tree { collapsed_pids } => {
+                self.get_tree_table_data(collapsed_pids, data_collection, search_query)
+            }
+            ProcWidgetMode::Grouped | ProcWidgetMode::Normal => {
+                self.get_normal_table_data(data_collection, search_query)
+            }
+        };
+
+        // Now also update the scroll position if needed (that is, the old scroll position was too big for the new list).
+        if self.table_state.current_scroll_position >= table_data.data.len() {
+            self.table_state.current_scroll_position = table_data.data.len().saturating_sub(1);
+            self.table_state.scroll_bar = 0;
+            self.table_state.scroll_direction = ScrollDirection::Down;
+        }
+
+        // Finally, move this data to the widget itself.
+        self.table_data = table_data;
+    }
+
+    fn get_tree_table_data(
+        &self, collapsed_pids: &FxHashSet<Pid>, data_collection: &DataCollection,
+        search_query: &Option<Query>,
+    ) -> TableData {
+        const BRANCH_ENDING: char = '└';
+        const BRANCH_VERTICAL: char = '│';
+        const BRANCH_SPLIT: char = '├';
+        const BRANCH_HORIZONTAL: char = '─';
+
+        let ProcessData {
+            process_harvest,
+            cmd_pid_map,
+            name_pid_map,
+            process_parent_mapping,
+            orphan_pids,
+            ..
+        } = &data_collection.process_data;
+
+        let mut col_widths = vec![
+            0;
+            self.table_state
+                .columns
+                .iter()
+                .filter(|c| c.is_skipped())
+                .count()
+        ];
+
+        let matching_pids = data_collection
+            .process_data
+            .process_harvest
+            .iter()
+            .map(|(pid, process)| {
+                (
+                    *pid,
+                    search_query
+                        .as_ref()
+                        .map(|q| q.check(process, self.is_using_command()))
+                        .unwrap_or(true),
+                )
+            })
+            .collect::<FxHashMap<_, _>>();
+
+        let filtered_tree = {
+            let mut filtered_tree = FxHashMap::default();
+
+            let mut stack = orphan_pids
+                .iter()
+                .filter_map(|process| process_harvest.get(process))
+                .collect_vec();
+            let mut visited_pids = FxHashMap::default();
+
+            while let Some(process) = stack.last() {
+                let is_process_matching = *matching_pids.get(&process.pid).unwrap_or(&false);
+
+                if let Some(children_pids) = process_parent_mapping.get(&process.pid) {
+                    if children_pids
+                        .iter()
+                        .all(|pid| visited_pids.contains_key(pid))
+                    {
+                        let shown_children = children_pids
+                            .iter()
+                            .filter(|pid| visited_pids.get(*pid).copied().unwrap_or(false))
+                            .collect_vec();
+                        let is_shown = is_process_matching || !shown_children.is_empty();
+                        visited_pids.insert(process.pid, is_shown);
+
+                        if is_shown {
+                            filtered_tree.insert(
+                                process.pid,
+                                shown_children
+                                    .into_iter()
+                                    .filter_map(|pid| {
+                                        process_harvest.get(pid).map(|process| process.pid)
+                                    })
+                                    .collect_vec(),
+                            );
+                        }
+
+                        stack.pop();
+                    } else {
+                        children_pids
+                            .iter()
+                            .filter_map(|process| process_harvest.get(process))
+                            .rev()
+                            .for_each(|process| {
+                                stack.push(process);
+                            });
+                    }
+                } else {
+                    visited_pids.insert(process.pid, is_process_matching);
+                    stack.pop();
+                }
+            }
+
+            filtered_tree
+        };
+
+        let mut resulting_strings = vec![];
+        let mut prefixes = vec![];
+        let mut stack = orphan_pids
+            .iter()
+            .filter(|pid| filtered_tree.contains_key(*pid))
+            .filter_map(|child| process_harvest.get(child))
+            .collect_vec();
+
+        self.try_sort(&mut stack, data_collection);
+
+        let mut length_stack = vec![stack.len()];
+
+        while let (Some(process), Some(siblings_left)) = (stack.pop(), length_stack.last_mut()) {
+            *siblings_left -= 1;
+
+            let is_disabled = !*matching_pids.get(&process.pid).unwrap_or(&false);
+            let is_last = *siblings_left == 0;
+
+            if collapsed_pids.contains(&process.pid) {
+                let mut summed_process = process.clone();
+
+                if let Some(children_pids) = filtered_tree.get(&process.pid) {
+                    let mut sum_queue = children_pids
+                        .iter()
+                        .filter_map(|child| process_harvest.get(child))
+                        .collect_vec();
+
+                    while let Some(process) = sum_queue.pop() {
+                        summed_process.add(process);
+
+                        if let Some(pids) = filtered_tree.get(&process.pid) {
+                            sum_queue.extend(pids.iter().filter_map(|c| process_harvest.get(c)));
+                        }
+                    }
+                }
+
+                let prefix = if prefixes.is_empty() {
+                    "+ ".to_string()
+                } else {
+                    format!(
+                        "{}{}{} + ",
+                        prefixes.join(""),
+                        if is_last { BRANCH_ENDING } else { BRANCH_SPLIT },
+                        BRANCH_HORIZONTAL
+                    )
+                };
+
+                let process_text = self.process_to_text(
+                    &summed_process,
+                    &mut col_widths,
+                    cmd_pid_map,
+                    name_pid_map,
+                    Some(prefix),
+                    is_disabled,
+                );
+                resulting_strings.push(process_text);
+            } else {
+                let prefix = if prefixes.is_empty() {
+                    String::default()
+                } else {
+                    format!(
+                        "{}{}{} ",
+                        prefixes.join(""),
+                        if is_last { BRANCH_ENDING } else { BRANCH_SPLIT },
+                        BRANCH_HORIZONTAL
+                    )
+                };
+                let process_text = self.process_to_text(
+                    process,
+                    &mut col_widths,
+                    cmd_pid_map,
+                    name_pid_map,
+                    Some(prefix),
+                    is_disabled,
+                );
+                resulting_strings.push(process_text);
+
+                if let Some(children_pids) = filtered_tree.get(&process.pid) {
+                    if prefixes.is_empty() {
+                        prefixes.push(String::default());
+                    } else {
+                        prefixes.push(if is_last {
+                            "   ".to_string()
+                        } else {
+                            format!("{}  ", BRANCH_VERTICAL)
+                        });
+                    }
+
+                    let mut children = children_pids
+                        .iter()
+                        .filter_map(|child_pid| process_harvest.get(child_pid))
+                        .collect_vec();
+                    self.try_sort(&mut children, data_collection);
+                    length_stack.push(children.len());
+                    stack.extend(children);
+                }
+            }
+
+            while let Some(children_left) = length_stack.last() {
+                if *children_left == 0 {
+                    length_stack.pop();
+                    prefixes.pop();
+                } else {
+                    break;
+                }
+            }
+        }
+
+        TableData {
+            data: resulting_strings,
+            col_widths,
+        }
+    }
+
+    fn get_normal_table_data(
+        &self, data_collection: &DataCollection, search_query: &Option<Query>,
+    ) -> TableData {
+        let mut id_pid_map: FxHashMap<String, ProcessHarvest>;
+        let filtered_iter = data_collection
+            .process_data
+            .process_harvest
+            .values()
+            .filter(|p| {
+                search_query
+                    .as_ref()
+                    .map(|q| q.check(p, self.is_using_command()))
+                    .unwrap_or(true)
+            });
+
+        let mut filtered_data = if let ProcWidgetMode::Grouped = self.mode {
+            id_pid_map = FxHashMap::default();
+            filtered_iter.for_each(|process| {
+                let id = if self.is_using_command() {
+                    &process.command
+                } else {
+                    &process.name
+                };
+
+                if let Some(grouped_process_harvest) = id_pid_map.get_mut(id) {
+                    grouped_process_harvest.add(process);
+                } else {
+                    id_pid_map.insert(id.clone(), process.clone());
+                }
+            });
+
+            id_pid_map.values().collect::<Vec<_>>()
+        } else {
+            filtered_iter.collect::<Vec<_>>()
+        };
+
+        self.try_sort(&mut filtered_data, data_collection);
+        self.harvest_to_table_data(&filtered_data, data_collection)
+    }
+
+    fn try_sort(&self, filtered_data: &mut [&ProcessHarvest], data_collection: &DataCollection) {
+        let cmd_pid_map = &data_collection.process_data.cmd_pid_map;
+        let name_pid_map = &data_collection.process_data.name_pid_map;
+
+        if let SortState::Sortable(state) = &self.table_state.sort_state {
+            let index = state.current_index;
+            let order = &state.order;
+
+            if let Some(column) = self.table_state.columns.get(index) {
+                column.header.sort(
+                    order.is_descending(),
+                    filtered_data,
+                    self.is_using_command(),
+                    cmd_pid_map,
+                    name_pid_map,
+                );
+            }
+        }
+    }
+
+    fn process_to_text(
+        &self, process: &ProcessHarvest, col_widths: &mut [usize], cmd_pid_map: &StringPidMap,
+        name_pid_map: &StringPidMap, proc_prefix: Option<String>, is_disabled: bool,
+    ) -> TableRow {
+        let mut contents = Vec::with_capacity(self.num_shown_columns());
+
+        contents.extend(
+            self.table_state
+                .columns
+                .iter()
+                .enumerate()
+                .map(|(itx, column)| {
+                    let col_text = match column.header {
+                        ProcWidgetColumn::CpuPercent => {
+                            format!("{:.1}%", process.cpu_usage_percent).into()
+                        }
+                        ProcWidgetColumn::Memory { show_percentage } => {
+                            if show_percentage {
+                                format!("{:.1}%", process.mem_usage_percent).into()
+                            } else {
+                                binary_byte_string(process.mem_usage_bytes).into()
+                            }
+                        }
+                        ProcWidgetColumn::PidOrCount { is_count } => {
+                            if is_count {
+                                if self.is_using_command() {
+                                    cmd_pid_map
+                                        .get(&process.command)
+                                        .map(|v| v.len())
+                                        .unwrap_or(0)
+                                        .to_string()
+                                        .into()
+                                } else {
+                                    name_pid_map
+                                        .get(&process.name)
+                                        .map(|v| v.len())
+                                        .unwrap_or(0)
+                                        .to_string()
+                                        .into()
+                                }
+                            } else {
+                                process.pid.to_string().into()
+                            }
+                        }
+                        ProcWidgetColumn::ProcNameOrCommand { is_command } => {
+                            let val = if is_command {
+                                process.command.clone()
+                            } else {
+                                process.name.clone()
+                            };
+
+                            if let Some(prefix) = &proc_prefix {
+                                concat_string!(prefix, val).into()
+                            } else {
+                                val.into()
+                            }
+                        }
+                        ProcWidgetColumn::ReadPerSecond => {
+                            dec_bytes_per_second_string(process.read_bytes_per_sec).into()
+                        }
+                        ProcWidgetColumn::WritePerSecond => {
+                            dec_bytes_per_second_string(process.write_bytes_per_sec).into()
+                        }
+                        ProcWidgetColumn::TotalRead => {
+                            dec_bytes_per_second_string(process.total_read_bytes).into()
+                        }
+                        ProcWidgetColumn::TotalWrite => {
+                            dec_bytes_per_second_string(process.total_write_bytes).into()
+                        }
+                        ProcWidgetColumn::State => CellContent::HasAlt {
+                            main: process.process_state.0.clone().into(),
+                            alt: process.process_state.1.to_string().into(),
+                        },
+                        ProcWidgetColumn::User => {
+                            #[cfg(target_family = "unix")]
+                            {
+                                process.user.clone().into()
+                            }
+                            #[cfg(not(target_family = "unix"))]
+                            {
+                                "".into()
+                            }
+                        }
+                    };
+
+                    if let Some(curr) = col_widths.get_mut(itx) {
+                        *curr = max(*curr, col_text.len());
+                    }
+
+                    col_text
+                }),
+        );
+
+        if is_disabled {
+            TableRow::Styled(contents, tui::style::Style::default())
+        } else {
+            TableRow::Raw(contents)
+        }
+    }
+
+    fn harvest_to_table_data(
+        &self, process_data: &[&ProcessHarvest], data_collection: &DataCollection,
+    ) -> TableData {
+        let cmd_pid_map = &data_collection.process_data.cmd_pid_map;
+        let name_pid_map = &data_collection.process_data.name_pid_map;
+
+        let mut col_widths = vec![0; self.table_state.columns.len()];
+
+        let data = process_data
+            .iter()
+            .map(|process| {
+                self.process_to_text(
+                    process,
+                    &mut col_widths,
+                    cmd_pid_map,
+                    name_pid_map,
+                    None,
+                    false,
+                )
+            })
+            .collect();
+
+        TableData { data, col_widths }
+    }
+
+    fn get_mut_proc_col(&mut self, index: usize) -> Option<&mut ProcWidgetColumn> {
+        self.table_state
+            .columns
+            .get_mut(index)
+            .map(|col| &mut col.header)
+    }
+
+    pub fn toggle_mem_percentage(&mut self) {
+        if let Some(ProcWidgetColumn::Memory { show_percentage }) = self.get_mut_proc_col(Self::MEM)
+        {
+            *show_percentage = !*show_percentage;
+            self.force_data_update();
+        }
+    }
+
+    /// Forces an update of the data stored.
+    #[inline]
+    pub fn force_data_update(&mut self) {
+        self.force_update_data = true;
+    }
+
+    /// Forces an entire rerender and update of the data stored.
+    #[inline]
+    pub fn force_rerender_and_update(&mut self) {
+        self.force_rerender = true;
+        self.force_update_data = true;
+    }
+
+    /// Marks the selected column as hidden, and automatically resets the selected column if currently selected.
+    fn hide_column(&mut self, index: usize) {
+        if let Some(col) = self.table_state.columns.get_mut(index) {
+            col.is_hidden = true;
+
+            if let SortState::Sortable(state) = &mut self.table_state.sort_state {
+                if state.current_index == index {
+                    state.current_index = Self::CPU;
+                    state.order = SortOrder::Descending;
+                }
+            }
+        }
+    }
+
+    /// Marks the selected column as shown.
+    fn show_column(&mut self, index: usize) {
+        if let Some(col) = self.table_state.columns.get_mut(index) {
+            col.is_hidden = false;
+        }
+    }
+
+    /// Select a column. If the column is already selected, then just toggle the sort order.
+    pub fn select_column(&mut self, new_sort_index: usize) {
+        if let SortState::Sortable(state) = &mut self.table_state.sort_state {
+            state.update_sort_index(new_sort_index);
+            self.force_data_update();
+        }
+    }
+
+    pub fn toggle_tree_branch(&mut self) {
+        if let ProcWidgetMode::Tree { collapsed_pids } = &mut self.mode {
+            let current_posn = self.table_state.current_scroll_position;
+            if let Some(current_row) = self.table_data.data.get(current_posn) {
+                if let Ok(pid) = current_row.row()[ProcWidget::PID_OR_COUNT]
+                    .main_text()
+                    .parse::<Pid>()
+                {
+                    if !collapsed_pids.remove(&pid) {
+                        collapsed_pids.insert(pid);
+                    }
+                    self.force_data_update();
+                }
+            }
+        }
+    }
+
+    pub fn toggle_command(&mut self) {
+        if let Some(col) = self.table_state.columns.get_mut(Self::PROC_NAME_OR_CMD) {
+            if let ProcWidgetColumn::ProcNameOrCommand { is_command } = &mut col.header {
+                *is_command = !*is_command;
+
+                if let WidthBounds::Soft { max_percentage, .. } = &mut col.width_bounds {
+                    if *is_command {
+                        *max_percentage = Some(0.7);
+                    } else {
+                        *max_percentage = match self.mode {
+                            ProcWidgetMode::Tree { .. } => Some(0.5),
+                            ProcWidgetMode::Grouped | ProcWidgetMode::Normal => Some(0.3),
+                        };
+                    }
+                }
+
+                self.force_rerender_and_update();
+            }
+        }
+    }
+
+    /// Toggles the appropriate columns/settings when tab is pressed.
+    ///
+    /// If count is enabled, we should set the mode to [`ProcWidgetMode::Grouped`], and switch off the User and State
+    /// columns. We should also move the user off of the columns if they were selected, as those columns are now hidden
+    /// (handled by internal method calls), and go back to the "defaults".
+    ///
+    /// Otherwise, if count is disabled, then the User and State columns should be re-enabled, and the mode switched
+    /// to [`ProcWidgetMode::Normal`].
+    pub fn toggle_tab(&mut self) {
+        if !matches!(self.mode, ProcWidgetMode::Tree { .. }) {
+            if let Some(ProcWidgetColumn::PidOrCount { is_count }) =
+                self.get_mut_proc_col(Self::PID_OR_COUNT)
+            {
+                *is_count = !*is_count;
+
+                if *is_count {
+                    #[cfg(target_family = "unix")]
+                    self.hide_column(Self::USER);
+                    self.hide_column(Self::STATE);
+                    self.mode = ProcWidgetMode::Grouped;
+
+                    self.sort_table_state.current_scroll_position = self
+                        .sort_table_state
+                        .current_scroll_position
+                        .clamp(0, self.num_enabled_columns().saturating_sub(1));
+                } else {
+                    #[cfg(target_family = "unix")]
+                    self.show_column(Self::USER);
+                    self.show_column(Self::STATE);
+                    self.mode = ProcWidgetMode::Normal;
+                }
+                self.force_rerender_and_update();
+            }
+        }
+    }
+
+    pub fn get_search_cursor_position(&self) -> usize {
+        self.proc_search.search_state.grapheme_cursor.cur_cursor()
+    }
+
+    pub fn get_char_cursor_position(&self) -> usize {
+        self.proc_search.search_state.char_cursor_position
+    }
+
+    pub fn is_search_enabled(&self) -> bool {
+        self.proc_search.search_state.is_enabled
+    }
+
+    pub fn get_current_search_query(&self) -> &String {
+        &self.proc_search.search_state.current_search_query
+    }
+
+    pub fn update_query(&mut self) {
+        if self
+            .proc_search
+            .search_state
+            .current_search_query
+            .is_empty()
+        {
+            self.proc_search.search_state.is_blank_search = true;
+            self.proc_search.search_state.is_invalid_search = false;
+            self.proc_search.search_state.error_message = None;
+        } else {
+            match parse_query(
+                &self.proc_search.search_state.current_search_query,
+                self.proc_search.is_searching_whole_word,
+                self.proc_search.is_ignoring_case,
+                self.proc_search.is_searching_with_regex,
+            ) {
+                Ok(parsed_query) => {
+                    self.proc_search.search_state.query = Some(parsed_query);
+                    self.proc_search.search_state.is_blank_search = false;
+                    self.proc_search.search_state.is_invalid_search = false;
+                    self.proc_search.search_state.error_message = None;
+                }
+                Err(err) => {
+                    self.proc_search.search_state.is_blank_search = false;
+                    self.proc_search.search_state.is_invalid_search = true;
+                    self.proc_search.search_state.error_message = Some(err.to_string());
+                }
+            }
+        }
+        self.table_state.scroll_bar = 0;
+        self.table_state.current_scroll_position = 0;
+
+        self.force_data_update();
+    }
+
+    pub fn clear_search(&mut self) {
+        self.proc_search.search_state.reset();
+        self.force_data_update();
+    }
+
+    pub fn search_walk_forward(&mut self, start_position: usize) {
+        self.proc_search
+            .search_state
+            .grapheme_cursor
+            .next_boundary(
+                &self.proc_search.search_state.current_search_query[start_position..],
+                start_position,
+            )
+            .unwrap();
+    }
+
+    pub fn search_walk_back(&mut self, start_position: usize) {
+        self.proc_search
+            .search_state
+            .grapheme_cursor
+            .prev_boundary(
+                &self.proc_search.search_state.current_search_query[..start_position],
+                0,
+            )
+            .unwrap();
+    }
+
+    /// Returns the number of columns *visible*.
+    pub fn num_shown_columns(&self) -> usize {
+        self.table_state
+            .columns
+            .iter()
+            .filter(|c| !c.is_skipped())
+            .count()
+    }
+
+    /// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not
+    /// visible (e.g. off screen).
+    pub fn num_enabled_columns(&self) -> usize {
+        self.table_state
+            .columns
+            .iter()
+            .filter(|c| !c.is_hidden)
+            .count()
+    }
+
+    /// Sets the [`ProcWidget`]'s current sort index to whatever was in the sort table.
+    pub(crate) fn use_sort_table_value(&mut self) {
+        if let SortState::Sortable(st) = &mut self.table_state.sort_state {
+            st.update_sort_index(self.sort_table_state.current_scroll_position);
+
+            self.is_sort_open = false;
+            self.force_rerender_and_update();
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_sort() {}
+
+    #[test]
+    fn assert_correct_columns() {
+        #[track_caller]
+        fn test_columns(mode: ProcWidgetMode, mem_as_val: bool, is_cmd: bool) {
+            let is_count = matches!(mode, ProcWidgetMode::Grouped);
+            let is_command = is_cmd;
+            let show_percentage = !mem_as_val;
+
+            let proc = ProcWidget::init(mode, false, false, false, mem_as_val, is_command);
+            let columns = &proc.table_state.columns;
+
+            assert_eq!(
+                columns[ProcWidget::PID_OR_COUNT].header,
+                ProcWidgetColumn::PidOrCount { is_count }
+            );
+            assert_eq!(
+                columns[ProcWidget::PROC_NAME_OR_CMD].header,
+                ProcWidgetColumn::ProcNameOrCommand { is_command }
+            );
+            assert!(matches!(
+                columns[ProcWidget::CPU].header,
+                ProcWidgetColumn::CpuPercent
+            ));
+            assert_eq!(
+                columns[ProcWidget::MEM].header,
+                ProcWidgetColumn::Memory { show_percentage }
+            );
+            assert!(matches!(
+                columns[ProcWidget::RPS].header,
+                ProcWidgetColumn::ReadPerSecond
+            ));
+            assert!(matches!(
+                columns[ProcWidget::WPS].header,
+                ProcWidgetColumn::WritePerSecond
+            ));
+            assert!(matches!(
+                columns[ProcWidget::T_READ].header,
+                ProcWidgetColumn::TotalRead
+            ));
+            assert!(matches!(
+                columns[ProcWidget::T_WRITE].header,
+                ProcWidgetColumn::TotalWrite
+            ));
+            #[cfg(target_family = "unix")]
+            {
+                assert!(matches!(
+                    columns[ProcWidget::USER].header,
+                    ProcWidgetColumn::User
+                ));
+            }
+            assert!(matches!(
+                columns[ProcWidget::STATE].header,
+                ProcWidgetColumn::State
+            ));
+        }
+
+        test_columns(ProcWidgetMode::Grouped, true, true);
+        test_columns(ProcWidgetMode::Grouped, false, true);
+        test_columns(ProcWidgetMode::Grouped, true, false);
+        test_columns(
+            ProcWidgetMode::Tree {
+                collapsed_pids: Default::default(),
+            },
+            true,
+            true,
+        );
+        test_columns(ProcWidgetMode::Normal, true, true);
+    }
+}
diff --git a/src/bin/main.rs b/src/bin/main.rs
index 6a1f0aae..e3a86315 100644
--- a/src/bin/main.rs
+++ b/src/bin/main.rs
@@ -54,13 +54,8 @@ fn main() -> Result<()> {
     )?;
 
     // Create painter and set colours.
-    let mut painter = canvas::Painter::init(
-        widget_layout,
-        app.app_config_fields.table_gap,
-        app.app_config_fields.use_basic_mode,
-        &config,
-        get_color_scheme(&matches, &config)?,
-    )?;
+    let mut painter =
+        canvas::Painter::init(widget_layout, &config, get_color_scheme(&matches, &config)?)?;
 
     // Create termination mutex and cvar
     #[allow(clippy::mutex_atomic)]
@@ -135,11 +130,11 @@ fn main() -> Result<()> {
                     if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) {
                         break;
                     }
-                    handle_force_redraws(&mut app);
+                    update_data(&mut app);
                 }
                 BottomEvent::MouseInput(event) => {
                     handle_mouse_event(event, &mut app);
-                    handle_force_redraws(&mut app);
+                    update_data(&mut app);
                 }
                 BottomEvent::Update(data) => {
                     app.data_collection.eat_data(data);
@@ -158,46 +153,45 @@ fn main() -> Result<()> {
                         if app.used_widgets.use_net {
                             let network_data = convert_network_data_points(
                                 &app.data_collection,
-                                false,
                                 app.app_config_fields.use_basic_mode
                                     || app.app_config_fields.use_old_network_legend,
                                 &app.app_config_fields.network_scale_type,
                                 &app.app_config_fields.network_unit_type,
                                 app.app_config_fields.network_use_binary_prefix,
                             );
-                            app.canvas_data.network_data_rx = network_data.rx;
-                            app.canvas_data.network_data_tx = network_data.tx;
-                            app.canvas_data.rx_display = network_data.rx_display;
-                            app.canvas_data.tx_display = network_data.tx_display;
+                            app.converted_data.network_data_rx = network_data.rx;
+                            app.converted_data.network_data_tx = network_data.tx;
+                            app.converted_data.rx_display = network_data.rx_display;
+                            app.converted_data.tx_display = network_data.tx_display;
                             if let Some(total_rx_display) = network_data.total_rx_display {
-                                app.canvas_data.total_rx_display = total_rx_display;
+                                app.converted_data.total_rx_display = total_rx_display;
                             }
                             if let Some(total_tx_display) = network_data.total_tx_display {
-                                app.canvas_data.total_tx_display = total_tx_display;
+                                app.converted_data.total_tx_display = total_tx_display;
                             }
                         }
 
                         // Disk
                         if app.used_widgets.use_disk {
-                            app.canvas_data.disk_data = convert_disk_row(&app.data_collection);
+                            app.converted_data.disk_data = convert_disk_row(&app.data_collection);
                         }
 
                         // Temperatures
                         if app.used_widgets.use_temp {
-                            app.canvas_data.temp_sensor_data = convert_temp_row(&app);
+                            app.converted_data.temp_sensor_data = convert_temp_row(&app);
                         }
 
                         // Memory
                         if app.used_widgets.use_mem {
-                            app.canvas_data.mem_data =
-                                convert_mem_data_points(&app.data_collection, false);
-                            app.canvas_data.swap_data =
-                                convert_swap_data_points(&app.data_collection, false);
+                            app.converted_data.mem_data =
+                                convert_mem_data_points(&app.data_collection);
+                            app.converted_data.swap_data =
+                                convert_swap_data_points(&app.data_collection);
                             let (memory_labels, swap_labels) =
                                 convert_mem_labels(&app.data_collection);
 
-                            app.canvas_data.mem_labels = memory_labels;
-                            app.canvas_data.swap_labels = swap_labels;
+                            app.converted_data.mem_labels = memory_labels;
+                            app.converted_data.swap_labels = swap_labels;
                         }
 
                         if app.used_widgets.use_cpu {
@@ -205,25 +199,28 @@ fn main() -> Result<()> {
 
                             convert_cpu_data_points(
                                 &app.data_collection,
-                                &mut app.canvas_data.cpu_data,
-                                false,
+                                &mut app.converted_data.cpu_data,
                             );
-                            app.canvas_data.load_avg_data = app.data_collection.load_avg_harvest;
+                            app.converted_data.load_avg_data = app.data_collection.load_avg_harvest;
                         }
 
                         // Processes
                         if app.used_widgets.use_proc {
-                            update_all_process_lists(&mut app);
+                            for proc in app.proc_state.widget_states.values_mut() {
+                                proc.force_data_update();
+                            }
                         }
 
                         // Battery
                         #[cfg(feature = "battery")]
                         {
                             if app.used_widgets.use_battery {
-                                app.canvas_data.battery_data =
+                                app.converted_data.battery_data =
                                     convert_battery_harvest(&app.data_collection);
                             }
                         }
+
+                        update_data(&mut app);
                     }
                 }
                 BottomEvent::Clean => {
diff --git a/src/canvas.rs b/src/canvas.rs
index 2070327a..e69c5053 100644
--- a/src/canvas.rs
+++ b/src/canvas.rs
@@ -1,5 +1,5 @@
 use itertools::izip;
-use std::{collections::HashMap, str::FromStr};
+use std::str::FromStr;
 
 use tui::{
     backend::Backend,
@@ -18,11 +18,9 @@ use crate::{
         App,
     },
     constants::*,
-    data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData},
     options::Config,
     utils::error,
     utils::error::BottomError,
-    Pid,
 };
 
 pub use self::components::Point;
@@ -33,30 +31,6 @@ mod dialogs;
 mod drawing_utils;
 mod widgets;
 
-#[derive(Default)]
-pub struct DisplayableData {
-    pub rx_display: String,
-    pub tx_display: String,
-    pub total_rx_display: String,
-    pub total_tx_display: String,
-    pub network_data_rx: Vec<Point>,
-    pub network_data_tx: Vec<Point>,
-    pub disk_data: Vec<Vec<String>>,
-    pub temp_sensor_data: Vec<Vec<String>>,
-    pub single_process_data: HashMap<Pid, ConvertedProcessData>, // Contains single process data, key is PID
-    pub finalized_process_data_map: HashMap<u64, Vec<ConvertedProcessData>>, // What's actually displayed, key is the widget ID.
-    pub stringified_process_data_map: HashMap<u64, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID
-
-    pub mem_labels: Option<(String, String)>,
-    pub swap_labels: Option<(String, String)>,
-
-    pub mem_data: Vec<Point>, // TODO: Switch this and all data points over to a better data structure...
-    pub swap_data: Vec<Point>,
-    pub load_avg_data: [f32; 3],
-    pub cpu_data: Vec<ConvertedCpuData>,
-    pub battery_data: Vec<ConvertedBatteryData>,
-}
-
 #[derive(Debug)]
 pub enum ColourScheme {
     Default,
@@ -94,20 +68,20 @@ pub struct Painter {
     height: u16,
     width: u16,
     styled_help_text: Vec<Spans<'static>>,
-    is_mac_os: bool, // FIXME: This feels out of place...
+    is_mac_os: bool, // TODO: This feels out of place...
+
+    // TODO: Redo this entire thing.
     row_constraints: Vec<Constraint>,
     col_constraints: Vec<Vec<Constraint>>,
     col_row_constraints: Vec<Vec<Vec<Constraint>>>,
     layout_constraints: Vec<Vec<Vec<Vec<Constraint>>>>,
     derived_widget_draw_locs: Vec<Vec<Vec<Vec<Rect>>>>,
     widget_layout: BottomLayout,
-    table_height_offset: u16,
 }
 
 impl Painter {
     pub fn init(
-        widget_layout: BottomLayout, table_gap: u16, is_basic_mode: bool, config: &Config,
-        colour_scheme: ColourScheme,
+        widget_layout: BottomLayout, config: &Config, colour_scheme: ColourScheme,
     ) -> anyhow::Result<Self> {
         // Now for modularity; we have to also initialize the base layouts!
         // We want to do this ONCE and reuse; after this we can just construct
@@ -188,7 +162,6 @@ impl Painter {
             layout_constraints,
             widget_layout,
             derived_widget_draw_locs: Vec::default(),
-            table_height_offset: if is_basic_mode { 2 } else { 4 } + table_gap,
         };
 
         if let ColourScheme::Custom = colour_scheme {
@@ -338,12 +311,6 @@ impl Painter {
                 for battery_widget in app_state.battery_state.widget_states.values_mut() {
                     battery_widget.tab_click_locs = None;
                 }
-
-                // Reset column headers for sorting in process widget...
-                for proc_widget in app_state.proc_state.widget_states.values_mut() {
-                    proc_widget.columns.column_header_y_loc = None;
-                    proc_widget.columns.column_header_x_locs = None;
-                }
             }
 
             if app_state.help_dialog_state.is_showing_help {
@@ -506,7 +473,7 @@ impl Painter {
                                 _ => 0,
                             };
 
-                        self.draw_process_features(f, app_state, rect[0], true, widget_id);
+                        self.draw_process_widget(f, app_state, rect[0], true, widget_id);
                     }
                     Battery => self.draw_battery_display(
                         f,
@@ -524,7 +491,7 @@ impl Painter {
                     self.draw_frozen_indicator(f, frozen_draw_loc);
                 }
 
-                let actual_cpu_data_len = app_state.canvas_data.cpu_data.len().saturating_sub(1);
+                let actual_cpu_data_len = app_state.converted_data.cpu_data.len().saturating_sub(1);
 
                 // This fixes #397, apparently if the height is 1, it can't render the CPU bars...
                 let cpu_height = {
@@ -585,7 +552,7 @@ impl Painter {
                                         ProcSort => 2,
                                         _ => 0,
                                     };
-                                self.draw_process_features(
+                                self.draw_process_widget(
                                     f,
                                     app_state,
                                     vertical_chunks[3],
@@ -736,7 +703,7 @@ impl Painter {
                     Disk => {
                         self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id)
                     }
-                    Proc => self.draw_process_features(
+                    Proc => self.draw_process_widget(
                         f,
                         app_state,
                         *widget_draw_loc,
diff --git a/src/canvas/components/text_table.rs b/src/canvas/components/text_table.rs
index 8b137891..f5fc31e7 100644
--- a/src/canvas/components/text_table.rs
+++ b/src/canvas/components/text_table.rs
@@ -1 +1,502 @@
+use std::{
+    borrow::Cow,
+    cmp::{max, min},
+};
 
+use concat_string::concat_string;
+use tui::{
+    backend::Backend,
+    layout::{Constraint, Direction, Layout, Rect},
+    style::Style,
+    text::{Span, Spans, Text},
+    widgets::{Block, Borders, Row, Table},
+    Frame,
+};
+use unicode_segmentation::UnicodeSegmentation;
+
+use crate::{
+    app::{
+        self, layout_manager::BottomWidget, CellContent, SortState, TableComponentColumn,
+        TableComponentHeader, TableComponentState, WidthBounds,
+    },
+    constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
+    data_conversion::{TableData, TableRow},
+};
+
+pub struct TextTableTitle<'a> {
+    pub title: Cow<'a, str>,
+    pub is_expanded: bool,
+}
+
+pub struct TextTable<'a> {
+    pub table_gap: u16,
+    pub is_force_redraw: bool, // TODO: Is this force redraw thing needed? Or is there a better way?
+    pub recalculate_column_widths: bool,
+
+    /// The header style.
+    pub header_style: Style,
+
+    /// The border style.
+    pub border_style: Style,
+
+    /// The highlighted text style.
+    pub highlighted_text_style: Style,
+
+    /// The graph title and whether it is expanded (if there is one).
+    pub title: Option<TextTableTitle<'a>>,
+
+    /// Whether this widget is selected.
+    pub is_on_widget: bool,
+
+    /// Whether to draw all borders.
+    pub draw_border: bool,
+
+    /// Whether to show the scroll position.
+    pub show_table_scroll_position: bool,
+
+    /// The title style.
+    pub title_style: Style,
+
+    /// The text style.
+    pub text_style: Style,
+
+    /// Whether to determine widths from left to right.
+    pub left_to_right: bool,
+}
+
+impl<'a> TextTable<'a> {
+    /// Generates a title for the [`TextTable`] widget, given the available space.
+    fn generate_title(&self, draw_loc: Rect, pos: usize, total: usize) -> Option<Spans<'_>> {
+        self.title
+            .as_ref()
+            .map(|TextTableTitle { title, is_expanded }| {
+                let title = if self.show_table_scroll_position {
+                    let title_string = concat_string!(
+                        title,
+                        "(",
+                        pos.to_string(),
+                        " of ",
+                        total.to_string(),
+                        ") "
+                    );
+
+                    if title_string.len() + 2 <= draw_loc.width.into() {
+                        title_string
+                    } else {
+                        title.to_string()
+                    }
+                } else {
+                    title.to_string()
+                };
+
+                if *is_expanded {
+                    let title_base = concat_string!(title, "── Esc to go back ");
+                    let esc = concat_string!(
+                        "─",
+                        "─".repeat(usize::from(draw_loc.width).saturating_sub(
+                            UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2
+                        )),
+                        "─ Esc to go back "
+                    );
+                    Spans::from(vec![
+                        Span::styled(title, self.title_style),
+                        Span::styled(esc, self.border_style),
+                    ])
+                } else {
+                    Spans::from(Span::styled(title, self.title_style))
+                }
+            })
+    }
+
+    pub fn draw_text_table<B: Backend, H: TableComponentHeader>(
+        &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState<H>,
+        table_data: &TableData, btm_widget: Option<&mut BottomWidget>,
+    ) {
+        // TODO: This is a *really* ugly hack to get basic mode to hide the border when not selected, without shifting everything.
+        let is_not_basic = self.is_on_widget || self.draw_border;
+        let margined_draw_loc = Layout::default()
+            .constraints([Constraint::Percentage(100)])
+            .horizontal_margin(if is_not_basic { 0 } else { 1 })
+            .direction(Direction::Horizontal)
+            .split(draw_loc)[0];
+
+        let block = if self.draw_border {
+            let block = Block::default()
+                .borders(Borders::ALL)
+                .border_style(self.border_style);
+
+            if let Some(title) = self.generate_title(
+                draw_loc,
+                state.current_scroll_position.saturating_add(1),
+                table_data.data.len(),
+            ) {
+                block.title(title)
+            } else {
+                block
+            }
+        } else if self.is_on_widget {
+            Block::default()
+                .borders(SIDE_BORDERS)
+                .border_style(self.border_style)
+        } else {
+            Block::default().borders(Borders::NONE)
+        };
+
+        let inner_rect = block.inner(margined_draw_loc);
+        let (inner_width, inner_height) = { (inner_rect.width, inner_rect.height) };
+
+        if inner_width == 0 || inner_height == 0 {
+            f.render_widget(block, margined_draw_loc);
+        } else {
+            let show_header = inner_height > 1;
+            let header_height = if show_header { 1 } else { 0 };
+            let table_gap = if !show_header || draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
+                0
+            } else {
+                self.table_gap
+            };
+
+            let sliced_vec = {
+                let num_rows = usize::from(inner_height.saturating_sub(table_gap + header_height));
+                let start = get_start_position(
+                    num_rows,
+                    &state.scroll_direction,
+                    &mut state.scroll_bar,
+                    state.current_scroll_position,
+                    self.is_force_redraw,
+                );
+                let end = min(table_data.data.len(), start + num_rows);
+                state
+                    .table_state
+                    .select(Some(state.current_scroll_position.saturating_sub(start)));
+                &table_data.data[start..end]
+            };
+
+            // Calculate widths
+            if self.recalculate_column_widths {
+                state
+                    .columns
+                    .iter_mut()
+                    .zip(&table_data.col_widths)
+                    .for_each(|(column, data_width)| match &mut column.width_bounds {
+                        WidthBounds::Soft {
+                            min_width: _,
+                            desired,
+                            max_percentage: _,
+                        } => {
+                            *desired = max(
+                                *desired,
+                                max(column.header.header_text().len(), *data_width) as u16,
+                            );
+                        }
+                        WidthBounds::CellWidth => {}
+                        WidthBounds::Hard(_width) => {}
+                    });
+
+                state.calculate_column_widths(inner_width, self.left_to_right);
+
+                if let SortState::Sortable(st) = &mut state.sort_state {
+                    let row_widths = state
+                        .columns
+                        .iter()
+                        .filter_map(|c| {
+                            if c.calculated_width == 0 {
+                                None
+                            } else {
+                                Some(c.calculated_width)
+                            }
+                        })
+                        .collect::<Vec<_>>();
+
+                    st.update_visual_index(inner_rect, &row_widths);
+                }
+
+                // Update draw loc in widget map
+                if let Some(btm_widget) = btm_widget {
+                    btm_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
+                    btm_widget.bottom_right_corner =
+                        Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
+                }
+            }
+
+            let columns = &state.columns;
+            let header = build_header(columns, &state.sort_state)
+                .style(self.header_style)
+                .bottom_margin(table_gap);
+            let table_rows = sliced_vec.iter().map(|row| {
+                let (row, style) = match row {
+                    TableRow::Raw(row) => (row, None),
+                    TableRow::Styled(row, style) => (row, Some(*style)),
+                };
+
+                Row::new(row.iter().zip(columns).filter_map(|(cell, c)| {
+                    if c.calculated_width == 0 {
+                        None
+                    } else {
+                        Some(truncate_text(cell, c.calculated_width.into(), style))
+                    }
+                }))
+            });
+
+            if !table_data.data.is_empty() {
+                let widget = {
+                    let mut table = Table::new(table_rows)
+                        .block(block)
+                        .highlight_style(self.highlighted_text_style)
+                        .style(self.text_style);
+
+                    if show_header {
+                        table = table.header(header);
+                    }
+
+                    table
+                };
+
+                f.render_stateful_widget(
+                    widget.widths(
+                        &(columns
+                            .iter()
+                            .filter_map(|c| {
+                                if c.calculated_width == 0 {
+                                    None
+                                } else {
+                                    Some(Constraint::Length(c.calculated_width))
+                                }
+                            })
+                            .collect::<Vec<_>>()),
+                    ),
+                    margined_draw_loc,
+                    &mut state.table_state,
+                );
+            } else {
+                f.render_widget(block, margined_draw_loc);
+            }
+        }
+    }
+}
+
+/// Constructs the table header.
+fn build_header<'a, H: TableComponentHeader>(
+    columns: &'a [TableComponentColumn<H>], sort_state: &SortState,
+) -> Row<'a> {
+    use itertools::Either;
+
+    const UP_ARROW: &str = "▲";
+    const DOWN_ARROW: &str = "▼";
+
+    let iter = match sort_state {
+        SortState::Unsortable => Either::Left(columns.iter().filter_map(|c| {
+            if c.calculated_width == 0 {
+                None
+            } else {
+                Some(truncate_text(
+                    c.header.header_text(),
+                    c.calculated_width.into(),
+                    None,
+                ))
+            }
+        })),
+        SortState::Sortable(s) => {
+            let order = &s.order;
+            let index = s.current_index;
+
+            let arrow = match order {
+                app::SortOrder::Ascending => UP_ARROW,
+                app::SortOrder::Descending => DOWN_ARROW,
+            };
+
+            Either::Right(columns.iter().enumerate().filter_map(move |(itx, c)| {
+                if c.calculated_width == 0 {
+                    None
+                } else if itx == index {
+                    Some(truncate_suffixed_text(
+                        c.header.header_text(),
+                        arrow,
+                        c.calculated_width.into(),
+                        None,
+                    ))
+                } else {
+                    Some(truncate_text(
+                        c.header.header_text(),
+                        c.calculated_width.into(),
+                        None,
+                    ))
+                }
+            }))
+        }
+    };
+
+    Row::new(iter)
+}
+
+/// Truncates text if it is too long, and adds an ellipsis at the end if needed.
+fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>) -> Text<'_> {
+    let (main_text, alt_text) = match content {
+        CellContent::Simple(s) => (s, None),
+        CellContent::HasAlt {
+            alt: short,
+            main: long,
+        } => (long, Some(short)),
+    };
+
+    let mut text = {
+        let graphemes: Vec<&str> =
+            UnicodeSegmentation::graphemes(main_text.as_ref(), true).collect();
+        if graphemes.len() > width && width > 0 {
+            if let Some(s) = alt_text {
+                // If an alternative exists, use that.
+                Text::raw(s.as_ref())
+            } else {
+                // Truncate with ellipsis
+                let first_n = graphemes[..(width - 1)].concat();
+                Text::raw(concat_string!(first_n, "…"))
+            }
+        } else {
+            Text::raw(main_text.as_ref())
+        }
+    };
+
+    if let Some(row_style) = row_style {
+        text.patch_style(row_style);
+    }
+
+    text
+}
+
+fn truncate_suffixed_text<'a>(
+    content: &'a CellContent, suffix: &str, width: usize, row_style: Option<Style>,
+) -> Text<'a> {
+    let (main_text, alt_text) = match content {
+        CellContent::Simple(s) => (s, None),
+        CellContent::HasAlt {
+            alt: short,
+            main: long,
+        } => (long, Some(short)),
+    };
+
+    let mut text = {
+        let suffixed = concat_string!(main_text, suffix);
+        let graphemes: Vec<&str> =
+            UnicodeSegmentation::graphemes(suffixed.as_str(), true).collect();
+        if graphemes.len() > width && width > 1 {
+            if let Some(alt) = alt_text {
+                // If an alternative exists, use that + arrow.
+                Text::raw(concat_string!(alt, suffix))
+            } else {
+                // Truncate with ellipsis + arrow.
+                let first_n = graphemes[..(width - 2)].concat();
+                Text::raw(concat_string!(first_n, "…", suffix))
+            }
+        } else {
+            Text::raw(suffixed)
+        }
+    };
+
+    if let Some(row_style) = row_style {
+        text.patch_style(row_style);
+    }
+
+    text
+}
+
+/// Gets the starting position of a table.
+pub fn get_start_position(
+    num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize,
+    currently_selected_position: usize, is_force_redraw: bool,
+) -> usize {
+    if is_force_redraw {
+        *scroll_position_bar = 0;
+    }
+
+    match scroll_direction {
+        app::ScrollDirection::Down => {
+            if currently_selected_position < *scroll_position_bar + num_rows {
+                // If, using previous_scrolled_position, we can see the element
+                // (so within that and + num_rows) just reuse the current previously scrolled position
+                *scroll_position_bar
+            } else if currently_selected_position >= num_rows {
+                // Else if the current position past the last element visible in the list, omit
+                // until we can see that element
+                *scroll_position_bar = currently_selected_position - num_rows + 1;
+                *scroll_position_bar
+            } else {
+                // Else, if it is not past the last element visible, do not omit anything
+                0
+            }
+        }
+        app::ScrollDirection::Up => {
+            if currently_selected_position <= *scroll_position_bar {
+                // If it's past the first element, then show from that element downwards
+                *scroll_position_bar = currently_selected_position;
+            } else if currently_selected_position >= *scroll_position_bar + num_rows {
+                *scroll_position_bar = currently_selected_position - num_rows + 1;
+            }
+            // Else, don't change what our start position is from whatever it is set to!
+            *scroll_position_bar
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_get_start_position() {
+        use crate::app::ScrollDirection::{self, Down, Up};
+
+        #[track_caller]
+        fn test_get(
+            bar: usize, rows: usize, direction: ScrollDirection, selected: usize, force: bool,
+            expected_posn: usize, expected_bar: usize,
+        ) {
+            let mut bar = bar;
+            assert_eq!(
+                get_start_position(rows, &direction, &mut bar, selected, force),
+                expected_posn,
+                "returned start position should match"
+            );
+            assert_eq!(bar, expected_bar, "bar positions should match");
+        }
+
+        // Scrolling down from start
+        test_get(0, 10, Down, 0, false, 0, 0);
+
+        // Simple scrolling down
+        test_get(0, 10, Down, 1, false, 0, 0);
+
+        // Scrolling down from the middle high up
+        test_get(0, 10, Down, 4, false, 0, 0);
+
+        // Scrolling down into boundary
+        test_get(0, 10, Down, 10, false, 1, 1);
+        test_get(0, 10, Down, 11, false, 2, 2);
+
+        // Scrolling down from the with non-zero bar
+        test_get(5, 10, Down, 14, false, 5, 5);
+
+        // Force redraw scrolling down (e.g. resize)
+        test_get(5, 15, Down, 14, true, 0, 0);
+
+        // Test jumping down
+        test_get(1, 10, Down, 19, true, 10, 10);
+
+        // Scrolling up from bottom
+        test_get(10, 10, Up, 19, false, 10, 10);
+
+        // Simple scrolling up
+        test_get(10, 10, Up, 18, false, 10, 10);
+
+        // Scrolling up from the middle
+        test_get(10, 10, Up, 10, false, 10, 10);
+
+        // Scrolling up into boundary
+        test_get(10, 10, Up, 9, false, 9, 9);
+
+        // Force redraw scrolling up (e.g. resize)
+        test_get(5, 10, Up, 14, true, 5, 5);
+
+        // Test jumping up
+        test_get(10, 10, Up, 0, false, 0, 0);
+    }
+}
diff --git a/src/canvas/components/time_chart.rs b/src/canvas/components/time_chart.rs
index 5bb0ff15..7b791d1d 100644
--- a/src/canvas/components/time_chart.rs
+++ b/src/canvas/components/time_chart.rs
@@ -1,7 +1,4 @@
-use std::{
-    borrow::Cow,
-    cmp::{max, Ordering},
-};
+use std::{borrow::Cow, cmp::max};
 use tui::{
     buffer::Buffer,
     layout::{Constraint, Rect},
@@ -15,6 +12,8 @@ use tui::{
 };
 use unicode_width::UnicodeWidthStr;
 
+use crate::utils::gen_util::partial_ordering;
+
 /// An X or Y axis for the chart widget
 #[derive(Debug, Clone)]
 pub struct Axis<'a> {
@@ -556,16 +555,11 @@ impl<'a> Widget for TimeChart<'a> {
     }
 }
 
-fn bin_cmp(a: &f64, b: &f64) -> Ordering {
-    // TODO: Switch to `total_cmp` on 1.62
-    a.partial_cmp(b).unwrap_or(Ordering::Equal)
-}
-
 /// Returns the start index and potential interpolation index given the start time and the dataset.
 fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>) {
     match dataset
         .data
-        .binary_search_by(|(x, _y)| bin_cmp(x, &start_bound))
+        .binary_search_by(|(x, _y)| partial_ordering(x, &start_bound))
     {
         Ok(index) => (index, None),
         Err(index) => (index, index.checked_sub(1)),
@@ -576,7 +570,7 @@ fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>)
 fn get_end(dataset: &Dataset<'_>, end_bound: f64) -> (usize, Option<usize>) {
     match dataset
         .data
-        .binary_search_by(|(x, _y)| bin_cmp(x, &end_bound))
+        .binary_search_by(|(x, _y)| partial_ordering(x, &end_bound))
     {
         // In the success case, this means we found an index. Add one since we want to include this index and we
         // expect to use the returned index as part of a (m..n) range.
@@ -621,20 +615,20 @@ mod test {
         assert_eq!(interpolate_point(&data[0], &data[1], -3.0), 8.0);
     }
 
+    #[test]
+    fn time_chart_empty_dataset() {
+        let data = [];
+        let dataset = Dataset::default().data(&data);
+
+        assert_eq!(get_start(&dataset, -100.0), (0, None));
+        assert_eq!(get_start(&dataset, -3.0), (0, None));
+
+        assert_eq!(get_end(&dataset, 0.0), (0, None));
+        assert_eq!(get_end(&dataset, 100.0), (0, None));
+    }
+
     #[test]
     fn time_chart_test_data_trimming() {
-        // Quick test on a completely empty dataset...
-        {
-            let data = [];
-            let dataset = Dataset::default().data(&data);
-
-            assert_eq!(get_start(&dataset, -100.0), (0, None));
-            assert_eq!(get_start(&dataset, -3.0), (0, None));
-
-            assert_eq!(get_end(&dataset, 0.0), (0, None));
-            assert_eq!(get_end(&dataset, 100.0), (0, None));
-        }
-
         let data = [
             (-3.0, 8.0),
             (-2.5, 15.0),
diff --git a/src/canvas/components/time_graph.rs b/src/canvas/components/time_graph.rs
index ab5cec2e..7448ff5c 100644
--- a/src/canvas/components/time_graph.rs
+++ b/src/canvas/components/time_graph.rs
@@ -25,7 +25,6 @@ pub struct GraphData<'a> {
     pub name: Option<Cow<'a, str>>,
 }
 
-#[derive(Default)]
 pub struct TimeGraph<'a> {
     /// Whether to use a dot marker over the default braille markers.
     pub use_dot: bool,
@@ -144,14 +143,14 @@ impl<'a> TimeGraph<'a> {
                 .collect()
         };
 
+        let block = Block::default()
+            .title(self.generate_title(draw_loc))
+            .borders(Borders::ALL)
+            .border_style(self.border_style);
+
         f.render_widget(
             TimeChart::new(data)
-                .block(
-                    Block::default()
-                        .title(self.generate_title(draw_loc))
-                        .borders(Borders::ALL)
-                        .border_style(self.border_style),
-                )
+                .block(block)
                 .x_axis(x_axis)
                 .y_axis(y_axis)
                 .hidden_legend_constraints(
diff --git a/src/canvas/dialogs/dd_dialog.rs b/src/canvas/dialogs/dd_dialog.rs
index f7edbedd..1ee4282b 100644
--- a/src/canvas/dialogs/dd_dialog.rs
+++ b/src/canvas/dialogs/dd_dialog.rs
@@ -9,7 +9,7 @@ use tui::{
 };
 
 use crate::{
-    app::{App, KillSignal},
+    app::{widgets::ProcWidgetMode, App, KillSignal},
     canvas::Painter,
 };
 
@@ -29,7 +29,13 @@ impl Painter {
             if let Some(first_pid) = to_kill_processes.1.first() {
                 return Some(Text::from(vec![
                     Spans::from(""),
-                    if app_state.is_grouped(app_state.current_widget.widget_id) {
+                    if app_state
+                        .proc_state
+                        .widget_states
+                        .get(&app_state.current_widget.widget_id)
+                        .map(|p| matches!(p.mode, ProcWidgetMode::Grouped))
+                        .unwrap_or(false)
+                    {
                         if to_kill_processes.1.len() != 1 {
                             Spans::from(format!(
                                 "Kill {} processes with the name \"{}\"?  Press ENTER to confirm.",
diff --git a/src/canvas/drawing_utils.rs b/src/canvas/drawing_utils.rs
index f7b81281..7ef1b85d 100644
--- a/src/canvas/drawing_utils.rs
+++ b/src/canvas/drawing_utils.rs
@@ -1,124 +1,10 @@
 use tui::layout::Rect;
 
-use crate::app;
-use std::{
-    cmp::{max, min},
-    time::Instant,
-};
-
-/// Return a (hard)-width vector for column widths.
-///
-/// * `total_width` is the, well, total width available.  **NOTE:** This function automatically
-/// takes away 2 from the width as part of the left/right
-/// bounds.
-/// * `hard_widths` is inflexible column widths.  Use a `None` to represent a soft width.
-/// * `soft_widths_min` is the lower limit for a soft width.  Use `None` if a hard width goes there.
-/// * `soft_widths_max` is the upper limit for a soft width, in percentage of the total width.  Use
-///   `None` if a hard width goes there.
-/// * `soft_widths_desired` is the desired soft width.  Use `None` if a hard width goes there.
-/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
-///   false.
-///
-/// **NOTE:** This function ASSUMES THAT ALL PASSED SLICES ARE OF THE SAME SIZE.
-///
-/// **NOTE:** The returned vector may not be the same size as the slices, this is because including
-/// 0-constraints breaks tui-rs.
-pub fn get_column_widths(
-    total_width: u16, hard_widths: &[Option<u16>], soft_widths_min: &[Option<u16>],
-    soft_widths_max: &[Option<f64>], soft_widths_desired: &[Option<u16>], left_to_right: bool,
-) -> Vec<u16> {
-    debug_assert!(
-        hard_widths.len() == soft_widths_min.len(),
-        "hard width length != soft width min length!"
-    );
-    debug_assert!(
-        soft_widths_min.len() == soft_widths_max.len(),
-        "soft width min length != soft width max length!"
-    );
-    debug_assert!(
-        soft_widths_max.len() == soft_widths_desired.len(),
-        "soft width max length != soft width desired length!"
-    );
-
-    if total_width > 2 {
-        let initial_width = total_width - 2;
-        let mut total_width_left = initial_width;
-        let mut column_widths: Vec<u16> = vec![0; hard_widths.len()];
-        let range: Vec<usize> = if left_to_right {
-            (0..hard_widths.len()).collect()
-        } else {
-            (0..hard_widths.len()).rev().collect()
-        };
-
-        for itx in &range {
-            if let Some(Some(hard_width)) = hard_widths.get(*itx) {
-                // Hard width...
-                let space_taken = min(*hard_width, total_width_left);
-
-                // TODO [COLUMN MOVEMENT]: Remove this
-                if *hard_width > space_taken {
-                    break;
-                }
-
-                column_widths[*itx] = space_taken;
-                total_width_left -= space_taken;
-                total_width_left = total_width_left.saturating_sub(1);
-            } else if let (
-                Some(Some(soft_width_max)),
-                Some(Some(soft_width_min)),
-                Some(Some(soft_width_desired)),
-            ) = (
-                soft_widths_max.get(*itx),
-                soft_widths_min.get(*itx),
-                soft_widths_desired.get(*itx),
-            ) {
-                // Soft width...
-                let soft_limit = max(
-                    if soft_width_max.is_sign_negative() {
-                        *soft_width_desired
-                    } else {
-                        (*soft_width_max * initial_width as f64).ceil() as u16
-                    },
-                    *soft_width_min,
-                );
-                let space_taken = min(min(soft_limit, *soft_width_desired), total_width_left);
-
-                // TODO [COLUMN MOVEMENT]: Remove this
-                if *soft_width_min > space_taken {
-                    break;
-                }
-
-                column_widths[*itx] = space_taken;
-                total_width_left -= space_taken;
-                total_width_left = total_width_left.saturating_sub(1);
-            }
-        }
-
-        while let Some(0) = column_widths.last() {
-            column_widths.pop();
-        }
-
-        if !column_widths.is_empty() {
-            // Redistribute remaining.
-            let amount_per_slot = total_width_left / column_widths.len() as u16;
-            total_width_left %= column_widths.len() as u16;
-            for (index, width) in column_widths.iter_mut().enumerate() {
-                if index < total_width_left.into() {
-                    *width += amount_per_slot + 1;
-                } else {
-                    *width += amount_per_slot;
-                }
-            }
-        }
-
-        column_widths
-    } else {
-        vec![]
-    }
-}
+use crate::app::CursorDirection;
+use std::{cmp::min, time::Instant};
 
 pub fn get_search_start_position(
-    num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
+    num_columns: usize, cursor_direction: &CursorDirection, cursor_bar: &mut usize,
     current_cursor_position: usize, is_force_redraw: bool,
 ) -> usize {
     if is_force_redraw {
@@ -126,24 +12,24 @@ pub fn get_search_start_position(
     }
 
     match cursor_direction {
-        app::CursorDirection::Right => {
+        CursorDirection::Right => {
             if current_cursor_position < *cursor_bar + num_columns {
                 // If, using previous_scrolled_position, we can see the element
-                // (so within that and + num_rows) just reuse the current previously scrolled position
+                // (so within that and + num_rows) just reuse the current previously scrolled position.
                 *cursor_bar
             } else if current_cursor_position >= num_columns {
                 // Else if the current position past the last element visible in the list, omit
-                // until we can see that element
+                // until we can see that element.
                 *cursor_bar = current_cursor_position - num_columns;
                 *cursor_bar
             } else {
-                // Else, if it is not past the last element visible, do not omit anything
+                // Else, if it is not past the last element visible, do not omit anything.
                 0
             }
         }
-        app::CursorDirection::Left => {
+        CursorDirection::Left => {
             if current_cursor_position <= *cursor_bar {
-                // If it's past the first element, then show from that element downwards
+                // If it's past the first element, then show from that element downwards.
                 *cursor_bar = current_cursor_position;
             } else if current_cursor_position >= *cursor_bar + num_columns {
                 *cursor_bar = current_cursor_position - num_columns;
@@ -154,46 +40,9 @@ pub fn get_search_start_position(
     }
 }
 
-pub fn get_start_position(
-    num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize,
-    currently_selected_position: usize, is_force_redraw: bool,
-) -> usize {
-    if is_force_redraw {
-        *scroll_position_bar = 0;
-    }
-
-    match scroll_direction {
-        app::ScrollDirection::Down => {
-            if currently_selected_position < *scroll_position_bar + num_rows {
-                // If, using previous_scrolled_position, we can see the element
-                // (so within that and + num_rows) just reuse the current previously scrolled position
-                *scroll_position_bar
-            } else if currently_selected_position >= num_rows {
-                // Else if the current position past the last element visible in the list, omit
-                // until we can see that element
-                *scroll_position_bar = currently_selected_position - num_rows;
-                *scroll_position_bar
-            } else {
-                // Else, if it is not past the last element visible, do not omit anything
-                0
-            }
-        }
-        app::ScrollDirection::Up => {
-            if currently_selected_position <= *scroll_position_bar {
-                // If it's past the first element, then show from that element downwards
-                *scroll_position_bar = currently_selected_position;
-            } else if currently_selected_position >= *scroll_position_bar + num_rows {
-                *scroll_position_bar = currently_selected_position - num_rows;
-            }
-            // Else, don't change what our start position is from whatever it is set to!
-            *scroll_position_bar
-        }
-    }
-}
-
 /// Calculate how many bars are to be drawn within basic mode's components.
 pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
-    std::cmp::min(
+    min(
         (num_bars_available as f64 * use_percentage / 100.0).round() as usize,
         num_bars_available,
     )
@@ -224,62 +73,6 @@ mod test {
 
     use super::*;
 
-    #[test]
-    fn test_get_start_position() {
-        use crate::app::ScrollDirection::{self, Down, Up};
-
-        fn test(
-            bar: usize, num: usize, direction: ScrollDirection, selected: usize, force: bool,
-            expected_posn: usize, expected_bar: usize,
-        ) {
-            let mut bar = bar;
-            assert_eq!(
-                get_start_position(num, &direction, &mut bar, selected, force),
-                expected_posn
-            );
-            assert_eq!(bar, expected_bar);
-        }
-
-        // Scrolling down from start
-        test(0, 10, Down, 0, false, 0, 0);
-
-        // Simple scrolling down
-        test(0, 10, Down, 1, false, 0, 0);
-
-        // Scrolling down from the middle high up
-        test(0, 10, Down, 5, false, 0, 0);
-
-        // Scrolling down into boundary
-        test(0, 10, Down, 11, false, 1, 1);
-
-        // Scrolling down from the with non-zero bar
-        test(5, 10, Down, 15, false, 5, 5);
-
-        // Force redraw scrolling down (e.g. resize)
-        test(5, 15, Down, 15, true, 0, 0);
-
-        // Test jumping down
-        test(1, 10, Down, 20, true, 10, 10);
-
-        // Scrolling up from bottom
-        test(10, 10, Up, 20, false, 10, 10);
-
-        // Simple scrolling up
-        test(10, 10, Up, 19, false, 10, 10);
-
-        // Scrolling up from the middle
-        test(10, 10, Up, 10, false, 10, 10);
-
-        // Scrolling up into boundary
-        test(10, 10, Up, 9, false, 9, 9);
-
-        // Force redraw scrolling up (e.g. resize)
-        test(5, 10, Up, 15, true, 5, 5);
-
-        // Test jumping up
-        test(10, 10, Up, 0, false, 0, 0);
-    }
-
     #[test]
     fn test_calculate_basic_use_bars() {
         // Testing various breakpoints and edge cases.
@@ -327,49 +120,4 @@ mod test {
         ));
         assert!(over_timer.is_none());
     }
-
-    #[test]
-    fn test_zero_width() {
-        assert_eq!(
-            get_column_widths(
-                0,
-                &[Some(1), None, None],
-                &[None, Some(1), Some(2)],
-                &[None, Some(0.125), Some(0.5)],
-                &[None, Some(10), Some(10)],
-                true
-            ),
-            vec![],
-        );
-    }
-
-    #[test]
-    fn test_two_width() {
-        assert_eq!(
-            get_column_widths(
-                2,
-                &[Some(1), None, None],
-                &[None, Some(1), Some(2)],
-                &[None, Some(0.125), Some(0.5)],
-                &[None, Some(10), Some(10)],
-                true
-            ),
-            vec![],
-        );
-    }
-
-    #[test]
-    fn test_non_zero_width() {
-        assert_eq!(
-            get_column_widths(
-                16,
-                &[Some(1), None, None],
-                &[None, Some(1), Some(2)],
-                &[None, Some(0.125), Some(0.5)],
-                &[None, Some(10), Some(10)],
-                true
-            ),
-            vec![2, 2, 7],
-        );
-    }
 }
diff --git a/src/canvas/widgets/battery_display.rs b/src/canvas/widgets/battery_display.rs
index e020cf2c..48ed9c46 100644
--- a/src/canvas/widgets/battery_display.rs
+++ b/src/canvas/widgets/battery_display.rs
@@ -69,7 +69,7 @@ impl Painter {
             };
 
             let battery_names = app_state
-                .canvas_data
+                .converted_data
                 .battery_data
                 .iter()
                 .map(|battery| &battery.battery_name)
@@ -106,7 +106,7 @@ impl Painter {
                 .split(draw_loc)[0];
 
             if let Some(battery_details) = app_state
-                .canvas_data
+                .converted_data
                 .battery_data
                 .get(battery_widget_state.currently_selected_battery_index)
             {
diff --git a/src/canvas/widgets/cpu_basic.rs b/src/canvas/widgets/cpu_basic.rs
index aef2187b..27a05586 100644
--- a/src/canvas/widgets/cpu_basic.rs
+++ b/src/canvas/widgets/cpu_basic.rs
@@ -20,8 +20,8 @@ impl Painter {
         &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
     ) {
         // Skip the first element, it's the "all" element
-        if app_state.canvas_data.cpu_data.len() > 1 {
-            let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data[1..];
+        if app_state.converted_data.cpu_data.len() > 1 {
+            let cpu_data: &[ConvertedCpuData] = &app_state.converted_data.cpu_data[1..];
 
             // This is a bit complicated, but basically, we want to draw SOME number
             // of columns to draw all CPUs.  Ideally, as well, we want to not have
diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs
index fcb7daa2..f482b75b 100644
--- a/src/canvas/widgets/cpu_graph.rs
+++ b/src/canvas/widgets/cpu_graph.rs
@@ -1,38 +1,34 @@
-use std::borrow::Cow;
+use std::{borrow::Cow, iter};
 
 use crate::{
-    app::{layout_manager::WidgetDirection, App},
+    app::{layout_manager::WidgetDirection, App, CellContent, CpuWidgetState},
     canvas::{
-        components::{GraphData, TimeGraph},
-        drawing_utils::{get_column_widths, get_start_position, should_hide_x_label},
+        components::{GraphData, TextTable, TimeGraph},
+        drawing_utils::should_hide_x_label,
         Painter,
     },
-    constants::*,
-    data_conversion::ConvertedCpuData,
+    data_conversion::{ConvertedCpuData, TableData, TableRow},
 };
 
 use concat_string::concat_string;
 
+use itertools::Either;
 use tui::{
     backend::Backend,
     layout::{Constraint, Direction, Layout, Rect},
     terminal::Frame,
-    text::Text,
-    widgets::{Block, Borders, Row, Table},
 };
 
-const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
 const AVG_POSITION: usize = 1;
 const ALL_POSITION: usize = 0;
 
-static CPU_LEGEND_HEADER_LENS: [usize; 2] =
-    [CPU_LEGEND_HEADER[0].len(), CPU_LEGEND_HEADER[1].len()];
-
 impl Painter {
     pub fn draw_cpu<B: Backend>(
         &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
     ) {
-        if draw_loc.width as f64 * 0.15 <= 6.0 {
+        let legend_width = (draw_loc.width as f64 * 0.15) as u16;
+
+        if legend_width < 6 {
             // Skip drawing legend
             if app_state.current_widget.widget_id == (widget_id + 1) {
                 if app_state.app_config_fields.left_legend {
@@ -55,18 +51,25 @@ impl Painter {
                 }
             }
         } else {
+            let graph_width = draw_loc.width - legend_width;
             let (graph_index, legend_index, constraints) =
                 if app_state.app_config_fields.left_legend {
                     (
                         1,
                         0,
-                        [Constraint::Percentage(15), Constraint::Percentage(85)],
+                        [
+                            Constraint::Length(legend_width),
+                            Constraint::Length(graph_width),
+                        ],
                     )
                 } else {
                     (
                         0,
                         1,
-                        [Constraint::Percentage(85), Constraint::Percentage(15)],
+                        [
+                            Constraint::Length(graph_width),
+                            Constraint::Length(legend_width),
+                        ],
                     )
                 };
 
@@ -115,6 +118,56 @@ impl Painter {
         }
     }
 
+    fn generate_points<'a>(
+        &self, cpu_widget_state: &CpuWidgetState, cpu_data: &'a [ConvertedCpuData],
+        show_avg_cpu: bool,
+    ) -> Vec<GraphData<'a>> {
+        let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };
+
+        let current_scroll_position = cpu_widget_state.table_state.current_scroll_position;
+        if current_scroll_position == ALL_POSITION {
+            // This case ensures the other cases cannot have the position be equal to 0.
+            cpu_data
+                .iter()
+                .enumerate()
+                .rev()
+                .map(|(itx, cpu)| {
+                    let style = if show_avg_cpu && itx == AVG_POSITION {
+                        self.colours.avg_colour_style
+                    } else if itx == ALL_POSITION {
+                        self.colours.all_colour_style
+                    } else {
+                        let offset_position = itx - 1; // Because of the all position
+                        self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
+                            % self.colours.cpu_colour_styles.len()]
+                    };
+
+                    GraphData {
+                        points: &cpu.cpu_data[..],
+                        style,
+                        name: None,
+                    }
+                })
+                .collect::<Vec<_>>()
+        } else if let Some(cpu) = cpu_data.get(current_scroll_position) {
+            let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {
+                self.colours.avg_colour_style
+            } else {
+                let offset_position = current_scroll_position - 1; // Because of the all position
+                self.colours.cpu_colour_styles
+                    [(offset_position - show_avg_offset) % self.colours.cpu_colour_styles.len()]
+            };
+
+            vec![GraphData {
+                points: &cpu.cpu_data[..],
+                style,
+                name: None,
+            }]
+        } else {
+            vec![]
+        }
+    }
+
     fn draw_cpu_graph<B: Backend>(
         &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
     ) {
@@ -122,7 +175,7 @@ impl Painter {
         const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed("  0%"), Cow::Borrowed("100%")];
 
         if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) {
-            let cpu_data = &app_state.canvas_data.cpu_data;
+            let cpu_data = &app_state.converted_data.cpu_data;
             let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
             let x_bounds = [0, cpu_widget_state.current_display_time];
             let hide_x_labels = should_hide_x_label(
@@ -131,56 +184,16 @@ impl Painter {
                 &mut cpu_widget_state.autohide_timer,
                 draw_loc,
             );
-            let show_avg_cpu = app_state.app_config_fields.show_average_cpu;
-            let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };
-            let points = {
-                let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position;
-                if current_scroll_position == ALL_POSITION {
-                    // This case ensures the other cases cannot have the position be equal to 0.
-                    cpu_data
-                        .iter()
-                        .enumerate()
-                        .rev()
-                        .map(|(itx, cpu)| {
-                            let style = if show_avg_cpu && itx == AVG_POSITION {
-                                self.colours.avg_colour_style
-                            } else if itx == ALL_POSITION {
-                                self.colours.all_colour_style
-                            } else {
-                                let offset_position = itx - 1; // Because of the all position
-                                self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
-                                    % self.colours.cpu_colour_styles.len()]
-                            };
 
-                            GraphData {
-                                points: &cpu.cpu_data[..],
-                                style,
-                                name: None,
-                            }
-                        })
-                        .collect::<Vec<_>>()
-                } else if let Some(cpu) = cpu_data.get(current_scroll_position) {
-                    let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {
-                        self.colours.avg_colour_style
-                    } else {
-                        let offset_position = current_scroll_position - 1; // Because of the all position
-                        self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
-                            % self.colours.cpu_colour_styles.len()]
-                    };
-
-                    vec![GraphData {
-                        points: &cpu.cpu_data[..],
-                        style,
-                        name: None,
-                    }]
-                } else {
-                    vec![]
-                }
-            };
+            let points = self.generate_points(
+                cpu_widget_state,
+                cpu_data,
+                app_state.app_config_fields.show_average_cpu,
+            );
 
             // TODO: Maybe hide load avg if too long? Or maybe the CPU part.
             let title = if cfg!(target_family = "unix") {
-                let load_avg = app_state.canvas_data.load_avg_data;
+                let load_avg = app_state.converted_data.load_avg_data;
                 let load_avg_str = format!(
                     "─ {:.2} {:.2} {:.2} ",
                     load_avg[0], load_avg[1], load_avg[2]
@@ -214,148 +227,86 @@ impl Painter {
         let recalculate_column_widths = app_state.should_get_widget_bounds();
         if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&(widget_id - 1))
         {
+            // TODO: This line (and the one above, see caller) is pretty dumb but I guess needed.
             cpu_widget_state.is_legend_hidden = false;
-            let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data;
-            let cpu_table_state = &mut cpu_widget_state.scroll_state.table_state;
-            let is_on_widget = widget_id == app_state.current_widget.widget_id;
-            let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
-                0
-            } else {
-                app_state.app_config_fields.table_gap
-            };
-            let start_position = get_start_position(
-                usize::from(
-                    (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
-                ),
-                &cpu_widget_state.scroll_state.scroll_direction,
-                &mut cpu_widget_state.scroll_state.scroll_bar,
-                cpu_widget_state.scroll_state.current_scroll_position,
-                app_state.is_force_redraw,
-            );
-            cpu_table_state.select(Some(
-                cpu_widget_state
-                    .scroll_state
-                    .current_scroll_position
-                    .saturating_sub(start_position),
-            ));
 
-            let sliced_cpu_data = &cpu_data[start_position..];
-
-            let offset_scroll_index = cpu_widget_state
-                .scroll_state
-                .current_scroll_position
-                .saturating_sub(start_position);
             let show_avg_cpu = app_state.app_config_fields.show_average_cpu;
-
-            // Calculate widths
-            if recalculate_column_widths {
-                cpu_widget_state.table_width_state.desired_column_widths = vec![6, 4];
-                cpu_widget_state.table_width_state.calculated_column_widths = get_column_widths(
-                    draw_loc.width,
-                    &[None, None],
-                    &(CPU_LEGEND_HEADER_LENS
-                        .iter()
-                        .map(|width| Some(*width as u16))
-                        .collect::<Vec<_>>()),
-                    &[Some(0.5), Some(0.5)],
-                    &(cpu_widget_state
-                        .table_width_state
-                        .desired_column_widths
-                        .iter()
-                        .map(|width| Some(*width))
-                        .collect::<Vec<_>>()),
-                    false,
-                );
-            }
-
-            let dcw = &cpu_widget_state.table_width_state.desired_column_widths;
-            let ccw = &cpu_widget_state.table_width_state.calculated_column_widths;
-            let cpu_rows = sliced_cpu_data.iter().enumerate().map(|(itx, cpu)| {
-                let mut truncated_name =
-                    if let (Some(desired_column_width), Some(calculated_column_width)) =
-                        (dcw.get(0), ccw.get(0))
-                    {
-                        if *desired_column_width > *calculated_column_width {
-                            Text::raw(&cpu.short_cpu_name)
-                        } else {
-                            Text::raw(&cpu.cpu_name)
-                        }
-                    } else {
-                        Text::raw(&cpu.cpu_name)
-                    };
-
-                let is_first_column_hidden = if let Some(calculated_column_width) = ccw.get(0) {
-                    *calculated_column_width == 0
+            let cpu_data = {
+                let col_widths = vec![1, 3]; // TODO: Should change this to take const generics (usize) and an array.
+                let colour_iter = if show_avg_cpu {
+                    Either::Left(
+                        iter::once(&self.colours.all_colour_style)
+                            .chain(iter::once(&self.colours.avg_colour_style))
+                            .chain(self.colours.cpu_colour_styles.iter().cycle()),
+                    )
                 } else {
-                    false
+                    Either::Right(
+                        iter::once(&self.colours.all_colour_style)
+                            .chain(self.colours.cpu_colour_styles.iter().cycle()),
+                    )
                 };
 
-                let truncated_legend = if is_first_column_hidden && cpu.legend_value.is_empty() {
-                    // For the case where we only have room for one column, display "All" in the normally blank area.
-                    Text::raw("All")
-                } else {
-                    Text::raw(&cpu.legend_value)
-                };
-
-                if !is_first_column_hidden
-                    && itx == offset_scroll_index
-                    && itx + start_position == ALL_POSITION
-                {
-                    truncated_name.patch_style(self.colours.currently_selected_text_style);
-                    Row::new(vec![truncated_name, truncated_legend])
-                } else {
-                    let cpu_string_row = vec![truncated_name, truncated_legend];
-
-                    Row::new(cpu_string_row).style(if itx == offset_scroll_index {
-                        self.colours.currently_selected_text_style
-                    } else if itx + start_position == ALL_POSITION {
-                        self.colours.all_colour_style
-                    } else if show_avg_cpu {
-                        if itx + start_position == AVG_POSITION {
-                            self.colours.avg_colour_style
-                        } else {
-                            self.colours.cpu_colour_styles[(itx + start_position
-                                - AVG_POSITION
-                                - 1)
-                                % self.colours.cpu_colour_styles.len()]
-                        }
+                let data = {
+                    let iter = app_state.converted_data.cpu_data.iter().zip(colour_iter);
+                    const CPU_WIDTH_CHECK: u16 = 10; // This is hard-coded, it's terrible.
+                    if draw_loc.width < CPU_WIDTH_CHECK {
+                        Either::Left(iter.map(|(cpu, style)| {
+                            let row = vec![
+                                CellContent::Simple("".into()),
+                                CellContent::Simple(if cpu.legend_value.is_empty() {
+                                    cpu.cpu_name.clone().into()
+                                } else {
+                                    cpu.legend_value.clone().into()
+                                }),
+                            ];
+                            TableRow::Styled(row, *style)
+                        }))
                     } else {
-                        self.colours.cpu_colour_styles[(itx + start_position - ALL_POSITION - 1)
-                            % self.colours.cpu_colour_styles.len()]
-                    })
+                        Either::Right(iter.map(|(cpu, style)| {
+                            let row = vec![
+                                CellContent::HasAlt {
+                                    alt: cpu.short_cpu_name.clone().into(),
+                                    main: cpu.cpu_name.clone().into(),
+                                },
+                                CellContent::Simple(cpu.legend_value.clone().into()),
+                            ];
+                            TableRow::Styled(row, *style)
+                        }))
+                    }
                 }
-            });
+                .collect();
 
-            // Note we don't set highlight_style, as it should always be shown for this widget.
-            let border_and_title_style = if is_on_widget {
+                TableData { data, col_widths }
+            };
+
+            let is_on_widget = widget_id == app_state.current_widget.widget_id;
+            let border_style = if is_on_widget {
                 self.colours.highlighted_border_style
             } else {
                 self.colours.border_style
             };
 
-            // Draw
-            f.render_stateful_widget(
-                Table::new(cpu_rows)
-                    .block(
-                        Block::default()
-                            .borders(Borders::ALL)
-                            .border_style(border_and_title_style),
-                    )
-                    .header(
-                        Row::new(CPU_LEGEND_HEADER.to_vec())
-                            .style(self.colours.table_header_style)
-                            .bottom_margin(table_gap),
-                    )
-                    .widths(
-                        &(cpu_widget_state
-                            .table_width_state
-                            .calculated_column_widths
-                            .iter()
-                            .map(|calculated_width| Constraint::Length(*calculated_width as u16))
-                            .collect::<Vec<_>>()),
-                    ),
+            TextTable {
+                table_gap: app_state.app_config_fields.table_gap,
+                is_force_redraw: app_state.is_force_redraw,
+                recalculate_column_widths,
+                header_style: self.colours.table_header_style,
+                border_style,
+                highlighted_text_style: self.colours.currently_selected_text_style, // We always highlight the selected CPU entry... not sure if I like this though.
+                title: None,
+                is_on_widget,
+                draw_border: true,
+                show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
+                title_style: self.colours.widget_title_style,
+                text_style: self.colours.text_style,
+                left_to_right: false,
+            }
+            .draw_text_table(
+                f,
                 draw_loc,
-                cpu_table_state,
+                &mut cpu_widget_state.table_state,
+                &cpu_data,
+                None,
             );
         }
     }
diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs
index 252ec70b..1e52cc5f 100644
--- a/src/canvas/widgets/disk_table.rs
+++ b/src/canvas/widgets/disk_table.rs
@@ -1,31 +1,12 @@
-use once_cell::sync::Lazy;
-use tui::{
-    backend::Backend,
-    layout::{Constraint, Direction, Layout, Rect},
-    terminal::Frame,
-    text::Span,
-    text::{Spans, Text},
-    widgets::{Block, Borders, Row, Table},
-};
+use tui::{backend::Backend, layout::Rect, terminal::Frame};
 
 use crate::{
     app,
     canvas::{
-        drawing_utils::{get_column_widths, get_start_position},
+        components::{TextTable, TextTableTitle},
         Painter,
     },
-    constants::*,
 };
-use unicode_segmentation::UnicodeSegmentation;
-
-const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"];
-
-static DISK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
-    DISK_HEADERS
-        .iter()
-        .map(|entry| entry.len() as u16)
-        .collect::<Vec<_>>()
-});
 
 impl Painter {
     pub fn draw_disk_table<B: Backend>(
@@ -34,120 +15,8 @@ impl Painter {
     ) {
         let recalculate_column_widths = app_state.should_get_widget_bounds();
         if let Some(disk_widget_state) = app_state.disk_state.widget_states.get_mut(&widget_id) {
-            let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
-                0
-            } else {
-                app_state.app_config_fields.table_gap
-            };
-            let start_position = get_start_position(
-                usize::from(
-                    (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
-                ),
-                &disk_widget_state.scroll_state.scroll_direction,
-                &mut disk_widget_state.scroll_state.scroll_bar,
-                disk_widget_state.scroll_state.current_scroll_position,
-                app_state.is_force_redraw,
-            );
             let is_on_widget = app_state.current_widget.widget_id == widget_id;
-            let disk_table_state = &mut disk_widget_state.scroll_state.table_state;
-            disk_table_state.select(Some(
-                disk_widget_state
-                    .scroll_state
-                    .current_scroll_position
-                    .saturating_sub(start_position),
-            ));
-            let sliced_vec = &app_state.canvas_data.disk_data[start_position..];
-
-            // Calculate widths
-            let hard_widths = [None, None, Some(4), Some(6), Some(6), Some(7), Some(7)];
-            if recalculate_column_widths {
-                disk_widget_state.table_width_state.desired_column_widths = {
-                    let mut column_widths = DISK_HEADERS_LENS.clone();
-                    for row in sliced_vec {
-                        for (col, entry) in row.iter().enumerate() {
-                            if entry.len() as u16 > column_widths[col] {
-                                column_widths[col] = entry.len() as u16;
-                            }
-                        }
-                    }
-                    column_widths
-                };
-                disk_widget_state.table_width_state.desired_column_widths = disk_widget_state
-                    .table_width_state
-                    .desired_column_widths
-                    .iter()
-                    .zip(&hard_widths)
-                    .map(|(current, hard)| {
-                        if let Some(hard) = hard {
-                            if *hard > *current {
-                                *hard
-                            } else {
-                                *current
-                            }
-                        } else {
-                            *current
-                        }
-                    })
-                    .collect::<Vec<_>>();
-
-                disk_widget_state.table_width_state.calculated_column_widths = get_column_widths(
-                    draw_loc.width,
-                    &hard_widths,
-                    &(DISK_HEADERS_LENS
-                        .iter()
-                        .map(|w| Some(*w))
-                        .collect::<Vec<_>>()),
-                    &[Some(0.2), Some(0.2), None, None, None, None, None],
-                    &(disk_widget_state
-                        .table_width_state
-                        .desired_column_widths
-                        .iter()
-                        .map(|w| Some(*w))
-                        .collect::<Vec<_>>()),
-                    true,
-                );
-            }
-
-            let dcw = &disk_widget_state.table_width_state.desired_column_widths;
-            let ccw = &disk_widget_state.table_width_state.calculated_column_widths;
-            let disk_rows =
-                sliced_vec.iter().map(|disk_row| {
-                    let truncated_data = disk_row.iter().zip(&hard_widths).enumerate().map(
-                        |(itx, (entry, width))| {
-                            if width.is_none() {
-                                if let (Some(desired_col_width), Some(calculated_col_width)) =
-                                    (dcw.get(itx), ccw.get(itx))
-                                {
-                                    if *desired_col_width > *calculated_col_width
-                                        && *calculated_col_width > 0
-                                    {
-                                        let calculated_col_width: usize =
-                                            (*calculated_col_width).into();
-
-                                        let graphemes =
-                                            UnicodeSegmentation::graphemes(entry.as_str(), true)
-                                                .collect::<Vec<&str>>();
-
-                                        if graphemes.len() > calculated_col_width
-                                            && calculated_col_width > 1
-                                        {
-                                            // Truncate with ellipsis
-                                            let first_n =
-                                                graphemes[..(calculated_col_width - 1)].concat();
-                                            return Text::raw(format!("{}…", first_n));
-                                        }
-                                    }
-                                }
-                            }
-
-                            Text::raw(entry)
-                        },
-                    );
-
-                    Row::new(truncated_data)
-                });
-
-            let (border_style, highlight_style) = if is_on_widget {
+            let (border_style, highlighted_text_style) = if is_on_widget {
                 (
                     self.colours.highlighted_border_style,
                     self.colours.currently_selected_text_style,
@@ -155,117 +24,31 @@ impl Painter {
             } else {
                 (self.colours.border_style, self.colours.text_style)
             };
-
-            let title_base = if app_state.app_config_fields.show_table_scroll_position {
-                let title_string = format!(
-                    " Disk ({} of {}) ",
-                    disk_widget_state
-                        .scroll_state
-                        .current_scroll_position
-                        .saturating_add(1),
-                    app_state.canvas_data.disk_data.len()
-                );
-
-                if title_string.len() <= draw_loc.width.into() {
-                    title_string
-                } else {
-                    " Disk ".to_string()
-                }
-            } else {
-                " Disk ".to_string()
-            };
-
-            let title = if app_state.is_expanded {
-                const ESCAPE_ENDING: &str = "── Esc to go back ";
-
-                let (chosen_title_base, expanded_title_base) = {
-                    let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING);
-
-                    if temp_title_base.len() > draw_loc.width.into() {
-                        (
-                            " Disk ".to_string(),
-                            format!("{}{}", " Disk ", ESCAPE_ENDING),
-                        )
-                    } else {
-                        (title_base, temp_title_base)
-                    }
-                };
-
-                Spans::from(vec![
-                    Span::styled(chosen_title_base, self.colours.widget_title_style),
-                    Span::styled(
-                        format!(
-                            "─{}─ Esc to go back ",
-                            "─".repeat(
-                                usize::from(draw_loc.width).saturating_sub(
-                                    UnicodeSegmentation::graphemes(
-                                        expanded_title_base.as_str(),
-                                        true
-                                    )
-                                    .count()
-                                        + 2
-                                )
-                            )
-                        ),
-                        border_style,
-                    ),
-                ])
-            } else {
-                Spans::from(Span::styled(title_base, self.colours.widget_title_style))
-            };
-
-            let disk_block = if draw_border {
-                Block::default()
-                    .title(title)
-                    .borders(Borders::ALL)
-                    .border_style(border_style)
-            } else if is_on_widget {
-                Block::default()
-                    .borders(SIDE_BORDERS)
-                    .border_style(self.colours.highlighted_border_style)
-            } else {
-                Block::default().borders(Borders::NONE)
-            };
-
-            let margined_draw_loc = Layout::default()
-                .constraints([Constraint::Percentage(100)])
-                .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
-                .direction(Direction::Horizontal)
-                .split(draw_loc)[0];
-
-            // Draw!
-            f.render_stateful_widget(
-                Table::new(disk_rows)
-                    .block(disk_block)
-                    .header(
-                        Row::new(DISK_HEADERS.to_vec())
-                            .style(self.colours.table_header_style)
-                            .bottom_margin(table_gap),
-                    )
-                    .highlight_style(highlight_style)
-                    .style(self.colours.text_style)
-                    .widths(
-                        &(disk_widget_state
-                            .table_width_state
-                            .calculated_column_widths
-                            .iter()
-                            .map(|calculated_width| Constraint::Length(*calculated_width))
-                            .collect::<Vec<_>>()),
-                    ),
-                margined_draw_loc,
-                disk_table_state,
-            );
-
-            if app_state.should_get_widget_bounds() {
-                // Update draw loc in widget map
-                if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
-                    widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
-                    widget.bottom_right_corner = Some((
-                        margined_draw_loc.x + margined_draw_loc.width,
-                        margined_draw_loc.y + margined_draw_loc.height,
-                    ));
-                }
+            TextTable {
+                table_gap: app_state.app_config_fields.table_gap,
+                is_force_redraw: app_state.is_force_redraw,
+                recalculate_column_widths,
+                header_style: self.colours.table_header_style,
+                border_style,
+                highlighted_text_style,
+                title: Some(TextTableTitle {
+                    title: " Disks ".into(),
+                    is_expanded: app_state.is_expanded,
+                }),
+                is_on_widget,
+                draw_border,
+                show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
+                title_style: self.colours.widget_title_style,
+                text_style: self.colours.text_style,
+                left_to_right: true,
             }
+            .draw_text_table(
+                f,
+                draw_loc,
+                &mut disk_widget_state.table_state,
+                &app_state.converted_data.disk_data,
+                app_state.widget_map.get_mut(&widget_id),
+            );
         }
     }
 }
diff --git a/src/canvas/widgets/mem_basic.rs b/src/canvas/widgets/mem_basic.rs
index 16d79572..ae2b7f1f 100644
--- a/src/canvas/widgets/mem_basic.rs
+++ b/src/canvas/widgets/mem_basic.rs
@@ -17,8 +17,8 @@ impl Painter {
     pub fn draw_basic_memory<B: Backend>(
         &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
     ) {
-        let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data;
-        let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data;
+        let mem_data: &[(f64, f64)] = &app_state.converted_data.mem_data;
+        let swap_data: &[(f64, f64)] = &app_state.converted_data.swap_data;
 
         let margined_loc = Layout::default()
             .constraints([Constraint::Percentage(100)])
@@ -48,14 +48,14 @@ impl Painter {
         const EMPTY_MEMORY_FRAC_STRING: &str = "0.0B/0.0B";
 
         let trimmed_memory_frac =
-            if let Some((_label_percent, label_frac)) = &app_state.canvas_data.mem_labels {
+            if let Some((_label_percent, label_frac)) = &app_state.converted_data.mem_labels {
                 label_frac.trim()
             } else {
                 EMPTY_MEMORY_FRAC_STRING
             };
 
         let trimmed_swap_frac =
-            if let Some((_label_percent, label_frac)) = &app_state.canvas_data.swap_labels {
+            if let Some((_label_percent, label_frac)) = &app_state.converted_data.swap_labels {
                 label_frac.trim()
             } else {
                 EMPTY_MEMORY_FRAC_STRING
diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/widgets/mem_graph.rs
index e5d0fbcc..442044aa 100644
--- a/src/canvas/widgets/mem_graph.rs
+++ b/src/canvas/widgets/mem_graph.rs
@@ -33,18 +33,18 @@ impl Painter {
             );
             let points = {
                 let mut points = Vec::with_capacity(2);
-                if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels {
+                if let Some((label_percent, label_frac)) = &app_state.converted_data.mem_labels {
                     let mem_label = format!("RAM:{}{}", label_percent, label_frac);
                     points.push(GraphData {
-                        points: &app_state.canvas_data.mem_data,
+                        points: &app_state.converted_data.mem_data,
                         style: self.colours.ram_style,
                         name: Some(mem_label.into()),
                     });
                 }
-                if let Some((label_percent, label_frac)) = &app_state.canvas_data.swap_labels {
+                if let Some((label_percent, label_frac)) = &app_state.converted_data.swap_labels {
                     let swap_label = format!("SWP:{}{}", label_percent, label_frac);
                     points.push(GraphData {
-                        points: &app_state.canvas_data.swap_data,
+                        points: &app_state.converted_data.swap_data,
                         style: self.colours.swap_style,
                         name: Some(swap_label.into()),
                     });
diff --git a/src/canvas/widgets/network_basic.rs b/src/canvas/widgets/network_basic.rs
index 8a5c5e50..b58da96f 100644
--- a/src/canvas/widgets/network_basic.rs
+++ b/src/canvas/widgets/network_basic.rs
@@ -38,10 +38,10 @@ impl Painter {
             );
         }
 
-        let rx_label = format!("RX: {}", &app_state.canvas_data.rx_display);
-        let tx_label = format!("TX: {}", &app_state.canvas_data.tx_display);
-        let total_rx_label = format!("Total RX: {}", &app_state.canvas_data.total_rx_display);
-        let total_tx_label = format!("Total TX: {}", &app_state.canvas_data.total_tx_display);
+        let rx_label = format!("RX: {}", &app_state.converted_data.rx_display);
+        let tx_label = format!("TX: {}", &app_state.converted_data.tx_display);
+        let total_rx_label = format!("Total RX: {}", &app_state.converted_data.total_rx_display);
+        let total_tx_label = format!("Total TX: {}", &app_state.converted_data.total_tx_display);
 
         let net_text = vec![
             Spans::from(Span::styled(rx_label, self.colours.rx_style)),
diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs
index 2c3256f0..f96be42f 100644
--- a/src/canvas/widgets/network_graph.rs
+++ b/src/canvas/widgets/network_graph.rs
@@ -1,14 +1,10 @@
-use once_cell::sync::Lazy;
-use std::cmp::max;
-
 use crate::{
     app::{App, AxisScaling},
     canvas::{
         components::{GraphData, TimeGraph},
-        drawing_utils::{get_column_widths, should_hide_x_label},
+        drawing_utils::should_hide_x_label,
         Painter, Point,
     },
-    constants::*,
     units::data_units::DataUnit,
     utils::gen_util::*,
 };
@@ -21,26 +17,18 @@ use tui::{
     widgets::{Block, Borders, Row, Table},
 };
 
-const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"];
-
-static NETWORK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
-    NETWORK_HEADERS
-        .iter()
-        .map(|entry| entry.len() as u16)
-        .collect::<Vec<_>>()
-});
-
 impl Painter {
     pub fn draw_network<B: Backend>(
         &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
     ) {
         if app_state.app_config_fields.use_old_network_legend {
+            const LEGEND_HEIGHT: u16 = 4;
             let network_chunk = Layout::default()
                 .direction(Direction::Vertical)
                 .margin(0)
                 .constraints([
-                    Constraint::Length(max(draw_loc.height as i64 - 5, 0) as u16),
-                    Constraint::Length(5),
+                    Constraint::Length(draw_loc.height.saturating_sub(LEGEND_HEIGHT)),
+                    Constraint::Length(LEGEND_HEIGHT),
                 ])
                 .split(draw_loc);
 
@@ -67,8 +55,8 @@ impl Painter {
         hide_legend: bool,
     ) {
         if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) {
-            let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx;
-            let network_data_tx: &[(f64, f64)] = &app_state.canvas_data.network_data_tx;
+            let network_data_rx: &[(f64, f64)] = &app_state.converted_data.network_data_rx;
+            let network_data_tx: &[(f64, f64)] = &app_state.converted_data.network_data_tx;
             let time_start = -(network_widget_state.current_display_time as f64);
             let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
             let x_bounds = [0, network_widget_state.current_display_time];
@@ -115,18 +103,18 @@ impl Painter {
                     GraphData {
                         points: network_data_rx,
                         style: self.colours.rx_style,
-                        name: Some(format!("RX: {:7}", app_state.canvas_data.rx_display).into()),
+                        name: Some(format!("RX: {:7}", app_state.converted_data.rx_display).into()),
                     },
                     GraphData {
                         points: network_data_tx,
                         style: self.colours.tx_style,
-                        name: Some(format!("TX: {:7}", app_state.canvas_data.tx_display).into()),
+                        name: Some(format!("TX: {:7}", app_state.converted_data.tx_display).into()),
                     },
                     GraphData {
                         points: &[],
                         style: self.colours.total_rx_style,
                         name: Some(
-                            format!("Total RX: {:7}", app_state.canvas_data.total_rx_display)
+                            format!("Total RX: {:7}", app_state.converted_data.total_rx_display)
                                 .into(),
                         ),
                     },
@@ -134,7 +122,7 @@ impl Painter {
                         points: &[],
                         style: self.colours.total_tx_style,
                         name: Some(
-                            format!("Total TX: {:7}", app_state.canvas_data.total_tx_display)
+                            format!("Total TX: {:7}", app_state.converted_data.total_tx_display)
                                 .into(),
                         ),
                     },
@@ -144,12 +132,12 @@ impl Painter {
                     GraphData {
                         points: network_data_rx,
                         style: self.colours.rx_style,
-                        name: Some((&app_state.canvas_data.rx_display).into()),
+                        name: Some((&app_state.converted_data.rx_display).into()),
                     },
                     GraphData {
                         points: network_data_tx,
                         style: self.colours.tx_style,
-                        name: Some((&app_state.canvas_data.tx_display).into()),
+                        name: Some((&app_state.converted_data.tx_display).into()),
                     },
                 ]
             };
@@ -174,52 +162,25 @@ impl Painter {
     fn draw_network_labels<B: Backend>(
         &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
     ) {
-        let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
-            0
-        } else {
-            app_state.app_config_fields.table_gap
-        };
+        const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"];
 
-        let rx_display = &app_state.canvas_data.rx_display;
-        let tx_display = &app_state.canvas_data.tx_display;
-        let total_rx_display = &app_state.canvas_data.total_rx_display;
-        let total_tx_display = &app_state.canvas_data.total_tx_display;
+        let rx_display = &app_state.converted_data.rx_display;
+        let tx_display = &app_state.converted_data.tx_display;
+        let total_rx_display = &app_state.converted_data.total_rx_display;
+        let total_tx_display = &app_state.converted_data.total_tx_display;
 
         // Gross but I need it to work...
-        let total_network = vec![vec![
-            Text::raw(rx_display),
-            Text::raw(tx_display),
-            Text::raw(total_rx_display),
-            Text::raw(total_tx_display),
-        ]];
-        let mapped_network = total_network
-            .into_iter()
-            .map(|val| Row::new(val).style(self.colours.text_style));
-
-        // Calculate widths
-        let intrinsic_widths = get_column_widths(
-            draw_loc.width,
-            &[None, None, None, None],
-            &(NETWORK_HEADERS_LENS
-                .iter()
-                .map(|s| Some(*s))
-                .collect::<Vec<_>>()),
-            &[Some(0.25); 4],
-            &(NETWORK_HEADERS_LENS
-                .iter()
-                .map(|s| Some(*s))
-                .collect::<Vec<_>>()),
-            true,
-        );
+        let total_network = vec![Row::new(vec![
+            Text::styled(rx_display, self.colours.rx_style),
+            Text::styled(tx_display, self.colours.tx_style),
+            Text::styled(total_rx_display, self.colours.total_rx_style),
+            Text::styled(total_tx_display, self.colours.total_tx_style),
+        ])];
 
         // Draw
         f.render_widget(
-            Table::new(mapped_network)
-                .header(
-                    Row::new(NETWORK_HEADERS.to_vec())
-                        .style(self.colours.table_header_style)
-                        .bottom_margin(table_gap),
-                )
+            Table::new(total_network)
+                .header(Row::new(NETWORK_HEADERS.to_vec()).style(self.colours.table_header_style))
                 .block(Block::default().borders(Borders::ALL).border_style(
                     if app_state.current_widget.widget_id == widget_id {
                         self.colours.highlighted_border_style
@@ -229,9 +190,9 @@ impl Painter {
                 ))
                 .style(self.colours.text_style)
                 .widths(
-                    &(intrinsic_widths
-                        .iter()
-                        .map(|calculated_width| Constraint::Length(*calculated_width))
+                    &((std::iter::repeat(draw_loc.width.saturating_sub(2) / 4))
+                        .take(4)
+                        .map(Constraint::Length)
                         .collect::<Vec<_>>()),
                 ),
             draw_loc,
@@ -295,7 +256,7 @@ fn get_max_entry(
         (None, Some(filtered_tx)) => {
             match filtered_tx
                 .iter()
-                .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
+                .max_by(|(_, data_a), (_, data_b)| partial_ordering(data_a, data_b))
             {
                 Some((best_time, max_val)) => {
                     if *max_val == 0.0 {
@@ -316,7 +277,7 @@ fn get_max_entry(
         (Some(filtered_rx), None) => {
             match filtered_rx
                 .iter()
-                .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
+                .max_by(|(_, data_a), (_, data_b)| partial_ordering(data_a, data_b))
             {
                 Some((best_time, max_val)) => {
                     if *max_val == 0.0 {
@@ -338,7 +299,7 @@ fn get_max_entry(
             match filtered_rx
                 .iter()
                 .chain(filtered_tx)
-                .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
+                .max_by(|(_, data_a), (_, data_b)| partial_ordering(data_a, data_b))
             {
                 Some((best_time, max_val)) => {
                     if *max_val == 0.0 {
diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs
index 4e7ca9f9..b38d09cb 100644
--- a/src/canvas/widgets/process_table.rs
+++ b/src/canvas/widgets/process_table.rs
@@ -1,106 +1,40 @@
 use crate::{
     app::App,
     canvas::{
-        drawing_utils::{get_column_widths, get_search_start_position, get_start_position},
+        components::{TextTable, TextTableTitle},
+        drawing_utils::get_search_start_position,
         Painter,
     },
     constants::*,
+    data_conversion::{TableData, TableRow},
 };
 
 use tui::{
     backend::Backend,
     layout::{Alignment, Constraint, Direction, Layout, Rect},
     terminal::Frame,
-    text::{Span, Spans, Text},
-    widgets::{Block, Borders, Paragraph, Row, Table},
+    text::{Span, Spans},
+    widgets::{Block, Borders, Paragraph},
 };
 
 use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation};
 use unicode_width::UnicodeWidthStr;
 
-const PROCESS_HEADERS_HARD_WIDTH_NO_GROUP: &[Option<u16>] = &[
-    Some(7),
-    None,
-    Some(8),
-    Some(8),
-    Some(8),
-    Some(8),
-    Some(7),
-    Some(8),
-    #[cfg(target_family = "unix")]
-    None,
-    None,
-];
-const PROCESS_HEADERS_HARD_WIDTH_GROUPED: &[Option<u16>] = &[
-    Some(7),
-    None,
-    Some(8),
-    Some(8),
-    Some(8),
-    Some(8),
-    Some(7),
-    Some(8),
-];
-
-const PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND: &[Option<f64>] =
-    &[None, Some(0.7), None, None, None, None, None, None];
-const PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE: &[Option<f64>] =
-    &[None, Some(0.3), None, None, None, None, None, None];
-
-const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND: &[Option<f64>] = &[
-    None,
-    Some(0.7),
-    None,
-    None,
-    None,
-    None,
-    None,
-    None,
-    #[cfg(target_family = "unix")]
-    Some(0.05),
-    Some(0.2),
-];
-const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE: &[Option<f64>] = &[
-    None,
-    Some(0.5),
-    None,
-    None,
-    None,
-    None,
-    None,
-    None,
-    #[cfg(target_family = "unix")]
-    Some(0.05),
-    Some(0.2),
-];
-const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: &[Option<f64>] = &[
-    None,
-    Some(0.3),
-    None,
-    None,
-    None,
-    None,
-    None,
-    None,
-    #[cfg(target_family = "unix")]
-    Some(0.05),
-    Some(0.2),
-];
+const SORT_MENU_WIDTH: u16 = 7;
 
 impl Painter {
     /// Draws and handles all process-related drawing.  Use this.
     /// - `widget_id` here represents the widget ID of the process widget itself!
-    pub fn draw_process_features<B: Backend>(
+    pub fn draw_process_widget<B: Backend>(
         &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
         widget_id: u64,
     ) {
-        if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) {
+        if let Some(proc_widget_state) = app_state.proc_state.widget_states.get(&widget_id) {
             let search_height = if draw_border { 5 } else { 3 };
-            let is_sort_open = process_widget_state.is_sort_open;
-            let header_len = process_widget_state.columns.longest_header_len;
+            let is_sort_open = proc_widget_state.is_sort_open;
 
             let mut proc_draw_loc = draw_loc;
-            if process_widget_state.is_search_enabled() {
+            if proc_widget_state.is_search_enabled() {
                 let processes_chunk = Layout::default()
                     .direction(Direction::Vertical)
                     .constraints([Constraint::Min(0), Constraint::Length(search_height)])
@@ -119,25 +53,26 @@ impl Painter {
             if is_sort_open {
                 let processes_chunk = Layout::default()
                     .direction(Direction::Horizontal)
-                    .constraints([Constraint::Length(header_len + 4), Constraint::Min(0)])
+                    .constraints([Constraint::Length(SORT_MENU_WIDTH + 4), Constraint::Min(0)])
                     .split(proc_draw_loc);
                 proc_draw_loc = processes_chunk[1];
 
-                self.draw_process_sort(
-                    f,
-                    app_state,
-                    processes_chunk[0],
-                    draw_border,
-                    widget_id + 2,
-                );
+                self.draw_sort_table(f, app_state, processes_chunk[0], draw_border, widget_id + 2);
             }
 
             self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id);
         }
+
+        if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
+            // Reset redraw marker.
+            if proc_widget_state.force_rerender {
+                proc_widget_state.force_rerender = false;
+            }
+        }
     }
 
     /// Draws the process sort box.
-    /// - `widget_id` represents the widget ID of the process widget itself.
+    /// - `widget_id` represents the widget ID of the process widget itself.an
     ///
     /// This should not be directly called.
     fn draw_processes_table<B: Backend>(
@@ -147,19 +82,10 @@ impl Painter {
         let should_get_widget_bounds = app_state.should_get_widget_bounds();
         if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
             let recalculate_column_widths =
-                should_get_widget_bounds || proc_widget_state.requires_redraw;
-            if proc_widget_state.requires_redraw {
-                proc_widget_state.requires_redraw = false;
-            }
+                should_get_widget_bounds || proc_widget_state.force_rerender;
 
             let is_on_widget = widget_id == app_state.current_widget.widget_id;
-            let margined_draw_loc = Layout::default()
-                .constraints([Constraint::Percentage(100)])
-                .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
-                .direction(Direction::Horizontal)
-                .split(draw_loc)[0];
-
-            let (border_style, highlight_style) = if is_on_widget {
+            let (border_style, highlighted_text_style) = if is_on_widget {
                 (
                     self.colours.highlighted_border_style,
                     self.colours.currently_selected_text_style,
@@ -168,357 +94,39 @@ impl Painter {
                 (self.colours.border_style, self.colours.text_style)
             };
 
-            let title_base = if app_state.app_config_fields.show_table_scroll_position {
-                if let Some(finalized_process_data) = app_state
-                    .canvas_data
-                    .finalized_process_data_map
-                    .get(&widget_id)
-                {
-                    let title = format!(
-                        " Processes ({} of {}) ",
-                        proc_widget_state
-                            .scroll_state
-                            .current_scroll_position
-                            .saturating_add(1),
-                        finalized_process_data.len()
-                    );
-
-                    if title.len() <= draw_loc.width.into() {
-                        title
-                    } else {
-                        " Processes ".to_string()
-                    }
-                } else {
-                    " Processes ".to_string()
-                }
-            } else {
-                " Processes ".to_string()
-            };
-
-            let title = if app_state.is_expanded
-                && !proc_widget_state
-                    .process_search_state
-                    .search_state
-                    .is_enabled
-                && !proc_widget_state.is_sort_open
-            {
-                const ESCAPE_ENDING: &str = "── Esc to go back ";
-
-                let (chosen_title_base, expanded_title_base) = {
-                    let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING);
-
-                    if temp_title_base.len() > draw_loc.width.into() {
-                        (
-                            " Processes ".to_string(),
-                            format!("{}{}", " Processes ", ESCAPE_ENDING),
-                        )
-                    } else {
-                        (title_base, temp_title_base)
-                    }
-                };
-
-                Spans::from(vec![
-                    Span::styled(chosen_title_base, self.colours.widget_title_style),
-                    Span::styled(
-                        format!(
-                            "─{}─ Esc to go back ",
-                            "─".repeat(
-                                usize::from(draw_loc.width).saturating_sub(
-                                    UnicodeSegmentation::graphemes(
-                                        expanded_title_base.as_str(),
-                                        true
-                                    )
-                                    .count()
-                                        + 2
-                                )
-                            )
-                        ),
-                        border_style,
-                    ),
-                ])
-            } else {
-                Spans::from(Span::styled(title_base, self.colours.widget_title_style))
-            };
-
-            let process_block = if draw_border {
-                Block::default()
-                    .title(title)
-                    .borders(Borders::ALL)
-                    .border_style(border_style)
-            } else if is_on_widget {
-                Block::default()
-                    .borders(SIDE_BORDERS)
-                    .border_style(self.colours.highlighted_border_style)
-            } else {
-                Block::default().borders(Borders::NONE)
-            };
-
-            if let Some(process_data) = &app_state
-                .canvas_data
-                .stringified_process_data_map
-                .get(&widget_id)
-            {
-                let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
-                    0
-                } else {
-                    app_state.app_config_fields.table_gap
-                };
-                let position = get_start_position(
-                    usize::from(
-                        (draw_loc.height + (1 - table_gap))
-                            .saturating_sub(self.table_height_offset),
-                    ),
-                    &proc_widget_state.scroll_state.scroll_direction,
-                    &mut proc_widget_state.scroll_state.scroll_bar,
-                    proc_widget_state.scroll_state.current_scroll_position,
-                    app_state.is_force_redraw,
-                );
-
-                // Sanity check
-                let start_position = if position >= process_data.len() {
-                    process_data.len().saturating_sub(1)
-                } else {
-                    position
-                };
-
-                let sliced_vec = &process_data[start_position..];
-                let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| {
-                    (
-                        data.iter()
-                            .map(|(entry, _alternative)| entry)
-                            .collect::<Vec<_>>(),
-                        disabled,
-                    )
-                });
-
-                let proc_table_state = &mut proc_widget_state.scroll_state.table_state;
-                proc_table_state.select(Some(
-                    proc_widget_state
-                        .scroll_state
-                        .current_scroll_position
-                        .saturating_sub(start_position),
-                ));
-
-                // Draw!
-                let process_headers = proc_widget_state.columns.get_column_headers(
-                    &proc_widget_state.process_sorting_type,
-                    proc_widget_state.is_process_sort_descending,
-                );
-
-                // Calculate widths
-                // FIXME: See if we can move this into the recalculate block?  I want to move column widths into the column widths
-                let hard_widths = if proc_widget_state.is_grouped {
-                    PROCESS_HEADERS_HARD_WIDTH_GROUPED
-                } else {
-                    PROCESS_HEADERS_HARD_WIDTH_NO_GROUP
-                };
-
-                if recalculate_column_widths {
-                    let mut column_widths = process_headers
-                        .iter()
-                        .map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16)
-                        .collect::<Vec<_>>();
-
-                    let soft_widths_min = column_widths
-                        .iter()
-                        .map(|width| Some(*width))
-                        .collect::<Vec<_>>();
-
-                    proc_widget_state.table_width_state.desired_column_widths = {
-                        for (row, _disabled) in processed_sliced_vec.clone() {
-                            for (col, entry) in row.iter().enumerate() {
-                                if let Some(col_width) = column_widths.get_mut(col) {
-                                    let grapheme_len = UnicodeWidthStr::width(entry.as_str());
-                                    if grapheme_len as u16 > *col_width {
-                                        *col_width = grapheme_len as u16;
-                                    }
-                                }
-                            }
-                        }
-                        column_widths
-                    };
-
-                    proc_widget_state.table_width_state.desired_column_widths = proc_widget_state
-                        .table_width_state
-                        .desired_column_widths
-                        .iter()
-                        .zip(hard_widths)
-                        .map(|(current, hard)| {
-                            if let Some(hard) = hard {
-                                if *hard > *current {
-                                    *hard
-                                } else {
-                                    *current
-                                }
-                            } else {
-                                *current
-                            }
-                        })
-                        .collect::<Vec<_>>();
-
-                    let soft_widths_max = if proc_widget_state.is_grouped {
-                        // Note grouped trees are not a thing.
-
-                        if proc_widget_state.is_using_command {
-                            PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND
-                        } else {
-                            PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE
-                        }
-                    } else if proc_widget_state.is_using_command {
-                        PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND
-                    } else if proc_widget_state.is_tree_mode {
-                        PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE
-                    } else {
-                        PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE
-                    };
-
-                    proc_widget_state.table_width_state.calculated_column_widths =
-                        get_column_widths(
-                            draw_loc.width,
-                            hard_widths,
-                            &soft_widths_min,
-                            soft_widths_max,
-                            &(proc_widget_state
-                                .table_width_state
-                                .desired_column_widths
-                                .iter()
-                                .map(|width| Some(*width))
-                                .collect::<Vec<_>>()),
-                            true,
-                        );
-
-                    // debug!(
-                    //     "DCW: {:?}",
-                    //     proc_widget_state.table_width_state.desired_column_widths
-                    // );
-                    // debug!(
-                    //     "CCW: {:?}",
-                    //     proc_widget_state.table_width_state.calculated_column_widths
-                    // );
-                }
-
-                let dcw = &proc_widget_state.table_width_state.desired_column_widths;
-                let ccw = &proc_widget_state.table_width_state.calculated_column_widths;
-
-                let process_rows = sliced_vec.iter().map(|(data, disabled)| {
-                    let truncated_data = data.iter().zip(hard_widths).enumerate().map(
-                        |(itx, ((entry, alternative), width))| {
-                            if let (Some(desired_col_width), Some(calculated_col_width)) =
-                                (dcw.get(itx), ccw.get(itx))
-                            {
-                                if width.is_none() {
-                                    if *desired_col_width > *calculated_col_width
-                                        && *calculated_col_width > 0
-                                    {
-                                        let calculated_col_width: usize =
-                                            (*calculated_col_width).into();
-
-                                        let graphemes =
-                                            UnicodeSegmentation::graphemes(entry.as_str(), true)
-                                                .collect::<Vec<&str>>();
-
-                                        if let Some(alternative) = alternative {
-                                            Text::raw(alternative)
-                                        } else if graphemes.len() > calculated_col_width
-                                            && calculated_col_width > 1
-                                        {
-                                            // Truncate with ellipsis
-                                            let first_n =
-                                                graphemes[..(calculated_col_width - 1)].concat();
-                                            Text::raw(format!("{}…", first_n))
-                                        } else {
-                                            Text::raw(entry)
-                                        }
-                                    } else {
-                                        Text::raw(entry)
-                                    }
-                                } else {
-                                    Text::raw(entry)
-                                }
-                            } else {
-                                Text::raw(entry)
-                            }
-                        },
-                    );
-
-                    if *disabled {
-                        Row::new(truncated_data).style(self.colours.disabled_text_style)
-                    } else {
-                        Row::new(truncated_data)
-                    }
-                });
-
-                f.render_stateful_widget(
-                    Table::new(process_rows)
-                        .header(
-                            Row::new(process_headers)
-                                .style(self.colours.table_header_style)
-                                .bottom_margin(table_gap),
-                        )
-                        .block(process_block)
-                        .highlight_style(highlight_style)
-                        .style(self.colours.text_style)
-                        .widths(
-                            &(proc_widget_state
-                                .table_width_state
-                                .calculated_column_widths
-                                .iter()
-                                .map(|calculated_width| Constraint::Length(*calculated_width))
-                                .collect::<Vec<_>>()),
-                        ),
-                    margined_draw_loc,
-                    proc_table_state,
-                );
-            } else {
-                f.render_widget(process_block, margined_draw_loc);
-            }
-
-            // Check if we need to update columnar bounds...
-            if recalculate_column_widths
-                || proc_widget_state.columns.column_header_x_locs.is_none()
-                || proc_widget_state.columns.column_header_y_loc.is_none()
-            {
-                // y location is just the y location of the widget + border size (1 normally, 0 in basic)
-                proc_widget_state.columns.column_header_y_loc =
-                    Some(draw_loc.y + if draw_border { 1 } else { 0 });
-
-                // x location is determined using the x locations of the widget; just offset from the left bound
-                // as appropriate, and use the right bound as limiter.
-
-                let mut current_x_left = draw_loc.x + 1;
-                let max_x_right = draw_loc.x + draw_loc.width - 1;
-
-                let mut x_locs = vec![];
-
-                for width in proc_widget_state
-                    .table_width_state
-                    .calculated_column_widths
-                    .iter()
-                {
-                    let right_bound = current_x_left + width;
-
-                    if right_bound < max_x_right {
-                        x_locs.push((current_x_left, right_bound));
-                        current_x_left = right_bound + 1;
-                    } else {
-                        x_locs.push((current_x_left, max_x_right));
-                        break;
-                    }
-                }
-
-                proc_widget_state.columns.column_header_x_locs = Some(x_locs);
-            }
-
-            if app_state.should_get_widget_bounds() {
-                // Update draw loc in widget map
-                if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
-                    widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
-                    widget.bottom_right_corner = Some((
-                        margined_draw_loc.x + margined_draw_loc.width,
-                        margined_draw_loc.y + margined_draw_loc.height,
-                    ));
+            // TODO: [Refactor] This is an ugly hack to add the disabled style...
+            // this could be solved by storing style locally to the widget.
+            for row in &mut proc_widget_state.table_data.data {
+                if let TableRow::Styled(_, style) = row {
+                    *style = style.patch(self.colours.disabled_text_style);
                 }
             }
+
+            TextTable {
+                table_gap: app_state.app_config_fields.table_gap,
+                is_force_redraw: app_state.is_force_redraw,
+                recalculate_column_widths,
+                header_style: self.colours.table_header_style,
+                border_style,
+                highlighted_text_style,
+                title: Some(TextTableTitle {
+                    title: " Processes ".into(),
+                    is_expanded: app_state.is_expanded,
+                }),
+                is_on_widget,
+                draw_border,
+                show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
+                title_style: self.colours.widget_title_style,
+                text_style: self.colours.text_style,
+                left_to_right: true,
+            }
+            .draw_text_table(
+                f,
+                draw_loc,
+                &mut proc_widget_state.table_state,
+                &proc_widget_state.table_data,
+                app_state.widget_map.get_mut(&widget_id),
+            );
         }
     }
 
@@ -583,14 +191,8 @@ impl Painter {
 
             let start_position: usize = get_search_start_position(
                 num_columns - num_chars_for_text - 5,
-                &proc_widget_state
-                    .process_search_state
-                    .search_state
-                    .cursor_direction,
-                &mut proc_widget_state
-                    .process_search_state
-                    .search_state
-                    .cursor_bar,
+                &proc_widget_state.proc_search.search_state.cursor_direction,
+                &mut proc_widget_state.proc_search.search_state.cursor_bar,
                 current_cursor_position,
                 app_state.is_force_redraw,
             );
@@ -625,32 +227,26 @@ impl Painter {
             })];
 
             // Text options shamelessly stolen from VS Code.
-            let case_style = if !proc_widget_state.process_search_state.is_ignoring_case {
+            let case_style = if !proc_widget_state.proc_search.is_ignoring_case {
                 self.colours.currently_selected_text_style
             } else {
                 self.colours.text_style
             };
 
-            let whole_word_style = if proc_widget_state
-                .process_search_state
-                .is_searching_whole_word
-            {
+            let whole_word_style = if proc_widget_state.proc_search.is_searching_whole_word {
                 self.colours.currently_selected_text_style
             } else {
                 self.colours.text_style
             };
 
-            let regex_style = if proc_widget_state
-                .process_search_state
-                .is_searching_with_regex
-            {
+            let regex_style = if proc_widget_state.proc_search.is_searching_with_regex {
                 self.colours.currently_selected_text_style
             } else {
                 self.colours.text_style
             };
 
-            // FIXME: [MOUSE] Mouse support for these in search
-            // FIXME: [MOVEMENT] Movement support for these in search
+            // TODO: [MOUSE] Mouse support for these in search
+            // TODO: [MOVEMENT] Movement support for these in search
             let option_text = Spans::from(vec![
                 Span::styled(
                     format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }),
@@ -669,11 +265,7 @@ impl Painter {
             ]);
 
             search_text.push(Spans::from(Span::styled(
-                if let Some(err) = &proc_widget_state
-                    .process_search_state
-                    .search_state
-                    .error_message
-                {
+                if let Some(err) = &proc_widget_state.proc_search.search_state.error_message {
                     err.as_str()
                 } else {
                     ""
@@ -682,17 +274,14 @@ impl Painter {
             )));
             search_text.push(option_text);
 
-            let current_border_style = if proc_widget_state
-                .process_search_state
-                .search_state
-                .is_invalid_search
-            {
-                self.colours.invalid_query_style
-            } else if is_on_widget {
-                self.colours.highlighted_border_style
-            } else {
-                self.colours.border_style
-            };
+            let current_border_style =
+                if proc_widget_state.proc_search.search_state.is_invalid_search {
+                    self.colours.invalid_query_style
+                } else if is_on_widget {
+                    self.colours.highlighted_border_style
+                } else {
+                    self.colours.border_style
+                };
 
             let title = Span::styled(
                 if draw_border {
@@ -751,127 +340,70 @@ impl Painter {
     /// state that is stored.
     ///
     /// This should not be directly called.
-    fn draw_process_sort<B: Backend>(
+    fn draw_sort_table<B: Backend>(
         &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
         widget_id: u64,
     ) {
-        let is_on_widget = widget_id == app_state.current_widget.widget_id;
-
+        let should_get_widget_bounds = app_state.should_get_widget_bounds();
         if let Some(proc_widget_state) =
             app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
         {
-            let current_scroll_position = proc_widget_state.columns.current_scroll_position;
-            let sort_string = proc_widget_state
-                .columns
-                .ordered_columns
-                .iter()
-                .filter(|column_type| {
-                    proc_widget_state
-                        .columns
-                        .column_mapping
-                        .get(column_type)
-                        .unwrap()
-                        .enabled
-                })
-                .map(|column_type| column_type.to_string())
-                .collect::<Vec<_>>();
+            let recalculate_column_widths =
+                should_get_widget_bounds || proc_widget_state.force_rerender;
 
-            let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
-                0
+            let is_on_widget = widget_id == app_state.current_widget.widget_id;
+            let (border_style, highlighted_text_style) = if is_on_widget {
+                (
+                    self.colours.highlighted_border_style,
+                    self.colours.currently_selected_text_style,
+                )
             } else {
-                app_state.app_config_fields.table_gap
-            };
-            let position = get_start_position(
-                usize::from(
-                    (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
-                ),
-                &proc_widget_state.columns.scroll_direction,
-                &mut proc_widget_state.columns.previous_scroll_position,
-                current_scroll_position,
-                app_state.is_force_redraw,
-            );
-
-            // Sanity check
-            let start_position = if position >= sort_string.len() {
-                sort_string.len().saturating_sub(1)
-            } else {
-                position
+                (self.colours.border_style, self.colours.text_style)
             };
 
-            let sliced_vec = &sort_string[start_position..];
-
-            let sort_options = sliced_vec
-                .iter()
-                .map(|column| Row::new(vec![column.as_str()]));
-
-            let column_state = &mut proc_widget_state.columns.column_state;
-            column_state.select(Some(
-                proc_widget_state
+            // TODO: [PROC] Perhaps move this generation elsewhere... or leave it as is but look at partial rendering?
+            let table_data = {
+                let data = proc_widget_state
+                    .table_state
                     .columns
-                    .current_scroll_position
-                    .saturating_sub(start_position),
-            ));
-            let current_border_style = if proc_widget_state
-                .process_search_state
-                .search_state
-                .is_invalid_search
-            {
-                self.colours.invalid_query_style
-            } else if is_on_widget {
-                self.colours.highlighted_border_style
-            } else {
-                self.colours.border_style
-            };
+                    .iter()
+                    .filter_map(|col| {
+                        if col.is_hidden {
+                            None
+                        } else {
+                            Some(TableRow::Raw(vec![col.header.text().clone()]))
+                        }
+                    })
+                    .collect();
 
-            let process_sort_block = if draw_border {
-                Block::default()
-                    .borders(Borders::ALL)
-                    .border_style(current_border_style)
-            } else if is_on_widget {
-                Block::default()
-                    .borders(SIDE_BORDERS)
-                    .border_style(current_border_style)
-            } else {
-                Block::default().borders(Borders::NONE)
-            };
-
-            let highlight_style = if is_on_widget {
-                self.colours.currently_selected_text_style
-            } else {
-                self.colours.text_style
-            };
-
-            let margined_draw_loc = Layout::default()
-                .constraints([Constraint::Percentage(100)])
-                .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
-                .direction(Direction::Horizontal)
-                .split(draw_loc)[0];
-
-            f.render_stateful_widget(
-                Table::new(sort_options)
-                    .header(
-                        Row::new(vec!["Sort By"])
-                            .style(self.colours.table_header_style)
-                            .bottom_margin(table_gap),
-                    )
-                    .block(process_sort_block)
-                    .highlight_style(highlight_style)
-                    .style(self.colours.text_style)
-                    .widths(&[Constraint::Percentage(100)]),
-                margined_draw_loc,
-                column_state,
-            );
-
-            if app_state.should_get_widget_bounds() {
-                // Update draw loc in widget map
-                if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
-                    widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
-                    widget.bottom_right_corner = Some((
-                        margined_draw_loc.x + margined_draw_loc.width,
-                        margined_draw_loc.y + margined_draw_loc.height,
-                    ));
+                TableData {
+                    data,
+                    col_widths: vec![usize::from(SORT_MENU_WIDTH)],
                 }
+            };
+
+            TextTable {
+                table_gap: app_state.app_config_fields.table_gap,
+                is_force_redraw: app_state.is_force_redraw,
+                recalculate_column_widths,
+                header_style: self.colours.table_header_style,
+                border_style,
+                highlighted_text_style,
+                title: None,
+                is_on_widget,
+                draw_border,
+                show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
+                title_style: self.colours.widget_title_style,
+                text_style: self.colours.text_style,
+                left_to_right: true,
             }
+            .draw_text_table(
+                f,
+                draw_loc,
+                &mut proc_widget_state.sort_table_state,
+                &table_data,
+                app_state.widget_map.get_mut(&widget_id),
+            );
         }
     }
 }
diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/widgets/temp_table.rs
index 974afd14..45554faa 100644
--- a/src/canvas/widgets/temp_table.rs
+++ b/src/canvas/widgets/temp_table.rs
@@ -1,31 +1,12 @@
-use once_cell::sync::Lazy;
-use tui::{
-    backend::Backend,
-    layout::{Constraint, Direction, Layout, Rect},
-    terminal::Frame,
-    text::Span,
-    text::{Spans, Text},
-    widgets::{Block, Borders, Row, Table},
-};
+use tui::{backend::Backend, layout::Rect, terminal::Frame};
 
 use crate::{
     app,
     canvas::{
-        drawing_utils::{get_column_widths, get_start_position},
+        components::{TextTable, TextTableTitle},
         Painter,
     },
-    constants::*,
 };
-use unicode_segmentation::UnicodeSegmentation;
-
-const TEMP_HEADERS: [&str; 2] = ["Sensor", "Temp"];
-
-static TEMP_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
-    TEMP_HEADERS
-        .iter()
-        .map(|entry| entry.len() as u16)
-        .collect::<Vec<_>>()
-});
 
 impl Painter {
     pub fn draw_temp_table<B: Backend>(
@@ -34,109 +15,9 @@ impl Painter {
     ) {
         let recalculate_column_widths = app_state.should_get_widget_bounds();
         if let Some(temp_widget_state) = app_state.temp_state.widget_states.get_mut(&widget_id) {
-            let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
-                0
-            } else {
-                app_state.app_config_fields.table_gap
-            };
-            let start_position = get_start_position(
-                usize::from(
-                    (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
-                ),
-                &temp_widget_state.scroll_state.scroll_direction,
-                &mut temp_widget_state.scroll_state.scroll_bar,
-                temp_widget_state.scroll_state.current_scroll_position,
-                app_state.is_force_redraw,
-            );
-            let is_on_widget = widget_id == app_state.current_widget.widget_id;
-            let temp_table_state = &mut temp_widget_state.scroll_state.table_state;
-            temp_table_state.select(Some(
-                temp_widget_state
-                    .scroll_state
-                    .current_scroll_position
-                    .saturating_sub(start_position),
-            ));
-            let sliced_vec = &app_state.canvas_data.temp_sensor_data[start_position..];
+            let is_on_widget = app_state.current_widget.widget_id == widget_id;
 
-            // Calculate widths
-            let hard_widths = [None, None];
-            if recalculate_column_widths {
-                temp_widget_state.table_width_state.desired_column_widths = {
-                    let mut column_widths = TEMP_HEADERS_LENS.clone();
-                    for row in sliced_vec {
-                        for (col, entry) in row.iter().enumerate() {
-                            if entry.len() as u16 > column_widths[col] {
-                                column_widths[col] = entry.len() as u16;
-                            }
-                        }
-                    }
-
-                    column_widths
-                };
-                temp_widget_state.table_width_state.calculated_column_widths = get_column_widths(
-                    draw_loc.width,
-                    &hard_widths,
-                    &(TEMP_HEADERS_LENS
-                        .iter()
-                        .map(|width| Some(*width))
-                        .collect::<Vec<_>>()),
-                    &[Some(0.80), Some(-1.0)],
-                    &temp_widget_state
-                        .table_width_state
-                        .desired_column_widths
-                        .iter()
-                        .map(|width| Some(*width))
-                        .collect::<Vec<_>>(),
-                    false,
-                );
-            }
-
-            let dcw = &temp_widget_state.table_width_state.desired_column_widths;
-            let ccw = &temp_widget_state.table_width_state.calculated_column_widths;
-            let temperature_rows =
-                sliced_vec.iter().map(|temp_row| {
-                    let truncated_data = temp_row.iter().zip(&hard_widths).enumerate().map(
-                        |(itx, (entry, width))| {
-                            if width.is_none() {
-                                if let (Some(desired_col_width), Some(calculated_col_width)) =
-                                    (dcw.get(itx), ccw.get(itx))
-                                {
-                                    if *desired_col_width > *calculated_col_width
-                                        && *calculated_col_width > 0
-                                    {
-                                        let calculated_col_width: usize =
-                                            (*calculated_col_width).into();
-
-                                        let graphemes =
-                                            UnicodeSegmentation::graphemes(entry.as_str(), true)
-                                                .collect::<Vec<&str>>();
-
-                                        if graphemes.len() > calculated_col_width
-                                            && calculated_col_width > 1
-                                        {
-                                            // Truncate with ellipsis
-                                            let first_n =
-                                                graphemes[..(calculated_col_width - 1)].concat();
-                                            Text::raw(format!("{}…", first_n))
-                                        } else {
-                                            Text::raw(entry)
-                                        }
-                                    } else {
-                                        Text::raw(entry)
-                                    }
-                                } else {
-                                    Text::raw(entry)
-                                }
-                            } else {
-                                Text::raw(entry)
-                            }
-                        },
-                    );
-
-                    Row::new(truncated_data)
-                });
-
-            let (border_style, highlight_style) = if is_on_widget {
+            let (border_style, highlighted_text_style) = if is_on_widget {
                 (
                     self.colours.highlighted_border_style,
                     self.colours.currently_selected_text_style,
@@ -144,118 +25,31 @@ impl Painter {
             } else {
                 (self.colours.border_style, self.colours.text_style)
             };
-
-            let title_base = if app_state.app_config_fields.show_table_scroll_position {
-                let title_string = format!(
-                    " Temperatures ({} of {}) ",
-                    temp_widget_state
-                        .scroll_state
-                        .current_scroll_position
-                        .saturating_add(1),
-                    app_state.canvas_data.temp_sensor_data.len()
-                );
-
-                if title_string.len() <= draw_loc.width.into() {
-                    title_string
-                } else {
-                    " Temperatures ".to_string()
-                }
-            } else {
-                " Temperatures ".to_string()
-            };
-
-            let title = if app_state.is_expanded {
-                const ESCAPE_ENDING: &str = "── Esc to go back ";
-
-                let (chosen_title_base, expanded_title_base) = {
-                    let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING);
-
-                    if temp_title_base.len() > draw_loc.width.into() {
-                        (
-                            " Temperatures ".to_string(),
-                            format!("{}{}", " Temperatures ", ESCAPE_ENDING),
-                        )
-                    } else {
-                        (title_base, temp_title_base)
-                    }
-                };
-
-                Spans::from(vec![
-                    Span::styled(chosen_title_base, self.colours.widget_title_style),
-                    Span::styled(
-                        format!(
-                            "─{}─ Esc to go back ",
-                            "─".repeat(
-                                usize::from(draw_loc.width).saturating_sub(
-                                    UnicodeSegmentation::graphemes(
-                                        expanded_title_base.as_str(),
-                                        true
-                                    )
-                                    .count()
-                                        + 2
-                                )
-                            )
-                        ),
-                        border_style,
-                    ),
-                ])
-            } else {
-                Spans::from(Span::styled(title_base, self.colours.widget_title_style))
-            };
-
-            let temp_block = if draw_border {
-                Block::default()
-                    .title(title)
-                    .borders(Borders::ALL)
-                    .border_style(border_style)
-            } else if is_on_widget {
-                Block::default()
-                    .borders(SIDE_BORDERS)
-                    .border_style(self.colours.highlighted_border_style)
-            } else {
-                Block::default().borders(Borders::NONE)
-            };
-
-            let margined_draw_loc = Layout::default()
-                .constraints([Constraint::Percentage(100)])
-                .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
-                .direction(Direction::Horizontal)
-                .split(draw_loc)[0];
-
-            // Draw
-            f.render_stateful_widget(
-                Table::new(temperature_rows)
-                    .header(
-                        Row::new(TEMP_HEADERS.to_vec())
-                            .style(self.colours.table_header_style)
-                            .bottom_margin(table_gap),
-                    )
-                    .block(temp_block)
-                    .highlight_style(highlight_style)
-                    .style(self.colours.text_style)
-                    .widths(
-                        &(temp_widget_state
-                            .table_width_state
-                            .calculated_column_widths
-                            .iter()
-                            .map(|calculated_width| Constraint::Length(*calculated_width))
-                            .collect::<Vec<_>>()),
-                    ),
-                margined_draw_loc,
-                temp_table_state,
-            );
-
-            if app_state.should_get_widget_bounds() {
-                // Update draw loc in widget map
-                // Note there is no difference between this and using draw_loc, but I'm too lazy to fix it.
-                if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
-                    widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
-                    widget.bottom_right_corner = Some((
-                        margined_draw_loc.x + margined_draw_loc.width,
-                        margined_draw_loc.y + margined_draw_loc.height,
-                    ));
-                }
+            TextTable {
+                table_gap: app_state.app_config_fields.table_gap,
+                is_force_redraw: app_state.is_force_redraw,
+                recalculate_column_widths,
+                header_style: self.colours.table_header_style,
+                border_style,
+                highlighted_text_style,
+                title: Some(TextTableTitle {
+                    title: " Temperatures ".into(),
+                    is_expanded: app_state.is_expanded,
+                }),
+                is_on_widget,
+                draw_border,
+                show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
+                title_style: self.colours.widget_title_style,
+                text_style: self.colours.text_style,
+                left_to_right: false,
             }
+            .draw_text_table(
+                f,
+                draw_loc,
+                &mut temp_widget_state.table_state,
+                &app_state.converted_data.temp_sensor_data,
+                app_state.widget_map.get_mut(&widget_id),
+            );
         }
     }
 }
diff --git a/src/clap.rs b/src/clap.rs
index 0fbd3223..c667088a 100644
--- a/src/clap.rs
+++ b/src/clap.rs
@@ -148,7 +148,7 @@ pub fn build_app() -> Command<'static> {
         .help("Uses a dot marker for graphs.")
         .long_help("Uses a dot marker for graphs as opposed to the default braille marker.");
 
-    let group = Arg::new("group") // FIXME: Rename this to something like "group_process", would be "breaking" though.
+    let group = Arg::new("group") // TODO: Rename this to something like "group_process", would be "breaking" though.
         .short('g')
         .long("group")
         .help("Groups processes with the same name by default.")
diff --git a/src/data_conversion.rs b/src/data_conversion.rs
index 7f9d184c..98bbee2c 100644
--- a/src/data_conversion.rs
+++ b/src/data_conversion.rs
@@ -1,17 +1,16 @@
 //! This mainly concerns converting collected data into things that the canvas
 //! can actually handle.
+
+use crate::app::CellContent;
+use crate::canvas::Point;
 use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
 use crate::{
-    app::{data_farmer, data_harvester, App, ProcWidgetState},
-    utils::{self, gen_util::*},
+    app::{data_farmer, data_harvester, App},
+    utils::gen_util::*,
 };
-use data_harvester::processes::ProcessSorting;
-use fxhash::FxBuildHasher;
-use indexmap::IndexSet;
-use std::collections::{HashMap, VecDeque};
 
-/// Point is of time, data
-type Point = (f64, f64);
+use concat_string::concat_string;
+use fxhash::FxHashMap;
 
 #[derive(Default, Debug)]
 pub struct ConvertedBatteryData {
@@ -23,6 +22,27 @@ pub struct ConvertedBatteryData {
     pub health: String,
 }
 
+#[derive(Default, Debug)]
+pub struct TableData {
+    pub data: Vec<TableRow>,
+    pub col_widths: Vec<usize>,
+}
+
+#[derive(Debug)]
+pub enum TableRow {
+    Raw(Vec<CellContent>),
+    Styled(Vec<CellContent>, tui::style::Style),
+}
+
+impl TableRow {
+    pub fn row(&self) -> &[CellContent] {
+        match self {
+            TableRow::Raw(data) => data,
+            TableRow::Styled(data, _) => data,
+        }
+    }
+}
+
 #[derive(Default, Debug)]
 pub struct ConvertedNetworkData {
     pub rx: Vec<Point>,
@@ -40,39 +60,6 @@ pub struct ConvertedNetworkData {
     // mean_tx: f64,
 }
 
-// TODO: [REFACTOR] Process data... stuff really needs a rewrite.  Again.
-#[derive(Clone, Default, Debug)]
-pub struct ConvertedProcessData {
-    pub pid: Pid,
-    pub ppid: Option<Pid>,
-    pub name: String,
-    pub command: String,
-    pub is_thread: Option<bool>,
-    pub cpu_percent_usage: f64,
-    pub mem_percent_usage: f64,
-    pub mem_usage_bytes: u64,
-    pub mem_usage_str: (f64, String),
-    pub group_pids: Vec<Pid>,
-    pub read_per_sec: String,
-    pub write_per_sec: String,
-    pub total_read: String,
-    pub total_write: String,
-    pub rps_f64: f64,
-    pub wps_f64: f64,
-    pub tr_f64: f64,
-    pub tw_f64: f64,
-    pub process_state: String,
-    pub process_char: char,
-    pub user: Option<String>,
-
-    /// Prefix printed before the process when displayed.
-    pub process_description_prefix: Option<String>,
-    /// Whether to mark this process entry as disabled (mostly for tree mode).
-    pub is_disabled_entry: bool,
-    /// Whether this entry is collapsed, hiding all its children (for tree mode).
-    pub is_collapsed_entry: bool,
-}
-
 #[derive(Clone, Default, Debug)]
 pub struct ConvertedCpuData {
     pub cpu_name: String,
@@ -83,35 +70,81 @@ pub struct ConvertedCpuData {
     pub legend_value: String,
 }
 
-pub fn convert_temp_row(app: &App) -> Vec<Vec<String>> {
+#[derive(Default)]
+pub struct ConvertedData {
+    pub rx_display: String,
+    pub tx_display: String,
+    pub total_rx_display: String,
+    pub total_tx_display: String,
+    pub network_data_rx: Vec<Point>,
+    pub network_data_tx: Vec<Point>,
+    pub disk_data: TableData,
+    pub temp_sensor_data: TableData,
+
+    /// A mapping from a process name to any PID with that name.
+    pub process_name_pid_map: FxHashMap<String, Vec<Pid>>,
+
+    /// A mapping from a process command to any PID with that name.
+    pub process_cmd_pid_map: FxHashMap<String, Vec<Pid>>,
+
+    pub mem_labels: Option<(String, String)>,
+    pub swap_labels: Option<(String, String)>,
+
+    pub mem_data: Vec<Point>, // TODO: Switch this and all data points over to a better data structure...
+    pub swap_data: Vec<Point>,
+    pub load_avg_data: [f32; 3],
+    pub cpu_data: Vec<ConvertedCpuData>,
+    pub battery_data: Vec<ConvertedBatteryData>,
+}
+
+pub fn convert_temp_row(app: &App) -> TableData {
     let current_data = &app.data_collection;
     let temp_type = &app.app_config_fields.temperature_type;
+    let mut col_widths = vec![0; 2];
 
-    let mut sensor_vector: Vec<Vec<String>> = current_data
+    let mut sensor_vector: Vec<TableRow> = current_data
         .temp_harvest
         .iter()
         .map(|temp_harvest| {
-            vec![
-                temp_harvest.name.clone(),
-                (temp_harvest.temperature.ceil() as u64).to_string()
-                    + match temp_type {
-                        data_harvester::temperature::TemperatureType::Celsius => "°C",
-                        data_harvester::temperature::TemperatureType::Kelvin => "K",
-                        data_harvester::temperature::TemperatureType::Fahrenheit => "°F",
-                    },
-            ]
+            let row = vec![
+                CellContent::Simple(temp_harvest.name.clone().into()),
+                CellContent::Simple(
+                    concat_string!(
+                        (temp_harvest.temperature.ceil() as u64).to_string(),
+                        match temp_type {
+                            data_harvester::temperature::TemperatureType::Celsius => "°C",
+                            data_harvester::temperature::TemperatureType::Kelvin => "K",
+                            data_harvester::temperature::TemperatureType::Fahrenheit => "°F",
+                        }
+                    )
+                    .into(),
+                ),
+            ];
+
+            col_widths.iter_mut().zip(&row).for_each(|(curr, r)| {
+                *curr = std::cmp::max(*curr, r.len());
+            });
+
+            TableRow::Raw(row)
         })
         .collect();
 
     if sensor_vector.is_empty() {
-        sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()]);
+        sensor_vector.push(TableRow::Raw(vec![
+            CellContent::Simple("No Sensors Found".into()),
+            CellContent::Simple("".into()),
+        ]));
     }
 
-    sensor_vector
+    TableData {
+        data: sensor_vector,
+        col_widths,
+    }
 }
 
-pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<String>> {
-    let mut disk_vector: Vec<Vec<String>> = Vec::new();
+pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> TableData {
+    let mut disk_vector: Vec<TableRow> = Vec::new();
+    let mut col_widths = vec![0; 8];
 
     current_data
         .disk_harvest
@@ -120,9 +153,9 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<S
         .for_each(|(disk, (io_read, io_write))| {
             let free_space_fmt = if let Some(free_space) = disk.free_space {
                 let converted_free_space = get_decimal_bytes(free_space);
-                format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1)
+                format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into()
             } else {
-                "N/A".to_string()
+                "N/A".into()
             };
             let total_space_fmt = if let Some(total_space) = disk.total_space {
                 let converted_total_space = get_decimal_bytes(total_space);
@@ -130,46 +163,52 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<S
                     "{:.*}{}",
                     0, converted_total_space.0, converted_total_space.1
                 )
+                .into()
             } else {
-                "N/A".to_string()
+                "N/A".into()
             };
 
             let usage_fmt = if let (Some(used_space), Some(total_space)) =
                 (disk.used_space, disk.total_space)
             {
-                format!("{:.0}%", used_space as f64 / total_space as f64 * 100_f64)
+                format!("{:.0}%", used_space as f64 / total_space as f64 * 100_f64).into()
             } else {
-                "N/A".to_string()
+                "N/A".into()
             };
 
-            disk_vector.push(vec![
-                disk.name.to_string(),
-                disk.mount_point.to_string(),
-                usage_fmt,
-                free_space_fmt,
-                total_space_fmt,
-                io_read.to_string(),
-                io_write.to_string(),
-            ]);
+            let row = vec![
+                CellContent::Simple(disk.name.clone().into()),
+                CellContent::Simple(disk.mount_point.clone().into()),
+                CellContent::Simple(usage_fmt),
+                CellContent::Simple(free_space_fmt),
+                CellContent::Simple(total_space_fmt),
+                CellContent::Simple(io_read.clone().into()),
+                CellContent::Simple(io_write.clone().into()),
+            ];
+            col_widths.iter_mut().zip(&row).for_each(|(curr, r)| {
+                *curr = std::cmp::max(*curr, r.len());
+            });
+            disk_vector.push(TableRow::Raw(row));
         });
 
     if disk_vector.is_empty() {
-        disk_vector.push(vec!["No Disks Found".to_string(), "".to_string()]);
+        disk_vector.push(TableRow::Raw(vec![
+            CellContent::Simple("No Disks Found".into()),
+            CellContent::Simple("".into()),
+        ]));
     }
 
-    disk_vector
+    TableData {
+        data: disk_vector,
+        col_widths,
+    }
 }
 
 pub fn convert_cpu_data_points(
     current_data: &data_farmer::DataCollection, existing_cpu_data: &mut Vec<ConvertedCpuData>,
-    is_frozen: bool,
 ) {
-    let current_time = if is_frozen {
-        if let Some(frozen_instant) = current_data.frozen_instant {
-            frozen_instant
-        } else {
-            current_data.current_instant
-        }
+    let current_time = if let Some(frozen_instant) = current_data.frozen_instant {
+        frozen_instant
     } else {
         current_data.current_instant
     };
@@ -179,7 +218,7 @@ pub fn convert_cpu_data_points(
         if data.cpu_data.len() + 1 != existing_cpu_data.len() {
             *existing_cpu_data = vec![ConvertedCpuData {
                 cpu_name: "All".to_string(),
-                short_cpu_name: "All".to_string(),
+                short_cpu_name: "".to_string(),
                 cpu_data: vec![],
                 legend_value: String::new(),
             }];
@@ -240,16 +279,10 @@ pub fn convert_cpu_data_points(
     }
 }
 
-pub fn convert_mem_data_points(
-    current_data: &data_farmer::DataCollection, is_frozen: bool,
-) -> Vec<Point> {
+pub fn convert_mem_data_points(current_data: &data_farmer::DataCollection) -> Vec<Point> {
     let mut result: Vec<Point> = Vec::new();
-    let current_time = if is_frozen {
-        if let Some(frozen_instant) = current_data.frozen_instant {
-            frozen_instant
-        } else {
-            current_data.current_instant
-        }
+    let current_time = if let Some(frozen_instant) = current_data.frozen_instant {
+        frozen_instant
     } else {
         current_data.current_instant
     };
@@ -268,16 +301,10 @@ pub fn convert_mem_data_points(
     result
 }
 
-pub fn convert_swap_data_points(
-    current_data: &data_farmer::DataCollection, is_frozen: bool,
-) -> Vec<Point> {
+pub fn convert_swap_data_points(current_data: &data_farmer::DataCollection) -> Vec<Point> {
     let mut result: Vec<Point> = Vec::new();
-    let current_time = if is_frozen {
-        if let Some(frozen_instant) = current_data.frozen_instant {
-            frozen_instant
-        } else {
-            current_data.current_instant
-        }
+    let current_time = if let Some(frozen_instant) = current_data.frozen_instant {
+        frozen_instant
     } else {
         current_data.current_instant
     };
@@ -367,18 +394,14 @@ pub fn convert_mem_labels(
 }
 
 pub fn get_rx_tx_data_points(
-    current_data: &data_farmer::DataCollection, is_frozen: bool, network_scale_type: &AxisScaling,
+    current_data: &data_farmer::DataCollection, network_scale_type: &AxisScaling,
     network_unit_type: &DataUnit, network_use_binary_prefix: bool,
 ) -> (Vec<Point>, Vec<Point>) {
     let mut rx: Vec<Point> = Vec::new();
     let mut tx: Vec<Point> = Vec::new();
 
-    let current_time = if is_frozen {
-        if let Some(frozen_instant) = current_data.frozen_instant {
-            frozen_instant
-        } else {
-            current_data.current_instant
-        }
+    let current_time = if let Some(frozen_instant) = current_data.frozen_instant {
+        frozen_instant
     } else {
         current_data.current_instant
     };
@@ -422,13 +445,12 @@ pub fn get_rx_tx_data_points(
 }
 
 pub fn convert_network_data_points(
-    current_data: &data_farmer::DataCollection, is_frozen: bool, need_four_points: bool,
+    current_data: &data_farmer::DataCollection, need_four_points: bool,
     network_scale_type: &AxisScaling, network_unit_type: &DataUnit,
     network_use_binary_prefix: bool,
 ) -> ConvertedNetworkData {
     let (rx, tx) = get_rx_tx_data_points(
         current_data,
-        is_frozen,
         network_scale_type,
         network_unit_type,
         network_use_binary_prefix,
@@ -550,816 +572,26 @@ pub fn convert_network_data_points(
     }
 }
 
-pub enum ProcessGroupingType {
-    Grouped,
-    Ungrouped,
+/// Returns a string given a value that is converted to the closest binary variant.
+/// If the value is greater than a gibibyte, then it will return a decimal place.
+pub fn binary_byte_string(value: u64) -> String {
+    let converted_values = get_binary_bytes(value);
+    if value >= GIBI_LIMIT {
+        format!("{:.*}{}", 1, converted_values.0, converted_values.1)
+    } else {
+        format!("{:.*}{}", 0, converted_values.0, converted_values.1)
+    }
 }
 
-pub enum ProcessNamingType {
-    Name,
-    Path,
-}
-
-/// Given read/s, write/s, total read, and total write values, return 4 strings that represent read/s, write/s, total read, and total write
-fn get_disk_io_strings(
-    rps: u64, wps: u64, total_read: u64, total_write: u64,
-) -> (String, String, String, String) {
-    // Note we always use bytes for total read/write here (for now).
-    let converted_rps = get_decimal_bytes(rps);
-    let converted_wps = get_decimal_bytes(wps);
-    let converted_total_read = get_decimal_bytes(total_read);
-    let converted_total_write = get_decimal_bytes(total_write);
-
-    (
-        if rps >= GIGA_LIMIT {
-            format!("{:.*}{}/s", 1, converted_rps.0, converted_rps.1)
-        } else {
-            format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1)
-        },
-        if wps >= GIGA_LIMIT {
-            format!("{:.*}{}/s", 1, converted_wps.0, converted_wps.1)
-        } else {
-            format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1)
-        },
-        if total_read >= GIGA_LIMIT {
-            format!("{:.*}{}", 1, converted_total_read.0, converted_total_read.1)
-        } else {
-            format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1)
-        },
-        if total_write >= GIGA_LIMIT {
-            format!(
-                "{:.*}{}",
-                1, converted_total_write.0, converted_total_write.1
-            )
-        } else {
-            format!(
-                "{:.*}{}",
-                0, converted_total_write.0, converted_total_write.1
-            )
-        },
-    )
-}
-
-/// Because we needed to UPDATE data entries rather than REPLACING entries, we instead update
-/// the existing vector.
-pub fn convert_process_data(
-    current_data: &data_farmer::DataCollection,
-    existing_converted_process_data: &mut HashMap<Pid, ConvertedProcessData>,
-    #[cfg(target_family = "unix")] user_table: &mut data_harvester::processes::UserTable,
-) {
-    // TODO [THREAD]: Thread highlighting and hiding support
-    // For macOS see https://github.com/hishamhm/htop/pull/848/files
-
-    let mut complete_pid_set: fxhash::FxHashSet<Pid> =
-        existing_converted_process_data.keys().copied().collect();
-
-    for process in &current_data.process_harvest {
-        let (read_per_sec, write_per_sec, total_read, total_write) = get_disk_io_strings(
-            process.read_bytes_per_sec,
-            process.write_bytes_per_sec,
-            process.total_read_bytes,
-            process.total_write_bytes,
-        );
-
-        let mem_usage_str = get_binary_bytes(process.mem_usage_bytes);
-
-        let user = {
-            #[cfg(target_family = "unix")]
-            {
-                if let Some(uid) = process.uid {
-                    user_table.get_uid_to_username_mapping(uid).ok()
-                } else {
-                    None
-                }
-            }
-            #[cfg(not(target_family = "unix"))]
-            {
-                None
-            }
-        };
-
-        if let Some(process_entry) = existing_converted_process_data.get_mut(&process.pid) {
-            complete_pid_set.remove(&process.pid);
-
-            // Very dumb way to see if there's PID reuse...
-            if process_entry.ppid == process.parent_pid {
-                process_entry.name = process.name.to_string();
-                process_entry.command = process.command.to_string();
-                process_entry.cpu_percent_usage = process.cpu_usage_percent;
-                process_entry.mem_percent_usage = process.mem_usage_percent;
-                process_entry.mem_usage_bytes = process.mem_usage_bytes;
-                process_entry.mem_usage_str = mem_usage_str;
-                process_entry.group_pids = vec![process.pid];
-                process_entry.read_per_sec = read_per_sec;
-                process_entry.write_per_sec = write_per_sec;
-                process_entry.total_read = total_read;
-                process_entry.total_write = total_write;
-                process_entry.rps_f64 = process.read_bytes_per_sec as f64;
-                process_entry.wps_f64 = process.write_bytes_per_sec as f64;
-                process_entry.tr_f64 = process.total_read_bytes as f64;
-                process_entry.tw_f64 = process.total_write_bytes as f64;
-                process_entry.process_state = process.process_state.to_owned();
-                process_entry.process_char = process.process_state_char;
-                process_entry.process_description_prefix = None;
-                process_entry.is_disabled_entry = false;
-                process_entry.user = user;
-            } else {
-                // ...I hate that I can't combine if let and an if statement in one line...
-                *process_entry = ConvertedProcessData {
-                    pid: process.pid,
-                    ppid: process.parent_pid,
-                    is_thread: None,
-                    name: process.name.to_string(),
-                    command: process.command.to_string(),
-                    cpu_percent_usage: process.cpu_usage_percent,
-                    mem_percent_usage: process.mem_usage_percent,
-                    mem_usage_bytes: process.mem_usage_bytes,
-                    mem_usage_str,
-                    group_pids: vec![process.pid],
-                    read_per_sec,
-                    write_per_sec,
-                    total_read,
-                    total_write,
-                    rps_f64: process.read_bytes_per_sec as f64,
-                    wps_f64: process.write_bytes_per_sec as f64,
-                    tr_f64: process.total_read_bytes as f64,
-                    tw_f64: process.total_write_bytes as f64,
-                    process_state: process.process_state.to_owned(),
-                    process_char: process.process_state_char,
-                    process_description_prefix: None,
-                    is_disabled_entry: false,
-                    is_collapsed_entry: false,
-                    user,
-                };
-            }
-        } else {
-            existing_converted_process_data.insert(
-                process.pid,
-                ConvertedProcessData {
-                    pid: process.pid,
-                    ppid: process.parent_pid,
-                    is_thread: None,
-                    name: process.name.to_string(),
-                    command: process.command.to_string(),
-                    cpu_percent_usage: process.cpu_usage_percent,
-                    mem_percent_usage: process.mem_usage_percent,
-                    mem_usage_bytes: process.mem_usage_bytes,
-                    mem_usage_str,
-                    group_pids: vec![process.pid],
-                    read_per_sec,
-                    write_per_sec,
-                    total_read,
-                    total_write,
-                    rps_f64: process.read_bytes_per_sec as f64,
-                    wps_f64: process.write_bytes_per_sec as f64,
-                    tr_f64: process.total_read_bytes as f64,
-                    tw_f64: process.total_write_bytes as f64,
-                    process_state: process.process_state.to_owned(),
-                    process_char: process.process_state_char,
-                    process_description_prefix: None,
-                    is_disabled_entry: false,
-                    is_collapsed_entry: false,
-                    user,
-                },
-            );
-        }
+/// Returns a string given a value that is converted to the closest SI-variant, per second.
+/// If the value is greater than a giga-X, then it will return a decimal place.
+pub fn dec_bytes_per_second_string(value: u64) -> String {
+    let converted_values = get_decimal_bytes(value);
+    if value >= GIGA_LIMIT {
+        format!("{:.*}{}/s", 1, converted_values.0, converted_values.1)
+    } else {
+        format!("{:.*}{}/s", 0, converted_values.0, converted_values.1)
     }
-
-    // Now clean up any spare entries that weren't visited, to avoid clutter:
-    complete_pid_set.iter().for_each(|pid| {
-        existing_converted_process_data.remove(pid);
-    })
-}
-
-const BRANCH_ENDING: char = '└';
-const BRANCH_VERTICAL: char = '│';
-const BRANCH_SPLIT: char = '├';
-const BRANCH_HORIZONTAL: char = '─';
-
-pub fn tree_process_data(
-    filtered_process_data: &[ConvertedProcessData], is_using_command: bool,
-    sorting_type: &ProcessSorting, is_sort_descending: bool,
-) -> Vec<ConvertedProcessData> {
-    // TODO: [TREE] Option to sort usage by total branch usage or individual value usage?
-
-    // Let's first build up a (really terrible) parent -> child mapping...
-    // At the same time, let's make a mapping of PID -> process data!
-    let mut parent_child_mapping: HashMap<Pid, IndexSet<Pid, FxBuildHasher>> = HashMap::default();
-    let mut pid_process_mapping: HashMap<Pid, &ConvertedProcessData> = HashMap::default(); // We actually already have this stored, but it's unfiltered... oh well.
-    let mut orphan_set: IndexSet<Pid, FxBuildHasher> =
-        IndexSet::with_hasher(FxBuildHasher::default());
-    let mut collapsed_set: IndexSet<Pid, FxBuildHasher> =
-        IndexSet::with_hasher(FxBuildHasher::default());
-
-    filtered_process_data.iter().for_each(|process| {
-        if let Some(ppid) = process.ppid {
-            orphan_set.insert(ppid);
-        }
-        orphan_set.insert(process.pid);
-    });
-
-    filtered_process_data.iter().for_each(|process| {
-        // Create a mapping for the process if it DNE.
-        parent_child_mapping
-            .entry(process.pid)
-            .or_insert_with(|| IndexSet::with_hasher(FxBuildHasher::default()));
-        pid_process_mapping.insert(process.pid, process);
-
-        if process.is_collapsed_entry {
-            collapsed_set.insert(process.pid);
-        }
-
-        // Insert its mapping to the process' parent if needed (create if it DNE).
-        if let Some(ppid) = process.ppid {
-            orphan_set.remove(&process.pid);
-            parent_child_mapping
-                .entry(ppid)
-                .or_insert_with(|| IndexSet::with_hasher(FxBuildHasher::default()))
-                .insert(process.pid);
-        }
-    });
-
-    // Keep only orphans, or promote children of orphans to a top-level orphan
-    // if their parents DNE in our pid to process mapping...
-    let old_orphan_set = orphan_set.clone();
-    old_orphan_set.iter().for_each(|pid| {
-        if pid_process_mapping.get(pid).is_none() {
-            // DNE!  Promote the mapped children and remove the current parent...
-            orphan_set.remove(pid);
-            if let Some(children) = parent_child_mapping.get(pid) {
-                orphan_set.extend(children);
-            }
-        }
-    });
-
-    // Turn the parent-child mapping into a "list" via DFS...
-    let mut pids_to_explore: VecDeque<Pid> = orphan_set.into_iter().collect();
-    let mut explored_pids: Vec<Pid> = vec![];
-    let mut lines: Vec<String> = vec![];
-
-    /// A post-order traversal to correctly prune entire branches that only contain children
-    /// that are disabled and themselves are also disabled ~~wait that sounds wrong~~.
-    /// Basically, go through the hashmap, and prune out all branches that are no longer relevant.
-    fn prune_disabled_pids(
-        current_pid: Pid, parent_child_mapping: &mut HashMap<Pid, IndexSet<Pid, FxBuildHasher>>,
-        pid_process_mapping: &HashMap<Pid, &ConvertedProcessData>,
-    ) -> bool {
-        // Let's explore all the children first, and make sure they (and their children)
-        // aren't all disabled...
-        let mut are_all_children_disabled = true;
-        if let Some(children) = parent_child_mapping.get(&current_pid) {
-            for child_pid in children.clone() {
-                let is_child_disabled =
-                    prune_disabled_pids(child_pid, parent_child_mapping, pid_process_mapping);
-
-                if is_child_disabled {
-                    if let Some(current_mapping) = parent_child_mapping.get_mut(&current_pid) {
-                        current_mapping.remove(&child_pid);
-                    }
-                } else if are_all_children_disabled {
-                    are_all_children_disabled = false;
-                }
-            }
-        }
-
-        // Now consider the current pid and whether to prune...
-        // If the node itself is not disabled, then never prune.  If it is, then check if all
-        // of its are disabled.
-        if let Some(process) = pid_process_mapping.get(&current_pid) {
-            if process.is_disabled_entry && are_all_children_disabled {
-                parent_child_mapping.remove(&current_pid);
-                return true;
-            }
-        }
-
-        false
-    }
-
-    fn sort_remaining_pids(
-        current_pid: Pid, sort_type: &ProcessSorting, is_sort_descending: bool,
-        parent_child_mapping: &mut HashMap<Pid, IndexSet<Pid, FxBuildHasher>>,
-        pid_process_mapping: &HashMap<Pid, &ConvertedProcessData>,
-    ) {
-        // Sorting is special for tree data.  So, by default, things are "sorted"
-        // via the DFS.  Otherwise, since this is DFS of the scanned PIDs (which are in order),
-        // you actually get a REVERSE order --- so, you get higher PIDs earlier than lower ones.
-        //
-        // So how do we "sort"?  The current idea is that:
-        // - We sort *per-level*.  Say, I want to sort by CPU.  The "first level" is sorted
-        //   by CPU in terms of its usage.  All its direct children are sorted by CPU
-        //   with *their* siblings.  Etc.
-        // - The default is thus PIDs in ascending order.  We set it to this when
-        //   we first enable the mode.
-
-        // So first, let's look at the children... (post-order again)
-        if let Some(children) = parent_child_mapping.get(&current_pid) {
-            let mut to_sort_vec: Vec<(Pid, &ConvertedProcessData)> = vec![];
-            for child_pid in children.clone() {
-                if let Some(child_process) = pid_process_mapping.get(&child_pid) {
-                    to_sort_vec.push((child_pid, child_process));
-                }
-                sort_remaining_pids(
-                    child_pid,
-                    sort_type,
-                    is_sort_descending,
-                    parent_child_mapping,
-                    pid_process_mapping,
-                );
-            }
-
-            // Now let's sort the immediate children!
-            sort_vec(&mut to_sort_vec, sort_type, is_sort_descending);
-
-            // Need to reverse what we got, apparently...
-            if let Some(current_mapping) = parent_child_mapping.get_mut(&current_pid) {
-                *current_mapping = to_sort_vec
-                    .iter()
-                    .rev()
-                    .map(|(pid, _proc)| *pid)
-                    .collect::<IndexSet<Pid, FxBuildHasher>>();
-            }
-        }
-    }
-
-    fn sort_vec(
-        to_sort_vec: &mut [(Pid, &ConvertedProcessData)], sort_type: &ProcessSorting,
-        is_sort_descending: bool,
-    ) {
-        // Sort by PID first (descending)
-        to_sort_vec.sort_by(|a, b| utils::gen_util::get_ordering(a.1.pid, b.1.pid, false));
-
-        match sort_type {
-            ProcessSorting::CpuPercent => {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(
-                        a.1.cpu_percent_usage,
-                        b.1.cpu_percent_usage,
-                        is_sort_descending,
-                    )
-                });
-            }
-            ProcessSorting::Mem => {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(
-                        a.1.mem_usage_bytes,
-                        b.1.mem_usage_bytes,
-                        is_sort_descending,
-                    )
-                });
-            }
-            ProcessSorting::MemPercent => {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(
-                        a.1.mem_percent_usage,
-                        b.1.mem_percent_usage,
-                        is_sort_descending,
-                    )
-                });
-            }
-            ProcessSorting::ProcessName => {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(
-                        &a.1.name.to_lowercase(),
-                        &b.1.name.to_lowercase(),
-                        is_sort_descending,
-                    )
-                });
-            }
-            ProcessSorting::Command => to_sort_vec.sort_by(|a, b| {
-                utils::gen_util::get_ordering(
-                    &a.1.command.to_lowercase(),
-                    &b.1.command.to_lowercase(),
-                    is_sort_descending,
-                )
-            }),
-            ProcessSorting::Pid => {
-                if is_sort_descending {
-                    to_sort_vec.sort_by(|a, b| {
-                        utils::gen_util::get_ordering(a.0, b.0, is_sort_descending)
-                    });
-                }
-            }
-            ProcessSorting::ReadPerSecond => {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(a.1.rps_f64, b.1.rps_f64, is_sort_descending)
-                });
-            }
-            ProcessSorting::WritePerSecond => {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(a.1.wps_f64, b.1.wps_f64, is_sort_descending)
-                });
-            }
-            ProcessSorting::TotalRead => {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(a.1.tr_f64, b.1.tr_f64, is_sort_descending)
-                });
-            }
-            ProcessSorting::TotalWrite => {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(a.1.tw_f64, b.1.tw_f64, is_sort_descending)
-                });
-            }
-            ProcessSorting::State => to_sort_vec.sort_by(|a, b| {
-                utils::gen_util::get_ordering(
-                    &a.1.process_state.to_lowercase(),
-                    &b.1.process_state.to_lowercase(),
-                    is_sort_descending,
-                )
-            }),
-            ProcessSorting::User => to_sort_vec.sort_by(|a, b| match (&a.1.user, &b.1.user) {
-                (Some(user_a), Some(user_b)) => utils::gen_util::get_ordering(
-                    user_a.to_lowercase(),
-                    user_b.to_lowercase(),
-                    is_sort_descending,
-                ),
-                (Some(_), None) => std::cmp::Ordering::Less,
-                (None, Some(_)) => std::cmp::Ordering::Greater,
-                (None, None) => std::cmp::Ordering::Less,
-            }),
-            ProcessSorting::Count => {
-                // Should never occur in this case, tree mode explicitly disables grouping.
-            }
-        }
-    }
-
-    /// A DFS traversal to correctly build the prefix lines (the pretty '├' and '─' lines) and
-    /// the correct order to the PID tree as a vector.
-    fn build_explored_pids(
-        current_pid: Pid, parent_child_mapping: &HashMap<Pid, IndexSet<Pid, FxBuildHasher>>,
-        prev_drawn_lines: &str, collapsed_set: &IndexSet<Pid, FxBuildHasher>,
-    ) -> (Vec<Pid>, Vec<String>) {
-        let mut explored_pids: Vec<Pid> = vec![current_pid];
-        let mut lines: Vec<String> = vec![];
-
-        if collapsed_set.contains(&current_pid) {
-            return (explored_pids, lines);
-        } else if let Some(children) = parent_child_mapping.get(&current_pid) {
-            for (itx, child) in children.iter().rev().enumerate() {
-                let new_drawn_lines = if itx == children.len() - 1 {
-                    format!("{}   ", prev_drawn_lines)
-                } else {
-                    format!("{}{}  ", prev_drawn_lines, BRANCH_VERTICAL)
-                };
-
-                let (pid_res, branch_res) = build_explored_pids(
-                    *child,
-                    parent_child_mapping,
-                    new_drawn_lines.as_str(),
-                    collapsed_set,
-                );
-
-                if itx == children.len() - 1 {
-                    lines.push(format!(
-                        "{}{}",
-                        prev_drawn_lines,
-                        if !new_drawn_lines.is_empty() {
-                            format!("{}{} ", BRANCH_ENDING, BRANCH_HORIZONTAL)
-                        } else {
-                            String::default()
-                        }
-                    ));
-                } else {
-                    lines.push(format!(
-                        "{}{}",
-                        prev_drawn_lines,
-                        if !new_drawn_lines.is_empty() {
-                            format!("{}{} ", BRANCH_SPLIT, BRANCH_HORIZONTAL)
-                        } else {
-                            String::default()
-                        }
-                    ));
-                }
-
-                explored_pids.extend(pid_res);
-                lines.extend(branch_res);
-            }
-        }
-
-        (explored_pids, lines)
-    }
-
-    /// Returns the total sum of CPU, MEM%, MEM, R/s, W/s, Total Read, and Total Write via DFS traversal.
-    fn get_usage_of_all_children(
-        parent_pid: Pid, parent_child_mapping: &HashMap<Pid, IndexSet<Pid, FxBuildHasher>>,
-        pid_process_mapping: &HashMap<Pid, &ConvertedProcessData>,
-    ) -> (f64, f64, u64, f64, f64, f64, f64) {
-        if let Some(&converted_process_data) = pid_process_mapping.get(&parent_pid) {
-            let (
-                mut cpu,
-                mut mem_percent,
-                mut mem,
-                mut rps,
-                mut wps,
-                mut total_read,
-                mut total_write,
-            ) = (
-                (converted_process_data.cpu_percent_usage * 10.0).round() / 10.0,
-                (converted_process_data.mem_percent_usage * 10.0).round() / 10.0,
-                converted_process_data.mem_usage_bytes,
-                (converted_process_data.rps_f64 * 10.0).round() / 10.0,
-                (converted_process_data.wps_f64 * 10.0).round() / 10.0,
-                (converted_process_data.tr_f64 * 10.0).round() / 10.0,
-                (converted_process_data.tw_f64 * 10.0).round() / 10.0,
-            );
-
-            if let Some(children) = parent_child_mapping.get(&parent_pid) {
-                for &child_pid in children {
-                    let (
-                        child_cpu,
-                        child_mem_percent,
-                        child_mem,
-                        child_rps,
-                        child_wps,
-                        child_total_read,
-                        child_total_write,
-                    ) = get_usage_of_all_children(
-                        child_pid,
-                        parent_child_mapping,
-                        pid_process_mapping,
-                    );
-
-                    cpu += child_cpu;
-                    mem_percent += child_mem_percent;
-                    mem += child_mem;
-                    rps += child_rps;
-                    wps += child_wps;
-                    total_read += child_total_read;
-                    total_write += child_total_write;
-                }
-            }
-
-            (cpu, mem_percent, mem, rps, wps, total_read, total_write)
-        } else {
-            (0.0_f64, 0.0_f64, 0, 0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64)
-        }
-    }
-
-    let mut to_sort_vec = Vec::new();
-    for pid in pids_to_explore {
-        if let Some(process) = pid_process_mapping.get(&pid) {
-            to_sort_vec.push((pid, *process));
-        }
-    }
-    sort_vec(&mut to_sort_vec, sorting_type, is_sort_descending);
-    pids_to_explore = to_sort_vec.iter().map(|(pid, _proc)| *pid).collect();
-
-    while let Some(current_pid) = pids_to_explore.pop_front() {
-        if !prune_disabled_pids(current_pid, &mut parent_child_mapping, &pid_process_mapping) {
-            sort_remaining_pids(
-                current_pid,
-                sorting_type,
-                is_sort_descending,
-                &mut parent_child_mapping,
-                &pid_process_mapping,
-            );
-
-            let (pid_res, branch_res) =
-                build_explored_pids(current_pid, &parent_child_mapping, "", &collapsed_set);
-            lines.push(String::default());
-            lines.extend(branch_res);
-            explored_pids.extend(pid_res);
-        }
-    }
-
-    // Now let's "rearrange" our current list of converted process data into the correct
-    // order required... and we're done!
-    explored_pids
-        .iter()
-        .zip(lines)
-        .filter_map(|(pid, prefix)| match pid_process_mapping.get(pid) {
-            Some(process) => {
-                let mut p = (*process).clone();
-                p.process_description_prefix = Some(format!(
-                    "{}{}{}",
-                    prefix,
-                    if p.is_collapsed_entry { "+ " } else { "" }, // I do the + sign thing here because I'm kinda too lazy to do it in the prefix, tbh.
-                    if is_using_command {
-                        &p.command
-                    } else {
-                        &p.name
-                    }
-                ));
-
-                // As part of https://github.com/ClementTsang/bottom/issues/424, also append their statistics to the parent if
-                // collapsed.
-                //
-                // Note that this will technically be "missing" entries, it collapses + sums based on what is visible
-                // since this runs *after* pruning steps.
-                if p.is_collapsed_entry {
-                    if let Some(children) = parent_child_mapping.get(&p.pid) {
-                        // Do some rounding.
-                        p.cpu_percent_usage = (p.cpu_percent_usage * 10.0).round() / 10.0;
-                        p.mem_percent_usage = (p.mem_percent_usage * 10.0).round() / 10.0;
-                        p.rps_f64 = (p.rps_f64 * 10.0).round() / 10.0;
-                        p.wps_f64 = (p.wps_f64 * 10.0).round() / 10.0;
-                        p.tr_f64 = (p.tr_f64 * 10.0).round() / 10.0;
-                        p.tw_f64 = (p.tw_f64 * 10.0).round() / 10.0;
-
-                        for &child_pid in children {
-                            // Let's just do a simple DFS traversal...
-                            let (
-                                child_cpu,
-                                child_mem_percent,
-                                child_mem,
-                                child_rps,
-                                child_wps,
-                                child_total_read,
-                                child_total_write,
-                            ) = get_usage_of_all_children(
-                                child_pid,
-                                &parent_child_mapping,
-                                &pid_process_mapping,
-                            );
-
-                            p.cpu_percent_usage += child_cpu;
-                            p.mem_percent_usage += child_mem_percent;
-                            p.mem_usage_bytes += child_mem;
-                            p.rps_f64 += child_rps;
-                            p.wps_f64 += child_wps;
-                            p.tr_f64 += child_total_read;
-                            p.tw_f64 += child_total_write;
-                        }
-
-                        let disk_io_strings = get_disk_io_strings(
-                            p.rps_f64 as u64,
-                            p.wps_f64 as u64,
-                            p.tr_f64 as u64,
-                            p.tw_f64 as u64,
-                        );
-
-                        p.mem_usage_str = get_binary_bytes(p.mem_usage_bytes);
-
-                        p.read_per_sec = disk_io_strings.0;
-                        p.write_per_sec = disk_io_strings.1;
-                        p.total_read = disk_io_strings.2;
-                        p.total_write = disk_io_strings.3;
-                    }
-                }
-
-                Some(p)
-            }
-            None => None,
-        })
-        .collect::<Vec<_>>()
-}
-
-// FIXME: [OPT] This is an easy target for optimization, too many to_strings!
-pub fn stringify_process_data(
-    proc_widget_state: &ProcWidgetState, finalized_process_data: &[ConvertedProcessData],
-) -> Vec<(Vec<(String, Option<String>)>, bool)> {
-    let is_proc_widget_grouped = proc_widget_state.is_grouped;
-    let is_using_command = proc_widget_state.is_using_command;
-    let is_tree = proc_widget_state.is_tree_mode;
-    let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem);
-
-    finalized_process_data
-        .iter()
-        .map(|process| {
-            (
-                vec![
-                    (
-                        if is_proc_widget_grouped {
-                            process.group_pids.len().to_string()
-                        } else {
-                            process.pid.to_string()
-                        },
-                        None,
-                    ),
-                    (
-                        if is_tree {
-                            if let Some(prefix) = &process.process_description_prefix {
-                                prefix.clone()
-                            } else {
-                                String::default()
-                            }
-                        } else if is_using_command {
-                            process.command.clone()
-                        } else {
-                            process.name.clone()
-                        },
-                        None,
-                    ),
-                    (format!("{:.1}%", process.cpu_percent_usage), None),
-                    (
-                        if mem_enabled {
-                            if process.mem_usage_bytes <= GIBI_LIMIT {
-                                format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1)
-                            } else {
-                                format!("{:.1}{}", process.mem_usage_str.0, process.mem_usage_str.1)
-                            }
-                        } else {
-                            format!("{:.1}%", process.mem_percent_usage)
-                        },
-                        None,
-                    ),
-                    (process.read_per_sec.clone(), None),
-                    (process.write_per_sec.clone(), None),
-                    (process.total_read.clone(), None),
-                    (process.total_write.clone(), None),
-                    #[cfg(target_family = "unix")]
-                    (
-                        if let Some(user) = &process.user {
-                            user.clone()
-                        } else {
-                            "N/A".to_string()
-                        },
-                        None,
-                    ),
-                    (
-                        process.process_state.clone(),
-                        Some(process.process_char.to_string()),
-                    ),
-                ],
-                process.is_disabled_entry,
-            )
-        })
-        .collect()
-}
-
-/// Takes a set of converted process data and groups it together.
-///
-/// To be honest, I really don't like how this is done, even though I've rewritten this like 3 times.
-pub fn group_process_data(
-    single_process_data: &[ConvertedProcessData], is_using_command: bool,
-) -> Vec<ConvertedProcessData> {
-    #[derive(Clone, Default, Debug)]
-    struct SingleProcessData {
-        pub pid: Pid,
-        pub cpu_percent_usage: f64,
-        pub mem_percent_usage: f64,
-        pub mem_usage_bytes: u64,
-        pub group_pids: Vec<Pid>,
-        pub read_per_sec: f64,
-        pub write_per_sec: f64,
-        pub total_read: f64,
-        pub total_write: f64,
-        pub process_state: String,
-    }
-
-    let mut grouped_hashmap: HashMap<String, SingleProcessData> = std::collections::HashMap::new();
-
-    single_process_data.iter().for_each(|process| {
-        let entry = grouped_hashmap
-            .entry(if is_using_command {
-                process.command.to_string()
-            } else {
-                process.name.to_string()
-            })
-            .or_insert(SingleProcessData {
-                pid: process.pid,
-                ..SingleProcessData::default()
-            });
-
-        (*entry).cpu_percent_usage += process.cpu_percent_usage;
-        (*entry).mem_percent_usage += process.mem_percent_usage;
-        (*entry).mem_usage_bytes += process.mem_usage_bytes;
-        (*entry).group_pids.push(process.pid);
-        (*entry).read_per_sec += process.rps_f64;
-        (*entry).write_per_sec += process.wps_f64;
-        (*entry).total_read += process.tr_f64;
-        (*entry).total_write += process.tw_f64;
-    });
-
-    grouped_hashmap
-        .iter()
-        .map(|(identifier, process_details)| {
-            let p = process_details.clone();
-
-            let (read_per_sec, write_per_sec, total_read, total_write) = get_disk_io_strings(
-                p.read_per_sec as u64,
-                p.write_per_sec as u64,
-                p.total_read as u64,
-                p.total_write as u64,
-            );
-
-            ConvertedProcessData {
-                pid: p.pid,
-                ppid: None,
-                is_thread: None,
-                name: identifier.to_string(),
-                command: identifier.to_string(),
-                cpu_percent_usage: p.cpu_percent_usage,
-                mem_percent_usage: p.mem_percent_usage,
-                mem_usage_bytes: p.mem_usage_bytes,
-                mem_usage_str: get_decimal_bytes(p.mem_usage_bytes),
-                group_pids: p.group_pids,
-                read_per_sec,
-                write_per_sec,
-                total_read,
-                total_write,
-                rps_f64: p.read_per_sec,
-                wps_f64: p.write_per_sec,
-                tr_f64: p.total_read,
-                tw_f64: p.total_write,
-                process_state: p.process_state,
-                process_description_prefix: None,
-                process_char: char::default(),
-                is_disabled_entry: false,
-                is_collapsed_entry: false,
-                user: None,
-            }
-        })
-        .collect::<Vec<_>>()
 }
 
 #[cfg(feature = "battery")]
@@ -1410,3 +642,68 @@ pub fn convert_battery_harvest(
         })
         .collect()
 }
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_binary_byte_string() {
+        assert_eq!(binary_byte_string(0), "0B".to_string());
+        assert_eq!(binary_byte_string(1), "1B".to_string());
+        assert_eq!(binary_byte_string(1000), "1000B".to_string());
+        assert_eq!(binary_byte_string(1023), "1023B".to_string());
+        assert_eq!(binary_byte_string(KIBI_LIMIT), "1KiB".to_string());
+        assert_eq!(binary_byte_string(KIBI_LIMIT + 1), "1KiB".to_string());
+        assert_eq!(binary_byte_string(MEBI_LIMIT), "1MiB".to_string());
+        assert_eq!(binary_byte_string(GIBI_LIMIT), "1.0GiB".to_string());
+        assert_eq!(binary_byte_string(2 * GIBI_LIMIT), "2.0GiB".to_string());
+        assert_eq!(
+            binary_byte_string((2.5 * GIBI_LIMIT as f64) as u64),
+            "2.5GiB".to_string()
+        );
+        assert_eq!(
+            binary_byte_string((10.34 * TEBI_LIMIT as f64) as u64),
+            "10.3TiB".to_string()
+        );
+        assert_eq!(
+            binary_byte_string((10.36 * TEBI_LIMIT as f64) as u64),
+            "10.4TiB".to_string()
+        );
+    }
+
+    #[test]
+    fn test_dec_bytes_per_second_string() {
+        assert_eq!(dec_bytes_per_second_string(0), "0B/s".to_string());
+        assert_eq!(dec_bytes_per_second_string(1), "1B/s".to_string());
+        assert_eq!(dec_bytes_per_second_string(900), "900B/s".to_string());
+        assert_eq!(dec_bytes_per_second_string(999), "999B/s".to_string());
+        assert_eq!(dec_bytes_per_second_string(KILO_LIMIT), "1KB/s".to_string());
+        assert_eq!(
+            dec_bytes_per_second_string(KILO_LIMIT + 1),
+            "1KB/s".to_string()
+        );
+        assert_eq!(dec_bytes_per_second_string(KIBI_LIMIT), "1KB/s".to_string());
+        assert_eq!(dec_bytes_per_second_string(MEGA_LIMIT), "1MB/s".to_string());
+        assert_eq!(
+            dec_bytes_per_second_string(GIGA_LIMIT),
+            "1.0GB/s".to_string()
+        );
+        assert_eq!(
+            dec_bytes_per_second_string(2 * GIGA_LIMIT),
+            "2.0GB/s".to_string()
+        );
+        assert_eq!(
+            dec_bytes_per_second_string((2.5 * GIGA_LIMIT as f64) as u64),
+            "2.5GB/s".to_string()
+        );
+        assert_eq!(
+            dec_bytes_per_second_string((10.34 * TERA_LIMIT as f64) as u64),
+            "10.3TB/s".to_string()
+        );
+        assert_eq!(
+            dec_bytes_per_second_string((10.36 * TERA_LIMIT as f64) as u64),
+            "10.4TB/s".to_string()
+        );
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 4ae7ed07..735ace77 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,7 +27,7 @@ use crossterm::{
 };
 
 use app::{
-    data_harvester::{self, processes::ProcessSorting},
+    data_harvester,
     layout_manager::{UsedWidgets, WidgetDirection},
     App,
 };
@@ -302,295 +302,40 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) {
     .unwrap();
 }
 
-pub fn handle_force_redraws(app: &mut App) {
-    // Currently we use an Option... because we might want to future-proof this
-    // if we eventually get widget-specific redrawing!
-    if app.proc_state.force_update_all {
-        update_all_process_lists(app);
-        app.proc_state.force_update_all = false;
-    } else if let Some(widget_id) = app.proc_state.force_update {
-        update_final_process_list(app, widget_id);
-        app.proc_state.force_update = None;
+pub fn update_data(app: &mut App) {
+    for proc in app.proc_state.widget_states.values_mut() {
+        if proc.force_update_data {
+            proc.update_displayed_process_data(&app.data_collection);
+            proc.force_update_data = false;
+        }
     }
 
     if app.cpu_state.force_update.is_some() {
-        convert_cpu_data_points(
-            &app.data_collection,
-            &mut app.canvas_data.cpu_data,
-            app.is_frozen,
-        );
-        app.canvas_data.load_avg_data = app.data_collection.load_avg_harvest;
+        convert_cpu_data_points(&app.data_collection, &mut app.converted_data.cpu_data);
+        app.converted_data.load_avg_data = app.data_collection.load_avg_harvest;
         app.cpu_state.force_update = None;
     }
 
-    // FIXME: [OPT] Prefer reassignment over new vectors?
+    // TODO: [OPT] Prefer reassignment over new vectors?
     if app.mem_state.force_update.is_some() {
-        app.canvas_data.mem_data = convert_mem_data_points(&app.data_collection, app.is_frozen);
-        app.canvas_data.swap_data = convert_swap_data_points(&app.data_collection, app.is_frozen);
+        app.converted_data.mem_data = convert_mem_data_points(&app.data_collection);
+        app.converted_data.swap_data = convert_swap_data_points(&app.data_collection);
         app.mem_state.force_update = None;
     }
 
     if app.net_state.force_update.is_some() {
         let (rx, tx) = get_rx_tx_data_points(
             &app.data_collection,
-            app.is_frozen,
             &app.app_config_fields.network_scale_type,
             &app.app_config_fields.network_unit_type,
             app.app_config_fields.network_use_binary_prefix,
         );
-        app.canvas_data.network_data_rx = rx;
-        app.canvas_data.network_data_tx = tx;
+        app.converted_data.network_data_rx = rx;
+        app.converted_data.network_data_tx = tx;
         app.net_state.force_update = None;
     }
 }
 
-#[allow(clippy::needless_collect)]
-pub fn update_all_process_lists(app: &mut App) {
-    // According to clippy, I can avoid a collect... but if I follow it,
-    // I end up conflicting with the borrow checker since app is used within the closure... hm.
-    if !app.is_frozen {
-        let widget_ids = app
-            .proc_state
-            .widget_states
-            .keys()
-            .cloned()
-            .collect::<Vec<_>>();
-
-        widget_ids.into_iter().for_each(|widget_id| {
-            update_final_process_list(app, widget_id);
-        });
-    }
-}
-
-fn update_final_process_list(app: &mut App, widget_id: u64) {
-    let process_states = app
-        .proc_state
-        .widget_states
-        .get(&widget_id)
-        .map(|process_state| {
-            (
-                process_state
-                    .process_search_state
-                    .search_state
-                    .is_invalid_or_blank_search(),
-                process_state.is_using_command,
-                process_state.is_grouped,
-                process_state.is_tree_mode,
-            )
-        });
-
-    if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states {
-        if !app.is_frozen {
-            convert_process_data(
-                &app.data_collection,
-                &mut app.canvas_data.single_process_data,
-                #[cfg(target_family = "unix")]
-                &mut app.user_table,
-            );
-        }
-        let process_filter = app.get_process_filter(widget_id);
-        let filtered_process_data: Vec<ConvertedProcessData> = if is_tree {
-            app.canvas_data
-                .single_process_data
-                .iter()
-                .map(|(_pid, process)| {
-                    let mut process_clone = process.clone();
-                    if !is_invalid_or_blank {
-                        if let Some(process_filter) = process_filter {
-                            process_clone.is_disabled_entry =
-                                !process_filter.check(&process_clone, is_using_command);
-                        }
-                    }
-                    process_clone
-                })
-                .collect::<Vec<_>>()
-        } else {
-            app.canvas_data
-                .single_process_data
-                .iter()
-                .filter_map(|(_pid, process)| {
-                    if !is_invalid_or_blank {
-                        if let Some(process_filter) = process_filter {
-                            if process_filter.check(process, is_using_command) {
-                                Some(process)
-                            } else {
-                                None
-                            }
-                        } else {
-                            Some(process)
-                        }
-                    } else {
-                        Some(process)
-                    }
-                })
-                .cloned()
-                .collect::<Vec<_>>()
-        };
-
-        if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) {
-            let mut finalized_process_data = if is_tree {
-                tree_process_data(
-                    &filtered_process_data,
-                    is_using_command,
-                    &proc_widget_state.process_sorting_type,
-                    proc_widget_state.is_process_sort_descending,
-                )
-            } else if is_grouped {
-                group_process_data(&filtered_process_data, is_using_command)
-            } else {
-                filtered_process_data
-            };
-
-            // Note tree mode is sorted well before this, as it's special.
-            if !is_tree {
-                sort_process_data(&mut finalized_process_data, proc_widget_state);
-            }
-
-            if proc_widget_state.scroll_state.current_scroll_position
-                >= finalized_process_data.len()
-            {
-                proc_widget_state.scroll_state.current_scroll_position =
-                    finalized_process_data.len().saturating_sub(1);
-                proc_widget_state.scroll_state.scroll_bar = 0;
-                proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down;
-            }
-
-            app.canvas_data.stringified_process_data_map.insert(
-                widget_id,
-                stringify_process_data(proc_widget_state, &finalized_process_data),
-            );
-            app.canvas_data
-                .finalized_process_data_map
-                .insert(widget_id, finalized_process_data);
-        }
-    }
-}
-
-fn sort_process_data(
-    to_sort_vec: &mut [ConvertedProcessData], proc_widget_state: &app::ProcWidgetState,
-) {
-    to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase());
-
-    match &proc_widget_state.process_sorting_type {
-        ProcessSorting::CpuPercent => {
-            to_sort_vec.sort_by(|a, b| {
-                utils::gen_util::get_ordering(
-                    a.cpu_percent_usage,
-                    b.cpu_percent_usage,
-                    proc_widget_state.is_process_sort_descending,
-                )
-            });
-        }
-        ProcessSorting::Mem => {
-            to_sort_vec.sort_by(|a, b| {
-                utils::gen_util::get_ordering(
-                    a.mem_usage_bytes,
-                    b.mem_usage_bytes,
-                    proc_widget_state.is_process_sort_descending,
-                )
-            });
-        }
-        ProcessSorting::MemPercent => {
-            to_sort_vec.sort_by(|a, b| {
-                utils::gen_util::get_ordering(
-                    a.mem_percent_usage,
-                    b.mem_percent_usage,
-                    proc_widget_state.is_process_sort_descending,
-                )
-            });
-        }
-        ProcessSorting::ProcessName => {
-            // Don't repeat if false... it sorts by name by default anyways.
-            if proc_widget_state.is_process_sort_descending {
-                to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase());
-                if proc_widget_state.is_process_sort_descending {
-                    to_sort_vec.reverse();
-                }
-            }
-        }
-        ProcessSorting::Command => {
-            to_sort_vec.sort_by_cached_key(|c| c.command.to_lowercase());
-            if proc_widget_state.is_process_sort_descending {
-                to_sort_vec.reverse();
-            }
-        }
-        ProcessSorting::Pid => {
-            if !proc_widget_state.is_grouped {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(
-                        a.pid,
-                        b.pid,
-                        proc_widget_state.is_process_sort_descending,
-                    )
-                });
-            }
-        }
-        ProcessSorting::ReadPerSecond => {
-            to_sort_vec.sort_by(|a, b| {
-                utils::gen_util::get_ordering(
-                    a.rps_f64,
-                    b.rps_f64,
-                    proc_widget_state.is_process_sort_descending,
-                )
-            });
-        }
-        ProcessSorting::WritePerSecond => {
-            to_sort_vec.sort_by(|a, b| {
-                utils::gen_util::get_ordering(
-                    a.wps_f64,
-                    b.wps_f64,
-                    proc_widget_state.is_process_sort_descending,
-                )
-            });
-        }
-        ProcessSorting::TotalRead => {
-            to_sort_vec.sort_by(|a, b| {
-                utils::gen_util::get_ordering(
-                    a.tr_f64,
-                    b.tr_f64,
-                    proc_widget_state.is_process_sort_descending,
-                )
-            });
-        }
-        ProcessSorting::TotalWrite => {
-            to_sort_vec.sort_by(|a, b| {
-                utils::gen_util::get_ordering(
-                    a.tw_f64,
-                    b.tw_f64,
-                    proc_widget_state.is_process_sort_descending,
-                )
-            });
-        }
-        ProcessSorting::State => {
-            to_sort_vec.sort_by_cached_key(|c| c.process_state.to_lowercase());
-            if proc_widget_state.is_process_sort_descending {
-                to_sort_vec.reverse();
-            }
-        }
-        ProcessSorting::User => to_sort_vec.sort_by(|a, b| match (&a.user, &b.user) {
-            (Some(user_a), Some(user_b)) => utils::gen_util::get_ordering(
-                user_a.to_lowercase(),
-                user_b.to_lowercase(),
-                proc_widget_state.is_process_sort_descending,
-            ),
-            (Some(_), None) => std::cmp::Ordering::Less,
-            (None, Some(_)) => std::cmp::Ordering::Greater,
-            (None, None) => std::cmp::Ordering::Less,
-        }),
-        ProcessSorting::Count => {
-            if proc_widget_state.is_grouped {
-                to_sort_vec.sort_by(|a, b| {
-                    utils::gen_util::get_ordering(
-                        a.group_pids.len(),
-                        b.group_pids.len(),
-                        proc_widget_state.is_process_sort_descending,
-                    )
-                });
-            }
-        }
-    }
-}
-
 pub fn create_input_thread(
     sender: std::sync::mpsc::Sender<
         BottomEvent<crossterm::event::KeyEvent, crossterm::event::MouseEvent>,
@@ -651,7 +396,7 @@ pub fn create_collection_thread(
     thread::spawn(move || {
         let mut data_state = data_harvester::DataCollector::new(filters);
 
-        data_state.set_collected_data(used_widget_set);
+        data_state.set_data_collection(used_widget_set);
         data_state.set_temperature_type(temp_type);
         data_state.set_use_current_cpu_total(use_current_cpu_total);
         data_state.set_show_average_cpu(show_average_cpu);
@@ -682,7 +427,7 @@ pub fn create_collection_thread(
                         data_state.set_show_average_cpu(app_config_fields.show_average_cpu);
                     }
                     ThreadControlEvent::UpdateUsedWidgets(used_widget_set) => {
-                        data_state.set_collected_data(*used_widget_set);
+                        data_state.set_data_collection(*used_widget_set);
                     }
                     ThreadControlEvent::UpdateUpdateTime(new_time) => {
                         update_time = new_time;
diff --git a/src/options.rs b/src/options.rs
index 1bebd502..2ff82be9 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -10,7 +10,11 @@ use std::{
 };
 
 use crate::{
-    app::{layout_manager::*, *},
+    app::{
+        layout_manager::*,
+        widgets::{ProcWidget, ProcWidgetMode},
+        *,
+    },
     canvas::ColourScheme,
     constants::*,
     units::data_units::DataUnit,
@@ -265,7 +269,7 @@ pub fn build_app(
     let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::new();
     let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::new();
     let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::new();
-    let mut proc_state_map: HashMap<u64, ProcWidgetState> = HashMap::new();
+    let mut proc_state_map: HashMap<u64, ProcWidget> = HashMap::new();
     let mut temp_state_map: HashMap<u64, TempWidgetState> = HashMap::new();
     let mut disk_state_map: HashMap<u64, DiskWidgetState> = HashMap::new();
     let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new();
@@ -344,33 +348,37 @@ pub fn build_app(
                         Net => {
                             net_state_map.insert(
                                 widget.widget_id,
-                                NetWidgetState::init(
-                                    default_time_value,
-                                    autohide_timer,
-                                    // network_unit_type.clone(),
-                                    // network_scale_type.clone(),
-                                ),
+                                NetWidgetState::init(default_time_value, autohide_timer),
                             );
                         }
                         Proc => {
+                            let mode = if is_grouped {
+                                ProcWidgetMode::Grouped
+                            } else if is_default_tree {
+                                ProcWidgetMode::Tree {
+                                    collapsed_pids: Default::default(),
+                                }
+                            } else {
+                                ProcWidgetMode::Normal
+                            };
+
                             proc_state_map.insert(
                                 widget.widget_id,
-                                ProcWidgetState::init(
+                                ProcWidget::init(
+                                    mode,
                                     is_case_sensitive,
                                     is_match_whole_word,
                                     is_use_regex,
-                                    is_grouped,
                                     show_memory_as_values,
-                                    is_default_tree,
                                     is_default_command,
                                 ),
                             );
                         }
                         Disk => {
-                            disk_state_map.insert(widget.widget_id, DiskWidgetState::init());
+                            disk_state_map.insert(widget.widget_id, DiskWidgetState::default());
                         }
                         Temp => {
-                            temp_state_map.insert(widget.widget_id, TempWidgetState::init());
+                            temp_state_map.insert(widget.widget_id, TempWidgetState::default());
                         }
                         Battery => {
                             battery_state_map
@@ -466,7 +474,7 @@ pub fn build_app(
                 let mapping = HashMap::new();
                 for widget in search_case_enabled_widgets {
                     if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
-                        proc_widget.process_search_state.is_ignoring_case = !widget.enabled;
+                        proc_widget.proc_search.is_ignoring_case = !widget.enabled;
                     }
                 }
                 flags.search_case_enabled_widgets_map = Some(mapping);
@@ -480,7 +488,7 @@ pub fn build_app(
                 let mapping = HashMap::new();
                 for widget in search_whole_word_enabled_widgets {
                     if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
-                        proc_widget.process_search_state.is_searching_whole_word = widget.enabled;
+                        proc_widget.proc_search.is_searching_whole_word = widget.enabled;
                     }
                 }
                 flags.search_whole_word_enabled_widgets_map = Some(mapping);
@@ -492,7 +500,7 @@ pub fn build_app(
                 let mapping = HashMap::new();
                 for widget in search_regex_enabled_widgets {
                     if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
-                        proc_widget.process_search_state.is_searching_with_regex = widget.enabled;
+                        proc_widget.proc_search.is_searching_with_regex = widget.enabled;
                     }
                 }
                 flags.search_regex_enabled_widgets_map = Some(mapping);
diff --git a/src/utils/gen_util.rs b/src/utils/gen_util.rs
index e0265290..c91a6852 100644
--- a/src/utils/gen_util.rs
+++ b/src/utils/gen_util.rs
@@ -92,30 +92,50 @@ pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
     }
 }
 
-/// Gotta get partial ordering?  No problem, here's something to deal with it~
-///
-/// Note that https://github.com/reem/rust-ordered-float exists, maybe move to it one day?  IDK.
-pub fn get_ordering<T: std::cmp::PartialOrd>(
-    a_val: T, b_val: T, reverse_order: bool,
-) -> std::cmp::Ordering {
-    match a_val.partial_cmp(&b_val) {
-        Some(x) => match x {
-            Ordering::Greater => {
-                if reverse_order {
-                    std::cmp::Ordering::Less
-                } else {
-                    std::cmp::Ordering::Greater
-                }
-            }
-            Ordering::Less => {
-                if reverse_order {
-                    std::cmp::Ordering::Greater
-                } else {
-                    std::cmp::Ordering::Less
-                }
-            }
-            Ordering::Equal => Ordering::Equal,
-        },
-        None => Ordering::Equal,
+#[inline]
+pub fn sort_partial_fn<T: std::cmp::PartialOrd>(is_reverse: bool) -> fn(T, T) -> Ordering {
+    if is_reverse {
+        partial_ordering_rev
+    } else {
+        partial_ordering
+    }
+}
+
+/// Returns an [`Ordering`] between two [`PartialOrd`]s.
+#[inline]
+pub fn partial_ordering<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
+    // TODO: Switch to `total_cmp` on 1.62
+    a.partial_cmp(&b).unwrap_or(Ordering::Equal)
+}
+
+/// Returns a reversed [`Ordering`] between two [`PartialOrd`]s.
+///
+/// This is simply a wrapper function around [`partial_ordering`] that reverses
+/// the result.
+#[inline]
+pub fn partial_ordering_rev<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
+    partial_ordering(a, b).reverse()
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_sort_partial_fn() {
+        let mut x = vec![9, 5, 20, 15, 10, 5];
+        let mut y = vec![1.0, 15.0, -1.0, -100.0, -100.1, 16.15, -100.0];
+
+        x.sort_by(|a, b| sort_partial_fn(false)(a, b));
+        assert_eq!(x, vec![5, 5, 9, 10, 15, 20]);
+
+        x.sort_by(|a, b| sort_partial_fn(true)(a, b));
+        assert_eq!(x, vec![20, 15, 10, 9, 5, 5]);
+
+        y.sort_by(|a, b| sort_partial_fn(false)(a, b));
+        assert_eq!(y, vec![-100.1, -100.0, -100.0, -1.0, 1.0, 15.0, 16.15]);
+
+        y.sort_by(|a, b| sort_partial_fn(true)(a, b));
+        assert_eq!(y, vec![16.15, 15.0, 1.0, -1.0, -100.0, -100.0, -100.1]);
     }
 }