From 65f87df1e06baf002db7608158e56a5b4e593c55 Mon Sep 17 00:00:00 2001
From: asimon-1 <40246417+asimon-1@users.noreply.github.com>
Date: Sat, 2 Dec 2023 12:02:43 -0500
Subject: [PATCH] Weighted Probability Menu Selections + TUI Backend Redesign
 (#655)

* Add replacement training_mod_tui crate

* Begin work on converting over to byteflags

* Allow StatefulTable.iter_mut()

* Additional merge work

* Fixing more compile errors from the merge

* Replace training_mod_tui with training_mod_tui_2, update byteflags dependency for random selection

* Fix button_config, hero buffs, caps issues. Move button logic into App

* Restore some functions that I cut too agressively

* Move to_vec into byteflags crate

* Fix src/training/ui

* Set `_` match arms on byteflags to `unreachable!()`

* Fix final few compiler errors

* Run formatter

* Reconsider whether unreachable parts are actually unreachable

* Adjust logging and remove dead code

* Fix some menu bugs

* Adjust icon sizes

* Separate checkmark from is_single_option

* Allow fast increment / decrement of slider handles with up/down

* Allow resetting current menu

* Change button behavior: `R/Z` now resets either the whole menu or the selected submenu depending on app.page, and `Y` now clears the current toggle so you don't have to press A six times. Only show keyhelp for `Y` if its relevant.

* Remove unneeded command line interface

* Take care of some minor nomenclature TODO's

* Increase stack size for menu::load_from_file() to avoid crashes

* Implement confirmation page before restoring from defaults

* Run rustfmt

* Fix warnings
---
 Cargo.toml                                    |    3 +-
 src/common/button_config.rs                   |   14 +-
 src/common/input.rs                           |   17 +-
 src/common/menu.rs                            |   65 +-
 src/common/release.rs                         |   16 +-
 src/hazard_manager/mod.rs                     |    2 +-
 src/hitbox_visualizer/mod.rs                  |    8 +-
 src/lib.rs                                    |   15 +-
 src/static/layout.arc                         |  Bin 3823684 -> 4204916 bytes
 src/training/buff.rs                          |   19 +-
 src/training/character_specific/items.rs      |    6 +-
 src/training/character_specific/pikmin.rs     |    3 +-
 src/training/combo.rs                         |    2 +-
 src/training/crouch.rs                        |    2 +-
 src/training/input_log.rs                     |  728 +++---
 src/training/input_record.rs                  |   26 +-
 src/training/ledge.rs                         |    2 +-
 src/training/mash.rs                          |   31 +-
 src/training/mod.rs                           |    2 +-
 src/training/save_states.rs                   |   41 +-
 src/training/shield.rs                        |   20 +-
 src/training/tech.rs                          |   14 +-
 src/training/ui/input_log.rs                  |    4 +-
 src/training/ui/menu.rs                       |  521 +++--
 src/training/ui/mod.rs                        |    2 +-
 training_mod_consts/Cargo.toml                |    7 +-
 training_mod_consts/src/lib.rs                | 1537 ++++++------
 training_mod_consts/src/options.rs            | 2064 ++++++-----------
 training_mod_tui/Cargo.toml                   |   16 +-
 training_mod_tui/src/containers/app.rs        |  390 ++++
 training_mod_tui/src/containers/mod.rs        |   24 +
 training_mod_tui/src/containers/submenu.rs    |  168 ++
 training_mod_tui/src/containers/tab.rs        |   54 +
 training_mod_tui/src/containers/toggle.rs     |   36 +
 training_mod_tui/src/gauge.rs                 |   27 -
 training_mod_tui/src/lib.rs                   |  916 +-------
 training_mod_tui/src/list.rs                  |  209 --
 training_mod_tui/src/main.rs                  |  360 ---
 training_mod_tui/src/structures/mod.rs        |    6 +
 .../src/structures/stateful_list.rs           |  124 +
 .../src/structures/stateful_slider.rs         |  146 ++
 .../src/structures/stateful_table.rs          |  274 +++
 training_mod_tui/tests/test_stateful_list.rs  |  182 ++
 .../tests/test_stateful_slider.rs             |  288 +++
 training_mod_tui/tests/test_stateful_table.rs |  342 +++
 training_mod_tui/tests/test_submenu.rs        |  502 ++++
 training_mod_tui/tests/test_toggle.rs         |   44 +
 training_mod_tui/training_mod_tui.iml         |   12 -
 48 files changed, 4922 insertions(+), 4369 deletions(-)
 create mode 100644 training_mod_tui/src/containers/app.rs
 create mode 100644 training_mod_tui/src/containers/mod.rs
 create mode 100644 training_mod_tui/src/containers/submenu.rs
 create mode 100644 training_mod_tui/src/containers/tab.rs
 create mode 100644 training_mod_tui/src/containers/toggle.rs
 delete mode 100644 training_mod_tui/src/gauge.rs
 delete mode 100644 training_mod_tui/src/list.rs
 delete mode 100644 training_mod_tui/src/main.rs
 create mode 100644 training_mod_tui/src/structures/mod.rs
 create mode 100644 training_mod_tui/src/structures/stateful_list.rs
 create mode 100644 training_mod_tui/src/structures/stateful_slider.rs
 create mode 100644 training_mod_tui/src/structures/stateful_table.rs
 create mode 100644 training_mod_tui/tests/test_stateful_list.rs
 create mode 100644 training_mod_tui/tests/test_stateful_slider.rs
 create mode 100644 training_mod_tui/tests/test_stateful_table.rs
 create mode 100644 training_mod_tui/tests/test_submenu.rs
 create mode 100644 training_mod_tui/tests/test_toggle.rs
 delete mode 100644 training_mod_tui/training_mod_tui.iml

diff --git a/Cargo.toml b/Cargo.toml
index 76573fb..bbf6795 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,10 +41,11 @@ byte-unit = "4.0.18"
 zip = { version = "0.6", default-features = false, features = ["deflate"] }
 anyhow = "1.0.72"
 
-
 [patch.crates-io]
 native-tls = { git = "https://github.com/skyline-rs/rust-native-tls", branch = "switch-timeout-panic" }
 nnsdk = { git = "https://github.com/ultimate-research/nnsdk-rs" }
+rand = { git = "https://github.com/skyline-rs/rand" }
+getrandom = { git = "https://github.com/Raytwo/getrandom" }
 
 [profile.dev]
 panic = "abort"
diff --git a/src/common/button_config.rs b/src/common/button_config.rs
index f188abc..c92a960 100644
--- a/src/common/button_config.rs
+++ b/src/common/button_config.rs
@@ -45,7 +45,7 @@ pub fn button_mapping(
         ButtonConfig::MINUS => b.minus(),
         ButtonConfig::LSTICK => b.stick_l(),
         ButtonConfig::RSTICK => b.stick_r(),
-        _ => false,
+        _ => panic!("Invalid value in button_mapping: {}", button_config),
     }
 }
 
@@ -166,7 +166,11 @@ pub enum ButtonCombo {
     InputPlayback,
 }
 
-pub const DEFAULT_OPEN_MENU_CONFIG: ButtonConfig = ButtonConfig::B.union(ButtonConfig::DPAD_UP);
+pub const DEFAULT_OPEN_MENU_CONFIG: ButtonConfig = ButtonConfig {
+    B: 1,
+    DPAD_UP: 1,
+    ..ButtonConfig::empty()
+};
 
 unsafe fn get_combo_keys(combo: ButtonCombo) -> ButtonConfig {
     match combo {
@@ -196,14 +200,14 @@ fn _combo_passes(p1_controller: Controller, combo: ButtonCombo) -> bool {
         let combo_keys = get_combo_keys(combo).to_vec();
         let mut this_combo_passes = false;
 
-        for hold_button in &combo_keys[..] {
+        for hold_button in combo_keys.iter() {
             if button_mapping(
                 *hold_button,
                 p1_controller.style,
                 p1_controller.current_buttons,
             ) && combo_keys
                 .iter()
-                .filter(|press_button| **press_button != *hold_button)
+                .filter(|press_button| press_button != &hold_button)
                 .all(|press_button| {
                     button_mapping(*press_button, p1_controller.style, p1_controller.just_down)
                 })
@@ -238,7 +242,7 @@ pub fn handle_final_input_mapping(player_idx: i32, controller_struct: &mut SomeC
         let mut start_menu_request = false;
 
         let menu_close_wait_frame = frame_counter::get_frame_count(*menu::MENU_CLOSE_FRAME_COUNTER);
-        if unsafe { MENU.menu_open_start_press == OnOff::On } {
+        if unsafe { MENU.menu_open_start_press == OnOff::ON } {
             let start_hold_frames = &mut *START_HOLD_FRAMES.lock();
             if p1_controller.current_buttons.plus() {
                 *start_hold_frames += 1;
diff --git a/src/common/input.rs b/src/common/input.rs
index 92a916b..8335a6b 100644
--- a/src/common/input.rs
+++ b/src/common/input.rs
@@ -1,5 +1,3 @@
-#![allow(dead_code)] // TODO: Yeah don't do this
-use crate::extra_bitflag_impls;
 use bitflags::bitflags;
 use modular_bitfield::{bitfield, specifiers::*};
 
@@ -215,14 +213,25 @@ bitflags! {
 }
 
 // This requires some imports to work
-use training_mod_consts::{random_option, ToggleTrait};
 impl std::fmt::Display for Buttons {
     fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         todo!()
     }
 }
 
-extra_bitflag_impls!(Buttons);
+impl Buttons {
+    pub fn to_vec(&self) -> Vec<Buttons> {
+        // Reimplemented for bitflags
+        let mut vec = Vec::<Buttons>::new();
+        let mut field = Buttons::from_bits_truncate(self.bits);
+        while !field.is_empty() {
+            let flag = Buttons::from_bits(1u32 << field.bits.trailing_zeros()).unwrap();
+            field -= flag;
+            vec.push(flag);
+        }
+        vec
+    }
+}
 
 // Controller class used internally by the game
 #[derive(Debug, Default, Copy, Clone)]
diff --git a/src/common/menu.rs b/src/common/menu.rs
index 0ed2a0c..340f7f7 100644
--- a/src/common/menu.rs
+++ b/src/common/menu.rs
@@ -1,11 +1,12 @@
 use once_cell::sync::Lazy;
 use std::collections::HashMap;
 use std::fs;
+use std::io::BufReader;
 
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use skyline::nn::hid::GetNpadStyleSet;
-use training_mod_consts::MenuJsonStruct;
+use training_mod_consts::{create_app, MenuJsonStruct};
 use training_mod_tui::AppPage;
 
 use crate::common::button_config::button_mapping;
@@ -24,11 +25,15 @@ pub unsafe fn menu_condition() -> bool {
 }
 
 pub fn load_from_file() {
+    // Note that this function requires a larger stack size
+    // With the switch default, it'll crash w/o a helpful error message
     info!("Checking for previous menu in {MENU_OPTIONS_PATH}...");
+    let err_msg = format!("Could not read {}", MENU_OPTIONS_PATH);
     if fs::metadata(MENU_OPTIONS_PATH).is_ok() {
-        let menu_conf = fs::read_to_string(MENU_OPTIONS_PATH)
-            .unwrap_or_else(|_| panic!("Could not remove {}", MENU_OPTIONS_PATH));
-        if let Ok(menu_conf_json) = serde_json::from_str::<MenuJsonStruct>(&menu_conf) {
+        let menu_conf = fs::File::open(MENU_OPTIONS_PATH).expect(&err_msg);
+        let reader = BufReader::new(menu_conf);
+        if let Ok(menu_conf_json) = serde_json::from_reader::<BufReader<_>, MenuJsonStruct>(reader)
+        {
             unsafe {
                 MENU = menu_conf_json.menu;
                 DEFAULTS_MENU = menu_conf_json.defaults_menu;
@@ -36,16 +41,22 @@ pub fn load_from_file() {
             }
         } else {
             warn!("Previous menu found but is invalid. Deleting...");
-            fs::remove_file(MENU_OPTIONS_PATH).unwrap_or_else(|_| {
-                panic!(
-                    "{} has invalid schema but could not be deleted!",
-                    MENU_OPTIONS_PATH
-                )
-            });
+            let err_msg = format!(
+                "{} has invalid schema but could not be deleted!",
+                MENU_OPTIONS_PATH
+            );
+            fs::remove_file(MENU_OPTIONS_PATH).expect(&err_msg);
         }
     } else {
         info!("No previous menu file found.");
     }
+    info!("Setting initial menu selections...");
+    unsafe {
+        let mut app = QUICK_MENU_APP.lock();
+        app.serialized_default_settings =
+            serde_json::to_string(&DEFAULTS_MENU).expect("Could not serialize DEFAULTS_MENU");
+        app.update_all_from_json(&serde_json::to_string(&MENU).expect("Could not serialize MENU"));
+    }
 }
 
 pub unsafe fn set_menu_from_json(message: &str) {
@@ -72,6 +83,8 @@ pub unsafe fn set_menu_from_json(message: &str) {
 pub fn spawn_menu() {
     unsafe {
         QUICK_MENU_ACTIVE = true;
+        let mut app = QUICK_MENU_APP.lock();
+        app.page = AppPage::SUBMENU;
         *MENU_RECEIVED_INPUT.data_ptr() = true;
     }
 }
@@ -89,14 +102,10 @@ enum DirectionButton {
 }
 
 lazy_static! {
-    pub static ref QUICK_MENU_APP: Mutex<training_mod_tui::App> = Mutex::new(
-        training_mod_tui::App::new(unsafe { ui_menu(MENU) }, unsafe {
-            (
-                ui_menu(DEFAULTS_MENU),
-                serde_json::to_string(&DEFAULTS_MENU).unwrap(),
-            )
-        })
-    );
+    pub static ref QUICK_MENU_APP: Mutex<training_mod_tui::App<'static>> = Mutex::new({
+        info!("Initialized lazy_static: QUICK_MENU_APP");
+        unsafe { create_app() }
+    });
     pub static ref P1_CONTROLLER_STYLE: Mutex<ControllerStyle> =
         Mutex::new(ControllerStyle::default());
     static ref DIRECTION_HOLD_FRAMES: Mutex<HashMap<DirectionButton, u32>> = {
@@ -185,43 +194,42 @@ pub fn handle_final_input_mapping(
                         }
                     });
 
-                let app = &mut *QUICK_MENU_APP.data_ptr();
+                let app = &mut *QUICK_MENU_APP.data_ptr(); // TODO: Why aren't we taking a lock here?
                 button_mapping(ButtonConfig::A, style, button_presses).then(|| {
                     app.on_a();
                     received_input = true;
                 });
                 button_mapping(ButtonConfig::B, style, button_presses).then(|| {
                     received_input = true;
-                    if app.page != AppPage::SUBMENU {
-                        app.on_b()
-                    } else {
+                    app.on_b();
+                    if app.page == AppPage::CLOSE {
                         // Leave menu.
                         frame_counter::start_counting(*MENU_CLOSE_FRAME_COUNTER);
                         QUICK_MENU_ACTIVE = false;
-                        let menu_json = app.get_menu_selections();
+                        let menu_json = app.get_serialized_settings_with_defaults();
                         set_menu_from_json(&menu_json);
                         EVENT_QUEUE.push(Event::menu_open(menu_json));
                     }
                 });
                 button_mapping(ButtonConfig::X, style, button_presses).then(|| {
-                    app.save_defaults();
+                    app.on_x();
                     received_input = true;
                 });
                 button_mapping(ButtonConfig::Y, style, button_presses).then(|| {
-                    app.reset_all_submenus();
+                    app.on_y();
                     received_input = true;
                 });
 
                 button_mapping(ButtonConfig::ZL, style, button_presses).then(|| {
-                    app.previous_tab();
+                    app.on_zl();
                     received_input = true;
                 });
                 button_mapping(ButtonConfig::ZR, style, button_presses).then(|| {
-                    app.next_tab();
+                    app.on_zr();
                     received_input = true;
                 });
                 button_mapping(ButtonConfig::R, style, button_presses).then(|| {
-                    app.reset_current_submenu();
+                    app.on_r();
                     received_input = true;
                 });
 
@@ -263,7 +271,6 @@ pub fn handle_final_input_mapping(
 
                 if received_input {
                     direction_hold_frames.iter_mut().for_each(|(_, f)| *f = 0);
-                    set_menu_from_json(&app.get_menu_selections());
                     *MENU_RECEIVED_INPUT.lock() = true;
                 }
             }
diff --git a/src/common/release.rs b/src/common/release.rs
index efe71e5..7a7b1d8 100644
--- a/src/common/release.rs
+++ b/src/common/release.rs
@@ -10,9 +10,12 @@ use serde_json::Value;
 use zip::ZipArchive;
 
 lazy_static! {
-    pub static ref CURRENT_VERSION: Mutex<String> = Mutex::new(match get_current_version() {
-        Ok(v) => v,
-        Err(e) => panic!("Could not find current modpack version!: {}", e),
+    pub static ref CURRENT_VERSION: Mutex<String> = Mutex::new({
+        info!("Initialized lazy_static: CURRENT_VERSION");
+        match get_current_version() {
+            Ok(v) => v,
+            Err(e) => panic!("Could not find current modpack version!: {}", e),
+        }
     });
 }
 
@@ -178,12 +181,13 @@ pub fn perform_version_check() {
     let update_policy = get_update_policy();
     info!("Update Policy is {}", update_policy);
     let mut release_to_apply = match update_policy {
-        UpdatePolicy::Stable => get_release(false),
-        UpdatePolicy::Beta => get_release(true),
-        UpdatePolicy::Disabled => {
+        UpdatePolicy::STABLE => get_release(false),
+        UpdatePolicy::BETA => get_release(true),
+        UpdatePolicy::DISABLED => {
             // User does not want to update at all
             Err(anyhow!("Updates are disabled per UpdatePolicy"))
         }
+        _ => panic!("Invalid value in perform_version_check: {}", update_policy),
     };
     if release_to_apply.is_ok() {
         let published_at = release_to_apply.as_ref().unwrap().published_at.clone();
diff --git a/src/hazard_manager/mod.rs b/src/hazard_manager/mod.rs
index a5643ee..05fbc0a 100644
--- a/src/hazard_manager/mod.rs
+++ b/src/hazard_manager/mod.rs
@@ -96,7 +96,7 @@ fn hazard_intercept(ctx: &skyline::hooks::InlineCtx) {
 
 fn mod_handle_hazards() {
     unsafe {
-        *HAZARD_FLAG_ADDRESS = (MENU.stage_hazards == OnOff::On) as u8;
+        *HAZARD_FLAG_ADDRESS = (MENU.stage_hazards == OnOff::ON) as u8;
     }
 }
 
diff --git a/src/hitbox_visualizer/mod.rs b/src/hitbox_visualizer/mod.rs
index eaa445a..c734941 100644
--- a/src/hitbox_visualizer/mod.rs
+++ b/src/hitbox_visualizer/mod.rs
@@ -135,7 +135,7 @@ pub unsafe fn get_command_flag_cat(module_accessor: &mut app::BattleObjectModule
     // Resume Effect AnimCMD incase we don't display hitboxes
     MotionAnimcmdModule::set_sleep_effect(module_accessor, false);
 
-    if MENU.hitbox_vis == OnOff::Off {
+    if MENU.hitbox_vis == OnOff::OFF {
         return;
     }
 
@@ -205,7 +205,7 @@ unsafe fn mod_handle_attack(lua_state: u64) {
 
     // necessary if param object fails
     // hacky way of forcing no shield damage on all hitboxes
-    if MENU.shield_state == Shield::Infinite {
+    if MENU.shield_state == Shield::INFINITE {
         let mut hitbox_params: Vec<L2CValue> =
             (0..36).map(|i| l2c_agent.pop_lua_stack(i + 1)).collect();
         l2c_agent.clear_lua_stack();
@@ -219,7 +219,7 @@ unsafe fn mod_handle_attack(lua_state: u64) {
     }
 
     // Hitbox Visualization
-    if MENU.hitbox_vis == OnOff::On {
+    if MENU.hitbox_vis == OnOff::ON {
         // get all necessary grabbox params
         let id = l2c_agent.pop_lua_stack(1); // int
         let joint = l2c_agent.pop_lua_stack(3); // hash40
@@ -274,7 +274,7 @@ unsafe fn handle_catch(lua_state: u64) {
 }
 
 unsafe fn mod_handle_catch(lua_state: u64) {
-    if MENU.hitbox_vis == OnOff::Off {
+    if MENU.hitbox_vis == OnOff::OFF {
         return;
     }
 
diff --git a/src/lib.rs b/src/lib.rs
index dcbb0c0..8890177 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -18,7 +18,7 @@ use std::fs;
 use std::path::PathBuf;
 
 use skyline::nro::{self, NroInfo};
-use training_mod_consts::{extra_bitflag_impls, OnOff, LEGACY_TRAINING_MODPACK_ROOT};
+use training_mod_consts::{OnOff, LEGACY_TRAINING_MODPACK_ROOT};
 
 use crate::common::button_config::DEFAULT_OPEN_MENU_CONFIG;
 use crate::common::events::events_loop;
@@ -66,7 +66,7 @@ pub fn main() {
         skyline::error::show_error(
             69,
             "SSBU Training Modpack has panicked! Please open the details and send a screenshot to the developer, then close the game.\n",
-            err_msg.as_str(),
+            &err_msg,
         );
     }));
     init_logger().unwrap();
@@ -101,7 +101,14 @@ pub fn main() {
         });
     }
 
-    menu::load_from_file();
+    info!("Performing saved data check...");
+    let data_loader = std::thread::Builder::new()
+        .stack_size(0x20000)
+        .spawn(move || {
+            menu::load_from_file();
+        })
+        .unwrap();
+    let _result = data_loader.join();
 
     if !is_emulator() {
         info!("Performing version check...");
@@ -120,7 +127,7 @@ pub fn main() {
         notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
         notification(
             "Open Menu".to_string(),
-            if MENU.menu_open_start_press == OnOff::On {
+            if MENU.menu_open_start_press == OnOff::ON {
                 "Hold Start".to_string()
             } else {
                 DEFAULT_OPEN_MENU_CONFIG.to_string()
diff --git a/src/static/layout.arc b/src/static/layout.arc
index 75611c47d9263e3f485fe873c85ed1b9533e2bff..da059da6af7cb79273b42fd569b6164d31ca4579 100644
GIT binary patch
delta 40254
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

delta 22497
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
OJ@^2#?FC!ySxNu~CVHm;

diff --git a/src/training/buff.rs b/src/training/buff.rs
index 55e2277..d5f04e3 100644
--- a/src/training/buff.rs
+++ b/src/training/buff.rs
@@ -10,8 +10,8 @@ use crate::training::handle_add_limit;
 
 use once_cell::sync::Lazy;
 
-static mut BUFF_REMAINING_PLAYER: i32 = 0;
-static mut BUFF_REMAINING_CPU: i32 = 0;
+static mut BUFF_REMAINING_PLAYER: usize = 0;
+static mut BUFF_REMAINING_CPU: usize = 0;
 
 static mut IS_BUFFING_PLAYER: bool = false;
 static mut IS_BUFFING_CPU: bool = false;
@@ -46,7 +46,10 @@ pub unsafe fn is_buffing_any() -> bool {
     IS_BUFFING_CPU || IS_BUFFING_PLAYER
 }
 
-pub unsafe fn set_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor, new_value: i32) {
+pub unsafe fn set_buff_rem(
+    module_accessor: &mut app::BattleObjectModuleAccessor,
+    new_value: usize,
+) {
     if is_operation_cpu(module_accessor) {
         BUFF_REMAINING_CPU = new_value;
         return;
@@ -54,7 +57,7 @@ pub unsafe fn set_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor
     BUFF_REMAINING_PLAYER = new_value;
 }
 
-pub unsafe fn get_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor) -> i32 {
+pub unsafe fn get_buff_rem(module_accessor: &mut app::BattleObjectModuleAccessor) -> usize {
     if is_operation_cpu(module_accessor) {
         return BUFF_REMAINING_CPU;
     }
@@ -76,7 +79,7 @@ pub unsafe fn handle_buffs(
     CameraModule::stop_quake(module_accessor, *CAMERA_QUAKE_KIND_M); // stops Psyche-Up quake
     CameraModule::stop_quake(module_accessor, *CAMERA_QUAKE_KIND_S); // stops Monado Art quake
 
-    let menu_vec = MENU.buff_state.to_vec();
+    let menu_vec = MENU.buff_state;
 
     if fighter_kind == *FIGHTER_KIND_BRAVE {
         return buff_hero(module_accessor, status);
@@ -101,11 +104,11 @@ pub unsafe fn handle_buffs(
 }
 
 unsafe fn buff_hero(module_accessor: &mut app::BattleObjectModuleAccessor, status: i32) -> bool {
-    let buff_vec = MENU.buff_state.hero_buffs().to_vec();
+    let buff_vec: Vec<BuffOption> = MENU.buff_state.to_vec();
     if !is_buffing(module_accessor) {
         // Initial set up for spells
         start_buff(module_accessor);
-        set_buff_rem(module_accessor, buff_vec.len() as i32);
+        set_buff_rem(module_accessor, buff_vec.len());
         // Since it's the first step of buffing, we need to set up how many buffs there are
     }
     if get_buff_rem(module_accessor) <= 0 {
@@ -133,7 +136,7 @@ unsafe fn buff_hero_single(
     }
     let spell_index = get_buff_rem(module_accessor) - 1;
     // Used to get spell from our vector
-    let spell_option = buff_vec.get(spell_index as usize);
+    let spell_option = buff_vec.get(spell_index);
     if spell_option.is_none() {
         // There are no spells selected, or something went wrong with making the vector
         return;
diff --git a/src/training/character_specific/items.rs b/src/training/character_specific/items.rs
index 77308c7..99ba1a1 100644
--- a/src/training/character_specific/items.rs
+++ b/src/training/character_specific/items.rs
@@ -447,15 +447,15 @@ pub unsafe fn apply_item(character_item: CharacterItem) {
     let cpu_fighter_kind = app::utility::get_kind(&mut *cpu_module_accessor);
     let character_item_num = character_item.as_idx();
     let (item_fighter_kind, variation_idx) =
-        if character_item_num <= CharacterItem::PlayerVariation8.as_idx() {
+        if character_item_num <= CharacterItem::PLAYER_VARIATION_8.as_idx() {
             (
                 player_fighter_kind,
-                (character_item_num - CharacterItem::PlayerVariation1.as_idx()) as usize,
+                (character_item_num - CharacterItem::PLAYER_VARIATION_1.as_idx()),
             )
         } else {
             (
                 cpu_fighter_kind,
-                (character_item_num - CharacterItem::CpuVariation1.as_idx()) as usize,
+                (character_item_num - CharacterItem::CPU_VARIATION_1.as_idx()),
             )
         };
     ALL_CHAR_ITEMS
diff --git a/src/training/character_specific/pikmin.rs b/src/training/character_specific/pikmin.rs
index edced29..9c69567 100644
--- a/src/training/character_specific/pikmin.rs
+++ b/src/training/character_specific/pikmin.rs
@@ -1,3 +1,4 @@
+use crate::info;
 use smash::app::{self, lua_bind::*, smashball::is_training_mode};
 use smash::lib::lua_const::*;
 
@@ -128,7 +129,7 @@ pub unsafe fn get_current_pikmin(
             }
         };
         let held_boid = WorkModule::get_int(module_accessor, held_work_var) as u32;
-        println!(", boid: {}", held_boid);
+        info!(", boid: {}", held_boid);
         pikmin_boid_vec.push(held_boid);
     }
     // Next, we get the order of the following pikmin
diff --git a/src/training/combo.rs b/src/training/combo.rs
index f440a90..d381d5a 100644
--- a/src/training/combo.rs
+++ b/src/training/combo.rs
@@ -47,7 +47,7 @@ unsafe fn is_actionable(module_accessor: *mut app::BattleObjectModuleAccessor) -
 fn update_frame_advantage(new_frame_adv: i32) {
     unsafe {
         FRAME_ADVANTAGE = new_frame_adv;
-        if MENU.frame_advantage == OnOff::On {
+        if MENU.frame_advantage == OnOff::ON {
             // Prioritize Frame Advantage over Input Recording Playback
             ui::notifications::clear_notifications("Input Recording");
             ui::notifications::clear_notifications("Frame Advantage");
diff --git a/src/training/crouch.rs b/src/training/crouch.rs
index 394d7da..39b318b 100644
--- a/src/training/crouch.rs
+++ b/src/training/crouch.rs
@@ -10,7 +10,7 @@ pub unsafe fn mod_get_stick_y(module_accessor: &mut BattleObjectModuleAccessor)
     }
     let fighter_status_kind = StatusModule::status_kind(module_accessor);
 
-    if MENU.crouch == OnOff::On
+    if MENU.crouch == OnOff::ON
         && [
             *FIGHTER_STATUS_KIND_WAIT,
             *FIGHTER_STATUS_KIND_SQUAT,
diff --git a/src/training/input_log.rs b/src/training/input_log.rs
index 1d089d1..eb6cbec 100644
--- a/src/training/input_log.rs
+++ b/src/training/input_log.rs
@@ -1,362 +1,366 @@
-use itertools::Itertools;
-use once_cell::sync::Lazy;
-use std::collections::VecDeque;
-
-use crate::common::{input::*, menu::QUICK_MENU_ACTIVE, try_get_module_accessor};
-use lazy_static::lazy_static;
-use parking_lot::Mutex;
-use skyline::nn::ui2d::ResColor;
-use smash::app::{lua_bind::*, utility};
-use training_mod_consts::{FighterId, InputDisplay, MENU};
-
-use super::{frame_counter, input_record::STICK_CLAMP_MULTIPLIER};
-
-const GREEN: ResColor = ResColor {
-    r: 22,
-    g: 156,
-    b: 0,
-    a: 0,
-};
-
-const RED: ResColor = ResColor {
-    r: 153,
-    g: 10,
-    b: 10,
-    a: 0,
-};
-
-const CYAN: ResColor = ResColor {
-    r: 0,
-    g: 255,
-    b: 255,
-    a: 0,
-};
-
-const BLUE: ResColor = ResColor {
-    r: 0,
-    g: 40,
-    b: 108,
-    a: 0,
-};
-
-const PURPLE: ResColor = ResColor {
-    r: 100,
-    g: 66,
-    b: 202,
-    a: 0,
-};
-
-pub const YELLOW: ResColor = ResColor {
-    r: 230,
-    g: 180,
-    b: 14,
-    a: 0,
-};
-
-pub const WHITE: ResColor = ResColor {
-    r: 255,
-    g: 255,
-    b: 255,
-    a: 0,
-};
-
-pub static PER_LOG_FRAME_COUNTER: Lazy<usize> =
-    Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset));
-pub static OVERALL_FRAME_COUNTER: Lazy<usize> =
-    Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset));
-
-pub const NUM_LOGS: usize = 15;
-pub static mut DRAW_LOG_BASE_IDX: Lazy<Mutex<usize>> = Lazy::new(|| Mutex::new(0));
-
-#[derive(PartialEq, Eq, Debug, Copy, Clone)]
-pub enum DirectionStrength {
-    None,
-    Weak,
-    // Strong,
-}
-
-#[derive(Copy, Clone, Default)]
-pub struct InputLog {
-    pub ttl: u32,
-    pub frames: u32,
-    pub overall_frame: u32,
-    pub raw_inputs: Controller,
-    pub smash_inputs: MappedInputs,
-    pub status: i32,
-    pub fighter_kind: i32,
-}
-
-impl PartialEq for InputLog {
-    fn eq(&self, other: &Self) -> bool {
-        self.frames == other.frames && !self.is_different(other)
-    }
-}
-impl Eq for InputLog {}
-
-const WALK_THRESHOLD_X: i8 = 20;
-const _DASH_THRESHOLD_X: i8 = 102;
-const DEADZONE_THRESHOLD_Y: i8 = 30;
-const _TAP_JUMP_THRESHOLD_Y: i8 = 90;
-
-fn bin_stick_values(x: i8, y: i8) -> (DirectionStrength, f32) {
-    (
-        // TODO
-        DirectionStrength::Weak,
-        match (x, y) {
-            // X only
-            (x, y) if y.abs() < DEADZONE_THRESHOLD_Y => match x {
-                x if x > WALK_THRESHOLD_X => 0.0,
-                x if x < -WALK_THRESHOLD_X => 180.0,
-                _ => return (DirectionStrength::None, 0.0),
-            },
-            // Y only
-            (x, y) if x.abs() < WALK_THRESHOLD_X => match y {
-                y if y > DEADZONE_THRESHOLD_Y => 90.0,
-                y if y < -DEADZONE_THRESHOLD_Y => 270.0,
-                _ => return (DirectionStrength::None, 0.0),
-            },
-            // Positive Y
-            (x, y) if y > DEADZONE_THRESHOLD_Y => match x {
-                x if x > WALK_THRESHOLD_X => 45.0,
-                x if x < -WALK_THRESHOLD_X => 135.0,
-                _ => return (DirectionStrength::Weak, 90.0),
-            },
-            // Negative Y
-            (x, y) if y < DEADZONE_THRESHOLD_Y => match x {
-                x if x > WALK_THRESHOLD_X => 315.0,
-                x if x < -WALK_THRESHOLD_X => 225.0,
-                _ => return (DirectionStrength::Weak, 270.0),
-            },
-            _ => return (DirectionStrength::None, 0.0),
-        },
-    )
-}
-
-impl InputLog {
-    pub fn is_different(&self, other: &InputLog) -> bool {
-        unsafe {
-            match MENU.input_display {
-                InputDisplay::Smash => self.is_smash_different(other),
-                InputDisplay::Raw => self.is_raw_different(other),
-                InputDisplay::None => false,
-            }
-        }
-    }
-
-    pub fn binned_lstick(&self) -> (DirectionStrength, f32) {
-        unsafe {
-            match MENU.input_display {
-                InputDisplay::Smash => self.smash_binned_lstick(),
-                InputDisplay::Raw => self.raw_binned_lstick(),
-                InputDisplay::None => panic!("Invalid input display to log"),
-            }
-        }
-    }
-
-    pub fn binned_rstick(&self) -> (DirectionStrength, f32) {
-        unsafe {
-            match MENU.input_display {
-                InputDisplay::Smash => self.smash_binned_rstick(),
-                InputDisplay::Raw => self.raw_binned_rstick(),
-                InputDisplay::None => panic!("Invalid input display to log"),
-            }
-        }
-    }
-
-    pub fn button_icons(&self) -> VecDeque<(&str, ResColor)> {
-        unsafe {
-            match MENU.input_display {
-                InputDisplay::Smash => self.smash_button_icons(),
-                InputDisplay::Raw => self.raw_button_icons(),
-                InputDisplay::None => panic!("Invalid input display to log"),
-            }
-        }
-    }
-
-    fn smash_button_icons(&self) -> VecDeque<(&str, ResColor)> {
-        self.smash_inputs
-            .buttons
-            .to_vec()
-            .iter()
-            .filter_map(|button| {
-                Some(match *button {
-                    Buttons::ATTACK | Buttons::ATTACK_RAW => ("a", GREEN),
-                    Buttons::SPECIAL | Buttons::SPECIAL_RAW2 => ("b", RED),
-                    Buttons::JUMP => ("x", CYAN),
-                    Buttons::GUARD | Buttons::GUARD_HOLD => ("lb", BLUE),
-                    Buttons::CATCH => ("zr", PURPLE),
-                    Buttons::STOCK_SHARE => ("plus", WHITE),
-                    Buttons::APPEAL_HI => ("dpad_up", WHITE),
-                    Buttons::APPEAL_LW => ("dpad_down", WHITE),
-                    Buttons::APPEAL_SL => ("dpad_right", WHITE),
-                    Buttons::APPEAL_SR => ("dpad_left", WHITE),
-                    _ => return None,
-                })
-            })
-            .unique_by(|(s, _)| *s)
-            .collect::<VecDeque<(&str, ResColor)>>()
-    }
-
-    fn raw_button_icons(&self) -> VecDeque<(&str, ResColor)> {
-        let buttons = self.raw_inputs.current_buttons;
-        let mut icons = VecDeque::new();
-        if buttons.a() {
-            icons.push_front(("a", GREEN));
-        }
-        if buttons.b() {
-            icons.push_front(("b", RED));
-        }
-        if buttons.x() {
-            icons.push_front(("x", CYAN));
-        }
-        if buttons.y() {
-            icons.push_front(("y", CYAN));
-        }
-        if buttons.l() || buttons.real_digital_l() {
-            icons.push_front(("lb", BLUE));
-        }
-        if buttons.r() || buttons.real_digital_r() {
-            icons.push_front(("rb", BLUE));
-        }
-        if buttons.zl() {
-            icons.push_front(("zl", PURPLE));
-        }
-        if buttons.zr() {
-            icons.push_front(("zr", PURPLE));
-        }
-        if buttons.plus() {
-            icons.push_front(("plus", WHITE));
-        }
-        if buttons.minus() {
-            icons.push_front(("minus", WHITE));
-        }
-        if buttons.dpad_up() {
-            icons.push_front(("dpad_up", WHITE));
-        }
-        if buttons.dpad_down() {
-            icons.push_front(("dpad_down", WHITE));
-        }
-        if buttons.dpad_left() {
-            icons.push_front(("dpad_left", WHITE));
-        }
-        if buttons.dpad_right() {
-            icons.push_front(("dpad_right", WHITE));
-        }
-
-        icons
-    }
-
-    fn is_smash_different(&self, other: &InputLog) -> bool {
-        self.smash_inputs.buttons != other.smash_inputs.buttons
-            || self.smash_binned_lstick() != other.smash_binned_lstick()
-            || self.smash_binned_rstick() != other.smash_binned_rstick()
-            || (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status)
-    }
-
-    fn smash_binned_lstick(&self) -> (DirectionStrength, f32) {
-        bin_stick_values(self.smash_inputs.lstick_x, self.smash_inputs.lstick_y)
-    }
-
-    fn smash_binned_rstick(&self) -> (DirectionStrength, f32) {
-        bin_stick_values(self.smash_inputs.rstick_x, self.smash_inputs.rstick_y)
-    }
-
-    fn is_raw_different(&self, other: &InputLog) -> bool {
-        self.raw_inputs.current_buttons != other.raw_inputs.current_buttons
-            || self.raw_binned_lstick() != other.raw_binned_lstick()
-            || self.raw_binned_rstick() != other.raw_binned_rstick()
-            || (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status)
-    }
-
-    fn raw_binned_lstick(&self) -> (DirectionStrength, f32) {
-        let x = (self.raw_inputs.left_stick_x / STICK_CLAMP_MULTIPLIER) as i8;
-        let y = (self.raw_inputs.left_stick_y / STICK_CLAMP_MULTIPLIER) as i8;
-        bin_stick_values(x, y)
-    }
-
-    fn raw_binned_rstick(&self) -> (DirectionStrength, f32) {
-        let x = (self.raw_inputs.right_stick_x / STICK_CLAMP_MULTIPLIER) as i8;
-        let y = (self.raw_inputs.right_stick_y / STICK_CLAMP_MULTIPLIER) as i8;
-        bin_stick_values(x, y)
-    }
-}
-
-fn insert_in_place<T>(array: &mut [T], value: T, index: usize) {
-    array[index..].rotate_right(1);
-    array[index] = value;
-}
-
-fn insert_in_front<T>(array: &mut [T], value: T) {
-    insert_in_place(array, value, 0);
-}
-
-lazy_static! {
-    pub static ref P1_INPUT_LOGS: Mutex<[InputLog; NUM_LOGS]> =
-        Mutex::new([InputLog::default(); NUM_LOGS]);
-}
-
-pub fn handle_final_input_mapping(
-    player_idx: i32,
-    controller_struct: &SomeControllerStruct,
-    out: *mut MappedInputs,
-) {
-    unsafe {
-        if MENU.input_display == InputDisplay::None {
-            return;
-        }
-
-        if QUICK_MENU_ACTIVE {
-            return;
-        }
-
-        if player_idx == 0 {
-            let module_accessor = try_get_module_accessor(FighterId::Player);
-            if module_accessor.is_none() {
-                return;
-            }
-            let module_accessor = module_accessor.unwrap();
-
-            let current_frame = frame_counter::get_frame_count(*PER_LOG_FRAME_COUNTER);
-            let current_overall_frame = frame_counter::get_frame_count(*OVERALL_FRAME_COUNTER);
-            // We should always be counting
-            frame_counter::start_counting(*PER_LOG_FRAME_COUNTER);
-            frame_counter::start_counting(*OVERALL_FRAME_COUNTER);
-
-            let potential_input_log = InputLog {
-                ttl: 600,
-                frames: 1,
-                overall_frame: current_overall_frame,
-                raw_inputs: *controller_struct.controller,
-                smash_inputs: *out,
-                status: StatusModule::status_kind(module_accessor),
-                fighter_kind: utility::get_kind(&mut *module_accessor),
-            };
-
-            let input_logs = &mut *P1_INPUT_LOGS.lock();
-            let latest_input_log = input_logs.first_mut().unwrap();
-            let prev_overall_frames = latest_input_log.overall_frame;
-            let prev_ttl = latest_input_log.ttl;
-            // Only update if we are on a new frame according to the latest log
-            let is_new_frame = prev_overall_frames != current_overall_frame;
-            if is_new_frame && latest_input_log.is_different(&potential_input_log) {
-                frame_counter::reset_frame_count(*PER_LOG_FRAME_COUNTER);
-                // We should count this frame already
-                frame_counter::tick_idx(*PER_LOG_FRAME_COUNTER);
-                insert_in_front(input_logs, potential_input_log);
-                let draw_log_base_idx = &mut *DRAW_LOG_BASE_IDX.data_ptr();
-                *draw_log_base_idx = (*draw_log_base_idx + 1) % NUM_LOGS;
-            } else if is_new_frame {
-                *latest_input_log = potential_input_log;
-                latest_input_log.frames = std::cmp::min(current_frame, 99);
-                latest_input_log.ttl = prev_ttl;
-            }
-
-            // Decrease TTL
-            for input_log in input_logs.iter_mut() {
-                if input_log.ttl > 0 && is_new_frame {
-                    input_log.ttl -= 1;
-                }
-            }
-        }
-    }
-}
+use itertools::Itertools;
+use once_cell::sync::Lazy;
+use std::collections::VecDeque;
+
+use crate::common::{input::*, menu::QUICK_MENU_ACTIVE, try_get_module_accessor};
+use lazy_static::lazy_static;
+use parking_lot::Mutex;
+use skyline::nn::ui2d::ResColor;
+use smash::app::{lua_bind::*, utility};
+use training_mod_consts::{FighterId, InputDisplay, MENU};
+
+use super::{frame_counter, input_record::STICK_CLAMP_MULTIPLIER};
+
+const GREEN: ResColor = ResColor {
+    r: 22,
+    g: 156,
+    b: 0,
+    a: 0,
+};
+
+const RED: ResColor = ResColor {
+    r: 153,
+    g: 10,
+    b: 10,
+    a: 0,
+};
+
+const CYAN: ResColor = ResColor {
+    r: 0,
+    g: 255,
+    b: 255,
+    a: 0,
+};
+
+const BLUE: ResColor = ResColor {
+    r: 0,
+    g: 40,
+    b: 108,
+    a: 0,
+};
+
+const PURPLE: ResColor = ResColor {
+    r: 100,
+    g: 66,
+    b: 202,
+    a: 0,
+};
+
+pub const YELLOW: ResColor = ResColor {
+    r: 230,
+    g: 180,
+    b: 14,
+    a: 0,
+};
+
+pub const WHITE: ResColor = ResColor {
+    r: 255,
+    g: 255,
+    b: 255,
+    a: 0,
+};
+
+pub static PER_LOG_FRAME_COUNTER: Lazy<usize> =
+    Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset));
+pub static OVERALL_FRAME_COUNTER: Lazy<usize> =
+    Lazy::new(|| frame_counter::register_counter(frame_counter::FrameCounterType::InGameNoReset));
+
+pub const NUM_LOGS: usize = 15;
+pub static mut DRAW_LOG_BASE_IDX: Lazy<Mutex<usize>> = Lazy::new(|| Mutex::new(0));
+
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
+pub enum DirectionStrength {
+    None,
+    Weak,
+    // Strong,
+}
+
+#[derive(Copy, Clone, Default)]
+pub struct InputLog {
+    pub ttl: u32,
+    pub frames: u32,
+    pub overall_frame: u32,
+    pub raw_inputs: Controller,
+    pub smash_inputs: MappedInputs,
+    pub status: i32,
+    pub fighter_kind: i32,
+}
+
+impl PartialEq for InputLog {
+    fn eq(&self, other: &Self) -> bool {
+        self.frames == other.frames && !self.is_different(other)
+    }
+}
+impl Eq for InputLog {}
+
+const WALK_THRESHOLD_X: i8 = 20;
+const _DASH_THRESHOLD_X: i8 = 102;
+const DEADZONE_THRESHOLD_Y: i8 = 30;
+const _TAP_JUMP_THRESHOLD_Y: i8 = 90;
+
+fn bin_stick_values(x: i8, y: i8) -> (DirectionStrength, f32) {
+    (
+        // TODO
+        DirectionStrength::Weak,
+        match (x, y) {
+            // X only
+            (x, y) if y.abs() < DEADZONE_THRESHOLD_Y => match x {
+                x if x > WALK_THRESHOLD_X => 0.0,
+                x if x < -WALK_THRESHOLD_X => 180.0,
+                _ => return (DirectionStrength::None, 0.0),
+            },
+            // Y only
+            (x, y) if x.abs() < WALK_THRESHOLD_X => match y {
+                y if y > DEADZONE_THRESHOLD_Y => 90.0,
+                y if y < -DEADZONE_THRESHOLD_Y => 270.0,
+                _ => return (DirectionStrength::None, 0.0),
+            },
+            // Positive Y
+            (x, y) if y > DEADZONE_THRESHOLD_Y => match x {
+                x if x > WALK_THRESHOLD_X => 45.0,
+                x if x < -WALK_THRESHOLD_X => 135.0,
+                _ => return (DirectionStrength::Weak, 90.0),
+            },
+            // Negative Y
+            (x, y) if y < DEADZONE_THRESHOLD_Y => match x {
+                x if x > WALK_THRESHOLD_X => 315.0,
+                x if x < -WALK_THRESHOLD_X => 225.0,
+                _ => return (DirectionStrength::Weak, 270.0),
+            },
+            _ => return (DirectionStrength::None, 0.0),
+        },
+    )
+}
+
+impl InputLog {
+    pub fn is_different(&self, other: &InputLog) -> bool {
+        unsafe {
+            match MENU.input_display {
+                InputDisplay::SMASH => self.is_smash_different(other),
+                InputDisplay::RAW => self.is_raw_different(other),
+                InputDisplay::NONE => false,
+                _ => panic!("Invalid value in is_different: {}", MENU.input_display),
+            }
+        }
+    }
+
+    pub fn binned_lstick(&self) -> (DirectionStrength, f32) {
+        unsafe {
+            match MENU.input_display {
+                InputDisplay::SMASH => self.smash_binned_lstick(),
+                InputDisplay::RAW => self.raw_binned_lstick(),
+                InputDisplay::NONE => panic!("Invalid input display to log"),
+                _ => panic!("Invalid value in binned_lstick: {}", MENU.input_display),
+            }
+        }
+    }
+
+    pub fn binned_rstick(&self) -> (DirectionStrength, f32) {
+        unsafe {
+            match MENU.input_display {
+                InputDisplay::SMASH => self.smash_binned_rstick(),
+                InputDisplay::RAW => self.raw_binned_rstick(),
+                InputDisplay::NONE => panic!("Invalid input display to log"),
+                _ => panic!("Invalid value in binned_rstick: {}", MENU.input_display),
+            }
+        }
+    }
+
+    pub fn button_icons(&self) -> VecDeque<(&str, ResColor)> {
+        unsafe {
+            match MENU.input_display {
+                InputDisplay::SMASH => self.smash_button_icons(),
+                InputDisplay::RAW => self.raw_button_icons(),
+                InputDisplay::NONE => panic!("Invalid input display to log"),
+                _ => unreachable!(),
+            }
+        }
+    }
+
+    fn smash_button_icons(&self) -> VecDeque<(&str, ResColor)> {
+        self.smash_inputs
+            .buttons
+            .to_vec()
+            .iter()
+            .filter_map(|button| {
+                Some(match *button {
+                    Buttons::ATTACK | Buttons::ATTACK_RAW => ("a", GREEN),
+                    Buttons::SPECIAL | Buttons::SPECIAL_RAW2 => ("b", RED),
+                    Buttons::JUMP => ("x", CYAN),
+                    Buttons::GUARD | Buttons::GUARD_HOLD => ("lb", BLUE),
+                    Buttons::CATCH => ("zr", PURPLE),
+                    Buttons::STOCK_SHARE => ("plus", WHITE),
+                    Buttons::APPEAL_HI => ("dpad_up", WHITE),
+                    Buttons::APPEAL_LW => ("dpad_down", WHITE),
+                    Buttons::APPEAL_SL => ("dpad_right", WHITE),
+                    Buttons::APPEAL_SR => ("dpad_left", WHITE),
+                    _ => return None,
+                })
+            })
+            .unique_by(|(s, _)| *s)
+            .collect::<VecDeque<(&str, ResColor)>>()
+    }
+
+    fn raw_button_icons(&self) -> VecDeque<(&str, ResColor)> {
+        let buttons = self.raw_inputs.current_buttons;
+        let mut icons = VecDeque::new();
+        if buttons.a() {
+            icons.push_front(("a", GREEN));
+        }
+        if buttons.b() {
+            icons.push_front(("b", RED));
+        }
+        if buttons.x() {
+            icons.push_front(("x", CYAN));
+        }
+        if buttons.y() {
+            icons.push_front(("y", CYAN));
+        }
+        if buttons.l() || buttons.real_digital_l() {
+            icons.push_front(("lb", BLUE));
+        }
+        if buttons.r() || buttons.real_digital_r() {
+            icons.push_front(("rb", BLUE));
+        }
+        if buttons.zl() {
+            icons.push_front(("zl", PURPLE));
+        }
+        if buttons.zr() {
+            icons.push_front(("zr", PURPLE));
+        }
+        if buttons.plus() {
+            icons.push_front(("plus", WHITE));
+        }
+        if buttons.minus() {
+            icons.push_front(("minus", WHITE));
+        }
+        if buttons.dpad_up() {
+            icons.push_front(("dpad_up", WHITE));
+        }
+        if buttons.dpad_down() {
+            icons.push_front(("dpad_down", WHITE));
+        }
+        if buttons.dpad_left() {
+            icons.push_front(("dpad_left", WHITE));
+        }
+        if buttons.dpad_right() {
+            icons.push_front(("dpad_right", WHITE));
+        }
+
+        icons
+    }
+
+    fn is_smash_different(&self, other: &InputLog) -> bool {
+        self.smash_inputs.buttons != other.smash_inputs.buttons
+            || self.smash_binned_lstick() != other.smash_binned_lstick()
+            || self.smash_binned_rstick() != other.smash_binned_rstick()
+            || (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status)
+    }
+
+    fn smash_binned_lstick(&self) -> (DirectionStrength, f32) {
+        bin_stick_values(self.smash_inputs.lstick_x, self.smash_inputs.lstick_y)
+    }
+
+    fn smash_binned_rstick(&self) -> (DirectionStrength, f32) {
+        bin_stick_values(self.smash_inputs.rstick_x, self.smash_inputs.rstick_y)
+    }
+
+    fn is_raw_different(&self, other: &InputLog) -> bool {
+        self.raw_inputs.current_buttons != other.raw_inputs.current_buttons
+            || self.raw_binned_lstick() != other.raw_binned_lstick()
+            || self.raw_binned_rstick() != other.raw_binned_rstick()
+            || (unsafe { MENU.input_display_status.as_bool() } && self.status != other.status)
+    }
+
+    fn raw_binned_lstick(&self) -> (DirectionStrength, f32) {
+        let x = (self.raw_inputs.left_stick_x / STICK_CLAMP_MULTIPLIER) as i8;
+        let y = (self.raw_inputs.left_stick_y / STICK_CLAMP_MULTIPLIER) as i8;
+        bin_stick_values(x, y)
+    }
+
+    fn raw_binned_rstick(&self) -> (DirectionStrength, f32) {
+        let x = (self.raw_inputs.right_stick_x / STICK_CLAMP_MULTIPLIER) as i8;
+        let y = (self.raw_inputs.right_stick_y / STICK_CLAMP_MULTIPLIER) as i8;
+        bin_stick_values(x, y)
+    }
+}
+
+fn insert_in_place<T>(array: &mut [T], value: T, index: usize) {
+    array[index..].rotate_right(1);
+    array[index] = value;
+}
+
+fn insert_in_front<T>(array: &mut [T], value: T) {
+    insert_in_place(array, value, 0);
+}
+
+lazy_static! {
+    pub static ref P1_INPUT_LOGS: Mutex<[InputLog; NUM_LOGS]> =
+        Mutex::new([InputLog::default(); NUM_LOGS]);
+}
+
+pub fn handle_final_input_mapping(
+    player_idx: i32,
+    controller_struct: &SomeControllerStruct,
+    out: *mut MappedInputs,
+) {
+    unsafe {
+        if MENU.input_display == InputDisplay::NONE {
+            return;
+        }
+
+        if QUICK_MENU_ACTIVE {
+            return;
+        }
+
+        if player_idx == 0 {
+            let module_accessor = try_get_module_accessor(FighterId::Player);
+            if module_accessor.is_none() {
+                return;
+            }
+            let module_accessor = module_accessor.unwrap();
+
+            let current_frame = frame_counter::get_frame_count(*PER_LOG_FRAME_COUNTER);
+            let current_overall_frame = frame_counter::get_frame_count(*OVERALL_FRAME_COUNTER);
+            // We should always be counting
+            frame_counter::start_counting(*PER_LOG_FRAME_COUNTER);
+            frame_counter::start_counting(*OVERALL_FRAME_COUNTER);
+
+            let potential_input_log = InputLog {
+                ttl: 600,
+                frames: 1,
+                overall_frame: current_overall_frame,
+                raw_inputs: *controller_struct.controller,
+                smash_inputs: *out,
+                status: StatusModule::status_kind(module_accessor),
+                fighter_kind: utility::get_kind(&mut *module_accessor),
+            };
+
+            let input_logs = &mut *P1_INPUT_LOGS.lock();
+            let latest_input_log = input_logs.first_mut().unwrap();
+            let prev_overall_frames = latest_input_log.overall_frame;
+            let prev_ttl = latest_input_log.ttl;
+            // Only update if we are on a new frame according to the latest log
+            let is_new_frame = prev_overall_frames != current_overall_frame;
+            if is_new_frame && latest_input_log.is_different(&potential_input_log) {
+                frame_counter::reset_frame_count(*PER_LOG_FRAME_COUNTER);
+                // We should count this frame already
+                frame_counter::tick_idx(*PER_LOG_FRAME_COUNTER);
+                insert_in_front(input_logs, potential_input_log);
+                let draw_log_base_idx = &mut *DRAW_LOG_BASE_IDX.data_ptr();
+                *draw_log_base_idx = (*draw_log_base_idx + 1) % NUM_LOGS;
+            } else if is_new_frame {
+                *latest_input_log = potential_input_log;
+                latest_input_log.frames = std::cmp::min(current_frame, 99);
+                latest_input_log.ttl = prev_ttl;
+            }
+
+            // Decrease TTL
+            for input_log in input_logs.iter_mut() {
+                if input_log.ttl > 0 && is_new_frame {
+                    input_log.ttl -= 1;
+                }
+            }
+        }
+    }
+}
diff --git a/src/training/input_record.rs b/src/training/input_record.rs
index ba6ad6a..0e5fdc8 100644
--- a/src/training/input_record.rs
+++ b/src/training/input_record.rs
@@ -17,6 +17,7 @@ use crate::common::{
 };
 use crate::training::mash;
 use crate::training::ui::notifications::{clear_notifications, color_notification};
+use crate::{error, warn};
 
 #[derive(PartialEq, Debug)]
 pub enum InputRecordState {
@@ -102,17 +103,17 @@ unsafe fn should_mash_playback() {
 
     if is_in_hitstun(&mut *cpu_module_accessor) {
         // if we're in hitstun and want to enter the frame we start hitstop for SDI, start if we're in any damage status instantly
-        if MENU.hitstun_playback == HitstunPlayback::Instant {
+        if MENU.hitstun_playback == HitstunPlayback::INSTANT {
             should_playback = true;
         }
         // if we want to wait until we exit hitstop and begin flying away for shield art etc, start if we're not in hitstop
-        if MENU.hitstun_playback == HitstunPlayback::Hitstop
+        if MENU.hitstun_playback == HitstunPlayback::HITSTOP
             && !StopModule::is_stop(cpu_module_accessor)
         {
             should_playback = true;
         }
         // if we're in hitstun and want to wait till FAF to act, then we want to match our starting status to the correct transition term to see if we can hitstun cancel
-        if MENU.hitstun_playback == HitstunPlayback::Hitstun && can_transition(cpu_module_accessor)
+        if MENU.hitstun_playback == HitstunPlayback::HITSTUN && can_transition(cpu_module_accessor)
         {
             should_playback = true;
         }
@@ -191,12 +192,12 @@ unsafe fn handle_recording_for_fighter(module_accessor: &mut BattleObjectModuleA
     let fighter_kind = utility::get_kind(module_accessor);
     let fighter_is_nana = fighter_kind == *FIGHTER_KIND_NANA;
 
-    CURRENT_RECORD_SLOT = MENU.recording_slot.into_idx();
+    CURRENT_RECORD_SLOT = MENU.recording_slot.into_idx().unwrap_or(0);
 
     if entry_id_int == 0 && !fighter_is_nana {
         if button_config::combo_passes(button_config::ButtonCombo::InputPlayback) {
             playback(MENU.playback_button_slots.get_random().into_idx());
-        } else if MENU.record_trigger.contains(RecordTrigger::COMMAND)
+        } else if MENU.record_trigger.contains(&RecordTrigger::COMMAND)
             && button_config::combo_passes(button_config::ButtonCombo::InputRecord)
         {
             lockout_record();
@@ -215,7 +216,7 @@ unsafe fn handle_recording_for_fighter(module_accessor: &mut BattleObjectModuleA
 
             // If we need to crop the recording for neutral input
             // INPUT_RECORD_FRAME must be > 0 to prevent bounding errors
-            if INPUT_RECORD == Record && MENU.recording_crop == OnOff::On && INPUT_RECORD_FRAME > 0
+            if INPUT_RECORD == Record && MENU.recording_crop == OnOff::ON && INPUT_RECORD_FRAME > 0
             {
                 while INPUT_RECORD_FRAME > 0 && is_input_neutral(INPUT_RECORD_FRAME - 1) {
                     // Discard frames at the end of the recording until the last frame with input
@@ -227,7 +228,7 @@ unsafe fn handle_recording_for_fighter(module_accessor: &mut BattleObjectModuleA
 
             INPUT_RECORD_FRAME = 0;
 
-            if MENU.playback_loop == OnOff::On && INPUT_RECORD == Playback {
+            if MENU.playback_loop == OnOff::ON && INPUT_RECORD == Playback {
                 playback(Some(CURRENT_PLAYBACK_SLOT));
             } else {
                 INPUT_RECORD = None;
@@ -338,11 +339,11 @@ pub unsafe fn lockout_record() {
 // Returns whether we did playback
 pub unsafe fn playback(slot: Option<usize>) -> bool {
     if INPUT_RECORD == Pause {
-        println!("Tried to playback during lockout!");
+        warn!("Tried to playback during lockout!");
         return false;
     }
     if slot.is_none() {
-        println!("Tried to playback without a slot selected!");
+        warn!("Tried to playback without a slot selected!");
         return false;
     }
     let slot = slot.unwrap();
@@ -425,8 +426,6 @@ pub unsafe fn handle_final_input_mapping(player_idx: i32, out: *mut MappedInputs
 
             P1_FINAL_MAPPING.lock()[CURRENT_RECORD_SLOT][INPUT_RECORD_FRAME] = *out;
             *out = MappedInputs::empty(); // don't control player while recording
-
-            //println!("Stored Player Input! Frame: {}", INPUT_RECORD_FRAME);
         }
         // Don't allow for player input during Lockout
         if POSSESSION == Lockout {
@@ -468,7 +467,7 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) {
                 INPUT_RECORD = Record;
                 POSSESSION = Standby;
             }
-            Ordering::Less => println!("LOCKOUT_FRAME OUT OF BOUNDS"),
+            Ordering::Less => error!("LOCKOUT_FRAME OUT OF BOUNDS"),
         }
     }
 
@@ -504,8 +503,6 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) {
             );
         }
 
-        //println!("Overriding Cpu Player: {}, Frame: {}, BUFFER_FRAME: {}, STARTING_STATUS: {}, INPUT_RECORD: {:#?}, POSSESSION: {:#?}", controller_no, INPUT_RECORD_FRAME, BUFFER_FRAME, STARTING_STATUS, INPUT_RECORD, POSSESSION);
-
         let mut saved_mapped_inputs = P1_FINAL_MAPPING.lock()[if INPUT_RECORD == Record {
             CURRENT_RECORD_SLOT
         } else {
@@ -538,7 +535,6 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) {
         };
         (*controller_data).clamped_lstick_x = clamped_lstick_x;
         (*controller_data).clamped_lstick_y = clamped_lstick_y;
-        //println!("CPU Buttons: {:#018b}", (*controller_data).buttons);
 
         // Keep counting frames, unless we're in standby waiting for an input, or are buffering an option
         // When buffering an option, we keep inputting the first frame of input during the buffer window
diff --git a/src/training/ledge.rs b/src/training/ledge.rs
index af61b7c..14fd7a4 100644
--- a/src/training/ledge.rs
+++ b/src/training/ledge.rs
@@ -58,7 +58,7 @@ fn roll_ledge_case() {
 fn get_ledge_option() -> Option<Action> {
     unsafe {
         let mut override_action: Option<Action> = None;
-        let regular_action = if MENU.mash_triggers.contains(MashTrigger::LEDGE) {
+        let regular_action = if MENU.mash_triggers.contains(&MashTrigger::LEDGE) {
             Some(MENU.mash_state.get_random())
         } else {
             None
diff --git a/src/training/mash.rs b/src/training/mash.rs
index d54e8b0..d2d7448 100644
--- a/src/training/mash.rs
+++ b/src/training/mash.rs
@@ -80,14 +80,13 @@ pub fn buffer_action(action: Action) {
     unsafe {
         // exit playback if we want to perform mash actions out of it
         // TODO: Figure out some way to deal with trying to playback into another playback
-        if MENU.playback_mash == OnOff::On
+        if MENU.playback_mash == OnOff::ON
             && input_record::is_playback()
             && !input_record::is_recording()
             && !input_record::is_standby()
             && !is_playback_queued()
             && !action.is_playback()
         {
-            //println!("Stopping mash playback for menu option!");
             // if we don't want to leave playback on mash actions, then don't perform the mash
             if input_record::is_playback() {
                 input_record::stop_playback();
@@ -221,7 +220,7 @@ unsafe fn get_buffered_action(
         let action = MENU.tech_action_override.get_random();
         if action != Action::empty() {
             Some(action)
-        } else if MENU.mash_triggers.contains(MashTrigger::TECH) {
+        } else if MENU.mash_triggers.contains(&MashTrigger::TECH) {
             Some(MENU.mash_state.get_random())
         } else {
             None
@@ -230,7 +229,7 @@ unsafe fn get_buffered_action(
         let action = MENU.clatter_override.get_random();
         if action != Action::empty() {
             Some(action)
-        } else if MENU.mash_triggers.contains(MashTrigger::CLATTER) {
+        } else if MENU.mash_triggers.contains(&MashTrigger::CLATTER) {
             Some(MENU.mash_state.get_random())
         } else {
             None
@@ -241,7 +240,7 @@ unsafe fn get_buffered_action(
         let action = MENU.tumble_override.get_random();
         if action != Action::empty() {
             Some(action)
-        } else if MENU.mash_triggers.contains(MashTrigger::TUMBLE) {
+        } else if MENU.mash_triggers.contains(&MashTrigger::TUMBLE) {
             Some(MENU.mash_state.get_random())
         } else {
             None
@@ -250,7 +249,7 @@ unsafe fn get_buffered_action(
         let action = MENU.hitstun_override.get_random();
         if action != Action::empty() {
             Some(action)
-        } else if MENU.mash_triggers.contains(MashTrigger::HIT) {
+        } else if MENU.mash_triggers.contains(&MashTrigger::HIT) {
             Some(MENU.mash_state.get_random())
         } else {
             None
@@ -259,7 +258,7 @@ unsafe fn get_buffered_action(
         let action = MENU.parry_override.get_random();
         if action != Action::empty() {
             Some(action)
-        } else if MENU.mash_triggers.contains(MashTrigger::PARRY) {
+        } else if MENU.mash_triggers.contains(&MashTrigger::PARRY) {
             Some(MENU.mash_state.get_random())
         } else {
             None
@@ -268,7 +267,7 @@ unsafe fn get_buffered_action(
         let action = MENU.footstool_override.get_random();
         if action != Action::empty() {
             Some(action)
-        } else if MENU.mash_triggers.contains(MashTrigger::FOOTSTOOL) {
+        } else if MENU.mash_triggers.contains(&MashTrigger::FOOTSTOOL) {
             Some(MENU.mash_state.get_random())
         } else {
             None
@@ -277,7 +276,7 @@ unsafe fn get_buffered_action(
         let action = MENU.trump_override.get_random();
         if action != Action::empty() {
             Some(action)
-        } else if MENU.mash_triggers.contains(MashTrigger::TRUMP) {
+        } else if MENU.mash_triggers.contains(&MashTrigger::TRUMP) {
             Some(MENU.mash_state.get_random())
         } else {
             None
@@ -286,20 +285,20 @@ unsafe fn get_buffered_action(
         let action = MENU.landing_override.get_random();
         if action != Action::empty() {
             Some(action)
-        } else if MENU.mash_triggers.contains(MashTrigger::LANDING) {
+        } else if MENU.mash_triggers.contains(&MashTrigger::LANDING) {
             Some(MENU.mash_state.get_random())
         } else {
             None
         }
-    } else if (MENU.mash_triggers.contains(MashTrigger::GROUNDED) && is_grounded(module_accessor))
-        || (MENU.mash_triggers.contains(MashTrigger::AIRBORNE) && is_airborne(module_accessor))
-        || (MENU.mash_triggers.contains(MashTrigger::DISTANCE_CLOSE)
+    } else if (MENU.mash_triggers.contains(&MashTrigger::GROUNDED) && is_grounded(module_accessor))
+        || (MENU.mash_triggers.contains(&MashTrigger::AIRBORNE) && is_airborne(module_accessor))
+        || (MENU.mash_triggers.contains(&MashTrigger::DISTANCE_CLOSE)
             && fighter_distance < DISTANCE_CLOSE_THRESHOLD)
-        || (MENU.mash_triggers.contains(MashTrigger::DISTANCE_MID)
+        || (MENU.mash_triggers.contains(&MashTrigger::DISTANCE_MID)
             && fighter_distance < DISTANCE_MID_THRESHOLD)
-        || (MENU.mash_triggers.contains(MashTrigger::DISTANCE_FAR)
+        || (MENU.mash_triggers.contains(&MashTrigger::DISTANCE_FAR)
             && fighter_distance < DISTANCE_FAR_THRESHOLD)
-        || MENU.mash_triggers.contains(MashTrigger::ALWAYS)
+        || MENU.mash_triggers.contains(&MashTrigger::ALWAYS)
     {
         Some(MENU.mash_state.get_random())
     } else {
diff --git a/src/training/mod.rs b/src/training/mod.rs
index 2cd3fc6..98b29f5 100644
--- a/src/training/mod.rs
+++ b/src/training/mod.rs
@@ -770,7 +770,7 @@ pub unsafe fn handle_reused_ui(
         // If Little Mac is in the game and we're buffing him, set the meter to 100
         if (player_fighter_kind == *FIGHTER_KIND_LITTLEMAC
             || cpu_fighter_kind == *FIGHTER_KIND_LITTLEMAC)
-            && MENU.buff_state.to_vec().contains(&BuffOption::KO)
+            && MENU.buff_state.contains(&BuffOption::KO)
         {
             param_2 = 100;
         }
diff --git a/src/training/save_states.rs b/src/training/save_states.rs
index 263fbb2..75bf28c 100644
--- a/src/training/save_states.rs
+++ b/src/training/save_states.rs
@@ -143,7 +143,10 @@ pub struct SaveStateSlots {
 const NUM_SAVE_STATE_SLOTS: usize = 5;
 // I actually had to do it this way, a simple load-from-file in main() caused crashes.
 lazy_static::lazy_static! {
-    static ref SAVE_STATE_SLOTS : Mutex<SaveStateSlots> = Mutex::new(load_from_file());
+    static ref SAVE_STATE_SLOTS : Mutex<SaveStateSlots> = Mutex::new({
+        info!("Initialized lazy_static: SAVE_STATE_SLOTS");
+        load_from_file()
+    });
 }
 
 pub fn load_from_file() -> SaveStateSlots {
@@ -233,7 +236,7 @@ unsafe fn get_slot() -> usize {
     if random_slot != SaveStateSlot::empty() {
         RANDOM_SLOT
     } else {
-        MENU.save_state_slot.as_idx() as usize
+        MENU.save_state_slot.into_idx().unwrap_or(0)
     }
 }
 
@@ -253,9 +256,13 @@ pub unsafe fn is_loading() -> bool {
 
 pub unsafe fn should_mirror() -> f32 {
     match MENU.save_state_mirroring {
-        SaveStateMirroring::None => 1.0,
-        SaveStateMirroring::Alternate => -1.0 * MIRROR_STATE,
-        SaveStateMirroring::Random => ([-1.0, 1.0])[get_random_int(2) as usize],
+        SaveStateMirroring::NONE => 1.0,
+        SaveStateMirroring::ALTERNATE => -1.0 * MIRROR_STATE,
+        SaveStateMirroring::RANDOM => ([-1.0, 1.0])[get_random_int(2) as usize],
+        _ => panic!(
+            "Invalid value in should_mirror: {}",
+            MENU.save_state_mirroring
+        ),
     }
 }
 
@@ -409,7 +416,7 @@ pub unsafe fn on_death(fighter_kind: i32, module_accessor: &mut app::BattleObjec
 }
 
 pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor) {
-    if MENU.save_state_enable == OnOff::Off {
+    if MENU.save_state_enable == OnOff::OFF {
         return;
     }
 
@@ -441,7 +448,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
     .contains(&fighter_kind);
 
     // Reset state
-    let autoload_reset = MENU.save_state_autoload == OnOff::On
+    let autoload_reset = MENU.save_state_autoload == OnOff::ON
         && save_state.state == NoAction
         && is_dead(module_accessor);
     let mut triggered_reset: bool = false;
@@ -452,7 +459,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
         if save_state.state == NoAction {
             let random_slot = MENU.randomize_slots.get_random();
             let slot = if random_slot != SaveStateSlot::empty() {
-                RANDOM_SLOT = random_slot.as_idx();
+                RANDOM_SLOT = random_slot.into_idx().unwrap_or(0);
                 RANDOM_SLOT
             } else {
                 selected_slot
@@ -578,7 +585,10 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
                         set_damage(module_accessor, pct);
                     }
                     SaveDamage::DEFAULT => {}
-                    _ => {}
+                    _ => panic!(
+                        "Invalid value in save_states()::save_damage_player: {}",
+                        MENU.save_damage_player
+                    ),
                 }
             } else {
                 match MENU.save_damage_cpu {
@@ -594,12 +604,15 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
                         set_damage(module_accessor, pct);
                     }
                     SaveDamage::DEFAULT => {}
-                    _ => {}
+                    _ => panic!(
+                        "Invalid value in save_states()::save_damage_cpu: {}",
+                        MENU.save_damage_cpu
+                    ),
                 }
             }
 
             // Set to held item
-            if !is_cpu && !fighter_is_nana && MENU.character_item != CharacterItem::None {
+            if !is_cpu && !fighter_is_nana && MENU.character_item != CharacterItem::NONE {
                 apply_item(MENU.character_item);
             }
 
@@ -648,7 +661,7 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
         }
 
         // if we're recording on state load, record
-        if MENU.record_trigger.contains(RecordTrigger::SAVESTATE) {
+        if MENU.record_trigger.contains(&RecordTrigger::SAVESTATE) {
             input_record::lockout_record();
             return;
         }
@@ -693,8 +706,8 @@ pub unsafe fn save_states(module_accessor: &mut app::BattleObjectModuleAccessor)
     if button_config::combo_passes(button_config::ButtonCombo::SaveState) {
         // Don't begin saving state if Nana's delayed input is captured
         MIRROR_STATE = 1.0;
-        save_state_player(MENU.save_state_slot.as_idx() as usize).state = Save;
-        save_state_cpu(MENU.save_state_slot.as_idx() as usize).state = Save;
+        save_state_player(MENU.save_state_slot.into_idx().unwrap_or(0)).state = Save;
+        save_state_cpu(MENU.save_state_slot.into_idx().unwrap_or(0)).state = Save;
         notifications::clear_notifications("Save State");
         notifications::notification(
             "Save State".to_string(),
diff --git a/src/training/shield.rs b/src/training/shield.rs
index 43e9955..7d3d04e 100644
--- a/src/training/shield.rs
+++ b/src/training/shield.rs
@@ -107,7 +107,7 @@ pub unsafe fn get_param_float(
         return None;
     }
 
-    if MENU.shield_state != Shield::None {
+    if MENU.shield_state != Shield::NONE {
         handle_oos_offset(module_accessor);
     }
 
@@ -121,8 +121,8 @@ fn handle_shield_decay(param_type: u64, param_hash: u64) -> Option<f32> {
         menu_state = MENU.shield_state;
     }
 
-    if menu_state != Shield::Infinite
-        && menu_state != Shield::Constant
+    if menu_state != Shield::INFINITE
+        && menu_state != Shield::CONSTANT
         && !should_pause_shield_decay()
     {
         return None;
@@ -161,7 +161,7 @@ pub unsafe fn param_installer() {
             CACHED_SHIELD_DAMAGE_MUL = Some(common_params.shield_damage_mul);
         }
 
-        if is_training_mode() && (MENU.shield_state == Shield::Infinite) {
+        if is_training_mode() && (MENU.shield_state == Shield::INFINITE) {
             // if you are in training mode and have infinite shield enabled,
             // set the game's shield_damage_mul to 0.0
             common_params.shield_damage_mul = 0.0;
@@ -192,7 +192,7 @@ pub fn should_hold_shield(module_accessor: &mut app::BattleObjectModuleAccessor)
 
     // We should hold shield if the state requires it
     if unsafe { save_states::is_loading() }
-        || ![Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state)
+        || ![Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state)
     {
         return false;
     }
@@ -223,7 +223,7 @@ unsafe fn mod_handle_sub_guard_cont(fighter: &mut L2CFighterCommon) {
     }
 
     // Enable shield decay
-    if MENU.shield_state == Shield::Hold {
+    if MENU.shield_state == Shield::HOLD {
         set_shield_decay(true);
     }
 
@@ -245,7 +245,7 @@ unsafe fn mod_handle_sub_guard_cont(fighter: &mut L2CFighterCommon) {
         return;
     }
 
-    if MENU.mash_triggers.contains(MashTrigger::SHIELDSTUN) {
+    if MENU.mash_triggers.contains(&MashTrigger::SHIELDSTUN) {
         if MENU.shieldstun_override == Action::empty() {
             mash::external_buffer_menu_mash(MENU.mash_state.get_random())
         } else {
@@ -360,7 +360,7 @@ fn needs_oos_handling_drop_shield() -> bool {
             shield_state = &MENU.shield_state;
         }
         // If we're supposed to be holding shield, let airdodge make us drop shield
-        if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) {
+        if [Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) {
             suspend_shield(Action::AIR_DODGE);
         }
         return true;
@@ -373,7 +373,7 @@ fn needs_oos_handling_drop_shield() -> bool {
             shield_state = &MENU.shield_state;
         }
         // If we're supposed to be holding shield, let airdodge make us drop shield
-        if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) {
+        if [Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) {
             suspend_shield(Action::AIR_DODGE);
         }
         return true;
@@ -385,7 +385,7 @@ fn needs_oos_handling_drop_shield() -> bool {
             shield_state = &MENU.shield_state;
         }
         // Don't drop shield on shield hit if we're supposed to be holding shield
-        if [Shield::Hold, Shield::Infinite, Shield::Constant].contains(shield_state) {
+        if [Shield::HOLD, Shield::INFINITE, Shield::CONSTANT].contains(shield_state) {
             return false;
         }
         return true;
diff --git a/src/training/tech.rs b/src/training/tech.rs
index f5fc20a..2af4dba 100644
--- a/src/training/tech.rs
+++ b/src/training/tech.rs
@@ -123,7 +123,7 @@ unsafe fn handle_grnd_tech(
         }
         _ => false,
     };
-    if do_tech && MENU.mash_triggers.contains(MashTrigger::TECH) {
+    if do_tech && MENU.mash_triggers.contains(&MashTrigger::TECH) {
         if MENU.tech_action_override == Action::empty() {
             mash::external_buffer_menu_mash(MENU.mash_state.get_random())
         } else {
@@ -170,7 +170,7 @@ unsafe fn handle_wall_tech(
         }
         _ => false,
     };
-    if do_tech && MENU.mash_triggers.contains(MashTrigger::TECH) {
+    if do_tech && MENU.mash_triggers.contains(&MashTrigger::TECH) {
         if MENU.tech_action_override == Action::empty() {
             mash::external_buffer_menu_mash(MENU.mash_state.get_random())
         } else {
@@ -205,7 +205,7 @@ unsafe fn handle_ceil_tech(
 
     *status_kind = FIGHTER_STATUS_KIND_PASSIVE_CEIL.as_lua_int();
     *unk = LUA_TRUE;
-    if MENU.mash_triggers.contains(MashTrigger::TECH) {
+    if MENU.mash_triggers.contains(&MashTrigger::TECH) {
         if MENU.tech_action_override == Action::empty() {
             mash::external_buffer_menu_mash(MENU.mash_state.get_random())
         } else {
@@ -277,7 +277,7 @@ pub unsafe fn get_command_flag_cat(module_accessor: &mut BattleObjectModuleAcces
 
     if requested_status != 0 {
         StatusModule::change_status_force(module_accessor, requested_status, true);
-        if MENU.mash_triggers.contains(MashTrigger::MISTECH) {
+        if MENU.mash_triggers.contains(&MashTrigger::MISTECH) {
             if MENU.tech_action_override == Action::empty() {
                 mash::external_buffer_menu_mash(MENU.mash_state.get_random())
             } else {
@@ -347,7 +347,7 @@ unsafe fn get_snake_laydown_lockout_time(module_accessor: &mut BattleObjectModul
 }
 
 pub unsafe fn hide_tech() {
-    if !is_training_mode() || MENU.tech_hide == OnOff::Off {
+    if !is_training_mode() || MENU.tech_hide == OnOff::OFF {
         return;
     }
     let module_accessor = get_module_accessor(FighterId::CPU);
@@ -409,7 +409,7 @@ pub unsafe fn handle_fighter_req_quake_pos(
         return original!()(camera_module, quake_kind);
     }
     let status = StatusModule::status_kind(module_accessor);
-    if status == FIGHTER_STATUS_KIND_DOWN && MENU.tech_hide == OnOff::On {
+    if status == FIGHTER_STATUS_KIND_DOWN && MENU.tech_hide == OnOff::ON {
         // We're hiding techs, prevent mistech quake from giving away missed tech
         return original!()(camera_module, *CAMERA_QUAKE_KIND_NONE);
     }
@@ -452,7 +452,7 @@ pub struct CameraManager {
 
 unsafe fn set_fixed_camera_values() {
     let camera_manager = get_camera_manager();
-    if MENU.tech_hide == OnOff::Off {
+    if MENU.tech_hide == OnOff::OFF {
         // Use Stage's Default Values for fixed Camera
         camera_manager.fixed_camera_center = DEFAULT_FIXED_CAM_CENTER;
     } else {
diff --git a/src/training/ui/input_log.rs b/src/training/ui/input_log.rs
index 0f209d8..49e8cac 100644
--- a/src/training/ui/input_log.rs
+++ b/src/training/ui/input_log.rs
@@ -193,9 +193,9 @@ pub unsafe fn draw(root_pane: &Pane) {
         .find_pane_by_name_recursive("TrModInputLog")
         .unwrap();
     logs_pane.set_visible(
-        !QUICK_MENU_ACTIVE && !VANILLA_MENU_ACTIVE && MENU.input_display != InputDisplay::None,
+        !QUICK_MENU_ACTIVE && !VANILLA_MENU_ACTIVE && MENU.input_display != InputDisplay::NONE,
     );
-    if MENU.input_display == InputDisplay::None {
+    if MENU.input_display == InputDisplay::NONE {
         return;
     }
 
diff --git a/src/training/ui/menu.rs b/src/training/ui/menu.rs
index 7a1e2cc..81c69fd 100644
--- a/src/training/ui/menu.rs
+++ b/src/training/ui/menu.rs
@@ -3,19 +3,18 @@ use std::collections::HashMap;
 use lazy_static::lazy_static;
 use skyline::nn::ui2d::*;
 use smash::ui2d::{SmashPane, SmashTextBox};
-use training_mod_tui::gauge::GaugeState;
-use training_mod_tui::{App, AppPage, NUM_LISTS};
+use training_mod_tui::{
+    App, AppPage, ConfirmationState, SliderState, NX_SUBMENU_COLUMNS, NX_SUBMENU_ROWS,
+};
 
 use crate::common::menu::{MENU_CLOSE_FRAME_COUNTER, MENU_CLOSE_WAIT_FRAMES, MENU_RECEIVED_INPUT};
 use crate::training::frame_counter;
 use crate::{common, common::menu::QUICK_MENU_ACTIVE, input::*};
+use training_mod_consts::TOGGLE_MAX;
 
 use super::fade_out;
 use super::set_icon_text;
 
-pub static NUM_MENU_TEXT_OPTIONS: usize = 32;
-pub static _NUM_MENU_TABS: usize = 3;
-
 const BG_LEFT_ON_WHITE_COLOR: ResColor = ResColor {
     r: 0,
     g: 28,
@@ -78,171 +77,188 @@ lazy_static! {
     ]);
 }
 
-unsafe fn render_submenu_page(app: &App, root_pane: &Pane) {
-    let tab_selected = app.tab_selected();
-    let tab = app.menu_items.get(tab_selected).unwrap();
-    let submenu_ids = app.submenu_ids();
-
-    (0..NUM_MENU_TEXT_OPTIONS)
-        // Valid options in this submenu
-        .filter_map(|idx| tab.idx_to_list_idx_opt(idx))
-        .for_each(|(list_section, list_idx)| {
-            let menu_button_row = root_pane
-                .find_pane_by_name_recursive(format!("TrModMenuButtonRow{list_idx}").as_str())
-                .unwrap();
-            menu_button_row.set_visible(true);
-
-            let menu_button = menu_button_row
-                .find_pane_by_name_recursive(format!("Button{list_section}").as_str())
-                .unwrap();
-
-            let title_text = menu_button
-                .find_pane_by_name_recursive("TitleTxt")
-                .unwrap()
-                .as_textbox();
-
-            let title_bg = menu_button
-                .find_pane_by_name_recursive("TitleBg")
-                .unwrap()
-                .as_picture();
-
-            let title_bg_material = &mut *title_bg.material;
-
-            let list = &tab.lists[list_section];
-            let submenu = &list.items[list_idx];
-            let is_selected = list.state.selected().filter(|s| *s == list_idx).is_some();
-
-            title_text.set_text_string(submenu.submenu_title.as_str());
-
-            // In the actual 'layout.arc' file, every icon image is stacked
-            // into a single container pane, with each image directly on top of another.
-            // Hide all icon images, and strategically mark the icon that
-            // corresponds with a particular button to be visible.
-            submenu_ids.iter().for_each(|id| {
-                let pane = menu_button.find_pane_by_name_recursive(id);
-                if let Some(p) = pane {
-                    p.set_visible(id == &submenu.submenu_id);
-                }
-            });
-
-            menu_button
-                .find_pane_by_name_recursive("check")
-                .unwrap()
-                .set_visible(false);
-
-            if is_selected {
-                root_pane
-                    .find_pane_by_name_recursive("FooterTxt")
-                    .unwrap()
-                    .as_textbox()
-                    .set_text_string(submenu.help_text.as_str());
-
-                title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
-                title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
-
-                title_text.text_shadow_enable(true);
-                title_text.text_outline_enable(true);
-
-                title_text.set_color(255, 255, 255, 255);
-            } else {
-                title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
-                title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
-
-                title_text.text_shadow_enable(false);
-                title_text.text_outline_enable(false);
-
-                title_text.set_color(178, 199, 211, 255);
-            }
-
-            menu_button.set_visible(true);
-            menu_button
-                .find_pane_by_name_recursive("Icon")
-                .unwrap()
-                .set_visible(true);
-        });
-}
-
-unsafe fn render_toggle_page(app: &App, root_pane: &Pane) {
-    let (_title, _help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
-    (0..sub_menu_str_lists.len()).for_each(|list_section| {
-        let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
-        let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
-        sub_menu_str
-            .iter()
-            .enumerate()
-            .for_each(|(list_idx, (checked, name))| {
-                let menu_button_row = root_pane
-                    .find_pane_by_name_recursive(format!("TrModMenuButtonRow{list_idx}").as_str())
-                    .unwrap();
-                menu_button_row.set_visible(true);
-
+unsafe fn render_submenu_page(app: &mut App, root_pane: &Pane) {
+    let tabs_clone = app.tabs.clone(); // Need this to avoid double-borrow later on
+    let tab = app.selected_tab();
+    for row in 0..NX_SUBMENU_ROWS {
+        let menu_button_row = root_pane
+            .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row}").as_str())
+            .unwrap();
+        menu_button_row.set_visible(true);
+        for col in 0..NX_SUBMENU_COLUMNS {
+            if let Some(submenu) = tab.submenus.get(row, col) {
+                // Find all the panes we need to modify
                 let menu_button = menu_button_row
-                    .find_pane_by_name_recursive(format!("Button{list_section}").as_str())
+                    .find_pane_by_name_recursive(format!("Button{col}").as_str())
                     .unwrap();
-                menu_button.set_visible(true);
-
                 let title_text = menu_button
                     .find_pane_by_name_recursive("TitleTxt")
                     .unwrap()
                     .as_textbox();
-
                 let title_bg = menu_button
                     .find_pane_by_name_recursive("TitleBg")
                     .unwrap()
                     .as_picture();
+                let title_bg_material = &mut *title_bg.material;
+                let is_selected = row == tab.submenus.state.selected_row().unwrap()
+                    && col == tab.submenus.state.selected_col().unwrap();
 
-                let is_selected = sub_menu_state
-                    .selected()
-                    .filter(|s| *s == list_idx)
-                    .is_some();
+                // Set Pane Visibility
+                title_text.set_text_string(submenu.title);
 
-                let submenu_ids = app.submenu_ids();
+                // In the actual 'layout.arc' file, every icon image is stacked
+                // into a single container pane, with each image directly on top of another.
+                // Hide all icon images, and strategically mark the icon that
+                // corresponds with a particular button to be visible.
 
-                submenu_ids.iter().for_each(|id| {
-                    let pane = menu_button.find_pane_by_name_recursive(id);
-                    if let Some(p) = pane {
-                        p.set_visible(false);
+                for t in tabs_clone.iter() {
+                    for s in t.submenus.iter() {
+                        let pane = menu_button.find_pane_by_name_recursive(s.id);
+                        if let Some(p) = pane {
+                            p.set_visible(s.id == submenu.id);
+                        }
                     }
-                });
+                }
 
-                title_text.set_text_string(name);
                 menu_button
                     .find_pane_by_name_recursive("check")
                     .unwrap()
-                    .set_visible(true);
+                    .set_visible(false);
 
+                for value in 1..=TOGGLE_MAX {
+                    if let Some(pane) =
+                        menu_button.find_pane_by_name_recursive(format!("{}", value).as_str())
+                    {
+                        pane.set_visible(false);
+                    } else {
+                        break;
+                    }
+                }
+
+                if is_selected {
+                    // Help text
+                    root_pane
+                        .find_pane_by_name_recursive("FooterTxt")
+                        .unwrap()
+                        .as_textbox()
+                        .set_text_string(submenu.help_text);
+
+                    title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
+                    title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
+                    title_text.text_shadow_enable(true);
+                    title_text.text_outline_enable(true);
+                    title_text.set_color(255, 255, 255, 255);
+                } else {
+                    title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
+                    title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
+                    title_text.text_shadow_enable(false);
+                    title_text.text_outline_enable(false);
+                    title_text.set_color(178, 199, 211, 255);
+                }
+                menu_button.set_visible(true);
                 menu_button
                     .find_pane_by_name_recursive("Icon")
                     .unwrap()
-                    .set_visible(*checked);
+                    .set_visible(true);
+            }
+        }
+    }
+}
 
+unsafe fn render_toggle_page(app: &mut App, root_pane: &Pane) {
+    let tabs_clone = app.tabs.clone(); // Need this to avoid double-borrow later on
+    let submenu = app.selected_submenu();
+    // If the options can only be toggled on or off, then use the check icon
+    // instead of the number icons
+    let use_check_icon = submenu.toggles.get(0, 0).unwrap().max == 1;
+    for row in 0..NX_SUBMENU_ROWS {
+        let menu_button_row = root_pane
+            .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row}").as_str())
+            .unwrap();
+        menu_button_row.set_visible(true);
+        for col in 0..NX_SUBMENU_COLUMNS {
+            if let Some(toggle) = submenu.toggles.get(row, col) {
+                let menu_button = menu_button_row
+                    .find_pane_by_name_recursive(format!("Button{col}").as_str())
+                    .unwrap();
+                menu_button.set_visible(true);
+                let title_text = menu_button
+                    .find_pane_by_name_recursive("TitleTxt")
+                    .unwrap()
+                    .as_textbox();
+                let title_bg = menu_button
+                    .find_pane_by_name_recursive("TitleBg")
+                    .unwrap()
+                    .as_picture();
                 let title_bg_material = &mut *title_bg.material;
+                let is_selected = row == submenu.toggles.state.selected_row().unwrap()
+                    && col == submenu.toggles.state.selected_col().unwrap();
 
                 if is_selected {
                     title_text.text_shadow_enable(true);
                     title_text.text_outline_enable(true);
-
                     title_text.set_color(255, 255, 255, 255);
-
                     title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
                     title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
                 } else {
                     title_text.text_shadow_enable(false);
                     title_text.text_outline_enable(false);
-
                     title_text.set_color(178, 199, 211, 255);
-
                     title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
                     title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
                 }
-            });
-    });
+
+                // Hide all submenu icons, since we're not on the submenu page
+                for t in tabs_clone.iter() {
+                    for s in t.submenus.iter() {
+                        let pane = menu_button.find_pane_by_name_recursive(s.id);
+                        if let Some(p) = pane {
+                            p.set_visible(false);
+                        }
+                    }
+                }
+
+                title_text.set_text_string(toggle.title);
+
+                if use_check_icon {
+                    menu_button
+                        .find_pane_by_name_recursive("check")
+                        .unwrap()
+                        .set_visible(true);
+
+                    menu_button
+                        .find_pane_by_name_recursive("Icon")
+                        .unwrap()
+                        .set_visible(toggle.value > 0);
+                } else {
+                    menu_button
+                        .find_pane_by_name_recursive("check")
+                        .unwrap()
+                        .set_visible(false);
+                    menu_button
+                        .find_pane_by_name_recursive("Icon")
+                        .unwrap()
+                        .set_visible(toggle.value > 0);
+
+                    // Note there's no pane for 0
+                    for value in 1..=toggle.max {
+                        let err_msg = format!("Could not find pane with name {}", value);
+                        menu_button
+                            .find_pane_by_name_recursive(format!("{}", value).as_str())
+                            .expect(&err_msg)
+                            .set_visible(value == toggle.value);
+                    }
+                }
+            }
+        }
+    }
 }
 
-unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
-    let (title, _help_text, gauge_vals) = app.sub_menu_strs_for_slider();
-    let selected_min = gauge_vals.selected_min;
-    let selected_max = gauge_vals.selected_max;
+unsafe fn render_slider_page(app: &mut App, root_pane: &Pane) {
+    let submenu = app.selected_submenu();
+    let slider = submenu.slider.unwrap();
+    let selected_min = slider.lower;
+    let selected_max = slider.upper;
 
     let slider_pane = root_pane
         .find_pane_by_name_recursive("TrModSlider")
@@ -257,7 +273,7 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
         .find_pane_by_name_recursive("Header")
         .unwrap()
         .as_textbox();
-    header.set_text_string(title.as_str());
+    header.set_text_string(submenu.title);
     let min_button = slider_pane
         .find_pane_by_name_recursive("MinButton")
         .unwrap()
@@ -292,8 +308,8 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
         .as_textbox();
 
     min_title_text.set_text_string("Min");
-    match gauge_vals.state {
-        GaugeState::MinHover | GaugeState::MinSelected => {
+    match slider.state {
+        SliderState::LowerHover | SliderState::LowerSelected => {
             min_title_text.text_shadow_enable(true);
             min_title_text.text_outline_enable(true);
             min_title_text.set_color(255, 255, 255, 255);
@@ -306,8 +322,8 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
     }
 
     max_title_text.set_text_string("Max");
-    match gauge_vals.state {
-        GaugeState::MaxHover | GaugeState::MaxSelected => {
+    match slider.state {
+        SliderState::UpperHover | SliderState::UpperSelected => {
             max_title_text.text_shadow_enable(true);
             max_title_text.text_outline_enable(true);
             max_title_text.set_color(255, 255, 255, 255);
@@ -323,9 +339,9 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
     max_value_text.set_text_string(&format!("{selected_max}"));
 
     let min_title_bg_material = &mut *min_title_bg.as_picture().material;
-    let min_colors = match gauge_vals.state {
-        GaugeState::MinHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR),
-        GaugeState::MinSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR),
+    let min_colors = match slider.state {
+        SliderState::LowerHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR),
+        SliderState::LowerSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR),
         _ => (BG_LEFT_OFF_WHITE_COLOR, BG_LEFT_OFF_BLACK_COLOR),
     };
 
@@ -333,9 +349,9 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
     min_title_bg_material.set_black_res_color(min_colors.1);
 
     let max_title_bg_material = &mut *max_title_bg.as_picture().material;
-    let max_colors = match gauge_vals.state {
-        GaugeState::MaxHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR),
-        GaugeState::MaxSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR),
+    let max_colors = match slider.state {
+        SliderState::UpperHover => (BG_LEFT_ON_WHITE_COLOR, BG_LEFT_ON_BLACK_COLOR),
+        SliderState::UpperSelected => (BG_LEFT_SELECTED_WHITE_COLOR, BG_LEFT_SELECTED_BLACK_COLOR),
         _ => (BG_LEFT_OFF_WHITE_COLOR, BG_LEFT_OFF_BLACK_COLOR),
     };
 
@@ -352,6 +368,91 @@ unsafe fn render_slider_page(app: &App, root_pane: &Pane) {
     });
 }
 
+unsafe fn render_confirmation_page(app: &mut App, root_pane: &Pane) {
+    let show_row = 3; // Row in the middle of the page
+    let show_cols = [1, 2]; // Columns in the middle of the page
+    let no_col = show_cols[0]; // Left
+    let yes_col = show_cols[1]; // Right
+    let help_text = match app.confirmation_return_page {
+        AppPage::TOGGLE | AppPage::SLIDER => {
+            "Are you sure you want to reset the current setting to the defaults?"
+        }
+        AppPage::SUBMENU => "Are you sure you want to reset ALL settings to the defaults?",
+        _ => "", // Shouldn't ever get this case, but don't panic if we do
+    };
+
+    // Set help text
+    root_pane
+        .find_pane_by_name_recursive("FooterTxt")
+        .unwrap()
+        .as_textbox()
+        .set_text_string(help_text);
+
+    // Show only the buttons that we care about
+    for row in 0..NX_SUBMENU_ROWS {
+        let should_show_row = row == show_row;
+        let menu_button_row = root_pane
+            .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row}").as_str())
+            .unwrap();
+        menu_button_row.set_visible(should_show_row);
+        if should_show_row {
+            for col in 0..NX_SUBMENU_COLUMNS {
+                let should_show_col = show_cols.contains(&col);
+                let menu_button = menu_button_row
+                    .find_pane_by_name_recursive(format!("Button{col}").as_str())
+                    .unwrap();
+                menu_button.set_visible(should_show_col);
+                if should_show_col {
+                    let title_text = menu_button
+                        .find_pane_by_name_recursive("TitleTxt")
+                        .unwrap()
+                        .as_textbox();
+                    let title_bg = menu_button
+                        .find_pane_by_name_recursive("TitleBg")
+                        .unwrap()
+                        .as_picture();
+                    let title_bg_material = &mut *title_bg.material;
+
+                    if col == no_col {
+                        title_text.set_text_string("No");
+                    } else if col == yes_col {
+                        title_text.set_text_string("Yes");
+                    }
+                    let is_selected = (col == no_col
+                        && app.confirmation_state == ConfirmationState::HoverNo)
+                        || (col == yes_col
+                            && app.confirmation_state == ConfirmationState::HoverYes);
+
+                    if is_selected {
+                        title_text.text_shadow_enable(true);
+                        title_text.text_outline_enable(true);
+                        title_text.set_color(255, 255, 255, 255);
+                        title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR);
+                        title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR);
+                    } else {
+                        title_text.text_shadow_enable(false);
+                        title_text.text_outline_enable(false);
+                        title_text.set_color(178, 199, 211, 255);
+                        title_bg_material.set_white_res_color(BG_LEFT_OFF_WHITE_COLOR);
+                        title_bg_material.set_black_res_color(BG_LEFT_OFF_BLACK_COLOR);
+                    }
+
+                    // Hide all submenu icons, since we're not on the submenu page
+                    // TODO: Do we want to show the check on "Yes" and a red "X" on "No?"
+                    for t in app.tabs.iter() {
+                        for s in t.submenus.iter() {
+                            let pane = menu_button.find_pane_by_name_recursive(s.id);
+                            if let Some(p) = pane {
+                                p.set_visible(false);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
 pub unsafe fn draw(root_pane: &Pane) {
     // Determine if we're in the menu by seeing if the "help" footer has
     // begun moving upward. It starts at -80 and moves to 0 over 10 frames
@@ -388,25 +489,24 @@ pub unsafe fn draw(root_pane: &Pane) {
     }
 
     // Make all invisible first
-    (0..NUM_MENU_TEXT_OPTIONS).for_each(|idx| {
-        let col_idx = idx % NUM_LISTS;
-        let row_idx = idx / NUM_LISTS;
+    for row_idx in 0..NX_SUBMENU_ROWS {
+        for col_idx in 0..NX_SUBMENU_COLUMNS {
+            let menu_button_row = root_pane
+                .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row_idx}").as_str())
+                .unwrap();
+            menu_button_row.set_visible(false);
 
-        let menu_button_row = root_pane
-            .find_pane_by_name_recursive(format!("TrModMenuButtonRow{row_idx}").as_str())
-            .unwrap();
-        menu_button_row.set_visible(false);
+            let menu_button = menu_button_row
+                .find_pane_by_name_recursive(format!("Button{col_idx}").as_str())
+                .unwrap();
+            menu_button.set_visible(false);
 
-        let menu_button = menu_button_row
-            .find_pane_by_name_recursive(format!("Button{col_idx}").as_str())
-            .unwrap();
-        menu_button.set_visible(false);
-
-        menu_button
-            .find_pane_by_name_recursive("ValueTxt")
-            .unwrap()
-            .set_visible(false);
-    });
+            menu_button
+                .find_pane_by_name_recursive("ValueTxt")
+                .unwrap()
+                .set_visible(false);
+        }
+    }
 
     // Make normal training panes invisible if we're active
     // InfluencedAlpha means "Should my children panes' alpha be influenced by mine, as the parent?"
@@ -423,21 +523,20 @@ pub unsafe fn draw(root_pane: &Pane) {
 
     // Update menu display
     // Grabbing lock as read-only, essentially
-    let app = &*crate::common::menu::QUICK_MENU_APP.data_ptr();
+    // We don't really need to change anything, but get_before_selected requires &mut self
+    let app = &mut *crate::common::menu::QUICK_MENU_APP.data_ptr();
 
-    let app_tabs = &app.tabs.items;
-    let tab_selected = app.tabs.state.selected().unwrap();
-    let prev_tab = if tab_selected == 0 {
-        app_tabs.len() - 1
-    } else {
-        tab_selected - 1
-    };
-    let next_tab = if tab_selected == app_tabs.len() - 1 {
-        0
-    } else {
-        tab_selected + 1
-    };
-    let tab_titles = [prev_tab, tab_selected, next_tab].map(|idx| app_tabs[idx].clone());
+    let tab_titles = [
+        app.tabs
+            .get_before_selected()
+            .expect("No tab selected!")
+            .title,
+        app.tabs.get_selected().expect("No tab selected!").title,
+        app.tabs
+            .get_after_selected()
+            .expect("No tab selected!")
+            .title,
+    ];
 
     let is_gcc = (*common::menu::P1_CONTROLLER_STYLE.data_ptr()) == ControllerStyle::GCController;
     let button_mapping = if is_gcc {
@@ -456,12 +555,11 @@ pub unsafe fn draw(root_pane: &Pane) {
         button_mapping.get("Z"),
     );
 
-    let (left_tab_key, right_tab_key, save_defaults_key, reset_current_key, reset_all_key) =
-        if is_gcc {
-            (l_key, r_key, x_key, z_key, y_key)
-        } else {
-            (zl_key, zr_key, x_key, r_key, y_key)
-        };
+    let (left_tab_key, right_tab_key, save_defaults_key, reset_key, clear_toggle_key) = if is_gcc {
+        (l_key, r_key, x_key, z_key, y_key)
+    } else {
+        (zl_key, zr_key, x_key, r_key, y_key)
+    };
 
     [
         (left_tab_key, "LeftTab"),
@@ -494,34 +592,77 @@ pub unsafe fn draw(root_pane: &Pane) {
             help_pane.set_default_material_colors();
             help_pane.set_color(255, 255, 0, 255);
         }
-        help_pane.set_text_string(tab_titles[idx].as_str());
+        help_pane.set_text_string(tab_titles[idx]);
     });
-    [
-        (save_defaults_key, "SaveDefaults", "Save Defaults"),
-        (reset_current_key, "ResetCurrentDefaults", "Reset Current"),
-        (reset_all_key, "ResetAllDefaults", "Reset All"),
-    ]
-    .iter()
-    .for_each(|(key, name, title)| {
-        let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap();
 
+    // Save Defaults Keyhelp
+    let name = "SaveDefaults";
+    let key = save_defaults_key;
+    let title = "Save Defaults";
+    let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap();
+    let icon_pane = key_help_pane
+        .find_pane_by_name_recursive("set_txt_icon")
+        .unwrap()
+        .as_textbox();
+    set_icon_text(icon_pane, &vec![*key.unwrap()]);
+    key_help_pane
+        .find_pane_by_name_recursive("set_txt_help")
+        .unwrap()
+        .as_textbox()
+        .set_text_string(title);
+
+    // Reset Keyhelp
+    let name = "ResetDefaults";
+    let key = reset_key;
+    let title = match app.page {
+        AppPage::SUBMENU => "Reset All",
+        AppPage::SLIDER => "Reset Current",
+        AppPage::TOGGLE => "Reset Current",
+        AppPage::CONFIRMATION => "",
+        AppPage::CLOSE => "",
+    };
+    if !title.is_empty() {
+        let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap();
         let icon_pane = key_help_pane
             .find_pane_by_name_recursive("set_txt_icon")
             .unwrap()
             .as_textbox();
         set_icon_text(icon_pane, &vec![*key.unwrap()]);
-
         key_help_pane
             .find_pane_by_name_recursive("set_txt_help")
             .unwrap()
             .as_textbox()
             .set_text_string(title);
-    });
+    }
+
+    // Clear Toggle Keyhelp
+    let name = "ClearToggle";
+    let key_help_pane = root_pane.find_pane_by_name_recursive(name).unwrap();
+    let icon_pane = key_help_pane
+        .find_pane_by_name_recursive("set_txt_icon")
+        .unwrap();
+    if app.should_show_clear_keyhelp() {
+        // This is only displayed when you're in a multiple selection toggle menu w/ toggle.max > 1
+        let key = clear_toggle_key;
+        let title = "Clear Toggle";
+        set_icon_text(icon_pane.as_textbox(), &vec![*key.unwrap()]);
+        key_help_pane
+            .find_pane_by_name_recursive("set_txt_help")
+            .unwrap()
+            .as_textbox()
+            .set_text_string(title);
+        icon_pane.set_visible(true);
+        key_help_pane.set_visible(true);
+    } else {
+        icon_pane.set_visible(false);
+        key_help_pane.set_visible(false);
+    }
 
     match app.page {
         AppPage::SUBMENU => render_submenu_page(app, root_pane),
         AppPage::SLIDER => render_slider_page(app, root_pane),
         AppPage::TOGGLE => render_toggle_page(app, root_pane),
-        AppPage::CONFIRMATION => todo!(),
+        AppPage::CONFIRMATION => render_confirmation_page(app, root_pane),
+        AppPage::CLOSE => {}
     }
 }
diff --git a/src/training/ui/mod.rs b/src/training/ui/mod.rs
index 1a3a129..2a6290f 100644
--- a/src/training/ui/mod.rs
+++ b/src/training/ui/mod.rs
@@ -71,7 +71,7 @@ pub unsafe fn handle_draw(layout: *mut Layout, draw_info: u64, cmd_buffer: u64)
     {
         // InfluencedAlpha means "Should my children panes' alpha be influenced by mine, as the parent?"
         root_pane.flags |= 1 << PaneFlag::InfluencedAlpha as u8;
-        root_pane.set_visible(MENU.hud == OnOff::On && !QUICK_MENU_ACTIVE);
+        root_pane.set_visible(MENU.hud == OnOff::ON && !QUICK_MENU_ACTIVE);
     }
 
     damage::draw(root_pane, &layout_name);
diff --git a/training_mod_consts/Cargo.toml b/training_mod_consts/Cargo.toml
index 123ff14..e6d0480 100644
--- a/training_mod_consts/Cargo.toml
+++ b/training_mod_consts/Cargo.toml
@@ -4,9 +4,7 @@ version = "0.1.0"
 edition = "2018"
 
 [dependencies]
-bitflags = "1.2.1"
-strum = "0.21.0"
-strum_macros = "0.21.0"
+byteflags = { git = "https://github.com/asimon-1/byteflags.git", branch = "rand-0.7.4" }
 num = "0.4.0"
 num-derive = "0.3"
 num-traits = "0.2"
@@ -14,11 +12,12 @@ paste = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 serde_repr = "0.1.8"
 serde_json = "1"
-bitflags_serde_shim = "0.2"
 skyline = { git = "https://github.com/ultimate-research/skyline-rs.git" }
 skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git", optional = true }
 toml = "0.5.9"
 anyhow = "1.0.72"
+rand = { git = "https://github.com/skyline-rs/rand" }
+training_mod_tui = { path = "../training_mod_tui"}
 
 [features]
 default = ["smash"]
diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs
index e86ffb7..e05a6eb 100644
--- a/training_mod_consts/src/lib.rs
+++ b/training_mod_consts/src/lib.rs
@@ -1,11 +1,5 @@
-#![feature(iter_intersperse)]
-#[macro_use]
-extern crate bitflags;
-
-#[macro_use]
-extern crate bitflags_serde_shim;
-
-#[macro_use]
+#![allow(non_snake_case)]
+extern crate byteflags;
 extern crate num_derive;
 
 use serde::{Deserialize, Serialize};
@@ -17,6 +11,11 @@ pub use files::*;
 pub mod config;
 pub use config::*;
 
+use paste::paste;
+pub use training_mod_tui::*;
+
+pub const TOGGLE_MAX: u8 = 5;
+
 #[repr(C)]
 #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
 pub struct TrainingModpackMenu {
@@ -101,10 +100,8 @@ pub struct TrainingModpackMenu {
 pub struct MenuJsonStruct {
     pub menu: TrainingModpackMenu,
     pub defaults_menu: TrainingModpackMenu,
-    // pub last_focused_submenu: &str
 }
 
-// Fighter Ids
 #[repr(i32)]
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum FighterId {
@@ -112,41 +109,25 @@ pub enum FighterId {
     CPU = 1,
 }
 
-#[derive(Clone)]
-pub enum SubMenuType {
-    TOGGLE,
-    SLIDER,
-}
-
-impl SubMenuType {
-    pub fn from_string(s: &String) -> SubMenuType {
-        match s.as_str() {
-            "toggle" => SubMenuType::TOGGLE,
-            "slider" => SubMenuType::SLIDER,
-            _ => panic!("Unexpected SubMenuType!"),
-        }
-    }
-}
-
 pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
     aerial_delay: Delay::empty(),
     air_dodge_dir: Direction::empty(),
     attack_angle: AttackAngle::empty(),
     buff_state: BuffOption::empty(),
-    character_item: CharacterItem::None,
-    clatter_strength: ClatterFrequency::None,
-    crouch: OnOff::Off,
+    character_item: CharacterItem::NONE,
+    clatter_strength: ClatterFrequency::NONE,
+    crouch: OnOff::OFF,
     di_state: Direction::empty(),
     falling_aerials: BoolFlag::FALSE,
     fast_fall_delay: Delay::empty(),
     fast_fall: BoolFlag::FALSE,
     follow_up: Action::empty(),
-    frame_advantage: OnOff::Off,
+    frame_advantage: OnOff::OFF,
     full_hop: BoolFlag::TRUE,
-    hitbox_vis: OnOff::Off,
-    input_display: InputDisplay::Smash,
-    input_display_status: OnOff::Off,
-    hud: OnOff::On,
+    hitbox_vis: OnOff::OFF,
+    input_display: InputDisplay::SMASH,
+    input_display_status: OnOff::OFF,
+    hud: OnOff::ON,
     input_delay: Delay::D0,
     ledge_delay: LongDelay::empty(),
     ledge_state: LedgeOption::default(),
@@ -160,17 +141,17 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
     save_damage_limits_cpu: DamagePercent::default(),
     save_damage_player: SaveDamage::DEFAULT,
     save_damage_limits_player: DamagePercent::default(),
-    save_state_autoload: OnOff::Off,
-    save_state_enable: OnOff::On,
+    save_state_autoload: OnOff::OFF,
+    save_state_enable: OnOff::ON,
     save_state_slot: SaveStateSlot::S1,
     randomize_slots: SaveStateSlot::empty(),
-    save_state_mirroring: SaveStateMirroring::None,
+    save_state_mirroring: SaveStateMirroring::NONE,
     save_state_playback: PlaybackSlot::empty(),
     sdi_state: Direction::empty(),
-    sdi_strength: SdiFrequency::None,
-    shield_state: Shield::None,
+    sdi_strength: SdiFrequency::NONE,
+    shield_state: Shield::NONE,
     shield_tilt: Direction::empty(),
-    stage_hazards: OnOff::Off,
+    stage_hazards: OnOff::OFF,
     tech_state: TechFlags::all(),
     throw_delay: MedDelay::empty(),
     throw_state: ThrowOption::NONE,
@@ -191,745 +172,793 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
     recording_duration: RecordingDuration::F150,
     record_trigger: RecordTrigger::COMMAND,
     playback_button_slots: PlaybackSlot::S1,
-    hitstun_playback: HitstunPlayback::Hitstun,
-    playback_mash: OnOff::On,
-    playback_loop: OnOff::Off,
-    menu_open_start_press: OnOff::On,
-    save_state_save: ButtonConfig::ZL.union(ButtonConfig::DPAD_DOWN),
-    save_state_load: ButtonConfig::ZL.union(ButtonConfig::DPAD_UP),
-    input_record: ButtonConfig::ZR.union(ButtonConfig::DPAD_DOWN),
-    input_playback: ButtonConfig::ZR.union(ButtonConfig::DPAD_UP),
-    recording_crop: OnOff::On,
-    stale_dodges: OnOff::On,
-    tech_hide: OnOff::Off,
+    hitstun_playback: HitstunPlayback::HITSTUN,
+    playback_mash: OnOff::ON,
+    playback_loop: OnOff::OFF,
+    menu_open_start_press: OnOff::ON,
+    save_state_save: ButtonConfig {
+        ZL: 1,
+        DPAD_DOWN: 1,
+        ..ButtonConfig::empty()
+    },
+    save_state_load: ButtonConfig {
+        ZL: 1,
+        DPAD_UP: 1,
+        ..ButtonConfig::empty()
+    },
+    input_record: ButtonConfig {
+        ZR: 1,
+        DPAD_DOWN: 1,
+        ..ButtonConfig::empty()
+    },
+    input_playback: ButtonConfig {
+        ZR: 1,
+        DPAD_UP: 1,
+        ..ButtonConfig::empty()
+    },
+    recording_crop: OnOff::ON,
+    stale_dodges: OnOff::ON,
+    tech_hide: OnOff::OFF,
     update_policy: UpdatePolicy::default(),
 };
 
 pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU;
 
-#[derive(Clone, Serialize)]
-pub struct Slider {
-    pub selected_min: u32,
-    pub selected_max: u32,
-    pub abs_min: u32,
-    pub abs_max: u32,
+impl_toggletrait! {
+    OnOff,
+    "Menu Open Start Press",
+    "menu_open_start_press",
+    "Menu Open Start Press: Hold start or press minus to open the mod menu. To open the original menu, press start.\nThe default menu open option is always available as Hold DPad Up + Press B.",
+    true,
+    1,
+}
+impl_toggletrait! {
+    ButtonConfig,
+    "Save State Save",
+    "save_state_save",
+    "Save State Save: Hold any one button and press the others to trigger",
+    false,
+    1,
+}
+impl_toggletrait! {
+    ButtonConfig,
+    "Save State Load",
+    "save_state_load",
+    "Save State Load: Hold any one button and press the others to trigger",
+    false,
+    1,
+}
+impl_toggletrait! {
+    ButtonConfig,
+    "Input Record",
+    "input_record",
+    "Input Record: Hold any one button and press the others to trigger",
+    false,
+    1,
+}
+impl_toggletrait! {
+    ButtonConfig,
+    "Input Playback",
+    "input_playback",
+    "Input Playback: Hold any one button and press the others to trigger",
+    false,
+    1,
+}
+impl_toggletrait! {
+    Action,
+    "Mash Toggles",
+    "mash_state",
+    "Mash Toggles: Actions to be performed as soon as possible",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Followup Toggles",
+    "follow_up",
+    "Followup Toggles: Actions to be performed after a Mash option",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    MashTrigger,
+    "Mash Triggers",
+    "mash_triggers",
+    "Mash triggers: Configure what causes the CPU to perform a Mash option",
+    false,
+    1,
+}
+impl_toggletrait! {
+    AttackAngle,
+    "Attack Angle",
+    "attack_angle",
+    "Attack Angle: For attacks that can be angled, such as some forward tilts",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    ThrowOption,
+    "Throw Options",
+    "throw_state",
+    "Throw Options: Throw to be performed when a grab is landed",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    MedDelay,
+    "Throw Delay",
+    "throw_delay",
+    "Throw Delay: How many frames to delay the throw option",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    MedDelay,
+    "Pummel Delay",
+    "pummel_delay",
+    "Pummel Delay: How many frames after a grab to wait before starting to pummel",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    BoolFlag,
+    "Falling Aerials",
+    "falling_aerials",
+    "Falling Aerials: Should aerials be performed when rising or when falling",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    BoolFlag,
+    "Full Hop",
+    "full_hop",
+    "Full Hop: Should the CPU perform a full hop or a short hop",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Delay,
+    "Aerial Delay",
+    "aerial_delay",
+    "Aerial Delay: How long to delay a Mash aerial attack",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    BoolFlag,
+    "Fast Fall",
+    "fast_fall",
+    "Fast Fall: Should the CPU fastfall during a jump",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Delay,
+    "Fast Fall Delay",
+    "fast_fall_delay",
+    "Fast Fall Delay: How many frames the CPU should delay their fastfall",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Delay,
+    "OoS Offset",
+    "oos_offset",
+    "OoS Offset: How many times the CPU shield can be hit before performing a Mash option",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Delay,
+    "Reaction Time",
+    "reaction_time",
+    "Reaction Time: How many frames to delay before performing a mash option",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Ledge Neutral Getup",
+    "ledge_neutral_override",
+    "Neutral Getup Override: Mash Actions to be performed after a Neutral Getup from ledge",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Ledge Roll",
+    "ledge_roll_override",
+    "Ledge Roll Override: Mash Actions to be performed after a Roll Getup from ledge",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Ledge Jump",
+    "ledge_jump_override",
+    "Ledge Jump Override: Mash Actions to be performed after a Jump Getup from ledge",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Ledge Attack",
+    "ledge_attack_override",
+    "Ledge Attack Override: Mash Actions to be performed after a Getup Attack from ledge",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Tech Action",
+    "tech_action_override",
+    "Tech Action Override: Mash Actions to be performed after any tech action",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Clatter",
+    "clatter_override",
+    "Clatter Override: Mash Actions to be performed after leaving a clatter situation (grab, bury, etc)",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Tumble",
+    "tumble_override",
+    "Tumble Override: Mash Actions to be performed after exiting a tumble state",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Hitstun",
+    "hitstun_override",
+    "Hitstun Override: Mash Actions to be performed after exiting a hitstun state",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Parry",
+    "parry_override",
+    "Parry Override: Mash Actions to be performed after a parry",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Shieldstun",
+    "shieldstun_override",
+    "Shieldstun Override: Mash Actions to be performed after exiting a shieldstun state",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Footstool",
+    "footstool_override",
+    "Footstool Override: Mash Actions to be performed after exiting a footstool state",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Landing",
+    "landing_override",
+    "Landing Override: Mash Actions to be performed after landing on the ground",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Action,
+    "Ledge Trump",
+    "trump_override",
+    "Ledge Trump Override: Mash Actions to be performed after leaving a ledgetrump state",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Direction,
+    "Airdodge Direction",
+    "air_dodge_dir",
+    "Airdodge Direction: Direction to angle airdodges",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Direction,
+    "DI Direction",
+    "di_state",
+    "DI Direction: Direction to angle the directional influence during hitlag",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Direction,
+    "SDI Direction",
+    "sdi_state",
+    "SDI Direction: Direction to angle the smash directional influence during hitlag",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    SdiFrequency,
+    "SDI Strength",
+    "sdi_strength",
+    "SDI Strength: Relative strength of the smash directional influence inputs",
+    true,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    ClatterFrequency,
+    "Clatter Strength",
+    "clatter_strength",
+    "Clatter Strength: Configure how rapidly the CPU will mash out of grabs, buries, etc.",
+    true,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    LedgeOption,
+    "Ledge Options",
+    "ledge_state",
+    "Ledge Options: Actions to be taken when on the ledge",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    LongDelay,
+    "Ledge Delay",
+    "ledge_delay",
+    "Ledge Delay: How many frames to delay the ledge option",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    TechFlags,
+    "Tech Options",
+    "tech_state",
+    "Tech Options: Actions to take when slammed into a hard surface",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    MissTechFlags,
+    "Mistech Options",
+    "miss_tech_state",
+    "Mistech Options: Actions to take after missing a tech",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    Shield,
+    "Shield Toggles",
+    "shield_state",
+    "Shield Toggles: CPU Shield Behavior",
+    true,
+    1,
+}
+impl_toggletrait! {
+    Direction,
+    "Shield Tilt",
+    "shield_tilt",
+    "Shield Tilt: Direction to tilt the shield",
+    true,
+    1,
 }
 
-#[derive(Clone, Serialize)]
-pub struct Toggle {
-    pub toggle_value: u32,
-    pub toggle_title: String,
-    pub checked: bool,
+impl_toggletrait! {
+    OnOff,
+    "Crouch",
+    "crouch",
+    "Crouch: Have the CPU crouch when on the ground",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "Dodge Staling",
+    "stale_dodges",
+    "Dodge Staling: Controls whether the CPU's dodges will worsen with repetitive use\n(Note: This can setting can cause combo behavior not possible in the original game)",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "Hide Tech Animations",
+    "tech_hide",
+    "Hide Tech Animations: Hides tech animations and effects after 7 frames to help with reacting to tech animation startup",
+    true,
+    1,
+}
+impl_toggletrait! {
+    SaveStateMirroring,
+    "Mirroring",
+    "save_state_mirroring",
+    "Mirroring: Flips save states in the left-right direction across the stage center",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "Auto Save States",
+    "save_state_autoload",
+    "Auto Save States: Load save state when any fighter dies",
+    true,
+    1,
+}
+impl_toggletrait! {
+    SaveDamage,
+    "Save Dmg (CPU)",
+    "save_damage_cpu",
+    "Save Damage: Should save states retain CPU damage",
+    true,
+    1,
+}
+impl_slidertrait! {
+    DamagePercent,
+    "Dmg Range (CPU)",
+    "save_damage_limits_cpu",
+    "Limits on random damage to apply to the CPU when loading a save state",
+}
+impl_toggletrait! {
+    SaveDamage,
+    "Save Dmg (Player)",
+    "save_damage_player",
+    "Save Damage: Should save states retain player damage",
+    true,
+    1,
+}
+impl_slidertrait! {
+    DamagePercent,
+    "Dmg Range (Player)",
+    "save_damage_limits_player",
+    "Limits on random damage to apply to the player when loading a save state",
+}
+impl_toggletrait! {
+    OnOff,
+    "Enable Save States",
+    "save_state_enable",
+    "Save States: Enable save states! Save a state with Shield+Down Taunt, load it with Shield+Up Taunt.",
+    true,
+    1,
+}
+impl_toggletrait! {
+    SaveStateSlot,
+    "Save State Slot",
+    "save_state_slot",
+    "Save State Slot: Save and load states from different slots.",
+    true,
+    1,
+}
+impl_toggletrait! {
+    SaveStateSlot,
+    "Randomize Slots",
+    "randomize_slots",
+    "Randomize Slots: Slots to randomize when loading save state.",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    CharacterItem,
+    "Character Item",
+    "character_item",
+    "Character Item: The item to give to the player's fighter when loading a save state",
+    true,
+    1,
+}
+impl_toggletrait! {
+    BuffOption,
+    "Buff Options",
+    "buff_state",
+    "Buff Options: Buff(s) to be applied to the respective fighters when loading a save state",
+    false,
+    1,
+}
+impl_toggletrait! {
+    PlaybackSlot,
+    "Save State Playback",
+    "save_state_playback",
+    "Save State Playback: Choose which slots to playback input recording upon loading a save state",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    OnOff,
+    "Frame Advantage",
+    "frame_advantage",
+    "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "Hitbox Visualization",
+    "hitbox_vis",
+    "Hitbox Visualization: Display a visual representation for active hitboxes (hides other visual effects)",
+    true,
+    1,
+}
+impl_toggletrait! {
+    InputDisplay,
+    "Input Display",
+    "input_display",
+    "Input Display: Log inputs in a queue on the left of the screen",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "Input Display Status",
+    "input_display_status",
+    "Input Display Status: Group input logs by status in which they occurred",
+    true,
+    1,
+}
+impl_toggletrait! {
+    Delay,
+    "Input Delay",
+    "input_delay",
+    "Input Delay: Frames to delay player inputs by",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "Stage Hazards",
+    "stage_hazards",
+    "Stage Hazards: Turn stage hazards on/off",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "HUD",
+    "hud",
+    "HUD: Show/hide elements of the UI",
+    true,
+    1,
+}
+impl_toggletrait! {
+    UpdatePolicy,
+    "Auto-Update",
+    "update_policy",
+    "Auto-Update: What type of Training Modpack updates to automatically apply. (Console Only!)",
+    true,
+    1,
+}
+impl_toggletrait! {
+    RecordSlot,
+    "Recording Slot",
+    "recording_slot",
+    "Recording Slot: Choose which slot to record into",
+    true,
+    1,
+}
+impl_toggletrait! {
+    RecordTrigger,
+    "Recording Trigger",
+    "record_trigger",
+    "Recording Trigger: Whether to begin recording via button combination or upon loading a Save State",
+    false,
+    1,
+}
+impl_toggletrait! {
+    RecordingDuration,
+    "Recording Duration",
+    "recording_duration",
+    "Recording Duration: How long an input recording should last in frames",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "Recording Crop",
+    "recording_crop",
+    "Recording Crop: Remove neutral input frames at the end of your recording",
+    true,
+    1,
+}
+impl_toggletrait! {
+    PlaybackSlot,
+    "Playback Button Slots",
+    "playback_button_slots",
+    "Playback Button Slots: Choose which slots to playback input recording upon pressing button combination",
+    false,
+    TOGGLE_MAX,
+}
+impl_toggletrait! {
+    HitstunPlayback,
+    "Playback Hitstun Timing",
+    "hitstun_playback",
+    "Playback Hitstun Timing: When to begin playing back inputs when a hitstun mash trigger occurs",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "Playback Mash Interrupt",
+    "playback_mash",
+    "Playback Mash Interrupt: End input playback when a mash trigger occurs",
+    true,
+    1,
+}
+impl_toggletrait! {
+    OnOff,
+    "Playback Loop",
+    "playback_loop",
+    "Playback Loop: Repeat triggered input playbacks indefinitely",
+    true,
+    1,
 }
 
-#[derive(Clone, Serialize)]
-pub struct SubMenu {
-    pub submenu_title: String,
-    pub submenu_id: String,
-    pub help_text: String,
-    pub is_single_option: bool,
-    pub toggles: Vec<Toggle>,
-    pub slider: Option<Slider>,
-    pub _type: String,
-}
+pub unsafe fn create_app<'a>() -> App<'a> {
+    // Note that the to_submenu_xxx() functions are defined in the `impl_toggletrait` and `impl_slidertrait` macros
+    let mut overall_menu = App::new();
 
-impl SubMenu {
-    pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: String, checked: bool) {
-        self.toggles.push(Toggle {
-            toggle_value,
-            toggle_title,
-            checked,
-        });
-    }
-    pub fn new_with_toggles<T: ToggleTrait>(
-        submenu_title: String,
-        submenu_id: String,
-        help_text: String,
-        is_single_option: bool,
-        initial_value: &u32,
-    ) -> SubMenu {
-        let mut instance = SubMenu {
-            submenu_title: submenu_title,
-            submenu_id: submenu_id,
-            help_text: help_text,
-            is_single_option: is_single_option,
-            toggles: Vec::new(),
-            slider: None,
-            _type: "toggle".to_string(),
-        };
-
-        let values = T::to_toggle_vals();
-        let titles = T::to_toggle_strings();
-        for i in 0..values.len() {
-            let checked: bool =
-                (values[i] & initial_value) > 0 || (!values[i] == 0 && initial_value == &0);
-            instance.add_toggle(values[i], titles[i].clone(), checked);
-        }
-        // Select the first option if there's nothing selected atm but it's a single option submenu
-        if is_single_option && instance.toggles.iter().all(|t| !t.checked) {
-            instance.toggles[0].checked = true;
-        }
-        instance
-    }
-    pub fn new_with_slider<S: SliderTrait>(
-        submenu_title: String,
-        submenu_id: String,
-        help_text: String,
-        initial_lower_value: &u32,
-        initial_upper_value: &u32,
-    ) -> SubMenu {
-        let min_max = S::get_limits();
-        SubMenu {
-            submenu_title: submenu_title,
-            submenu_id: submenu_id,
-            help_text: help_text,
-            is_single_option: false,
-            toggles: Vec::new(),
-            slider: Some(Slider {
-                selected_min: *initial_lower_value,
-                selected_max: *initial_upper_value,
-                abs_min: min_max.0,
-                abs_max: min_max.1,
-            }),
-            _type: "slider".to_string(),
-        }
-    }
-}
-
-#[derive(Serialize, Clone)]
-pub struct Tab {
-    pub tab_id: String,
-    pub tab_title: String,
-    pub tab_submenus: Vec<SubMenu>,
-}
-
-impl Tab {
-    pub fn add_submenu_with_toggles<T: ToggleTrait>(
-        &mut self,
-        submenu_title: String,
-        submenu_id: String,
-        help_text: String,
-        is_single_option: bool,
-        initial_value: &u32,
-    ) {
-        self.tab_submenus.push(SubMenu::new_with_toggles::<T>(
-            submenu_title.to_string(),
-            submenu_id.to_string(),
-            help_text.to_string(),
-            is_single_option,
-            initial_value,
-        ));
-    }
-
-    pub fn add_submenu_with_slider<S: SliderTrait>(
-        &mut self,
-        submenu_title: String,
-        submenu_id: String,
-        help_text: String,
-        initial_lower_value: &u32,
-        initial_upper_value: &u32,
-    ) {
-        self.tab_submenus.push(SubMenu::new_with_slider::<S>(
-            submenu_title.to_string(),
-            submenu_id.to_string(),
-            help_text.to_string(),
-            initial_lower_value,
-            initial_upper_value,
-        ))
-    }
-}
-
-#[derive(Serialize, Clone)]
-pub struct UiMenu {
-    pub tabs: Vec<Tab>,
-}
-
-pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu {
-    let mut overall_menu = UiMenu { tabs: Vec::new() };
-
-    let mut button_tab = Tab {
-        tab_id: "button".to_string(),
-        tab_title: "Button Config".to_string(),
-        tab_submenus: Vec::new(),
+    // Mash Tab
+    let mut mash_tab_submenus: Vec<SubMenu> = Vec::new();
+    mash_tab_submenus.push(to_submenu_mash_state());
+    mash_tab_submenus.push(to_submenu_follow_up());
+    mash_tab_submenus.push(to_submenu_mash_triggers());
+    mash_tab_submenus.push(to_submenu_attack_angle());
+    mash_tab_submenus.push(to_submenu_throw_state());
+    mash_tab_submenus.push(to_submenu_throw_delay());
+    mash_tab_submenus.push(to_submenu_pummel_delay());
+    mash_tab_submenus.push(to_submenu_falling_aerials());
+    mash_tab_submenus.push(to_submenu_full_hop());
+    mash_tab_submenus.push(to_submenu_aerial_delay());
+    mash_tab_submenus.push(to_submenu_fast_fall());
+    mash_tab_submenus.push(to_submenu_fast_fall_delay());
+    mash_tab_submenus.push(to_submenu_oos_offset());
+    mash_tab_submenus.push(to_submenu_reaction_time());
+    let mash_tab = Tab {
+        id: "mash",
+        title: "Mash Settings",
+        submenus: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, mash_tab_submenus),
     };
-    button_tab.add_submenu_with_toggles::<OnOff>(
-        "Menu Open Start Press".to_string(),
-        "menu_open_start_press".to_string(),
-        "Menu Open Start Press: Hold start or press minus to open the mod menu. To open the original menu, press start.\nThe default menu open option is always available as Hold DPad Up + Press B.".to_string(),
-        true,
-        &(menu.menu_open_start_press as u32),
-    );
-    button_tab.add_submenu_with_toggles::<ButtonConfig>(
-        "Save State Save".to_string(),
-        "save_state_save".to_string(),
-        "Save State Save: Hold any one button and press the others to trigger".to_string(),
-        false,
-        &(menu.save_state_save.bits() as u32),
-    );
-    button_tab.add_submenu_with_toggles::<ButtonConfig>(
-        "Save State Load".to_string(),
-        "save_state_load".to_string(),
-        "Save State Load: Hold any one button and press the others to trigger".to_string(),
-        false,
-        &(menu.save_state_load.bits() as u32),
-    );
-    button_tab.add_submenu_with_toggles::<ButtonConfig>(
-        "Input Record".to_string(),
-        "input_record".to_string(),
-        "Input Record: Hold any one button and press the others to trigger".to_string(),
-        false,
-        &(menu.input_record.bits() as u32),
-    );
-    button_tab.add_submenu_with_toggles::<ButtonConfig>(
-        "Input Playback".to_string(),
-        "input_playback".to_string(),
-        "Input Playback: Hold any one button and press the others to trigger".to_string(),
-        false,
-        &(menu.input_playback.bits() as u32),
-    );
-    overall_menu.tabs.push(button_tab);
-
-    let mut mash_tab = Tab {
-        tab_id: "mash".to_string(),
-        tab_title: "Mash Settings".to_string(),
-        tab_submenus: Vec::new(),
-    };
-    mash_tab.add_submenu_with_toggles::<Action>(
-        "Mash Toggles".to_string(),
-        "mash_state".to_string(),
-        "Mash Toggles: Actions to be performed as soon as possible".to_string(),
-        false,
-        &(menu.mash_state.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<Action>(
-        "Followup Toggles".to_string(),
-        "follow_up".to_string(),
-        "Followup Toggles: Actions to be performed after a Mash option".to_string(),
-        false,
-        &(menu.follow_up.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<MashTrigger>(
-        "Mash Triggers".to_string(),
-        "mash_triggers".to_string(),
-        "Mash triggers: Configure what causes the CPU to perform a Mash option".to_string(),
-        false,
-        &(menu.mash_triggers.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<AttackAngle>(
-        "Attack Angle".to_string(),
-        "attack_angle".to_string(),
-        "Attack Angle: For attacks that can be angled, such as some forward tilts".to_string(),
-        false,
-        &(menu.attack_angle.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<ThrowOption>(
-        "Throw Options".to_string(),
-        "throw_state".to_string(),
-        "Throw Options: Throw to be performed when a grab is landed".to_string(),
-        false,
-        &(menu.throw_state.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<MedDelay>(
-        "Throw Delay".to_string(),
-        "throw_delay".to_string(),
-        "Throw Delay: How many frames to delay the throw option".to_string(),
-        false,
-        &(menu.throw_delay.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<MedDelay>(
-        "Pummel Delay".to_string(),
-        "pummel_delay".to_string(),
-        "Pummel Delay: How many frames after a grab to wait before starting to pummel".to_string(),
-        false,
-        &(menu.pummel_delay.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<BoolFlag>(
-        "Falling Aerials".to_string(),
-        "falling_aerials".to_string(),
-        "Falling Aerials: Should aerials be performed when rising or when falling".to_string(),
-        false,
-        &(menu.falling_aerials.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<BoolFlag>(
-        "Full Hop".to_string(),
-        "full_hop".to_string(),
-        "Full Hop: Should the CPU perform a full hop or a short hop".to_string(),
-        false,
-        &(menu.full_hop.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<Delay>(
-        "Aerial Delay".to_string(),
-        "aerial_delay".to_string(),
-        "Aerial Delay: How long to delay a Mash aerial attack".to_string(),
-        false,
-        &(menu.aerial_delay.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<BoolFlag>(
-        "Fast Fall".to_string(),
-        "fast_fall".to_string(),
-        "Fast Fall: Should the CPU fastfall during a jump".to_string(),
-        false,
-        &(menu.fast_fall.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<Delay>(
-        "Fast Fall Delay".to_string(),
-        "fast_fall_delay".to_string(),
-        "Fast Fall Delay: How many frames the CPU should delay their fastfall".to_string(),
-        false,
-        &(menu.fast_fall_delay.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<Delay>(
-        "OoS Offset".to_string(),
-        "oos_offset".to_string(),
-        "OoS Offset: How many times the CPU shield can be hit before performing a Mash option"
-            .to_string(),
-        false,
-        &(menu.oos_offset.bits()),
-    );
-    mash_tab.add_submenu_with_toggles::<Delay>(
-        "Reaction Time".to_string(),
-        "reaction_time".to_string(),
-        "Reaction Time: How many frames to delay before performing a mash option".to_string(),
-        false,
-        &(menu.reaction_time.bits()),
-    );
     overall_menu.tabs.push(mash_tab);
 
-    let mut override_tab = Tab {
-        tab_id: "override".to_string(),
-        tab_title: "Override Settings".to_string(),
-        tab_submenus: Vec::new(),
+    // Mash Override Tab
+    let mut override_tab_submenus: Vec<SubMenu> = Vec::new();
+    override_tab_submenus.push(to_submenu_ledge_neutral_override());
+    override_tab_submenus.push(to_submenu_ledge_roll_override());
+    override_tab_submenus.push(to_submenu_ledge_jump_override());
+    override_tab_submenus.push(to_submenu_ledge_attack_override());
+    override_tab_submenus.push(to_submenu_tech_action_override());
+    override_tab_submenus.push(to_submenu_clatter_override());
+    override_tab_submenus.push(to_submenu_tumble_override());
+    override_tab_submenus.push(to_submenu_hitstun_override());
+    override_tab_submenus.push(to_submenu_parry_override());
+    override_tab_submenus.push(to_submenu_shieldstun_override());
+    override_tab_submenus.push(to_submenu_footstool_override());
+    override_tab_submenus.push(to_submenu_landing_override());
+    override_tab_submenus.push(to_submenu_trump_override());
+    let override_tab = Tab {
+        id: "override",
+        title: "Override Settings",
+        submenus: StatefulTable::with_items(
+            NX_SUBMENU_ROWS,
+            NX_SUBMENU_COLUMNS,
+            override_tab_submenus,
+        ),
     };
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Ledge Neutral Getup".to_string(),
-        "ledge_neutral_override".to_string(),
-        "Neutral Getup Override: Mash Actions to be performed after a Neutral Getup from ledge"
-            .to_string(),
-        false,
-        &(menu.ledge_neutral_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Ledge Roll".to_string(),
-        "ledge_roll_override".to_string(),
-        "Ledge Roll Override: Mash Actions to be performed after a Roll Getup from ledge"
-            .to_string(),
-        false,
-        &(menu.ledge_roll_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Ledge Jump".to_string(),
-        "ledge_jump_override".to_string(),
-        "Ledge Jump Override: Mash Actions to be performed after a Jump Getup from ledge"
-            .to_string(),
-        false,
-        &(menu.ledge_jump_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Ledge Attack".to_string(),
-        "ledge_attack_override".to_string(),
-        "Ledge Attack Override: Mash Actions to be performed after a Getup Attack from ledge"
-            .to_string(),
-        false,
-        &(menu.ledge_attack_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Tech Action".to_string(),
-        "tech_action_override".to_string(),
-        "Tech Action Override: Mash Actions to be performed after any tech action".to_string(),
-        false,
-        &(menu.tech_action_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Clatter".to_string(),
-        "clatter_override".to_string(),
-        "Clatter Override: Mash Actions to be performed after leaving a clatter situation (grab.to_string(), bury, etc)".to_string(),
-        false,
-        &(menu.clatter_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Tumble".to_string(),
-        "tumble_override".to_string(),
-        "Tumble Override: Mash Actions to be performed after exiting a tumble state".to_string(),
-        false,
-        &(menu.tumble_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Hitstun".to_string(),
-        "hitstun_override".to_string(),
-        "Hitstun Override: Mash Actions to be performed after exiting a hitstun state".to_string(),
-        false,
-        &(menu.hitstun_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Parry".to_string(),
-        "parry_override".to_string(),
-        "Parry Override: Mash Actions to be performed after a parry".to_string(),
-        false,
-        &(menu.parry_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Shieldstun".to_string(),
-        "shieldstun_override".to_string(),
-        "Shieldstun Override: Mash Actions to be performed after exiting a shieldstun state"
-            .to_string(),
-        false,
-        &(menu.shieldstun_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Footstool".to_string(),
-        "footstool_override".to_string(),
-        "Footstool Override: Mash Actions to be performed after exiting a footstool state"
-            .to_string(),
-        false,
-        &(menu.footstool_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Landing".to_string(),
-        "landing_override".to_string(),
-        "Landing Override: Mash Actions to be performed after landing on the ground".to_string(),
-        false,
-        &(menu.landing_override.bits()),
-    );
-    override_tab.add_submenu_with_toggles::<Action>(
-        "Ledge Trump".to_string(),
-        "trump_override".to_string(),
-        "Ledge Trump Override: Mash Actions to be performed after leaving a ledgetrump state"
-            .to_string(),
-        false,
-        &(menu.trump_override.bits()),
-    );
     overall_menu.tabs.push(override_tab);
 
-    let mut defensive_tab = Tab {
-        tab_id: "defensive".to_string(),
-        tab_title: "Defensive Settings".to_string(),
-        tab_submenus: Vec::new(),
+    // Defensive Tab
+    let mut defensive_tab_submenus: Vec<SubMenu> = Vec::new();
+    defensive_tab_submenus.push(to_submenu_air_dodge_dir());
+    defensive_tab_submenus.push(to_submenu_di_state());
+    defensive_tab_submenus.push(to_submenu_sdi_state());
+    defensive_tab_submenus.push(to_submenu_sdi_strength());
+    defensive_tab_submenus.push(to_submenu_clatter_strength());
+    defensive_tab_submenus.push(to_submenu_ledge_state());
+    defensive_tab_submenus.push(to_submenu_ledge_delay());
+    defensive_tab_submenus.push(to_submenu_tech_state());
+    defensive_tab_submenus.push(to_submenu_miss_tech_state());
+    defensive_tab_submenus.push(to_submenu_shield_state());
+    defensive_tab_submenus.push(to_submenu_shield_tilt());
+    defensive_tab_submenus.push(to_submenu_crouch());
+    defensive_tab_submenus.push(to_submenu_stale_dodges());
+    defensive_tab_submenus.push(to_submenu_tech_hide());
+    let defensive_tab = Tab {
+        id: "defensive",
+        title: "Defensive Settings",
+        submenus: StatefulTable::with_items(
+            NX_SUBMENU_ROWS,
+            NX_SUBMENU_COLUMNS,
+            defensive_tab_submenus,
+        ),
     };
-    defensive_tab.add_submenu_with_toggles::<Direction>(
-        "Airdodge Direction".to_string(),
-        "air_dodge_dir".to_string(),
-        "Airdodge Direction: Direction to angle airdodges".to_string(),
-        false,
-        &(menu.air_dodge_dir.bits()),
-    );
-    defensive_tab.add_submenu_with_toggles::<Direction>(
-        "DI Direction".to_string(),
-        "di_state".to_string(),
-        "DI Direction: Direction to angle the directional influence during hitlag".to_string(),
-        false,
-        &(menu.di_state.bits()),
-    );
-    defensive_tab.add_submenu_with_toggles::<Direction>(
-        "SDI Direction".to_string(),
-        "sdi_state".to_string(),
-        "SDI Direction: Direction to angle the smash directional influence during hitlag"
-            .to_string(),
-        false,
-        &(menu.sdi_state.bits()),
-    );
-    defensive_tab.add_submenu_with_toggles::<SdiFrequency>(
-        "SDI Strength".to_string(),
-        "sdi_strength".to_string(),
-        "SDI Strength: Relative strength of the smash directional influence inputs".to_string(),
-        true,
-        &(menu.sdi_strength as u32),
-    );
-    defensive_tab.add_submenu_with_toggles::<ClatterFrequency>(
-        "Clatter Strength".to_string(),
-        "clatter_strength".to_string(),
-        "Clatter Strength: Configure how rapidly the CPU will mash out of grabs, buries, etc."
-            .to_string(),
-        true,
-        &(menu.clatter_strength as u32),
-    );
-    defensive_tab.add_submenu_with_toggles::<LedgeOption>(
-        "Ledge Options".to_string(),
-        "ledge_state".to_string(),
-        "Ledge Options: Actions to be taken when on the ledge".to_string(),
-        false,
-        &(menu.ledge_state.bits()),
-    );
-    defensive_tab.add_submenu_with_toggles::<LongDelay>(
-        "Ledge Delay".to_string(),
-        "ledge_delay".to_string(),
-        "Ledge Delay: How many frames to delay the ledge option".to_string(),
-        false,
-        &(menu.ledge_delay.bits()),
-    );
-    defensive_tab.add_submenu_with_toggles::<TechFlags>(
-        "Tech Options".to_string(),
-        "tech_state".to_string(),
-        "Tech Options: Actions to take when slammed into a hard surface".to_string(),
-        false,
-        &(menu.tech_state.bits()),
-    );
-    defensive_tab.add_submenu_with_toggles::<MissTechFlags>(
-        "Mistech Options".to_string(),
-        "miss_tech_state".to_string(),
-        "Mistech Options: Actions to take after missing a tech".to_string(),
-        false,
-        &(menu.miss_tech_state.bits()),
-    );
-    defensive_tab.add_submenu_with_toggles::<Shield>(
-        "Shield Toggles".to_string(),
-        "shield_state".to_string(),
-        "Shield Toggles: CPU Shield Behavior".to_string(),
-        true,
-        &(menu.shield_state as u32),
-    );
-    defensive_tab.add_submenu_with_toggles::<Direction>(
-        "Shield Tilt".to_string(),
-        "shield_tilt".to_string(),
-        "Shield Tilt: Direction to tilt the shield".to_string(),
-        false, // TODO: Should this be true?
-        &(menu.shield_tilt.bits()),
-    );
-
-    defensive_tab.add_submenu_with_toggles::<OnOff>(
-        "Crouch".to_string(),
-        "crouch".to_string(),
-        "Crouch: Have the CPU crouch when on the ground".to_string(),
-        true,
-        &(menu.crouch as u32),
-    );
-    defensive_tab.add_submenu_with_toggles::<OnOff>(
-        "Dodge Staling".to_string(),
-        "stale_dodges".to_string(),
-        "Dodge Staling: Controls whether the CPU's dodges will worsen with repetitive use\n(Note: This can setting can cause combo behavior not possible in the original game)"
-            .to_string(),
-        true,
-        &(menu.stale_dodges as u32),
-    );
-    defensive_tab.add_submenu_with_toggles::<OnOff>(
-        "Hide Tech Animations".to_string(),
-        "tech_hide".to_string(),
-        "Hide Tech Animations: Hides tech animations and effects after 7 frames to help with reacting to tech animation startup"
-            .to_string(),
-        true,
-        &(menu.tech_hide as u32),
-    );
     overall_menu.tabs.push(defensive_tab);
 
-    let mut save_state_tab = Tab {
-        tab_id: "save_state".to_string(),
-        tab_title: "Save States".to_string(),
-        tab_submenus: Vec::new(),
+    // Input Recording Tab
+    let mut input_recording_tab_submenus: Vec<SubMenu> = Vec::new();
+    input_recording_tab_submenus.push(to_submenu_recording_slot());
+    input_recording_tab_submenus.push(to_submenu_record_trigger());
+    input_recording_tab_submenus.push(to_submenu_recording_duration());
+    input_recording_tab_submenus.push(to_submenu_recording_crop());
+    input_recording_tab_submenus.push(to_submenu_playback_button_slots());
+    input_recording_tab_submenus.push(to_submenu_hitstun_playback());
+    input_recording_tab_submenus.push(to_submenu_save_state_playback());
+    input_recording_tab_submenus.push(to_submenu_playback_mash());
+    input_recording_tab_submenus.push(to_submenu_playback_loop());
+    let input_tab = Tab {
+        id: "input",
+        title: "Input Recording",
+        submenus: StatefulTable::with_items(
+            NX_SUBMENU_ROWS,
+            NX_SUBMENU_COLUMNS,
+            input_recording_tab_submenus,
+        ),
+    };
+    overall_menu.tabs.push(input_tab);
+
+    // Button Tab
+    let mut button_tab_submenus: Vec<SubMenu> = Vec::new();
+    button_tab_submenus.push(to_submenu_menu_open_start_press());
+    button_tab_submenus.push(to_submenu_save_state_save());
+    button_tab_submenus.push(to_submenu_save_state_load());
+    button_tab_submenus.push(to_submenu_input_record());
+    button_tab_submenus.push(to_submenu_input_playback());
+    let button_tab = Tab {
+        id: "button",
+        title: "Button Config",
+        submenus: StatefulTable::with_items(
+            NX_SUBMENU_ROWS,
+            NX_SUBMENU_COLUMNS,
+            button_tab_submenus,
+        ),
+    };
+    overall_menu.tabs.push(button_tab);
+
+    // Save State Tab
+    let mut save_state_tab_submenus: Vec<SubMenu> = Vec::new();
+    save_state_tab_submenus.push(to_submenu_save_state_mirroring());
+    save_state_tab_submenus.push(to_submenu_save_state_autoload());
+    save_state_tab_submenus.push(to_submenu_save_damage_cpu());
+    save_state_tab_submenus.push(to_submenu_save_damage_limits_cpu());
+    save_state_tab_submenus.push(to_submenu_save_damage_player());
+    save_state_tab_submenus.push(to_submenu_save_damage_limits_player());
+    save_state_tab_submenus.push(to_submenu_save_state_enable());
+    save_state_tab_submenus.push(to_submenu_save_state_slot());
+    save_state_tab_submenus.push(to_submenu_randomize_slots());
+    save_state_tab_submenus.push(to_submenu_character_item());
+    save_state_tab_submenus.push(to_submenu_buff_state());
+    let save_state_tab = Tab {
+        id: "save_state",
+        title: "Save States",
+        submenus: StatefulTable::with_items(
+            NX_SUBMENU_ROWS,
+            NX_SUBMENU_COLUMNS,
+            save_state_tab_submenus,
+        ),
     };
-    save_state_tab.add_submenu_with_toggles::<SaveStateMirroring>(
-        "Mirroring".to_string(),
-        "save_state_mirroring".to_string(),
-        "Mirroring: Flips save states in the left-right direction across the stage center"
-            .to_string(),
-        true,
-        &(menu.save_state_mirroring as u32),
-    );
-    save_state_tab.add_submenu_with_toggles::<OnOff>(
-        "Auto Save States".to_string(),
-        "save_state_autoload".to_string(),
-        "Auto Save States: Load save state when any fighter dies".to_string(),
-        true,
-        &(menu.save_state_autoload as u32),
-    );
-    save_state_tab.add_submenu_with_toggles::<SaveDamage>(
-        "Save Dmg (CPU)".to_string(),
-        "save_damage_cpu".to_string(),
-        "Save Damage: Should save states retain CPU damage".to_string(),
-        true,
-        &(menu.save_damage_cpu.bits()),
-    );
-    save_state_tab.add_submenu_with_slider::<DamagePercent>(
-        "Dmg Range (CPU)".to_string(),
-        "save_damage_limits_cpu".to_string(),
-        "Limits on random damage to apply to the CPU when loading a save state".to_string(),
-        &(menu.save_damage_limits_cpu.0 as u32),
-        &(menu.save_damage_limits_cpu.1 as u32),
-    );
-    save_state_tab.add_submenu_with_toggles::<SaveDamage>(
-        "Save Dmg (Player)".to_string(),
-        "save_damage_player".to_string(),
-        "Save Damage: Should save states retain player damage".to_string(),
-        true,
-        &(menu.save_damage_player.bits() as u32),
-    );
-    save_state_tab.add_submenu_with_slider::<DamagePercent>(
-        "Dmg Range (Player)".to_string(),
-        "save_damage_limits_player".to_string(),
-        "Limits on random damage to apply to the player when loading a save state".to_string(),
-        &(menu.save_damage_limits_player.0 as u32),
-        &(menu.save_damage_limits_player.1 as u32),
-    );
-    save_state_tab.add_submenu_with_toggles::<OnOff>(
-        "Enable Save States".to_string(),
-        "save_state_enable".to_string(),
-        "Save States: Enable save states! Save a state with Shield+Down Taunt, load it with Shield+Up Taunt.".to_string(),
-        true,
-        &(menu.save_state_enable as u32),
-    );
-    save_state_tab.add_submenu_with_toggles::<SaveStateSlot>(
-        "Save State Slot".to_string(),
-        "save_state_slot".to_string(),
-        "Save State Slot: Save and load states from different slots.".to_string(),
-        true,
-        &(menu.save_state_slot.bits() as u32),
-    );
-    save_state_tab.add_submenu_with_toggles::<SaveStateSlot>(
-        "Randomize Slots".to_string(),
-        "randomize_slots".to_string(),
-        "Randomize Slots: Slots to randomize when loading save state.".to_string(),
-        false,
-        &(menu.randomize_slots.bits() as u32),
-    );
-    save_state_tab.add_submenu_with_toggles::<CharacterItem>(
-        "Character Item".to_string(),
-        "character_item".to_string(),
-        "Character Item: The item to give to the player's fighter when loading a save state"
-            .to_string(),
-        true,
-        &(menu.character_item as u32),
-    );
-    save_state_tab.add_submenu_with_toggles::<BuffOption>(
-        "Buff Options".to_string(),
-        "buff_state".to_string(),
-        "Buff Options: Buff(s) to be applied to the respective fighters when loading a save state"
-            .to_string(),
-        false,
-        &(menu.buff_state.bits()),
-    );
-    save_state_tab.add_submenu_with_toggles::<PlaybackSlot>(
-        "Save State Playback".to_string(),
-        "save_state_playback".to_string(),
-        "Save State Playback: Choose which slots to playback input recording upon loading a save state".to_string(),
-        false,
-        &(menu.save_state_playback.bits() as u32),
-    );
     overall_menu.tabs.push(save_state_tab);
 
-    let mut misc_tab = Tab {
-        tab_id: "misc".to_string(),
-        tab_title: "Misc Settings".to_string(),
-        tab_submenus: Vec::new(),
+    // Miscellaneous Tab
+    let mut misc_tab_submenus: Vec<SubMenu> = Vec::new();
+    misc_tab_submenus.push(to_submenu_frame_advantage());
+    misc_tab_submenus.push(to_submenu_hitbox_vis());
+    misc_tab_submenus.push(to_submenu_input_display());
+    misc_tab_submenus.push(to_submenu_input_display_status());
+    misc_tab_submenus.push(to_submenu_input_delay());
+    misc_tab_submenus.push(to_submenu_stage_hazards());
+    misc_tab_submenus.push(to_submenu_hud());
+    misc_tab_submenus.push(to_submenu_update_policy());
+    let misc_tab = Tab {
+        id: "misc",
+        title: "Misc Settings",
+        submenus: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, misc_tab_submenus),
     };
-    misc_tab.add_submenu_with_toggles::<OnOff>(
-        "Frame Advantage".to_string(),
-        "frame_advantage".to_string(),
-        "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable".to_string(),
-        true,
-        &(menu.frame_advantage as u32),
-    );
-    misc_tab.add_submenu_with_toggles::<OnOff>(
-        "Hitbox Visualization".to_string(),
-        "hitbox_vis".to_string(),
-        "Hitbox Visualization: Display a visual representation for active hitboxes (hides other visual effects)".to_string(),
-        true,
-        &(menu.hitbox_vis as u32),
-    );
-    misc_tab.add_submenu_with_toggles::<InputDisplay>(
-        "Input Display".to_string(),
-        "input_display".to_string(),
-        "Input Display: Log inputs in a queue on the left of the screen".to_string(),
-        true,
-        &(menu.input_display as u32),
-    );
-    misc_tab.add_submenu_with_toggles::<OnOff>(
-        "Input Display Status".to_string(),
-        "input_display_status".to_string(),
-        "Input Display Status: Group input logs by status in which they occurred".to_string(),
-        true,
-        &(menu.input_display_status as u32),
-    );
-    misc_tab.add_submenu_with_toggles::<Delay>(
-        "Input Delay".to_string(),
-        "input_delay".to_string(),
-        "Input Delay: Frames to delay player inputs by".to_string(),
-        true,
-        &(menu.input_delay.bits()),
-    );
-    misc_tab.add_submenu_with_toggles::<OnOff>(
-        "Stage Hazards".to_string(),
-        "stage_hazards".to_string(),
-        "Stage Hazards: Turn stage hazards on/off".to_string(),
-        true,
-        &(menu.stage_hazards as u32),
-    );
-    misc_tab.add_submenu_with_toggles::<OnOff>(
-        "HUD".to_string(),
-        "hud".to_string(),
-        "HUD: Show/hide elements of the UI".to_string(),
-        true,
-        &(menu.hud as u32),
-    );
-    misc_tab.add_submenu_with_toggles::<UpdatePolicy>(
-        "Auto-Update".to_string(),
-        "update_policy".to_string(),
-        "Auto-Update: What type of Training Modpack updates to automatically apply. (Console Only!)"
-            .to_string(),
-        true,
-        &(menu.update_policy as u32),
-    );
     overall_menu.tabs.push(misc_tab);
 
-    let mut input_tab = Tab {
-        tab_id: "input".to_string(),
-        tab_title: "Input Recording".to_string(),
-        tab_submenus: Vec::new(),
-    };
-    input_tab.add_submenu_with_toggles::<RecordSlot>(
-        "Recording Slot".to_string(),
-        "recording_slot".to_string(),
-        "Recording Slot: Choose which slot to record into".to_string(),
-        true,
-        &(menu.recording_slot as u32),
-    );
-    input_tab.add_submenu_with_toggles::<RecordTrigger>(
-        "Recording Trigger".to_string(),
-        "record_trigger".to_string(),
-        format!("Recording Trigger: Whether to begin recording via button combination ({}) or upon loading a Save State", menu.input_record.combination_string()),
-        false,
-        &(menu.record_trigger.bits() as u32),
-    );
-    input_tab.add_submenu_with_toggles::<RecordingDuration>(
-        "Recording Duration".to_string(),
-        "recording_duration".to_string(),
-        "Recording Duration: Number of frames to record for in the current slot".to_string(),
-        true,
-        &(menu.recording_duration as u32),
-    );
-    input_tab.add_submenu_with_toggles::<OnOff>(
-        "Recording Crop".to_string(),
-        "recording_crop".to_string(),
-        "Recording Crop: Remove neutral input frames at the end of your recording".to_string(),
-        true,
-        &(menu.recording_crop as u32),
-    );
-    input_tab.add_submenu_with_toggles::<PlaybackSlot>(
-        "Playback Button Slots".to_string(),
-        "playback_button_slots".to_string(),
-        format!("Playback Button Slots: Choose which slots to playback input recording upon pressing button combination ({})", menu.input_playback.combination_string()),
-        false,
-        &(menu.playback_button_slots.bits() as u32),
-    );
-    input_tab.add_submenu_with_toggles::<HitstunPlayback>(
-        "Playback Hitstun Timing".to_string(),
-        "hitstun_playback".to_string(),
-        "Playback Hitstun Timing: When to begin playing back inputs when a hitstun mash trigger occurs".to_string(),
-        true,
-        &(menu.hitstun_playback as u32),
-    );
-    input_tab.add_submenu_with_toggles::<OnOff>(
-        "Playback Mash Interrupt".to_string(),
-        "playback_mash".to_string(),
-        "Playback Mash Interrupt: End input playback when a mash trigger occurs".to_string(),
-        true,
-        &(menu.playback_mash as u32),
-    );
-    input_tab.add_submenu_with_toggles::<OnOff>(
-        "Playback Loop".to_string(),
-        "playback_loop".to_string(),
-        "Playback Loop: Repeat triggered input playbacks indefinitely".to_string(),
-        true,
-        &(menu.playback_loop as u32),
-    );
-    overall_menu.tabs.push(input_tab);
+    // Ensure that a tab is always selected
+    if overall_menu.tabs.get_selected().is_none() {
+        overall_menu.tabs.state.select(Some(0));
+    }
 
     overall_menu
 }
diff --git a/training_mod_consts/src/options.rs b/training_mod_consts/src/options.rs
index cfaf8e3..ce198ec 100644
--- a/training_mod_consts/src/options.rs
+++ b/training_mod_consts/src/options.rs
@@ -1,94 +1,64 @@
+use byteflags::*;
 use core::f64::consts::PI;
 use serde::{Deserialize, Serialize};
-use serde_repr::{Deserialize_repr, Serialize_repr};
 #[cfg(feature = "smash")]
 use smash::lib::lua_const::*;
-use std::fmt;
-use strum::IntoEnumIterator;
-use strum_macros::EnumIter;
 
-const fn num_bits<T>() -> u32 {
-    (std::mem::size_of::<T>() * 8) as u32
-}
-
-fn log_2(x: u32) -> u32 {
-    if x == 0 {
-        0
-    } else {
-        num_bits::<u32>() - x.leading_zeros() - 1
+#[macro_export]
+macro_rules! impl_toggletrait {
+    (
+        $e:ty,
+        $title:literal,
+        $id:literal,
+        $help_text:literal,
+        $single:literal,
+        $max:expr,
+    ) => {
+        paste! {
+            fn [<to_submenu_ $id>]<'a>() -> SubMenu<'a> {
+                let submenu_type = if $single { SubMenuType::ToggleSingle } else { SubMenuType::ToggleMultiple };
+                let value = 0;
+                let max: u8 = $max;
+                let toggles_vec: Vec<Toggle> = <$e>::ALL_NAMES
+                    .iter()
+                    .map(|title| Toggle { title, value, max })
+                    .collect();
+                SubMenu {
+                    title: $title,
+                    id: $id,
+                    help_text: $help_text,
+                    submenu_type: submenu_type,
+                    toggles: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, toggles_vec),
+                    slider: None
+                }
+            }
+        }
     }
 }
 
-pub trait ToggleTrait {
-    fn to_toggle_vals() -> Vec<u32>;
-    fn to_toggle_strings() -> Vec<String>;
-}
-
-pub trait SliderTrait {
-    fn get_limits() -> (u32, u32);
-}
-
-// bitflag helper function macro
 #[macro_export]
-macro_rules! extra_bitflag_impls {
-    ($e:ty) => {
-        impl $e {
-            pub fn to_vec(&self) -> Vec::<$e> {
-                let mut vec = Vec::<$e>::new();
-                let mut field = <$e>::from_bits_truncate(self.bits);
-                while !field.is_empty() {
-                    let flag = <$e>::from_bits(1u32 << field.bits.trailing_zeros()).unwrap();
-                    field -= flag;
-                    vec.push(flag);
+macro_rules! impl_slidertrait {
+    (
+        $e:ty,
+        $title:literal,
+        $id:literal,
+        $help_text:literal,
+    ) => {
+        paste! {
+            fn [<to_submenu_ $id>]<'a>() -> SubMenu<'a> {
+                let slider = StatefulSlider {
+                    lower: 0,
+                    upper: 150,
+                    ..StatefulSlider::new()
+                };
+                SubMenu {
+                    title: $title,
+                    id: $id,
+                    help_text: $help_text,
+                    submenu_type: SubMenuType::Slider,
+                    toggles: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, Vec::new()),
+                    slider: Some(slider)
                 }
-                return vec;
-            }
-
-            pub fn to_index(&self) -> u32 {
-                if self.bits == 0 {
-                    0
-                } else {
-                    self.bits.trailing_zeros()
-                }
-            }
-
-            pub fn get_random(&self) -> $e {
-                let options = self.to_vec();
-                match options.len() {
-                    0 => {
-                        return <$e>::empty();
-                    }
-                    1 => {
-                        return options[0];
-                    }
-                    _ => {
-                        return *random_option(&options);
-                    }
-                }
-            }
-
-            pub fn combination_string(&self) -> String {
-                // Avoid infinite recursion lol
-                if self.to_vec().len() <= 1 {
-                    return "".to_string();
-                }
-
-                self.to_vec()
-                    .iter()
-                    .map(|item| item.to_string())
-                    .intersperse(" + ".to_owned())
-                    .collect::<String>()
-            }
-        }
-        impl ToggleTrait for $e {
-            fn to_toggle_vals() -> Vec<u32> {
-                let all_options = <$e>::all().to_vec();
-                all_options.iter().map(|i| i.bits() as u32).collect()
-            }
-
-            fn to_toggle_strings() -> Vec<String> {
-                let all_options = <$e>::all().to_vec();
-                all_options.iter().map(|i| i.to_string()).collect()
             }
         }
     }
@@ -127,19 +97,19 @@ pub fn random_option<T>(arg: &[T]) -> &T {
 */
 
 // DI / Left stick
-bitflags! {
-    pub struct Direction : u32 {
-        const OUT = 0x1;
-        const UP_OUT = 0x2;
-        const UP = 0x4;
-        const UP_IN = 0x8;
-        const IN = 0x10;
-        const DOWN_IN = 0x20;
-        const DOWN = 0x40;
-        const DOWN_OUT = 0x80;
-        const NEUTRAL = 0x100;
-        const LEFT = 0x200;
-        const RIGHT = 0x400;
+byteflags! {
+    pub struct Direction {
+        pub OUT = "Out",
+        pub UP_OUT = "Up Out",
+        pub UP = "Up",
+        pub UP_IN = "Up In",
+        pub IN = "In",
+        pub DOWN_IN = "Down In",
+        pub DOWN = "Down",
+        pub DOWN_OUT = "Down Out",
+        pub NEUTRAL = "Neutral",
+        pub LEFT = "Left",
+        pub RIGHT = "Right",
     }
 }
 
@@ -147,71 +117,47 @@ impl Direction {
     pub fn into_angle(self) -> Option<f64> {
         let index = self.into_index();
 
-        if index == 0 {
+        if index == 0.0 {
             None
         } else {
-            Some((index as i32 - 1) as f64 * PI / 4.0)
+            Some((index - 1.0) * PI / 4.0)
         }
     }
-    fn into_index(self) -> i32 {
+    fn into_index(self) -> f64 {
+        if self == Direction::empty() {
+            return 0.0;
+        };
         match self {
-            Direction::OUT => 1,
-            Direction::UP_OUT => 2,
-            Direction::UP => 3,
-            Direction::UP_IN => 4,
-            Direction::IN => 5,
-            Direction::DOWN_IN => 6,
-            Direction::DOWN => 7,
-            Direction::DOWN_OUT => 8,
-            Direction::NEUTRAL => 0,
-            Direction::LEFT => 5,
-            Direction::RIGHT => 1,
-            _ => 0,
+            Direction::OUT => 1.0,
+            Direction::UP_OUT => 2.0,
+            Direction::UP => 3.0,
+            Direction::UP_IN => 4.0,
+            Direction::IN => 5.0,
+            Direction::DOWN_IN => 6.0,
+            Direction::DOWN => 7.0,
+            Direction::DOWN_OUT => 8.0,
+            Direction::NEUTRAL => 0.0,
+            Direction::LEFT => 5.0,
+            Direction::RIGHT => 1.0,
+            _ => panic!("Invalid value in Direction::into_index: {}", self),
         }
     }
 }
 
-impl fmt::Display for Direction {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                Direction::OUT => "Away",
-                Direction::UP_OUT => "Up and Away",
-                Direction::UP => "Up",
-                Direction::UP_IN => "Up and In",
-                Direction::IN => "In",
-                Direction::DOWN_IN => "Down and In",
-                Direction::DOWN => "Down",
-                Direction::DOWN_OUT => "Down and Away",
-                Direction::NEUTRAL => "Neutral",
-                Direction::LEFT => "Left",
-                Direction::RIGHT => "Right",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {Direction}
-impl_serde_for_bitflags!(Direction);
-
 // Ledge Option
-bitflags! {
-    pub struct LedgeOption : u32
+byteflags! {
+    pub struct LedgeOption
     {
-        const NEUTRAL = 0x1;
-        const ROLL = 0x2;
-        const JUMP = 0x4;
-        const ATTACK = 0x8;
-        const WAIT = 0x10;
-        const PLAYBACK_1 = 0x20;
-        const PLAYBACK_2 = 0x40;
-        const PLAYBACK_3 = 0x80;
-        const PLAYBACK_4 = 0x100;
-        const PLAYBACK_5 = 0x200;
+        pub NEUTRAL = "Neutral Getup",
+        pub ROLL = "Roll",
+        pub JUMP = "Jump",
+        pub ATTACK = "Getup Attack",
+        pub WAIT = "Wait",
+        pub PLAYBACK_1 = "Playback Slot 1",
+        pub PLAYBACK_2 = "Playback Slot 2",
+        pub PLAYBACK_3 = "Playback Slot 3",
+        pub PLAYBACK_4 = "Playback Slot 4",
+        pub PLAYBACK_5 = "Playback Slot 5",
     }
 }
 
@@ -262,248 +208,110 @@ impl LedgeOption {
 
     pub const fn default() -> LedgeOption {
         // Neutral,Roll,Jump,Attack (everything except wait)
-        LedgeOption::NEUTRAL
-            .union(LedgeOption::ROLL)
-            .union(LedgeOption::JUMP)
-            .union(LedgeOption::ATTACK)
+        LedgeOption {
+            NEUTRAL: 1,
+            ROLL: 1,
+            JUMP: 1,
+            ATTACK: 1,
+            ..LedgeOption::empty()
+        }
     }
 }
 
-impl fmt::Display for LedgeOption {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                LedgeOption::NEUTRAL => "Neutral Getup",
-                LedgeOption::ROLL => "Roll",
-                LedgeOption::JUMP => "Jump",
-                LedgeOption::ATTACK => "Getup Attack",
-                LedgeOption::WAIT => "Wait",
-                LedgeOption::PLAYBACK_1 => "Playback Slot 1",
-                LedgeOption::PLAYBACK_2 => "Playback Slot 2",
-                LedgeOption::PLAYBACK_3 => "Playback Slot 3",
-                LedgeOption::PLAYBACK_4 => "Playback Slot 4",
-                LedgeOption::PLAYBACK_5 => "Playback Slot 5",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {LedgeOption}
-impl_serde_for_bitflags!(LedgeOption);
-
 // Tech options
-bitflags! {
-    pub struct TechFlags : u32 {
-        const NO_TECH = 0x1;
-        const ROLL_F = 0x2;
-        const ROLL_B = 0x4;
-        const IN_PLACE = 0x8;
+byteflags! {
+    pub struct TechFlags {
+        pub NO_TECH = "No Tech",
+        pub ROLL_F = "Roll Forwards",
+        pub ROLL_B = "Roll Backwards",
+        pub IN_PLACE = "Tech In Place",
     }
 }
 
-impl fmt::Display for TechFlags {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                TechFlags::NO_TECH => "No Tech",
-                TechFlags::ROLL_F => "Roll Forwards",
-                TechFlags::ROLL_B => "Roll Backwards",
-                TechFlags::IN_PLACE => "Tech In Place",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {TechFlags}
-impl_serde_for_bitflags!(TechFlags);
-
 // Missed Tech Options
-bitflags! {
-    pub struct MissTechFlags : u32 {
-        const GETUP = 0x1;
-        const ATTACK = 0x2;
-        const ROLL_F = 0x4;
-        const ROLL_B = 0x8;
+byteflags! {
+    pub struct MissTechFlags {
+        pub GETUP = "Neutral Getup",
+        pub ATTACK = "Getup Attack",
+        pub ROLL_F = "Roll Forwards",
+        pub ROLL_B = "Roll Backwards",
     }
 }
 
-impl fmt::Display for MissTechFlags {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                MissTechFlags::GETUP => "Neutral Getup",
-                MissTechFlags::ATTACK => "Getup Attack",
-                MissTechFlags::ROLL_F => "Roll Forwards",
-                MissTechFlags::ROLL_B => "Roll Backwards",
-                _ => combination_string.as_str(),
-            }
-        )
+byteflags! {
+    pub struct Shield {
+        pub NONE = "None",
+        pub INFINITE = "Infinite",
+        pub HOLD = "Hold",
+        pub CONSTANT = "Constant",
     }
 }
 
-extra_bitflag_impls! {MissTechFlags}
-impl_serde_for_bitflags!(MissTechFlags);
-
-/// Shield States
-#[repr(i32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum Shield {
-    None = 0x0,
-    Infinite = 0x1,
-    Hold = 0x2,
-    Constant = 0x4,
-}
-
-impl fmt::Display for Shield {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                Shield::None => "None",
-                Shield::Infinite => "Infinite",
-                Shield::Hold => "Hold",
-                Shield::Constant => "Constant",
-            }
-        )
+byteflags! {
+    pub struct SaveStateMirroring {
+        pub NONE = "None",
+        pub ALTERNATE = "Alternate",
+        pub RANDOM = "Random",
     }
 }
 
-impl ToggleTrait for Shield {
-    fn to_toggle_vals() -> Vec<u32> {
-        Shield::iter().map(|i| i as u32).collect()
+byteflags! {
+    pub struct OnOff {
+        pub ON = "On",
+        pub OFF = "Off",
     }
-    fn to_toggle_strings() -> Vec<String> {
-        Shield::iter().map(|i| i.to_string()).collect()
-    }
-}
-
-// Save State Mirroring
-#[repr(i32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum SaveStateMirroring {
-    None = 0x0,
-    Alternate = 0x1,
-    Random = 0x2,
-}
-
-impl fmt::Display for SaveStateMirroring {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                SaveStateMirroring::None => "None",
-                SaveStateMirroring::Alternate => "Alternate",
-                SaveStateMirroring::Random => "Random",
-            }
-        )
-    }
-}
-
-impl ToggleTrait for SaveStateMirroring {
-    fn to_toggle_vals() -> Vec<u32> {
-        SaveStateMirroring::iter().map(|i| i as u32).collect()
-    }
-
-    fn to_toggle_strings() -> Vec<String> {
-        SaveStateMirroring::iter().map(|i| i.to_string()).collect()
-    }
-}
-
-#[repr(i32)]
-#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)]
-pub enum OnOff {
-    Off = 0,
-    On = 1,
 }
 
 impl OnOff {
     pub fn from_val(val: u32) -> Option<Self> {
         match val {
-            1 => Some(OnOff::On),
-            0 => Some(OnOff::Off),
+            1 => Some(OnOff::ON),
+            0 => Some(OnOff::OFF),
             _ => None,
         }
     }
 
     pub fn as_bool(self) -> bool {
         match self {
-            OnOff::Off => false,
-            OnOff::On => true,
+            OnOff::OFF => false,
+            OnOff::ON => true,
+            _ => panic!("Invalid value in OnOff::as_bool: {}", self),
         }
     }
 }
 
-impl fmt::Display for OnOff {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                OnOff::Off => "Off",
-                OnOff::On => "On",
-            }
-        )
-    }
-}
-
-impl ToggleTrait for OnOff {
-    fn to_toggle_vals() -> Vec<u32> {
-        vec![0, 1]
-    }
-    fn to_toggle_strings() -> Vec<String> {
-        vec!["Off".to_string(), "On".to_string()]
-    }
-}
-
-bitflags! {
-    pub struct Action : u32 {
-        const AIR_DODGE = 0x1;
-        const JUMP = 0x2;
-        const SHIELD = 0x4;
-        const SPOT_DODGE = 0x8;
-        const ROLL_F = 0x10;
-        const ROLL_B = 0x20;
-        const NAIR = 0x40;
-        const FAIR = 0x80;
-        const BAIR = 0x100;
-        const UAIR = 0x200;
-        const DAIR = 0x400;
-        const NEUTRAL_B = 0x800;
-        const SIDE_B = 0x1000;
-        const UP_B = 0x2000;
-        const DOWN_B = 0x4000;
-        const F_SMASH = 0x8000;
-        const U_SMASH = 0x10000;
-        const D_SMASH = 0x20000;
-        const JAB = 0x40000;
-        const F_TILT = 0x80000;
-        const U_TILT  = 0x0010_0000;
-        const D_TILT  = 0x0020_0000;
-        const GRAB = 0x0040_0000;
-        const DASH = 0x0080_0000;
-        const DASH_ATTACK = 0x0100_0000;
-        const PLAYBACK_1 = 0x0200_0000;
-        const PLAYBACK_2 = 0x0400_0000;
-        const PLAYBACK_3 = 0x0800_0000;
-        const PLAYBACK_4 = 0x1000_0000;
-        const PLAYBACK_5 = 0x2000_0000;
+byteflags! {
+    pub struct Action {
+        pub AIR_DODGE = "Air Dodge",
+        pub JUMP = "Jump",
+        pub SHIELD = "Shield",
+        pub SPOT_DODGE = "Spot Dodge",
+        pub ROLL_F = "Roll Forwards",
+        pub ROLL_B = "Roll Backwards",
+        pub NAIR = "Neutral Air",
+        pub FAIR = "Forward Air",
+        pub BAIR = "Back Air",
+        pub UAIR = "Up Air",
+        pub DAIR = "Down Air",
+        pub NEUTRAL_B = "Neutral Special",
+        pub SIDE_B = "Side Special",
+        pub UP_B = "Up Special",
+        pub DOWN_B = "Down Special",
+        pub F_SMASH = "Forward Smash",
+        pub U_SMASH = "Up Smash",
+        pub D_SMASH = "Down Smash",
+        pub JAB = "Jab",
+        pub F_TILT = "Forward Tilt",
+        pub U_TILT  = "Up Tilt",
+        pub D_TILT  = "Down Tilt",
+        pub GRAB = "Grab",
+        pub DASH = "Dash",
+        pub DASH_ATTACK = "Dash Attack",
+        pub PLAYBACK_1 = "Playback Slot 1",
+        pub PLAYBACK_2 = "Playback Slot 2",
+        pub PLAYBACK_3 = "Playback Slot 3",
+        pub PLAYBACK_4 = "Playback Slot 4",
+        pub PLAYBACK_5 = "Playback Slot 5",
     }
 }
 
@@ -547,196 +355,269 @@ impl Action {
         }
     }
 }
-
-impl fmt::Display for Action {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                Action::AIR_DODGE => "Airdodge",
-                Action::JUMP => "Jump",
-                Action::SHIELD => "Shield",
-                Action::SPOT_DODGE => "Spotdodge",
-                Action::ROLL_F => "Roll Forwards",
-                Action::ROLL_B => "Roll Backwards",
-                Action::NAIR => "Neutral Aerial",
-                Action::FAIR => "Forward Aerial",
-                Action::BAIR => "Backward Aerial",
-                Action::UAIR => "Up Aerial",
-                Action::DAIR => "Down Aerial",
-                Action::NEUTRAL_B => "Neutral Special",
-                Action::SIDE_B => "Side Special",
-                Action::UP_B => "Up Special",
-                Action::DOWN_B => "Down Special",
-                Action::F_SMASH => "Forward Smash",
-                Action::U_SMASH => "Up Smash",
-                Action::D_SMASH => "Down Smash",
-                Action::JAB => "Jab",
-                Action::F_TILT => "Forward Tilt",
-                Action::U_TILT => "Up Tilt",
-                Action::D_TILT => "Down Tilt",
-                Action::GRAB => "Grab",
-                Action::DASH => "Dash",
-                Action::DASH_ATTACK => "Dash Attack",
-                Action::PLAYBACK_1 => "Playback Slot 1",
-                Action::PLAYBACK_2 => "Playback Slot 2",
-                Action::PLAYBACK_3 => "Playback Slot 3",
-                Action::PLAYBACK_4 => "Playback Slot 4",
-                Action::PLAYBACK_5 => "Playback Slot 5",
-                _ => combination_string.as_str(),
-            }
-        )
+byteflags! {
+    pub struct AttackAngle {
+        pub NEUTRAL = "Neutral",
+        pub UP = "Up",
+        pub DOWN = "Down",
     }
 }
 
-extra_bitflag_impls! {Action}
-impl_serde_for_bitflags!(Action);
-
-bitflags! {
-    pub struct AttackAngle : u32 {
-        const NEUTRAL = 0x1;
-        const UP = 0x2;
-        const DOWN = 0x4;
-    }
-}
-
-impl fmt::Display for AttackAngle {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                AttackAngle::NEUTRAL => "Neutral",
-                AttackAngle::UP => "Up",
-                AttackAngle::DOWN => "Down",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {AttackAngle}
-impl_serde_for_bitflags!(AttackAngle);
-
-bitflags! {
-    pub struct Delay : u32 {
-        const D0 = 0x1;
-        const D1 = 0x2;
-        const D2 = 0x4;
-        const D3 = 0x8;
-        const D4 = 0x10;
-        const D5 = 0x20;
-        const D6 = 0x40;
-        const D7 = 0x80;
-        const D8 = 0x100;
-        const D9 = 0x200;
-        const D10 = 0x400;
-        const D11 = 0x800;
-        const D12 = 0x1000;
-        const D13 = 0x2000;
-        const D14 = 0x4000;
-        const D15 = 0x8000;
-        const D16 = 0x10000;
-        const D17 = 0x20000;
-        const D18 = 0x40000;
-        const D19 = 0x80000;
-        const D20 = 0x0010_0000;
-        const D21 = 0x0020_0000;
-        const D22 = 0x0040_0000;
-        const D23 = 0x0080_0000;
-        const D24 = 0x0100_0000;
-        const D25 = 0x0200_0000;
-        const D26 = 0x0400_0000;
-        const D27 = 0x0800_0000;
-        const D28 = 0x1000_0000;
-        const D29 = 0x2000_0000;
-        const D30 = 0x4000_0000;
+byteflags! {
+    pub struct Delay {
+        pub D0 = "0",
+        pub D1 = "1",
+        pub D2 = "2",
+        pub D3 = "3",
+        pub D4 = "4",
+        pub D5 = "5",
+        pub D6 = "6",
+        pub D7 = "7",
+        pub D8 = "8",
+        pub D9 = "9",
+        pub D10 = "10",
+        pub D11 = "11",
+        pub D12 = "12",
+        pub D13 = "13",
+        pub D14 = "14",
+        pub D15 = "15",
+        pub D16 = "16",
+        pub D17 = "17",
+        pub D18 = "18",
+        pub D19 = "19",
+        pub D20 = "20",
+        pub D21 = "21",
+        pub D22 = "22",
+        pub D23 = "23",
+        pub D24 = "24",
+        pub D25 = "25",
+        pub D26 = "26",
+        pub D27 = "27",
+        pub D28 = "28",
+        pub D29 = "29",
+        pub D30 = "30",
     }
 }
 
 impl Delay {
     pub fn into_delay(&self) -> u32 {
-        self.to_index()
-    }
-}
-
-// Throw Option
-bitflags! {
-    pub struct ThrowOption : u32
-    {
-        const NONE = 0x1;
-        const FORWARD = 0x2;
-        const BACKWARD = 0x4;
-        const UP = 0x8;
-        const DOWN = 0x10;
-    }
-}
-
-impl ThrowOption {
-    pub fn into_cmd(self) -> Option<i32> {
-        #[cfg(feature = "smash")]
-        {
-            Some(match self {
-                ThrowOption::NONE => 0,
-                ThrowOption::FORWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_F,
-                ThrowOption::BACKWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_B,
-                ThrowOption::UP => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_HI,
-                ThrowOption::DOWN => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_LW,
-                _ => return None,
-            })
+        if *self == Delay::empty() {
+            return 0;
+        };
+        match *self {
+            Delay::D0 => 0,
+            Delay::D1 => 1,
+            Delay::D2 => 2,
+            Delay::D3 => 3,
+            Delay::D4 => 4,
+            Delay::D5 => 5,
+            Delay::D6 => 6,
+            Delay::D7 => 7,
+            Delay::D8 => 8,
+            Delay::D9 => 9,
+            Delay::D10 => 10,
+            Delay::D11 => 11,
+            Delay::D12 => 12,
+            Delay::D13 => 13,
+            Delay::D14 => 14,
+            Delay::D15 => 15,
+            Delay::D16 => 16,
+            Delay::D17 => 17,
+            Delay::D18 => 18,
+            Delay::D19 => 19,
+            Delay::D20 => 20,
+            Delay::D21 => 21,
+            Delay::D22 => 22,
+            Delay::D23 => 23,
+            Delay::D24 => 24,
+            Delay::D25 => 25,
+            Delay::D26 => 26,
+            Delay::D27 => 27,
+            Delay::D28 => 28,
+            Delay::D29 => 29,
+            Delay::D30 => 30,
+            _ => panic!("Invalid value in Delay::into_delay: {}", self),
         }
-
-        #[cfg(not(feature = "smash"))]
-        None
     }
 }
 
-impl fmt::Display for ThrowOption {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                ThrowOption::NONE => "None",
-                ThrowOption::FORWARD => "Forward Throw",
-                ThrowOption::BACKWARD => "Back Throw",
-                ThrowOption::UP => "Up Throw",
-                ThrowOption::DOWN => "Down Throw",
-                _ => combination_string.as_str(),
-            }
-        )
+byteflags! {
+    pub struct MedDelay {
+        pub D0 = "0",
+        pub D5 = "5",
+        pub D10 = "10",
+        pub D15 = "15",
+        pub D20 = "20",
+        pub D25 = "25",
+        pub D30 = "30",
+        pub D35 = "35",
+        pub D40 = "40",
+        pub D45 = "45",
+        pub D50 = "50",
+        pub D55 = "55",
+        pub D60 = "60",
+        pub D65 = "65",
+        pub D70 = "70",
+        pub D75 = "75",
+        pub D80 = "80",
+        pub D85 = "85",
+        pub D90 = "90",
+        pub D95 = "95",
+        pub D100 = "100",
+        pub D105 = "105",
+        pub D110 = "110",
+        pub D115 = "115",
+        pub D120 = "120",
+        pub D125 = "125",
+        pub D130 = "130",
+        pub D135 = "135",
+        pub D140 = "140",
+        pub D145 = "145",
+        pub D150 = "150",
     }
 }
 
-extra_bitflag_impls! {ThrowOption}
-impl_serde_for_bitflags!(ThrowOption);
+impl MedDelay {
+    pub fn into_meddelay(&self) -> u32 {
+        if *self == MedDelay::empty() {
+            return 0;
+        };
+        match *self {
+            MedDelay::D0 => 0,
+            MedDelay::D5 => 5,
+            MedDelay::D10 => 10,
+            MedDelay::D15 => 15,
+            MedDelay::D20 => 20,
+            MedDelay::D25 => 25,
+            MedDelay::D30 => 30,
+            MedDelay::D35 => 35,
+            MedDelay::D40 => 40,
+            MedDelay::D45 => 45,
+            MedDelay::D50 => 50,
+            MedDelay::D55 => 55,
+            MedDelay::D60 => 60,
+            MedDelay::D65 => 65,
+            MedDelay::D70 => 70,
+            MedDelay::D75 => 75,
+            MedDelay::D80 => 80,
+            MedDelay::D85 => 85,
+            MedDelay::D90 => 90,
+            MedDelay::D95 => 95,
+            MedDelay::D100 => 100,
+            MedDelay::D105 => 105,
+            MedDelay::D110 => 110,
+            MedDelay::D115 => 115,
+            MedDelay::D120 => 120,
+            MedDelay::D125 => 125,
+            MedDelay::D130 => 130,
+            MedDelay::D135 => 135,
+            MedDelay::D140 => 140,
+            MedDelay::D145 => 145,
+            MedDelay::D150 => 150,
+            _ => panic!("Invalid value in MedDelay::into_meddelay: {}", self),
+        }
+    }
+}
 
-// Buff Option
-bitflags! {
-    pub struct BuffOption : u32
+byteflags! {
+    pub struct LongDelay {
+        pub D0 = "0",
+        pub D10 = "10",
+        pub D20 = "20",
+        pub D30 = "30",
+        pub D40 = "40",
+        pub D50 = "50",
+        pub D60 = "60",
+        pub D70 = "70",
+        pub D80 = "80",
+        pub D90 = "90",
+        pub D100 = "100",
+        pub D110 = "110",
+        pub D120 = "120",
+        pub D130 = "130",
+        pub D140 = "140",
+        pub D150 = "150",
+        pub D160 = "160",
+        pub D170 = "170",
+        pub D180 = "180",
+        pub D190 = "190",
+        pub D200 = "200",
+        pub D210 = "210",
+        pub D220 = "220",
+        pub D230 = "230",
+        pub D240 = "240",
+        pub D250 = "250",
+        pub D260 = "260",
+        pub D270 = "270",
+        pub D280 = "280",
+        pub D290 = "290",
+        pub D300 = "300",
+    }
+}
+
+impl LongDelay {
+    pub fn into_longdelay(&self) -> u32 {
+        if *self == LongDelay::empty() {
+            return 0;
+        };
+        match *self {
+            LongDelay::D0 => 0,
+            LongDelay::D10 => 10,
+            LongDelay::D20 => 20,
+            LongDelay::D30 => 30,
+            LongDelay::D40 => 40,
+            LongDelay::D50 => 50,
+            LongDelay::D60 => 60,
+            LongDelay::D70 => 70,
+            LongDelay::D80 => 80,
+            LongDelay::D90 => 90,
+            LongDelay::D100 => 100,
+            LongDelay::D110 => 110,
+            LongDelay::D120 => 120,
+            LongDelay::D130 => 130,
+            LongDelay::D140 => 140,
+            LongDelay::D150 => 150,
+            LongDelay::D160 => 160,
+            LongDelay::D170 => 170,
+            LongDelay::D180 => 180,
+            LongDelay::D190 => 190,
+            LongDelay::D200 => 200,
+            LongDelay::D210 => 210,
+            LongDelay::D220 => 220,
+            LongDelay::D230 => 230,
+            LongDelay::D240 => 240,
+            LongDelay::D250 => 250,
+            LongDelay::D260 => 260,
+            LongDelay::D270 => 270,
+            LongDelay::D280 => 280,
+            LongDelay::D290 => 290,
+            LongDelay::D300 => 300,
+            _ => panic!("Invalid value in LongDelay::into_longdelay: {}", self),
+        }
+    }
+}
+
+byteflags! {
+    pub struct BuffOption
     {
-        const ACCELERATLE = 0x1;
-        const OOMPH = 0x2;
-        const PSYCHE = 0x4;
-        const BOUNCE = 0x8;
-        const ARSENE = 0x10;
-        const BREATHING = 0x20;
-        const LIMIT = 0x40;
-        const KO = 0x80;
-        const WING = 0x100;
-        const MONAD_JUMP = 0x200;
-        const MONAD_SPEED = 0x400;
-        const MONAD_SHIELD = 0x800;
-        const MONAD_BUSTER = 0x1000;
-        const MONAD_SMASH = 0x2000;
-        const POWER_DRAGON = 0x4000;
-        const WAFT_MINI = 0x8000;
-        const WAFT_HALF = 0x10000;
-        const WAFT_FULL = 0x20000;
+        pub ACCELERATLE = "Acceleratle",
+        pub OOMPH = "Oomph",
+        pub PSYCHE = "Psyche Up",
+        pub BOUNCE = "Bounce",
+        pub ARSENE = "Arsene",
+        pub BREATHING = "Deep Breathing",
+        pub LIMIT = "Limit",
+        pub KO = "KO Punch",
+        pub WING = "1-Winged Angel",
+        pub MONAD_JUMP = "Jump",
+        pub MONAD_SPEED = "Speed",
+        pub MONAD_SHIELD = "Shield",
+        pub MONAD_BUSTER = "Buster",
+        pub MONAD_SMASH = "Smash",
+        pub POWER_DRAGON = "Power Dragon",
+        pub WAFT_MINI = "Mini Waft",
+        pub WAFT_HALF = "Half Waft",
+        pub WAFT_FULL = "Full Waft",
     }
 }
 
@@ -773,1005 +654,392 @@ impl BuffOption {
 
     pub fn hero_buffs(self) -> BuffOption {
         // Return a struct with only Hero's selected buffs
-        let hero_buffs_bitflags = BuffOption::ACCELERATLE
+        let hero_buffs_byteflags = BuffOption::ACCELERATLE
             .union(BuffOption::OOMPH)
             .union(BuffOption::BOUNCE)
             .union(BuffOption::PSYCHE);
-        self.intersection(hero_buffs_bitflags)
+        self.left_intersection(hero_buffs_byteflags)
     }
 
     pub fn shulk_buffs(self) -> BuffOption {
         // Return a struct with only Shulk's selected arts
-        let shulk_buffs_bitflags = BuffOption::MONAD_JUMP
+        let shulk_buffs_byteflags = BuffOption::MONAD_JUMP
             .union(BuffOption::MONAD_SPEED)
             .union(BuffOption::MONAD_SHIELD)
             .union(BuffOption::MONAD_BUSTER)
             .union(BuffOption::MONAD_SMASH);
-        self.intersection(shulk_buffs_bitflags)
+        self.left_intersection(shulk_buffs_byteflags)
     }
 
     pub fn wario_buffs(self) -> BuffOption {
-        let wario_buffs_bitflags = BuffOption::WAFT_MINI
+        let wario_buffs_byteflags = BuffOption::WAFT_MINI
             .union(BuffOption::WAFT_HALF)
             .union(BuffOption::WAFT_FULL);
-        self.intersection(wario_buffs_bitflags)
+        self.left_intersection(wario_buffs_byteflags)
     }
 }
 
-impl fmt::Display for BuffOption {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                BuffOption::ACCELERATLE => "Acceleratle",
-                BuffOption::OOMPH => "Oomph",
-                BuffOption::BOUNCE => "Bounce",
-                BuffOption::PSYCHE => "Psyche Up",
-                BuffOption::BREATHING => "Deep Breathing",
-                BuffOption::ARSENE => "Arsene",
-                BuffOption::LIMIT => "Limit Break",
-                BuffOption::KO => "KO Punch",
-                BuffOption::WING => "1-Winged Angel",
-                BuffOption::MONAD_JUMP => "Jump",
-                BuffOption::MONAD_SPEED => "Speed",
-                BuffOption::MONAD_SHIELD => "Shield",
-                BuffOption::MONAD_BUSTER => "Buster",
-                BuffOption::MONAD_SMASH => "Smash",
-                BuffOption::POWER_DRAGON => "Power Dragon",
-                BuffOption::WAFT_MINI => "Mini Waft",
-                BuffOption::WAFT_HALF => "Half Waft",
-                BuffOption::WAFT_FULL => "Full Waft",
-                _ => combination_string.as_str(),
-            }
-        )
+byteflags! {
+    pub struct ThrowOption
+    {
+        NONE = "None",
+        FORWARD = "Forward Throw",
+        BACKWARD = "Backward Throw",
+        UP = "Up Throw",
+        DOWN = "Down Throw",
     }
 }
 
-extra_bitflag_impls! {BuffOption}
-impl_serde_for_bitflags!(BuffOption);
+impl ThrowOption {
+    pub fn into_cmd(self) -> Option<i32> {
+        #[cfg(feature = "smash")]
+        {
+            Some(match self {
+                ThrowOption::NONE => 0,
+                ThrowOption::FORWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_F,
+                ThrowOption::BACKWARD => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_B,
+                ThrowOption::UP => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_HI,
+                ThrowOption::DOWN => *FIGHTER_PAD_CMD_CAT2_FLAG_THROW_LW,
+                _ => return None,
+            })
+        }
 
-impl fmt::Display for Delay {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                Delay::D0 => "0",
-                Delay::D1 => "1",
-                Delay::D2 => "2",
-                Delay::D3 => "3",
-                Delay::D4 => "4",
-                Delay::D5 => "5",
-                Delay::D6 => "6",
-                Delay::D7 => "7",
-                Delay::D8 => "8",
-                Delay::D9 => "9",
-                Delay::D10 => "10",
-                Delay::D11 => "11",
-                Delay::D12 => "12",
-                Delay::D13 => "13",
-                Delay::D14 => "14",
-                Delay::D15 => "15",
-                Delay::D16 => "16",
-                Delay::D17 => "17",
-                Delay::D18 => "18",
-                Delay::D19 => "19",
-                Delay::D20 => "20",
-                Delay::D21 => "21",
-                Delay::D22 => "22",
-                Delay::D23 => "23",
-                Delay::D24 => "24",
-                Delay::D25 => "25",
-                Delay::D26 => "26",
-                Delay::D27 => "27",
-                Delay::D28 => "28",
-                Delay::D29 => "29",
-                Delay::D30 => "30",
-                _ => combination_string.as_str(),
-            }
-        )
+        #[cfg(not(feature = "smash"))]
+        None
     }
 }
 
-extra_bitflag_impls! {Delay}
-impl_serde_for_bitflags!(Delay);
-
-bitflags! {
-    pub struct MedDelay : u32 {
-        const D0 = 0x1;
-        const D5 = 0x2;
-        const D10 = 0x4;
-        const D15 = 0x8;
-        const D20 = 0x10;
-        const D25 = 0x20;
-        const D30 = 0x40;
-        const D35 = 0x80;
-        const D40 = 0x100;
-        const D45 = 0x200;
-        const D50 = 0x400;
-        const D55 = 0x800;
-        const D60 = 0x1000;
-        const D65 = 0x2000;
-        const D70 = 0x4000;
-        const D75 = 0x8000;
-        const D80 = 0x10000;
-        const D85 = 0x20000;
-        const D90 = 0x40000;
-        const D95 = 0x80000;
-        const D100 = 0x0010_0000;
-        const D105 = 0x0020_0000;
-        const D110 = 0x0040_0000;
-        const D115 = 0x0080_0000;
-        const D120 = 0x0100_0000;
-        const D125 = 0x0200_0000;
-        const D130 = 0x0400_0000;
-        const D135 = 0x0800_0000;
-        const D140 = 0x1000_0000;
-        const D145 = 0x2000_0000;
-        const D150 = 0x4000_0000;
+// TODO!() Is this redundant with OnOff?
+byteflags! {
+    pub struct BoolFlag {
+        pub TRUE = "True",
+        pub FALSE = "False",
     }
 }
 
-impl MedDelay {
-    pub fn into_meddelay(&self) -> u32 {
-        self.to_index() * 5
-    }
-}
-
-impl fmt::Display for MedDelay {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                MedDelay::D0 => "0",
-                MedDelay::D5 => "5",
-                MedDelay::D10 => "10",
-                MedDelay::D15 => "15",
-                MedDelay::D20 => "20",
-                MedDelay::D25 => "25",
-                MedDelay::D30 => "30",
-                MedDelay::D35 => "35",
-                MedDelay::D40 => "40",
-                MedDelay::D45 => "45",
-                MedDelay::D50 => "50",
-                MedDelay::D55 => "55",
-                MedDelay::D60 => "60",
-                MedDelay::D65 => "65",
-                MedDelay::D70 => "70",
-                MedDelay::D75 => "75",
-                MedDelay::D80 => "80",
-                MedDelay::D85 => "85",
-                MedDelay::D90 => "90",
-                MedDelay::D95 => "95",
-                MedDelay::D100 => "100",
-                MedDelay::D105 => "105",
-                MedDelay::D110 => "110",
-                MedDelay::D115 => "115",
-                MedDelay::D120 => "120",
-                MedDelay::D125 => "125",
-                MedDelay::D130 => "130",
-                MedDelay::D135 => "135",
-                MedDelay::D140 => "140",
-                MedDelay::D145 => "145",
-                MedDelay::D150 => "150",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {MedDelay}
-impl_serde_for_bitflags!(MedDelay);
-
-bitflags! {
-    pub struct LongDelay : u32 {
-        const D0 = 0x1;
-        const D10 = 0x2;
-        const D20 = 0x4;
-        const D30 = 0x8;
-        const D40 = 0x10;
-        const D50 = 0x20;
-        const D60 = 0x40;
-        const D70 = 0x80;
-        const D80 = 0x100;
-        const D90 = 0x200;
-        const D100 = 0x400;
-        const D110 = 0x800;
-        const D120 = 0x1000;
-        const D130 = 0x2000;
-        const D140 = 0x4000;
-        const D150 = 0x8000;
-        const D160 = 0x10000;
-        const D170 = 0x20000;
-        const D180 = 0x40000;
-        const D190 = 0x80000;
-        const D200 = 0x0010_0000;
-        const D210 = 0x0020_0000;
-        const D220 = 0x0040_0000;
-        const D230 = 0x0080_0000;
-        const D240 = 0x0100_0000;
-        const D250 = 0x0200_0000;
-        const D260 = 0x0400_0000;
-        const D270 = 0x0800_0000;
-        const D280 = 0x1000_0000;
-        const D290 = 0x2000_0000;
-        const D300 = 0x4000_0000;
-    }
-}
-
-impl LongDelay {
-    pub fn into_longdelay(&self) -> u32 {
-        self.to_index() * 10
-    }
-}
-
-impl fmt::Display for LongDelay {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                LongDelay::D0 => "0",
-                LongDelay::D10 => "10",
-                LongDelay::D20 => "20",
-                LongDelay::D30 => "30",
-                LongDelay::D40 => "40",
-                LongDelay::D50 => "50",
-                LongDelay::D60 => "60",
-                LongDelay::D70 => "70",
-                LongDelay::D80 => "80",
-                LongDelay::D90 => "90",
-                LongDelay::D100 => "100",
-                LongDelay::D110 => "110",
-                LongDelay::D120 => "120",
-                LongDelay::D130 => "130",
-                LongDelay::D140 => "140",
-                LongDelay::D150 => "150",
-                LongDelay::D160 => "160",
-                LongDelay::D170 => "170",
-                LongDelay::D180 => "180",
-                LongDelay::D190 => "190",
-                LongDelay::D200 => "200",
-                LongDelay::D210 => "210",
-                LongDelay::D220 => "220",
-                LongDelay::D230 => "230",
-                LongDelay::D240 => "240",
-                LongDelay::D250 => "250",
-                LongDelay::D260 => "260",
-                LongDelay::D270 => "270",
-                LongDelay::D280 => "280",
-                LongDelay::D290 => "290",
-                LongDelay::D300 => "300",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {LongDelay}
-impl_serde_for_bitflags!(LongDelay);
-
-bitflags! {
-    pub struct BoolFlag : u32 {
-        const TRUE = 0x1;
-        const FALSE = 0x2;
-    }
-}
-
-extra_bitflag_impls! {BoolFlag}
-impl_serde_for_bitflags!(BoolFlag);
-
 impl BoolFlag {
     pub fn into_bool(self) -> bool {
         matches!(self, BoolFlag::TRUE)
     }
 }
 
-impl fmt::Display for BoolFlag {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                BoolFlag::TRUE => "True",
-                BoolFlag::FALSE => "False",
-                _ => combination_string.as_str(),
-            }
-        )
+byteflags! {
+    pub struct SdiFrequency {
+        pub NONE = "None",
+        pub NORMAL = "Normal",
+        pub MEDIUM = "Medium",
+        pub HIGH = "High",
     }
 }
 
-#[repr(u32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum SdiFrequency {
-    None = 0,
-    Normal = 1,
-    Medium = 2,
-    High = 4,
-}
-
 impl SdiFrequency {
     pub fn into_u32(self) -> u32 {
         match self {
-            SdiFrequency::None => u32::MAX,
-            SdiFrequency::Normal => 8,
-            SdiFrequency::Medium => 6,
-            SdiFrequency::High => 4,
+            SdiFrequency::NONE => u32::MAX,
+            SdiFrequency::NORMAL => 8,
+            SdiFrequency::MEDIUM => 6,
+            SdiFrequency::HIGH => 4,
+            _ => panic!("Invalid value in SdiFrequency::into_u32: {}", self),
         }
     }
 }
 
-impl fmt::Display for SdiFrequency {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                SdiFrequency::None => "None",
-                SdiFrequency::Normal => "Normal",
-                SdiFrequency::Medium => "Medium",
-                SdiFrequency::High => "High",
-            }
-        )
+byteflags! {
+    pub struct ClatterFrequency {
+        pub NONE = "None",
+        pub NORMAL = "Normal",
+        pub MEDIUM = "Medium",
+        pub HIGH = "High",
     }
 }
 
-impl ToggleTrait for SdiFrequency {
-    fn to_toggle_vals() -> Vec<u32> {
-        SdiFrequency::iter().map(|i| i as u32).collect()
-    }
-
-    fn to_toggle_strings() -> Vec<String> {
-        SdiFrequency::iter().map(|i| i.to_string()).collect()
-    }
-}
-
-#[repr(u32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum ClatterFrequency {
-    None = 0,
-    Normal = 1,
-    Medium = 2,
-    High = 4,
-}
-
 impl ClatterFrequency {
     pub fn into_u32(self) -> u32 {
         match self {
-            ClatterFrequency::None => u32::MAX,
-            ClatterFrequency::Normal => 8,
-            ClatterFrequency::Medium => 5,
-            ClatterFrequency::High => 2,
+            ClatterFrequency::NONE => u32::MAX,
+            ClatterFrequency::NORMAL => 8,
+            ClatterFrequency::MEDIUM => 5,
+            ClatterFrequency::HIGH => 2,
+            _ => panic!("Invalid value in ClatterFrequency::into_u32: {}", self),
         }
     }
 }
 
-impl fmt::Display for ClatterFrequency {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                ClatterFrequency::None => "None",
-                ClatterFrequency::Normal => "Normal",
-                ClatterFrequency::Medium => "Medium",
-                ClatterFrequency::High => "High",
-            }
-        )
+byteflags! {
+    pub struct CharacterItem {
+        pub NONE = "None",
+        pub PLAYER_VARIATION_1 = "Player 1st Var.",
+        pub PLAYER_VARIATION_2 = "Player 2nd Var.",
+        pub PLAYER_VARIATION_3 = "Player 3rd Var.",
+        pub PLAYER_VARIATION_4 = "Player 4th Var.",
+        pub PLAYER_VARIATION_5 = "Player 5th Var.",
+        pub PLAYER_VARIATION_6 = "Player 6th Var.",
+        pub PLAYER_VARIATION_7 = "Player 7th Var.",
+        pub PLAYER_VARIATION_8 = "Player 8th Var.",
+        pub CPU_VARIATION_1 = "CPU 1st Var.",
+        pub CPU_VARIATION_2 = "CPU 2nd Var.",
+        pub CPU_VARIATION_3 = "CPU 3rd Var.",
+        pub CPU_VARIATION_4 = "CPU 4th Var.",
+        pub CPU_VARIATION_5 = "CPU 5th Var.",
+        pub CPU_VARIATION_6 = "CPU 6th Var.",
+        pub CPU_VARIATION_7 = "CPU 7th Var.",
+        pub CPU_VARIATION_8 = "CPU 8th Var.",
     }
 }
 
-impl ToggleTrait for ClatterFrequency {
-    fn to_toggle_vals() -> Vec<u32> {
-        ClatterFrequency::iter().map(|i| i as u32).collect()
-    }
-
-    fn to_toggle_strings() -> Vec<String> {
-        ClatterFrequency::iter().map(|i| i.to_string()).collect()
-    }
-}
-
-/// Item Selections
-#[repr(i32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum CharacterItem {
-    None = 0,
-    PlayerVariation1 = 0x1,
-    PlayerVariation2 = 0x2,
-    PlayerVariation3 = 0x4,
-    PlayerVariation4 = 0x8,
-    PlayerVariation5 = 0x10,
-    PlayerVariation6 = 0x20,
-    PlayerVariation7 = 0x40,
-    PlayerVariation8 = 0x80,
-    CpuVariation1 = 0x100,
-    CpuVariation2 = 0x200,
-    CpuVariation3 = 0x400,
-    CpuVariation4 = 0x800,
-    CpuVariation5 = 0x1000,
-    CpuVariation6 = 0x2000,
-    CpuVariation7 = 0x4000,
-    CpuVariation8 = 0x8000,
-}
-
 impl CharacterItem {
-    pub fn as_idx(self) -> u32 {
-        log_2(self as i32 as u32)
+    pub fn as_idx(&self) -> usize {
+        match *self {
+            CharacterItem::NONE => 0,
+            CharacterItem::PLAYER_VARIATION_1 => 1,
+            CharacterItem::PLAYER_VARIATION_2 => 2,
+            CharacterItem::PLAYER_VARIATION_3 => 3,
+            CharacterItem::PLAYER_VARIATION_4 => 4,
+            CharacterItem::PLAYER_VARIATION_5 => 5,
+            CharacterItem::PLAYER_VARIATION_6 => 6,
+            CharacterItem::PLAYER_VARIATION_7 => 7,
+            CharacterItem::PLAYER_VARIATION_8 => 8,
+            CharacterItem::CPU_VARIATION_1 => 9,
+            CharacterItem::CPU_VARIATION_2 => 10,
+            CharacterItem::CPU_VARIATION_3 => 11,
+            CharacterItem::CPU_VARIATION_4 => 12,
+            CharacterItem::CPU_VARIATION_5 => 13,
+            CharacterItem::CPU_VARIATION_6 => 14,
+            CharacterItem::CPU_VARIATION_7 => 15,
+            CharacterItem::CPU_VARIATION_8 => 16,
+            _ => panic!("Invalid value in CharacterItem::as_idx: {}", self),
+        }
     }
 }
 
-impl fmt::Display for CharacterItem {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                CharacterItem::PlayerVariation1 => "Player 1st Var.",
-                CharacterItem::PlayerVariation2 => "Player 2nd Var.",
-                CharacterItem::PlayerVariation3 => "Player 3rd Var.",
-                CharacterItem::PlayerVariation4 => "Player 4th Var.",
-                CharacterItem::PlayerVariation5 => "Player 5th Var.",
-                CharacterItem::PlayerVariation6 => "Player 6th Var.",
-                CharacterItem::PlayerVariation7 => "Player 7th Var.",
-                CharacterItem::PlayerVariation8 => "Player 8th Var.",
-                CharacterItem::CpuVariation1 => "CPU 1st Var.",
-                CharacterItem::CpuVariation2 => "CPU 2nd Var.",
-                CharacterItem::CpuVariation3 => "CPU 3rd Var.",
-                CharacterItem::CpuVariation4 => "CPU 4th Var.",
-                CharacterItem::CpuVariation5 => "CPU 5th Var.",
-                CharacterItem::CpuVariation6 => "CPU 6th Var.",
-                CharacterItem::CpuVariation7 => "CPU 7th Var.",
-                CharacterItem::CpuVariation8 => "CPU 8th Var.",
-                CharacterItem::None => "None",
-            }
-        )
-    }
-}
-
-impl ToggleTrait for CharacterItem {
-    fn to_toggle_vals() -> Vec<u32> {
-        CharacterItem::iter().map(|i| i as u32).collect()
-    }
-
-    fn to_toggle_strings() -> Vec<String> {
-        CharacterItem::iter().map(|i| i.to_string()).collect()
-    }
-}
-
-bitflags! {
-    pub struct MashTrigger : u32 {
-        const HIT =            0b0000_0000_0000_0000_0001;
-        const SHIELDSTUN =     0b0000_0000_0000_0000_0010;
-        const PARRY =          0b0000_0000_0000_0000_0100;
-        const TUMBLE =         0b0000_0000_0000_0000_1000;
-        const LANDING =        0b0000_0000_0000_0001_0000;
-        const TRUMP =          0b0000_0000_0000_0010_0000;
-        const FOOTSTOOL =      0b0000_0000_0000_0100_0000;
-        const CLATTER =        0b0000_0000_0000_1000_0000;
-        const LEDGE =          0b0000_0000_0001_0000_0000;
-        const TECH =           0b0000_0000_0010_0000_0000;
-        const MISTECH =        0b0000_0000_0100_0000_0000;
-        const GROUNDED =       0b0000_0000_1000_0000_0000;
-        const AIRBORNE =       0b0000_0001_0000_0000_0000;
-        const DISTANCE_CLOSE = 0b0000_0010_0000_0000_0000;
-        const DISTANCE_MID =   0b0000_0100_0000_0000_0000;
-        const DISTANCE_FAR =   0b0000_1000_0000_0000_0000;
-        const ALWAYS =         0b0001_0000_0000_0000_0000;
+byteflags! {
+    pub struct MashTrigger {
+        pub HIT = "Hitstun",
+        pub SHIELDSTUN = "Shieldstun",
+        pub PARRY = "Parry",
+        pub TUMBLE = "Tumble",
+        pub LANDING = "Landing",
+        pub TRUMP = "Ledge Trump",
+        pub FOOTSTOOL = "Footstool",
+        pub CLATTER = "Clatter",
+        pub LEDGE = "Ledge Option",
+        pub TECH = "Tech Option",
+        pub MISTECH = "Mistech Option",
+        pub GROUNDED = "Grounded",
+        pub AIRBORNE = "Airborne",
+        pub DISTANCE_CLOSE = "Distance: Close",
+        pub DISTANCE_MID = "Distance: Mid",
+        pub DISTANCE_FAR = "Distance: Far",
+        pub ALWAYS = "Always",
     }
 }
 
 impl MashTrigger {
     pub const fn default() -> MashTrigger {
         // Hit, block, clatter
-        MashTrigger::HIT
-            .union(MashTrigger::TUMBLE)
-            .union(MashTrigger::SHIELDSTUN)
-            .union(MashTrigger::CLATTER)
+        MashTrigger {
+            HIT: 1,
+            TUMBLE: 1,
+            SHIELDSTUN: 1,
+            CLATTER: 1,
+            ..MashTrigger::empty()
+        }
     }
 }
 
-impl fmt::Display for MashTrigger {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                MashTrigger::HIT => "Hitstun",
-                MashTrigger::SHIELDSTUN => "Shieldstun",
-                MashTrigger::PARRY => "Parry",
-                MashTrigger::TUMBLE => "Tumble",
-                MashTrigger::LANDING => "Landing",
-                MashTrigger::TRUMP => "Ledge Trump",
-                MashTrigger::FOOTSTOOL => "Footstool",
-                MashTrigger::CLATTER => "Clatter",
-                MashTrigger::LEDGE => "Ledge Option",
-                MashTrigger::TECH => "Tech Option",
-                MashTrigger::MISTECH => "Mistech Option",
-                MashTrigger::GROUNDED => "Grounded",
-                MashTrigger::AIRBORNE => "Airborne",
-                MashTrigger::DISTANCE_CLOSE => "Distance: Close",
-                MashTrigger::DISTANCE_MID => "Distance: Mid",
-                MashTrigger::DISTANCE_FAR => "Distance: Far",
-                MashTrigger::ALWAYS => "Always",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {MashTrigger}
-impl_serde_for_bitflags!(MashTrigger);
-
 #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
 pub struct DamagePercent(pub u32, pub u32);
 
-impl SliderTrait for DamagePercent {
-    fn get_limits() -> (u32, u32) {
-        (0, 150)
-    }
-}
-
 impl DamagePercent {
     pub const fn default() -> DamagePercent {
         DamagePercent(0, 150)
     }
 }
 
-bitflags! {
-    pub struct SaveDamage : u32
-    {
-        const DEFAULT = 0b001;
-        const SAVED =   0b010;
-        const RANDOM =  0b100;
+byteflags! {
+    pub struct SaveDamage {
+        pub DEFAULT = "Default",
+        pub SAVED = "Save State",
+        pub RANDOM = "Random Value",
     }
 }
 
-impl fmt::Display for SaveDamage {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                SaveDamage::DEFAULT => "Default",
-                SaveDamage::SAVED => "Save State",
-                SaveDamage::RANDOM => "Random Value",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {SaveDamage}
-impl_serde_for_bitflags!(SaveDamage);
-
-// Save State Slots
-bitflags! {
-    pub struct SaveStateSlot : u32
+byteflags! {
+    pub struct SaveStateSlot
     {
-        const S1 = 0x1;
-        const S2 = 0x2;
-        const S3 = 0x4;
-        const S4 = 0x8;
-        const S5 = 0x10;
+        pub S1 = "Slot 1",
+        pub S2 = "Slot 2",
+        pub S3 = "Slot 3",
+        pub S4 = "Slot 4",
+        pub S5 = "Slot 5",
     }
 }
 
 impl SaveStateSlot {
-    pub fn into_idx(self) -> Option<usize> {
-        Some(match self {
-            SaveStateSlot::S1 => 0,
-            SaveStateSlot::S2 => 1,
-            SaveStateSlot::S3 => 2,
-            SaveStateSlot::S4 => 3,
-            SaveStateSlot::S5 => 4,
-            _ => return None,
-        })
-    }
-
-    pub fn as_idx(self) -> usize {
-        match self {
-            SaveStateSlot::S1 => 0,
-            SaveStateSlot::S2 => 1,
-            SaveStateSlot::S3 => 2,
-            SaveStateSlot::S4 => 3,
-            SaveStateSlot::S5 => 4,
-            _ => 0,
+    pub fn into_idx(&self) -> Option<usize> {
+        match *self {
+            SaveStateSlot::S1 => Some(0),
+            SaveStateSlot::S2 => Some(1),
+            SaveStateSlot::S3 => Some(2),
+            SaveStateSlot::S4 => Some(3),
+            SaveStateSlot::S5 => Some(4),
+            _ => panic!("Invalid value in SaveStateSlot::into_idx: {}", self),
         }
     }
 }
 
-impl fmt::Display for SaveStateSlot {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                SaveStateSlot::S1 => "1",
-                SaveStateSlot::S2 => "2",
-                SaveStateSlot::S3 => "3",
-                SaveStateSlot::S4 => "4",
-                SaveStateSlot::S5 => "5",
-                _ => combination_string.as_str(),
-            }
-        )
+byteflags! {
+    pub struct RecordSlot {
+        pub S1 = "Slot 1",
+        pub S2 = "Slot 2",
+        pub S3 = "Slot 3",
+        pub S4 = "Slot 4",
+        pub S5 = "Slot 5",
     }
 }
 
-extra_bitflag_impls! {SaveStateSlot}
-impl_serde_for_bitflags!(SaveStateSlot);
-
-// Input Recording Slot
-#[repr(u32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum RecordSlot {
-    S1 = 0x1,
-    S2 = 0x2,
-    S3 = 0x4,
-    S4 = 0x8,
-    S5 = 0x10,
-}
-
 impl RecordSlot {
-    pub fn into_idx(self) -> usize {
-        match self {
-            RecordSlot::S1 => 0,
-            RecordSlot::S2 => 1,
-            RecordSlot::S3 => 2,
-            RecordSlot::S4 => 3,
-            RecordSlot::S5 => 4,
+    pub fn into_idx(&self) -> Option<usize> {
+        match *self {
+            RecordSlot::S1 => Some(0),
+            RecordSlot::S2 => Some(1),
+            RecordSlot::S3 => Some(2),
+            RecordSlot::S4 => Some(3),
+            RecordSlot::S5 => Some(4),
+            _ => panic!("Invalid value in RecordSlot::into_idx: {}", self),
         }
     }
 }
 
-impl fmt::Display for RecordSlot {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                RecordSlot::S1 => "Slot One",
-                RecordSlot::S2 => "Slot Two",
-                RecordSlot::S3 => "Slot Three",
-                RecordSlot::S4 => "Slot Four",
-                RecordSlot::S5 => "Slot Five",
-            }
-        )
-    }
-}
-
-impl ToggleTrait for RecordSlot {
-    fn to_toggle_vals() -> Vec<u32> {
-        RecordSlot::iter().map(|i| i as u32).collect()
-    }
-
-    fn to_toggle_strings() -> Vec<String> {
-        RecordSlot::iter().map(|i| i.to_string()).collect()
-    }
-}
-
-// Input Playback Slot
-bitflags! {
-    pub struct PlaybackSlot : u32
-    {
-        const S1 = 0x1;
-        const S2 = 0x2;
-        const S3 = 0x4;
-        const S4 = 0x8;
-        const S5 = 0x10;
+byteflags! {
+    pub struct PlaybackSlot {
+        pub S1 = "Slot 1",
+        pub S2 = "Slot 2",
+        pub S3 = "Slot 3",
+        pub S4 = "Slot 4",
+        pub S5 = "Slot 5",
     }
 }
 
 impl PlaybackSlot {
-    pub fn into_idx(self) -> Option<usize> {
-        Some(match self {
-            PlaybackSlot::S1 => 0,
-            PlaybackSlot::S2 => 1,
-            PlaybackSlot::S3 => 2,
-            PlaybackSlot::S4 => 3,
-            PlaybackSlot::S5 => 4,
-            _ => return None,
-        })
+    pub fn into_idx(&self) -> Option<usize> {
+        match *self {
+            PlaybackSlot::S1 => Some(0),
+            PlaybackSlot::S2 => Some(1),
+            PlaybackSlot::S3 => Some(2),
+            PlaybackSlot::S4 => Some(3),
+            PlaybackSlot::S5 => Some(4),
+            _ => panic!("Invalid value in PlaybackSlot::into_idx: {}", self),
+        }
     }
 }
 
-impl fmt::Display for PlaybackSlot {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                PlaybackSlot::S1 => "Slot One",
-                PlaybackSlot::S2 => "Slot Two",
-                PlaybackSlot::S3 => "Slot Three",
-                PlaybackSlot::S4 => "Slot Four",
-                PlaybackSlot::S5 => "Slot Five",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {PlaybackSlot}
-impl_serde_for_bitflags!(PlaybackSlot);
-
 // If doing input recording out of hitstun, when does playback begin after?
-#[repr(u32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum HitstunPlayback {
-    // Should these start at 0? All of my new menu structs need some review, I'm just doing whatever atm
-    Hitstun = 0x1,
-    Hitstop = 0x2,
-    Instant = 0x4,
-}
-
-impl ToggleTrait for HitstunPlayback {
-    fn to_toggle_vals() -> Vec<u32> {
-        HitstunPlayback::iter().map(|i| i as u32).collect()
-    }
-
-    fn to_toggle_strings() -> Vec<String> {
-        HitstunPlayback::iter().map(|i| i.to_string()).collect()
+byteflags! {
+    pub struct HitstunPlayback {
+        pub HITSTUN = "As Hitstun Ends",
+        pub HITSTOP = "As Hitstop Ends",
+        pub INSTANT = "As Hitstop Begins",
     }
 }
 
-impl fmt::Display for HitstunPlayback {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                HitstunPlayback::Hitstun => "As Hitstun Ends",
-                HitstunPlayback::Hitstop => "As Hitstop Ends",
-                HitstunPlayback::Instant => "As Hitstop Begins",
-            }
-        )
+byteflags! {
+    pub struct RecordTrigger {
+        pub COMMAND = "Button Combination",
+        pub SAVESTATE = "Save State Load",
     }
 }
 
-// Input Recording Trigger Type
-bitflags! {
-    pub struct RecordTrigger : u32
-    {
-        const COMMAND = 0x1;
-        const SAVESTATE = 0x2;
+byteflags! {
+    pub struct RecordingDuration {
+        pub F60 = "60",
+        pub F90 = "90",
+        pub F120 = "120",
+        pub F150 = "150",
+        pub F180 = "180",
+        pub F210 = "210",
+        pub F240 = "240",
+        pub F270 = "270",
+        pub F300 = "300",
+        pub F330 = "330",
+        pub F360 = "360",
+        pub F390 = "390",
+        pub F420 = "420",
+        pub F450 = "450",
+        pub F480 = "480",
+        pub F510 = "510",
+        pub F540 = "540",
+        pub F570 = "570",
+        pub F600 = "600",
     }
 }
 
-impl fmt::Display for RecordTrigger {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                RecordTrigger::COMMAND => "Button Combination",
-                RecordTrigger::SAVESTATE => "Save State Load",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {RecordTrigger}
-impl_serde_for_bitflags!(RecordTrigger);
-
-#[repr(u32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum RecordingDuration {
-    F60 = 0x1,
-    F90 = 0x2,
-    F120 = 0x4,
-    F150 = 0x8,
-    F180 = 0x10,
-    F210 = 0x20,
-    F240 = 0x40,
-    F270 = 0x80,
-    F300 = 0x100,
-    F330 = 0x200,
-    F360 = 0x400,
-    F390 = 0x800,
-    F420 = 0x1000,
-    F450 = 0x2000,
-    F480 = 0x4000,
-    F510 = 0x8000,
-    F540 = 0x10000,
-    F570 = 0x20000,
-    F600 = 0x40000,
-}
-
 impl RecordingDuration {
-    pub fn into_frames(self) -> usize {
-        (log_2(self as u32) as usize * 30) + 60
+    pub fn into_frames(&self) -> usize {
+        match *self {
+            RecordingDuration::F60 => 60,
+            RecordingDuration::F90 => 90,
+            RecordingDuration::F120 => 120,
+            RecordingDuration::F150 => 150,
+            RecordingDuration::F180 => 180,
+            RecordingDuration::F210 => 210,
+            RecordingDuration::F240 => 240,
+            RecordingDuration::F270 => 270,
+            RecordingDuration::F300 => 300,
+            RecordingDuration::F330 => 330,
+            RecordingDuration::F360 => 360,
+            RecordingDuration::F390 => 390,
+            RecordingDuration::F420 => 420,
+            RecordingDuration::F450 => 450,
+            RecordingDuration::F480 => 480,
+            RecordingDuration::F510 => 510,
+            RecordingDuration::F540 => 540,
+            RecordingDuration::F570 => 570,
+            RecordingDuration::F600 => 600,
+            _ => panic!("Invalid value in RecordingDuration::into_frames: {}", self),
+        }
     }
 }
 
-impl ToggleTrait for RecordingDuration {
-    fn to_toggle_vals() -> Vec<u32> {
-        RecordingDuration::iter().map(|i| i as u32).collect()
-    }
-
-    fn to_toggle_strings() -> Vec<String> {
-        RecordingDuration::iter().map(|i| i.to_string()).collect()
+byteflags! {
+    pub struct  ButtonConfig {
+        pub A = "A",
+        pub B = "B",
+        pub X = "X",
+        pub Y = "Y",
+        pub L = "Pro L",
+        pub R = "Pro R; GCC Z",
+        pub ZL = "Pro ZL; GCC L",
+        pub ZR = "Pro ZR; GCC R",
+        pub DPAD_UP = "DPad Up",
+        pub DPAD_DOWN = "DPad Down",
+        pub DPAD_LEFT = "DPad Left",
+        pub DPAD_RIGHT = "DPad Right",
+        pub PLUS = "Plus",
+        pub MINUS = "Minus",
+        pub LSTICK = "Left Stick Press",
+        pub RSTICK = "Right Stick Press",
     }
 }
 
-impl fmt::Display for RecordingDuration {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        use RecordingDuration::*;
-        write!(
-            f,
-            "{}",
-            match self {
-                F60 => "60",
-                F90 => "90",
-                F120 => "120",
-                F150 => "150",
-                F180 => "180",
-                F210 => "210",
-                F240 => "240",
-                F270 => "270",
-                F300 => "300",
-                F330 => "330",
-                F360 => "360",
-                F390 => "390",
-                F420 => "420",
-                F450 => "450",
-                F480 => "480",
-                F510 => "510",
-                F540 => "540",
-                F570 => "570",
-                F600 => "600",
-            }
-        )
+byteflags! {
+    pub struct UpdatePolicy {
+        pub STABLE = "Stable",
+        pub BETA = "Beta",
+        pub DISABLED = "Disabled",
     }
 }
 
-bitflags! {
-    pub struct  ButtonConfig : u32 {
-        const A = 0b0000_0000_0000_0000_0001;
-        const B = 0b0000_0000_0000_0000_0010;
-        const X = 0b0000_0000_0000_0000_0100;
-        const Y = 0b0000_0000_0000_0000_1000;
-        const L = 0b0000_0000_0000_0001_0000;
-        const R = 0b0000_0000_0000_0010_0000;
-        const ZL = 0b0000_0000_0000_0100_0000;
-        const ZR = 0b0000_0000_0000_1000_0000;
-        const DPAD_UP = 0b0000_0000_0001_0000_0000;
-        const DPAD_DOWN = 0b0000_0000_0010_0000_0000;
-        const DPAD_LEFT = 0b0000_0000_0100_0000_0000;
-        const DPAD_RIGHT = 0b0000_0000_1000_0000_0000;
-        const PLUS = 0b0000_0001_0000_0000_0000;
-        const MINUS = 0b0000_0010_0000_0000_0000;
-        const LSTICK = 0b0000_0100_0000_0000_0000;
-        const RSTICK = 0b0000_1000_0000_0000_0000;
-    }
-}
-
-impl fmt::Display for ButtonConfig {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let combination_string = self.combination_string();
-        write!(
-            f,
-            "{}",
-            match *self {
-                ButtonConfig::A => "A",
-                ButtonConfig::B => "B",
-                ButtonConfig::X => "X",
-                ButtonConfig::Y => "Y",
-                ButtonConfig::L => "Pro L",
-                ButtonConfig::R => "Pro R; GCC Z",
-                ButtonConfig::ZL => "Pro ZL; GCC L",
-                ButtonConfig::ZR => "Pro ZR; GCC R",
-                ButtonConfig::DPAD_UP => "DPad Up",
-                ButtonConfig::DPAD_DOWN => "DPad Down",
-                ButtonConfig::DPAD_LEFT => "DPad Left",
-                ButtonConfig::DPAD_RIGHT => "DPad Right",
-                ButtonConfig::PLUS => "Plus",
-                ButtonConfig::MINUS => "Minus",
-                ButtonConfig::LSTICK => "Left Stick Press",
-                ButtonConfig::RSTICK => "Right Stick Press",
-                _ => combination_string.as_str(),
-            }
-        )
-    }
-}
-
-extra_bitflag_impls! {ButtonConfig}
-impl_serde_for_bitflags!(ButtonConfig);
-
-#[repr(u32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum UpdatePolicy {
-    Stable,
-    Beta,
-    Disabled,
-}
-
 impl UpdatePolicy {
     pub const fn default() -> UpdatePolicy {
-        UpdatePolicy::Stable
+        UpdatePolicy::STABLE
     }
 }
 
-impl fmt::Display for UpdatePolicy {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                UpdatePolicy::Stable => "Stable",
-                UpdatePolicy::Beta => "Beta",
-                UpdatePolicy::Disabled => "Disabled",
-            }
-        )
-    }
-}
-
-impl ToggleTrait for UpdatePolicy {
-    fn to_toggle_vals() -> Vec<u32> {
-        UpdatePolicy::iter().map(|i| i as u32).collect()
-    }
-
-    fn to_toggle_strings() -> Vec<String> {
-        UpdatePolicy::iter().map(|i| i.to_string()).collect()
-    }
-}
-
-#[repr(u32)]
-#[derive(
-    Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
-)]
-pub enum InputDisplay {
-    None,
-    Smash,
-    Raw,
-}
-
-impl fmt::Display for InputDisplay {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match *self {
-                InputDisplay::None => "None",
-                InputDisplay::Smash => "Smash Inputs",
-                InputDisplay::Raw => "Raw Inputs",
-            }
-        )
-    }
-}
-
-impl ToggleTrait for InputDisplay {
-    fn to_toggle_vals() -> Vec<u32> {
-        InputDisplay::iter().map(|i| i as u32).collect()
-    }
-
-    fn to_toggle_strings() -> Vec<String> {
-        InputDisplay::iter().map(|i| i.to_string()).collect()
+byteflags! {
+    pub struct InputDisplay {
+        pub NONE = "None",
+        pub SMASH = "Smash Inputs",
+        pub RAW = "Raw Inputs",
     }
 }
diff --git a/training_mod_tui/Cargo.toml b/training_mod_tui/Cargo.toml
index 3bcfada..4ff5130 100644
--- a/training_mod_tui/Cargo.toml
+++ b/training_mod_tui/Cargo.toml
@@ -1,18 +1,18 @@
 [package]
 name = "training_mod_tui"
 version = "0.1.0"
-edition = "2018"
+edition = "2021"
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[lib]
+doctest = false
 
 [dependencies]
-tui = { version = "0.16.0", default-features = false }
-unicode-width = "0.1.9"
-training_mod_consts = { path = "../training_mod_consts", default-features = false}
-serde_json = "1.0.79"
-bitflags = "1.2.1"
+itertools = "0.11.0"
+ratatui = { git = "https://github.com/tonogdlp/ratatui.git", branch = "single-cell", default-features = false}
+serde = { version = "1.0.188", features = ["derive"] }
+serde_json = "1.0.106"
 crossterm = { version = "0.22.1", optional = true }
 
 [features]
 default = []
-has_terminal = ["crossterm", "tui/crossterm"]
\ No newline at end of file
+has_terminal = ["crossterm", "ratatui/crossterm"]
\ No newline at end of file
diff --git a/training_mod_tui/src/containers/app.rs b/training_mod_tui/src/containers/app.rs
new file mode 100644
index 0000000..7d6dfab
--- /dev/null
+++ b/training_mod_tui/src/containers/app.rs
@@ -0,0 +1,390 @@
+use serde::ser::{SerializeMap, Serializer};
+use serde::Serialize;
+use std::collections::HashMap;
+
+use crate::{InputControl, StatefulList, SubMenu, SubMenuType, Tab};
+
+#[derive(PartialEq, Serialize, Clone, Copy)]
+pub enum AppPage {
+    SUBMENU,
+    TOGGLE,
+    SLIDER,
+    CONFIRMATION,
+    CLOSE,
+}
+
+#[derive(PartialEq)]
+pub enum ConfirmationState {
+    HoverNo,
+    HoverYes,
+}
+
+impl ConfirmationState {
+    pub fn switch(&self) -> ConfirmationState {
+        match self {
+            ConfirmationState::HoverNo => ConfirmationState::HoverYes,
+            ConfirmationState::HoverYes => ConfirmationState::HoverNo,
+        }
+    }
+}
+
+// Menu structure is:
+// App <StatefulTable<Tab>>
+// │
+// └─ Tab <StatefulTable<Submenu>>
+//    │
+//    └─ Submenu <Struct>
+//       │
+//       ├─ StatefulTable<Toggle>
+//       │
+//       │  OR
+//       │
+//       └─ Option<Slider>
+
+pub struct App<'a> {
+    pub tabs: StatefulList<Tab<'a>>,
+    pub page: AppPage,
+    pub serialized_settings: String,
+    pub serialized_default_settings: String,
+    pub confirmation_state: ConfirmationState,
+    pub confirmation_return_page: AppPage,
+}
+
+impl<'a> App<'a> {
+    pub fn new() -> App<'a> {
+        App {
+            tabs: StatefulList::new(),
+            page: AppPage::SUBMENU,
+            serialized_settings: String::new(),
+            serialized_default_settings: String::new(),
+            confirmation_state: ConfirmationState::HoverNo,
+            confirmation_return_page: AppPage::SUBMENU,
+        }
+    }
+
+    pub fn current_settings_to_json(&self) -> String {
+        serde_json::to_string(&self).expect("Could not serialize the menu to JSON!")
+    }
+
+    pub fn get_serialized_settings_with_defaults(&self) -> String {
+        format!(
+            "{{\"menu\":{}, \"defaults_menu\":{}}}",
+            self.serialized_settings, self.serialized_default_settings
+        )
+    }
+
+    pub fn save_settings(&mut self) {
+        self.serialized_settings = self.current_settings_to_json();
+    }
+
+    pub fn save_default_settings(&mut self) {
+        self.serialized_default_settings = self.current_settings_to_json();
+    }
+
+    pub fn load_defaults(&mut self) {
+        // TODO!() is there a way to do this without cloning?
+        let json = self.serialized_default_settings.clone();
+        self.update_all_from_json(&json);
+    }
+
+    pub fn load_defaults_for_current_submenu(&mut self) {
+        let submenu_id = self.selected_submenu().id;
+        let json = self.serialized_default_settings.clone();
+        self.update_one_from_json(&json, submenu_id);
+    }
+
+    pub fn update_all_from_json(&mut self, json: &str) {
+        let all_settings: HashMap<String, Vec<u8>> =
+            serde_json::from_str(json).expect("Could not parse the json!");
+        for tab in self.tabs.iter_mut() {
+            for submenu_opt in tab.submenus.iter_mut() {
+                if let Some(submenu) = submenu_opt {
+                    if let Some(val) = all_settings.get(submenu.id) {
+                        submenu.update_from_vec(val.clone());
+                    }
+                }
+            }
+        }
+        self.save_settings();
+    }
+
+    #[allow(unused_labels)]
+    pub fn update_one_from_json(&mut self, json: &str, submenu_id: &str) {
+        let all_settings: HashMap<String, Vec<u8>> =
+            serde_json::from_str(json).expect("Could not parse the json!");
+        if let Some(val) = all_settings.get(submenu_id) {
+            // No need to iterate through all the submenus if the id doesn't exist in the hashmap
+            'tabs_scope: for tab in self.tabs.iter_mut() {
+                'submenus_scope: for submenu_opt in tab.submenus.iter_mut() {
+                    if let Some(submenu) = submenu_opt {
+                        if submenu.id == submenu_id {
+                            submenu.update_from_vec(val.clone());
+                            break 'tabs_scope;
+                        }
+                    }
+                }
+            }
+        }
+        self.save_settings();
+    }
+
+    pub fn confirm(&mut self) -> bool {
+        self.confirmation_state == ConfirmationState::HoverYes
+    }
+
+    pub fn return_from_confirmation(&mut self) {
+        self.confirmation_state = ConfirmationState::HoverNo;
+        self.page = self.confirmation_return_page;
+    }
+
+    pub fn selected_tab(&mut self) -> &mut Tab<'a> {
+        self.tabs.get_selected().expect("No tab selected!")
+    }
+
+    pub fn selected_submenu(&mut self) -> &mut SubMenu<'a> {
+        self.selected_tab()
+            .submenus
+            .get_selected()
+            .expect("No submenu selected!")
+    }
+
+    pub fn should_show_clear_keyhelp(&mut self) -> bool {
+        // Only show the "Clear Toggle" keyhelp if all of the following are true
+        // 1. app.page is TOGGLE,
+        // 2. selected_submenu.submenu_type is ToggleMultiple
+        // 3. the toggle can be set to values greater than 1 (i.e. its not a boolean toggle)
+        if self.page != AppPage::TOGGLE {
+            return false;
+        }
+        let submenu = self.selected_submenu();
+        match submenu.submenu_type {
+            SubMenuType::ToggleMultiple => submenu.selected_toggle().max > 1,
+            _ => false,
+        }
+    }
+}
+
+impl<'a> Serialize for App<'a> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        // Serializes as a mapping between submenu titles and values
+        // Need to iterate through tabs to avoid making a list of mappings
+        let len: usize = self.tabs.iter().map(|tab| tab.len()).sum();
+        let mut map = serializer.serialize_map(Some(len))?;
+        for tab in self.tabs.iter() {
+            for submenu in tab.submenus.iter() {
+                map.serialize_entry(submenu.id, submenu)?;
+            }
+        }
+        map.end()
+    }
+}
+
+impl<'a> InputControl for App<'a> {
+    fn on_a(&mut self) {
+        match self.page {
+            AppPage::SUBMENU => {
+                self.page = match self.selected_submenu().submenu_type {
+                    SubMenuType::ToggleSingle => AppPage::TOGGLE,
+                    SubMenuType::ToggleMultiple => AppPage::TOGGLE,
+                    SubMenuType::Slider => AppPage::SLIDER,
+                };
+                self.selected_tab().on_a()
+            }
+            AppPage::TOGGLE => self.selected_submenu().on_a(),
+            AppPage::SLIDER => self.selected_submenu().on_a(),
+            AppPage::CONFIRMATION => {
+                // For resetting defaults
+                // TODO: Is this the right place for this logic?
+                if self.confirm() {
+                    match self.confirmation_return_page {
+                        AppPage::SUBMENU => {
+                            // Reset ALL settings to default
+                            self.load_defaults();
+                        }
+                        AppPage::TOGGLE | AppPage::SLIDER => {
+                            // Reset current submenu to default
+                            self.load_defaults_for_current_submenu();
+                        }
+                        _ => {}
+                    }
+                }
+                self.return_from_confirmation();
+            }
+            AppPage::CLOSE => {}
+        }
+        self.save_settings(); // A button can make changes, update the serialized settings
+    }
+    fn on_b(&mut self) {
+        match self.page {
+            AppPage::SUBMENU => {
+                // Exit the app
+                self.page = AppPage::CLOSE;
+            }
+            AppPage::TOGGLE => {
+                // Return to the list of submenus
+                self.page = AppPage::SUBMENU;
+            }
+            AppPage::SLIDER => {
+                // Return to the list of submenus if we don't have a slider handle selected
+                let slider = self
+                    .selected_submenu()
+                    .slider
+                    .as_mut()
+                    .expect("No slider selected!");
+                if !slider.is_handle_selected() {
+                    self.page = AppPage::SUBMENU;
+                } else {
+                    self.selected_submenu().on_b();
+                }
+            }
+            AppPage::CONFIRMATION => {
+                // Return to the list of submenus
+                self.return_from_confirmation();
+            }
+            AppPage::CLOSE => {}
+        }
+        self.save_settings(); // B button can make changes, update the serialized settings
+    }
+    fn on_x(&mut self) {
+        self.save_default_settings();
+    }
+    fn on_y(&mut self) {
+        // Clear current toggle, for toggles w/ weighted selections
+        match self.page {
+            AppPage::TOGGLE => self.selected_submenu().on_y(),
+            _ => {}
+        }
+    }
+    fn on_up(&mut self) {
+        match self.page {
+            AppPage::SUBMENU => self.tabs.get_selected().expect("No tab selected!").on_up(),
+            AppPage::TOGGLE => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .submenus
+                .get_selected()
+                .expect("No submenu selected!")
+                .on_up(),
+            AppPage::SLIDER => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .submenus
+                .get_selected()
+                .expect("No submenu selected!")
+                .on_up(),
+            AppPage::CONFIRMATION => {}
+            AppPage::CLOSE => {}
+        }
+    }
+    fn on_down(&mut self) {
+        match self.page {
+            AppPage::SUBMENU => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .on_down(),
+            AppPage::TOGGLE => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .submenus
+                .get_selected()
+                .expect("No submenu selected!")
+                .on_down(),
+            AppPage::SLIDER => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .submenus
+                .get_selected()
+                .expect("No submenu selected!")
+                .on_down(),
+            AppPage::CONFIRMATION => {}
+            AppPage::CLOSE => {}
+        }
+    }
+    fn on_left(&mut self) {
+        match self.page {
+            AppPage::SUBMENU => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .on_left(),
+            AppPage::TOGGLE => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .submenus
+                .get_selected()
+                .expect("No submenu selected!")
+                .on_left(),
+            AppPage::SLIDER => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .submenus
+                .get_selected()
+                .expect("No submenu selected!")
+                .on_left(),
+            AppPage::CONFIRMATION => self.confirmation_state = self.confirmation_state.switch(),
+            AppPage::CLOSE => {}
+        }
+    }
+    fn on_right(&mut self) {
+        match self.page {
+            AppPage::SUBMENU => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .on_right(),
+            AppPage::TOGGLE => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .submenus
+                .get_selected()
+                .expect("No submenu selected!")
+                .on_right(),
+            AppPage::SLIDER => self
+                .tabs
+                .get_selected()
+                .expect("No tab selected!")
+                .submenus
+                .get_selected()
+                .expect("No submenu selected!")
+                .on_right(),
+            AppPage::CONFIRMATION => self.confirmation_state = self.confirmation_state.switch(),
+            AppPage::CLOSE => {}
+        }
+    }
+    fn on_start(&mut self) {
+        // Close menu
+        self.page = AppPage::CLOSE;
+    }
+    fn on_l(&mut self) {}
+    fn on_r(&mut self) {
+        // Reset settings to default
+        // See App::on_a() for the logic
+        self.confirmation_return_page = self.page;
+        self.page = AppPage::CONFIRMATION;
+    }
+    fn on_zl(&mut self) {
+        match self.page {
+            AppPage::SUBMENU => {
+                self.tabs.previous();
+            }
+            _ => {}
+        }
+    }
+    fn on_zr(&mut self) {
+        match self.page {
+            AppPage::SUBMENU => self.tabs.next(),
+            _ => {}
+        }
+    }
+}
diff --git a/training_mod_tui/src/containers/mod.rs b/training_mod_tui/src/containers/mod.rs
new file mode 100644
index 0000000..de697a0
--- /dev/null
+++ b/training_mod_tui/src/containers/mod.rs
@@ -0,0 +1,24 @@
+mod app;
+mod submenu;
+mod tab;
+mod toggle;
+pub use app::*;
+pub use submenu::*;
+pub use tab::*;
+pub use toggle::*;
+
+pub trait InputControl {
+    fn on_a(&mut self);
+    fn on_b(&mut self);
+    fn on_x(&mut self);
+    fn on_y(&mut self);
+    fn on_up(&mut self);
+    fn on_down(&mut self);
+    fn on_left(&mut self);
+    fn on_right(&mut self);
+    fn on_start(&mut self);
+    fn on_l(&mut self);
+    fn on_r(&mut self);
+    fn on_zl(&mut self);
+    fn on_zr(&mut self);
+}
diff --git a/training_mod_tui/src/containers/submenu.rs b/training_mod_tui/src/containers/submenu.rs
new file mode 100644
index 0000000..18e46a0
--- /dev/null
+++ b/training_mod_tui/src/containers/submenu.rs
@@ -0,0 +1,168 @@
+use serde::ser::Serializer;
+use serde::Serialize;
+
+use crate::{InputControl, StatefulSlider, StatefulTable, Toggle};
+
+#[derive(Clone)]
+pub struct SubMenu<'a> {
+    pub title: &'a str,
+    pub id: &'a str,
+    pub help_text: &'a str,
+    pub submenu_type: SubMenuType,
+    pub toggles: StatefulTable<Toggle<'a>>,
+    pub slider: Option<StatefulSlider>,
+}
+
+impl<'a> Serialize for SubMenu<'a> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        match self.submenu_type {
+            SubMenuType::ToggleMultiple | SubMenuType::ToggleSingle => {
+                self.toggles.serialize(serializer)
+            }
+            SubMenuType::Slider => self.slider.serialize(serializer),
+        }
+    }
+}
+
+impl<'a> InputControl for SubMenu<'a> {
+    fn on_a(&mut self) {
+        match self.submenu_type {
+            SubMenuType::ToggleSingle => {
+                // Set all values to 0 first before incrementing the selected toggle
+                // This ensure that exactly one toggle has a nonzero value
+                for ind in 0..self.toggles.len() {
+                    self.toggles.get_by_idx_mut(ind).unwrap().value = 0;
+                }
+                self.selected_toggle().increment();
+            }
+            SubMenuType::ToggleMultiple => self.selected_toggle().increment(),
+            SubMenuType::Slider => {
+                let slider = self.slider.as_mut().expect("No slider selected!");
+                slider.select_deselect();
+            }
+        }
+    }
+    fn on_b(&mut self) {
+        match self.submenu_type {
+            SubMenuType::ToggleSingle => {}
+            SubMenuType::ToggleMultiple => {}
+            SubMenuType::Slider => {
+                let slider = self.slider.as_mut().expect("No slider selected!");
+                if slider.is_handle_selected() {
+                    slider.deselect()
+                }
+            }
+        }
+    }
+    fn on_x(&mut self) {}
+    fn on_y(&mut self) {
+        match self.submenu_type {
+            SubMenuType::ToggleMultiple => {
+                let toggle = self.selected_toggle();
+                if toggle.max > 1 {
+                    toggle.value = 0;
+                }
+            }
+            _ => {}
+        }
+    }
+    fn on_up(&mut self) {
+        match self.submenu_type {
+            SubMenuType::ToggleSingle => self.toggles.prev_row_checked(),
+            SubMenuType::ToggleMultiple => self.toggles.prev_row_checked(),
+            SubMenuType::Slider => {
+                let slider = self.slider.as_mut().expect("No slider selected!");
+                if slider.is_handle_selected() {
+                    slider.increment_selected_fast();
+                }
+            }
+        }
+    }
+    fn on_down(&mut self) {
+        match self.submenu_type {
+            SubMenuType::ToggleSingle => self.toggles.next_row_checked(),
+            SubMenuType::ToggleMultiple => self.toggles.next_row_checked(),
+            SubMenuType::Slider => {
+                let slider = self.slider.as_mut().expect("No slider selected!");
+                if slider.is_handle_selected() {
+                    slider.decrement_selected_fast();
+                }
+            }
+        }
+    }
+    fn on_left(&mut self) {
+        match self.submenu_type {
+            SubMenuType::ToggleSingle => self.toggles.prev_col_checked(),
+            SubMenuType::ToggleMultiple => self.toggles.prev_col_checked(),
+            SubMenuType::Slider => {
+                let slider = self.slider.as_mut().expect("No slider selected!");
+                if slider.is_handle_selected() {
+                    slider.decrement_selected_slow();
+                } else {
+                    slider.switch_hover();
+                }
+            }
+        }
+    }
+    fn on_right(&mut self) {
+        match self.submenu_type {
+            SubMenuType::ToggleSingle => self.toggles.next_col_checked(),
+            SubMenuType::ToggleMultiple => self.toggles.next_col_checked(),
+            SubMenuType::Slider => {
+                let slider = self.slider.as_mut().expect("No slider selected!");
+                if slider.is_handle_selected() {
+                    slider.increment_selected_slow();
+                } else {
+                    slider.switch_hover();
+                }
+            }
+        }
+    }
+    fn on_start(&mut self) {}
+    fn on_l(&mut self) {}
+    fn on_r(&mut self) {}
+    fn on_zl(&mut self) {}
+    fn on_zr(&mut self) {}
+}
+
+impl<'a> SubMenu<'a> {
+    pub fn selected_toggle(&mut self) -> &mut Toggle<'a> {
+        self.toggles.get_selected().expect("No toggle selected!")
+    }
+
+    pub fn update_from_vec(&mut self, values: Vec<u8>) {
+        match self.submenu_type {
+            SubMenuType::ToggleSingle | SubMenuType::ToggleMultiple => {
+                for (idx, value) in values.iter().enumerate() {
+                    if let Some(toggle) = self.toggles.get_by_idx_mut(idx) {
+                        toggle.value = *value;
+                    }
+                }
+            }
+            SubMenuType::Slider => {
+                assert_eq!(
+                    values.len(),
+                    2,
+                    "Exactly two values need to be passed to submenu.set() for slider!"
+                );
+                if let Some(s) = self.slider {
+                    self.slider = Some(StatefulSlider {
+                        lower: values[0].into(),
+                        upper: values[1].into(),
+                        ..s
+                    });
+                }
+            }
+        }
+    }
+}
+
+#[derive(Clone, Copy, Serialize)]
+pub enum SubMenuType {
+    ToggleSingle,
+    ToggleMultiple,
+    Slider,
+}
diff --git a/training_mod_tui/src/containers/tab.rs b/training_mod_tui/src/containers/tab.rs
new file mode 100644
index 0000000..763df7b
--- /dev/null
+++ b/training_mod_tui/src/containers/tab.rs
@@ -0,0 +1,54 @@
+use serde::ser::{SerializeMap, Serializer};
+use serde::Serialize;
+
+use crate::{InputControl, StatefulTable, SubMenu};
+
+#[derive(Clone)]
+pub struct Tab<'a> {
+    pub title: &'a str,
+    pub id: &'a str,
+    pub submenus: StatefulTable<SubMenu<'a>>,
+}
+
+impl<'a> Tab<'a> {
+    pub fn len(&self) -> usize {
+        self.submenus.len()
+    }
+}
+
+impl<'a> Serialize for Tab<'a> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut map = serializer.serialize_map(Some(self.submenus.len()))?;
+        for submenu in self.submenus.as_vec().iter() {
+            map.serialize_entry(&submenu.title, &submenu)?;
+        }
+        map.end()
+    }
+}
+
+impl<'a> InputControl for Tab<'a> {
+    fn on_a(&mut self) {}
+    fn on_b(&mut self) {}
+    fn on_x(&mut self) {}
+    fn on_y(&mut self) {}
+    fn on_up(&mut self) {
+        self.submenus.prev_row_checked()
+    }
+    fn on_down(&mut self) {
+        self.submenus.next_row_checked()
+    }
+    fn on_left(&mut self) {
+        self.submenus.prev_col_checked()
+    }
+    fn on_right(&mut self) {
+        self.submenus.next_col_checked()
+    }
+    fn on_start(&mut self) {}
+    fn on_l(&mut self) {}
+    fn on_r(&mut self) {}
+    fn on_zl(&mut self) {}
+    fn on_zr(&mut self) {}
+}
diff --git a/training_mod_tui/src/containers/toggle.rs b/training_mod_tui/src/containers/toggle.rs
new file mode 100644
index 0000000..19b24b2
--- /dev/null
+++ b/training_mod_tui/src/containers/toggle.rs
@@ -0,0 +1,36 @@
+use serde::ser::Serializer;
+use serde::Serialize;
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct Toggle<'a> {
+    pub title: &'a str,
+    pub value: u8,
+    pub max: u8,
+}
+
+impl<'a> Serialize for Toggle<'a> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_u8(self.value)
+    }
+}
+
+impl<'a> Toggle<'a> {
+    pub fn increment(&mut self) {
+        if self.value == self.max {
+            self.value = 0;
+        } else {
+            self.value += 1;
+        }
+    }
+
+    pub fn decrement(&mut self) {
+        if self.value == 0 {
+            self.value = self.max;
+        } else {
+            self.value -= 1;
+        }
+    }
+}
diff --git a/training_mod_tui/src/gauge.rs b/training_mod_tui/src/gauge.rs
deleted file mode 100644
index cb9f462..0000000
--- a/training_mod_tui/src/gauge.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-pub enum GaugeState {
-    MinHover,
-    MaxHover,
-    MinSelected,
-    MaxSelected,
-    None,
-}
-
-pub struct DoubleEndedGauge {
-    pub state: GaugeState,
-    pub selected_min: u32,
-    pub selected_max: u32,
-    pub abs_min: u32,
-    pub abs_max: u32,
-}
-
-impl DoubleEndedGauge {
-    pub fn new() -> DoubleEndedGauge {
-        DoubleEndedGauge {
-            state: GaugeState::None,
-            selected_min: 0,
-            selected_max: 150,
-            abs_min: 0,
-            abs_max: 150,
-        }
-    }
-}
diff --git a/training_mod_tui/src/lib.rs b/training_mod_tui/src/lib.rs
index 0ca02a1..5446433 100644
--- a/training_mod_tui/src/lib.rs
+++ b/training_mod_tui/src/lib.rs
@@ -1,911 +1,7 @@
-use training_mod_consts::{
-    ui_menu, MenuJsonStruct, Slider, SubMenu, SubMenuType, Toggle, TrainingModpackMenu, UiMenu,
-};
-use tui::{
-    backend::Backend,
-    layout::{Constraint, Corner, Direction, Layout, Rect},
-    style::{Modifier, Style},
-    text::{Span, Spans},
-    widgets::{Block, LineGauge, List, ListItem, ListState, Paragraph, Tabs},
-    Frame,
-};
+mod containers;
+pub use containers::*;
+mod structures;
+pub use structures::*;
 
-use serde_json::{json, Map};
-use std::collections::HashMap;
-pub use tui::{backend::TestBackend, style::Color, Terminal};
-
-pub mod gauge;
-mod list;
-
-use crate::gauge::{DoubleEndedGauge, GaugeState};
-use crate::list::{MultiStatefulList, StatefulList};
-
-static NX_TUI_WIDTH: u16 = 240;
-// Number of lists per page
-pub const NUM_LISTS: usize = 4;
-
-#[derive(PartialEq)]
-pub enum AppPage {
-    SUBMENU,
-    TOGGLE,
-    SLIDER,
-    CONFIRMATION,
-}
-
-/// We should hold a list of SubMenus.
-/// The currently selected SubMenu should also have an associated list with necessary information.
-/// We can convert the option types (Toggle, OnOff, Slider) to lists
-pub struct App {
-    pub tabs: StatefulList<String>,
-    pub menu_items: HashMap<String, MultiStatefulList<SubMenu>>,
-    pub selected_sub_menu_toggles: MultiStatefulList<Toggle>,
-    pub selected_sub_menu_slider: DoubleEndedGauge,
-    pub page: AppPage,
-    pub default_menu: (UiMenu, String),
-}
-
-impl<'a> App {
-    pub fn new(menu: UiMenu, default_menu: (UiMenu, String)) -> App {
-        let mut menu_items_stateful = HashMap::new();
-        menu.tabs.iter().for_each(|tab| {
-            menu_items_stateful.insert(
-                tab.tab_title.clone(),
-                MultiStatefulList::with_items(tab.tab_submenus.clone(), NUM_LISTS),
-            );
-        });
-
-        let mut app = App {
-            tabs: StatefulList::with_items(
-                menu.tabs.iter().map(|tab| tab.tab_title.clone()).collect(),
-            ),
-            menu_items: menu_items_stateful,
-            selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
-            selected_sub_menu_slider: DoubleEndedGauge::new(),
-            page: AppPage::SUBMENU,
-            default_menu: default_menu,
-        };
-        app.set_sub_menu_items();
-        app
-    }
-
-    /// Takes the currently selected tab/submenu and clones the options into
-    /// self.selected_sub_menu_toggles and self.selected_sub_menu_slider
-    pub fn set_sub_menu_items(&mut self) {
-        let (list_section, list_idx) = self
-            .menu_items
-            .get(self.tab_selected())
-            .unwrap()
-            .idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
-        let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists
-            [list_section]
-            .items
-            .get(list_idx)
-            .unwrap();
-
-        let toggles = selected_sub_menu.toggles.clone();
-        let slider = selected_sub_menu.slider.clone();
-        match SubMenuType::from_string(&self.sub_menu_selected()._type) {
-            SubMenuType::TOGGLE => {
-                self.selected_sub_menu_toggles = MultiStatefulList::with_items(
-                    toggles,
-                    if selected_sub_menu.toggles.len() >= NUM_LISTS {
-                        NUM_LISTS
-                    } else {
-                        selected_sub_menu.toggles.len()
-                    },
-                )
-            }
-            SubMenuType::SLIDER => {
-                let slider = slider.unwrap();
-                self.selected_sub_menu_slider = DoubleEndedGauge {
-                    state: GaugeState::None,
-                    selected_min: slider.selected_min,
-                    selected_max: slider.selected_max,
-                    abs_min: slider.abs_min,
-                    abs_max: slider.abs_max,
-                }
-            }
-        };
-    }
-
-    /// Returns the id of the currently selected tab
-    pub fn tab_selected(&self) -> &str {
-        self.tabs
-            .items
-            .get(self.tabs.state.selected().unwrap())
-            .unwrap()
-    }
-
-    /// Returns the currently selected SubMenu struct
-    ///
-    /// {
-    ///   submenu_title: String,
-    ///   submenu_id: String,
-    ///   help_text: String,
-    ///   is_single_option: bool,
-    ///   toggles: Vec<Toggle<'a>>,
-    ///   slider: Option<Slider>,
-    ///   _type: String,
-    /// }
-    fn sub_menu_selected(&self) -> &SubMenu {
-        let (list_section, list_idx) = self
-            .menu_items
-            .get(self.tab_selected())
-            .unwrap()
-            .idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
-        self.menu_items.get(self.tab_selected()).unwrap().lists[list_section]
-            .items
-            .get(list_idx)
-            .unwrap()
-    }
-
-    /// A "next()" function which differs per submenu type
-    /// Toggles: calls next()
-    /// Slider: Swaps between MinHover and MaxHover
-    pub fn sub_menu_next(&mut self) {
-        match SubMenuType::from_string(&self.sub_menu_selected()._type) {
-            SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(),
-            SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
-                GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
-                GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
-                _ => {}
-            },
-        }
-    }
-
-    /// A "next_list()" function which differs per submenu type
-    /// Toggles: Calls next_list()
-    /// Slider:
-    ///     * Swaps between MinHover and MaxHover
-    ///     * Increments the selected_min/max if possible
-    pub fn sub_menu_next_list(&mut self) {
-        match SubMenuType::from_string(&self.sub_menu_selected()._type) {
-            SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(),
-            SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
-                GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
-                GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
-                GaugeState::MinSelected => {
-                    if self.selected_sub_menu_slider.selected_min
-                        < self.selected_sub_menu_slider.selected_max
-                    {
-                        self.selected_sub_menu_slider.selected_min += 1;
-                    }
-                }
-                GaugeState::MaxSelected => {
-                    if self.selected_sub_menu_slider.selected_max
-                        < self.selected_sub_menu_slider.abs_max
-                    {
-                        self.selected_sub_menu_slider.selected_max += 1;
-                    }
-                }
-                GaugeState::None => {}
-            },
-        }
-    }
-
-    /// A "previous()" function which differs per submenu type
-    /// Toggles: calls previous()
-    /// Slider: Swaps between MinHover and MaxHover
-    pub fn sub_menu_previous(&mut self) {
-        match SubMenuType::from_string(&self.sub_menu_selected()._type) {
-            SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(),
-            SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
-                GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
-                GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
-                _ => {}
-            },
-        }
-    }
-
-    /// A "previous_list()" function which differs per submenu type
-    /// Toggles: Calls previous_list()
-    /// Slider:
-    ///     * Swaps between MinHover and MaxHover
-    ///     * Decrements the selected_min/max if possible
-    pub fn sub_menu_previous_list(&mut self) {
-        match SubMenuType::from_string(&self.sub_menu_selected()._type) {
-            SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(),
-            SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
-                GaugeState::MinHover => self.selected_sub_menu_slider.state = GaugeState::MaxHover,
-                GaugeState::MaxHover => self.selected_sub_menu_slider.state = GaugeState::MinHover,
-                GaugeState::MinSelected => {
-                    if self.selected_sub_menu_slider.selected_min
-                        > self.selected_sub_menu_slider.abs_min
-                    {
-                        self.selected_sub_menu_slider.selected_min -= 1;
-                    }
-                }
-                GaugeState::MaxSelected => {
-                    if self.selected_sub_menu_slider.selected_max
-                        > self.selected_sub_menu_slider.selected_min
-                    {
-                        self.selected_sub_menu_slider.selected_max -= 1;
-                    }
-                }
-                GaugeState::None => {}
-            },
-        }
-    }
-
-    /// Returns information about the currently selected submenu
-    ///
-    /// 0: Submenu Title
-    /// 1: Submenu Help Text
-    /// 2: Vec(toggle checked, title) for toggles, Vec(nothing) for slider
-    /// 3: ListState for toggles, ListState::new() for slider
-    /// TODO: Refactor return type into a nice struct
-    pub fn sub_menu_strs_and_states(
-        &self,
-    ) -> (String, String, Vec<(Vec<(bool, String)>, ListState)>) {
-        (
-            self.sub_menu_selected().submenu_title.clone(),
-            self.sub_menu_selected().help_text.clone(),
-            match SubMenuType::from_string(&self.sub_menu_selected()._type) {
-                SubMenuType::TOGGLE => self
-                    .selected_sub_menu_toggles
-                    .lists
-                    .iter()
-                    .map(|toggle_list| {
-                        (
-                            toggle_list
-                                .items
-                                .iter()
-                                .map(|toggle| (toggle.checked, toggle.toggle_title.clone()))
-                                .collect(),
-                            toggle_list.state.clone(),
-                        )
-                    })
-                    .collect(),
-                SubMenuType::SLIDER => {
-                    vec![(vec![], ListState::default())]
-                }
-            },
-        )
-    }
-
-    /// Returns information about the currently selected slider
-    /// 0: Title
-    /// 1: Help text
-    /// 2: Reference to self.selected_sub_menu_slider
-    /// TODO: Refactor return type into a nice struct
-    pub fn sub_menu_strs_for_slider(&self) -> (String, String, &DoubleEndedGauge) {
-        let slider = match SubMenuType::from_string(&self.sub_menu_selected()._type) {
-            SubMenuType::SLIDER => &self.selected_sub_menu_slider,
-            _ => {
-                panic!("Slider not selected!");
-            }
-        };
-        (
-            self.sub_menu_selected().submenu_title.clone(),
-            self.sub_menu_selected().help_text.clone(),
-            slider,
-        )
-    }
-
-    /// Different behavior depending on the current menu location
-    /// Submenu list: Enters toggle or slider submenu
-    /// Toggle submenu: Toggles the selected submenu toggle in self.selected_sub_menu_toggles and in the actual SubMenu struct
-    /// Slider submenu: Swaps hover/selected state. Updates the actual SubMenu struct if going from Selected -> Hover
-    pub fn on_a(&mut self) {
-        let tab_selected = self
-            .tabs
-            .items
-            .get(self.tabs.state.selected().unwrap())
-            .unwrap();
-        let (list_section, list_idx) = self
-            .menu_items
-            .get(tab_selected)
-            .unwrap()
-            .idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state);
-        let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists[list_section]
-            .items
-            .get_mut(list_idx)
-            .unwrap();
-        if self.page == AppPage::SUBMENU {
-            match SubMenuType::from_string(&selected_sub_menu._type) {
-                // Need to change the slider state to MinHover so the slider shows up initially
-                SubMenuType::SLIDER => {
-                    self.page = AppPage::SLIDER;
-                    self.selected_sub_menu_slider.state = GaugeState::MinHover;
-                }
-                SubMenuType::TOGGLE => self.page = AppPage::TOGGLE,
-            }
-        } else {
-            match SubMenuType::from_string(&selected_sub_menu._type) {
-                SubMenuType::TOGGLE => {
-                    let is_single_option = selected_sub_menu.is_single_option;
-                    let state = self.selected_sub_menu_toggles.state;
-                    // Change the toggles in self.selected_sub_menu_toggles (for display)
-                    self.selected_sub_menu_toggles
-                        .lists
-                        .iter_mut()
-                        .map(|list| (list.state.selected(), &mut list.items))
-                        .for_each(|(state, toggle_list)| {
-                            toggle_list.iter_mut().enumerate().for_each(|(i, o)| {
-                                if state.is_some() && i == state.unwrap() {
-                                    if !o.checked {
-                                        o.checked = true;
-                                    } else {
-                                        if is_single_option {
-                                            return;
-                                        }
-                                        o.checked = false;
-                                    }
-                                } else if is_single_option {
-                                    o.checked = false;
-                                }
-                            })
-                        });
-                    // Actually change the toggle values in the SubMenu struct
-                    selected_sub_menu
-                        .toggles
-                        .iter_mut()
-                        .enumerate()
-                        .for_each(|(i, o)| {
-                            if i == state {
-                                if !o.checked {
-                                    o.checked = true;
-                                } else {
-                                    if is_single_option {
-                                        return;
-                                    }
-                                    o.checked = false;
-                                }
-                            } else if is_single_option {
-                                o.checked = false;
-                            }
-                        });
-                }
-                SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
-                    GaugeState::MinHover => {
-                        self.selected_sub_menu_slider.state = GaugeState::MinSelected;
-                    }
-                    GaugeState::MaxHover => {
-                        self.selected_sub_menu_slider.state = GaugeState::MaxSelected;
-                    }
-                    GaugeState::MinSelected => {
-                        self.selected_sub_menu_slider.state = GaugeState::MinHover;
-                        selected_sub_menu.slider = Some(Slider {
-                            selected_min: self.selected_sub_menu_slider.selected_min,
-                            selected_max: self.selected_sub_menu_slider.selected_max,
-                            abs_min: self.selected_sub_menu_slider.abs_min,
-                            abs_max: self.selected_sub_menu_slider.abs_max,
-                        });
-                    }
-                    GaugeState::MaxSelected => {
-                        self.selected_sub_menu_slider.state = GaugeState::MaxHover;
-                        selected_sub_menu.slider = Some(Slider {
-                            selected_min: self.selected_sub_menu_slider.selected_min,
-                            selected_max: self.selected_sub_menu_slider.selected_max,
-                            abs_min: self.selected_sub_menu_slider.abs_min,
-                            abs_max: self.selected_sub_menu_slider.abs_max,
-                        });
-                    }
-                    GaugeState::None => {
-                        self.selected_sub_menu_slider.state = GaugeState::MinHover;
-                    }
-                },
-            }
-        }
-    }
-
-    /// Different behavior depending on the current menu location
-    /// Submenu selection: None
-    /// Toggle submenu: Sets page to submenu selection
-    /// Slider submenu: If in a selected state, then commit changes and change to hover. Else set page to submenu selection
-    pub fn on_b(&mut self) {
-        let tab_selected = self
-            .tabs
-            .items
-            .get(self.tabs.state.selected().unwrap())
-            .unwrap();
-        let (list_section, list_idx) = self
-            .menu_items
-            .get(tab_selected)
-            .unwrap()
-            .idx_to_list_idx(self.menu_items.get(tab_selected).unwrap().state);
-        let selected_sub_menu = self.menu_items.get_mut(tab_selected).unwrap().lists[list_section]
-            .items
-            .get_mut(list_idx)
-            .unwrap();
-        match SubMenuType::from_string(&selected_sub_menu._type) {
-            SubMenuType::SLIDER => match self.selected_sub_menu_slider.state {
-                GaugeState::MinSelected => {
-                    self.selected_sub_menu_slider.state = GaugeState::MinHover;
-                    selected_sub_menu.slider = Some(Slider {
-                        selected_min: self.selected_sub_menu_slider.selected_min,
-                        selected_max: self.selected_sub_menu_slider.selected_max,
-                        abs_min: self.selected_sub_menu_slider.abs_min,
-                        abs_max: self.selected_sub_menu_slider.abs_max,
-                    });
-                    // Don't go back to the outer list
-                    return;
-                }
-                GaugeState::MaxSelected => {
-                    self.selected_sub_menu_slider.state = GaugeState::MaxHover;
-                    selected_sub_menu.slider = Some(Slider {
-                        selected_min: self.selected_sub_menu_slider.selected_min,
-                        selected_max: self.selected_sub_menu_slider.selected_max,
-                        abs_min: self.selected_sub_menu_slider.abs_min,
-                        abs_max: self.selected_sub_menu_slider.abs_max,
-                    });
-                    // Don't go back to the outer list
-                    return;
-                }
-                _ => {}
-            },
-            _ => {}
-        }
-        self.page = AppPage::SUBMENU;
-        self.set_sub_menu_items();
-    }
-
-    /// Save defaults command
-    pub fn save_defaults(&mut self) {
-        if self.page == AppPage::SUBMENU {
-            let json = self.to_json();
-            unsafe {
-                self.default_menu = (
-                    ui_menu(serde_json::from_str::<TrainingModpackMenu>(&json).unwrap()),
-                    json,
-                );
-            }
-        }
-    }
-
-    /// Reset current submenu to defaults
-    pub fn reset_current_submenu(&mut self) {
-        if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
-            let json = self.to_json();
-            let mut json_value = serde_json::from_str::<serde_json::Value>(&json).unwrap();
-            let selected_sub_menu = self.sub_menu_selected();
-            let id = selected_sub_menu.submenu_id.clone();
-            let default_json_value =
-                serde_json::from_str::<serde_json::Value>(&self.default_menu.1).unwrap();
-            *json_value.get_mut(id.as_str()).unwrap() =
-                default_json_value.get(id.as_str()).unwrap().clone();
-            let new_menu = serde_json::from_value::<TrainingModpackMenu>(json_value).unwrap();
-            *self = App::new(unsafe { ui_menu(new_menu) }, self.default_menu.clone());
-        }
-    }
-
-    /// Reset all menus to defaults
-    pub fn reset_all_submenus(&mut self) {
-        *self = App::new(self.default_menu.0.clone(), self.default_menu.clone());
-    }
-
-    pub fn previous_tab(&mut self) {
-        if self.page == AppPage::SUBMENU {
-            self.tabs.previous();
-            self.set_sub_menu_items();
-        }
-    }
-
-    pub fn next_tab(&mut self) {
-        if self.page == AppPage::SUBMENU {
-            self.tabs.next();
-            self.set_sub_menu_items();
-        }
-    }
-
-    pub fn on_up(&mut self) {
-        if self.page == AppPage::SUBMENU {
-            self.menu_items
-                .get_mut(
-                    self.tabs
-                        .items
-                        .get(self.tabs.state.selected().unwrap())
-                        .unwrap(),
-                )
-                .unwrap()
-                .previous();
-            self.set_sub_menu_items();
-        } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
-            self.sub_menu_previous();
-        }
-    }
-
-    pub fn on_down(&mut self) {
-        if self.page == AppPage::SUBMENU {
-            self.menu_items
-                .get_mut(
-                    self.tabs
-                        .items
-                        .get(self.tabs.state.selected().unwrap())
-                        .unwrap(),
-                )
-                .unwrap()
-                .next();
-            self.set_sub_menu_items();
-        } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
-            self.sub_menu_next();
-        }
-    }
-
-    pub fn on_left(&mut self) {
-        if self.page == AppPage::SUBMENU {
-            self.menu_items
-                .get_mut(
-                    self.tabs
-                        .items
-                        .get(self.tabs.state.selected().unwrap())
-                        .unwrap(),
-                )
-                .unwrap()
-                .previous_list();
-            self.set_sub_menu_items();
-        } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
-            self.sub_menu_previous_list();
-        }
-    }
-
-    pub fn on_right(&mut self) {
-        if self.page == AppPage::SUBMENU {
-            self.menu_items
-                .get_mut(
-                    self.tabs
-                        .items
-                        .get(self.tabs.state.selected().unwrap())
-                        .unwrap(),
-                )
-                .unwrap()
-                .next_list();
-            self.set_sub_menu_items();
-        } else if self.page == AppPage::TOGGLE || self.page == AppPage::SLIDER {
-            self.sub_menu_next_list();
-        }
-    }
-
-    /// Returns JSON representation of current menu settings
-    pub fn to_json(&self) -> String {
-        let mut settings = Map::new();
-        for key in self.menu_items.keys() {
-            for list in &self.menu_items.get(key).unwrap().lists {
-                for sub_menu in &list.items {
-                    if !sub_menu.toggles.is_empty() {
-                        let val: u32 = sub_menu
-                            .toggles
-                            .iter()
-                            .filter(|t| t.checked)
-                            .map(|t| t.toggle_value)
-                            .sum();
-                        settings.insert(sub_menu.submenu_id.to_string(), json!(val));
-                    } else if sub_menu.slider.is_some() {
-                        let s: &Slider = sub_menu.slider.as_ref().unwrap();
-                        let val: Vec<u32> = vec![s.selected_min, s.selected_max];
-                        settings.insert(sub_menu.submenu_id.to_string(), json!(val));
-                    } else {
-                        panic!("Could not collect settings for {:?}", sub_menu.submenu_id);
-                    }
-                }
-            }
-        }
-        serde_json::to_string(&settings).unwrap()
-    }
-
-    /// Returns the current menu selections and the default menu selections.
-    pub fn get_menu_selections(&self) -> String {
-        serde_json::to_string(&MenuJsonStruct {
-            menu: serde_json::from_str(self.to_json().as_str()).unwrap(),
-            defaults_menu: serde_json::from_str(self.default_menu.1.clone().as_str()).unwrap(),
-        })
-        .unwrap()
-    }
-
-    pub fn submenu_ids(&self) -> Vec<String> {
-        return self
-            .menu_items
-            .values()
-            .flat_map(|multi_stateful_list| {
-                multi_stateful_list
-                    .lists
-                    .iter()
-                    .flat_map(|sub_stateful_list| {
-                        sub_stateful_list
-                            .items
-                            .iter()
-                            .map(|submenu| submenu.submenu_id.clone())
-                    })
-            })
-            .collect::<Vec<String>>();
-    }
-}
-
-fn render_submenu_page<B: Backend>(
-    f: &mut Frame<B>,
-    app: &mut App,
-    list_chunks: Vec<Rect>,
-    help_chunk: Rect,
-) {
-    let tab_selected = app.tab_selected();
-    let mut item_help = None;
-    for (list_section, stateful_list) in app
-        .menu_items
-        .get(tab_selected)
-        .unwrap()
-        .lists
-        .iter()
-        .enumerate()
-    {
-        let items: Vec<ListItem> = stateful_list
-            .items
-            .iter()
-            .map(|i| {
-                let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
-                    i.submenu_title.clone()
-                } else {
-                    format!("   {}", i.submenu_title.clone())
-                })];
-                ListItem::new(lines).style(Style::default().fg(Color::White))
-            })
-            .collect();
-
-        let list = List::new(items)
-            .block(
-                Block::default()
-                    .title(if list_section == 0 { "Options" } else { "" })
-                    .style(Style::default().fg(Color::LightRed)),
-            )
-            .highlight_style(
-                Style::default()
-                    .fg(Color::Green)
-                    .add_modifier(Modifier::BOLD),
-            )
-            .highlight_symbol(">> ");
-
-        let mut state = stateful_list.state.clone();
-        if state.selected().is_some() {
-            item_help = Some(
-                stateful_list.items[state.selected().unwrap()]
-                    .help_text
-                    .clone(),
-            );
-        }
-
-        f.render_stateful_widget(list, list_chunks[list_section], &mut state);
-    }
-
-    let help_paragraph = Paragraph::new(
-        item_help.unwrap_or("".to_string()).replace('\"', "")
-            + "\nZL/ZR: Next tab | X: Save Defaults | R: Reset All Menus",
-    )
-    .style(Style::default().fg(Color::Cyan));
-    f.render_widget(help_paragraph, help_chunk);
-}
-
-pub fn render_toggle_page<B: Backend>(
-    f: &mut Frame<B>,
-    app: &mut App,
-    list_chunks: Vec<Rect>,
-    help_chunk: Rect,
-) {
-    let (title, help_text, mut sub_menu_str_lists) = app.sub_menu_strs_and_states();
-    for list_section in 0..sub_menu_str_lists.len() {
-        let sub_menu_str = sub_menu_str_lists[list_section].0.clone();
-        let sub_menu_state = &mut sub_menu_str_lists[list_section].1;
-        let values_items: Vec<ListItem> = sub_menu_str
-            .iter()
-            .map(|s| {
-                ListItem::new(vec![Spans::from(if s.0 {
-                    format!("X {}", s.1)
-                } else {
-                    format!("  {}", s.1)
-                })])
-            })
-            .collect();
-
-        let values_list = List::new(values_items)
-            .block(Block::default().title(if list_section == 0 {
-                title.clone()
-            } else {
-                "".to_string()
-            }))
-            .start_corner(Corner::TopLeft)
-            .highlight_style(
-                Style::default()
-                    .fg(Color::LightGreen)
-                    .add_modifier(Modifier::BOLD),
-            )
-            .highlight_symbol(">> ");
-        f.render_stateful_widget(values_list, list_chunks[list_section], sub_menu_state);
-    }
-    let help_paragraph = Paragraph::new(help_text.replace('\"', "") + "\nL: Reset Current Menu")
-        .style(Style::default().fg(Color::Cyan));
-    f.render_widget(help_paragraph, help_chunk);
-}
-
-pub fn render_slider_page<B: Backend>(
-    f: &mut Frame<B>,
-    app: &mut App,
-    vertical_chunk: Rect,
-    help_chunk: Rect,
-) {
-    let (_title, help_text, gauge_vals) = app.sub_menu_strs_for_slider();
-    let abs_min = gauge_vals.abs_min;
-    let abs_max = gauge_vals.abs_max;
-    let selected_min = gauge_vals.selected_min;
-    let selected_max = gauge_vals.selected_max;
-    let lbl_ratio = 0.95; // Needed so that the upper limit label is visible
-    let constraints = [
-        Constraint::Ratio(
-            (lbl_ratio * (selected_min - abs_min) as f32) as u32,
-            abs_max - abs_min,
-        ),
-        Constraint::Ratio(
-            (lbl_ratio * (selected_max - selected_min) as f32) as u32,
-            abs_max - abs_min,
-        ),
-        Constraint::Ratio(
-            (lbl_ratio * (abs_max - selected_max) as f32) as u32,
-            abs_max - abs_min,
-        ),
-        Constraint::Min(3), // For upper limit label
-    ];
-    let gauge_chunks = Layout::default()
-        .direction(Direction::Horizontal)
-        .constraints(constraints)
-        .split(vertical_chunk);
-
-    let slider_lbls = [abs_min, selected_min, selected_max, abs_max];
-    for (idx, lbl) in slider_lbls.iter().enumerate() {
-        let mut line_set = tui::symbols::line::NORMAL;
-        line_set.horizontal = "-";
-        let mut gauge = LineGauge::default()
-            .ratio(1.0)
-            .label(format!("{}", lbl))
-            .style(Style::default().fg(Color::White))
-            .line_set(line_set)
-            .gauge_style(Style::default().fg(Color::White).bg(Color::Black));
-        if idx == 1 {
-            // Slider between selected_min and selected_max
-            match gauge_vals.state {
-                GaugeState::MinHover => gauge = gauge.style(Style::default().fg(Color::Red)),
-                GaugeState::MinSelected => gauge = gauge.style(Style::default().fg(Color::Green)),
-                _ => {}
-            }
-            gauge = gauge.gauge_style(Style::default().fg(Color::Yellow).bg(Color::Black));
-        } else if idx == 2 {
-            // Slider between selected_max and abs_max
-            match gauge_vals.state {
-                GaugeState::MaxHover => gauge = gauge.style(Style::default().fg(Color::Red)),
-                GaugeState::MaxSelected => gauge = gauge.style(Style::default().fg(Color::Green)),
-                _ => {}
-            }
-        } else if idx == 3 {
-            // Slider for abs_max
-            // We only want the label to show, so set the line character to " "
-            let mut line_set = tui::symbols::line::NORMAL;
-            line_set.horizontal = " ";
-            gauge = gauge.line_set(line_set);
-
-            // For some reason, the selected_max slider displays on top
-            // So we need to change the abs_max slider styling to match
-            // If the selected_max is close enough to the abs_max
-            if (selected_max as f32 / abs_max as f32) > 0.95 {
-                gauge = gauge.style(match gauge_vals.state {
-                    GaugeState::MaxHover => Style::default().fg(Color::Red),
-                    GaugeState::MaxSelected => Style::default().fg(Color::Green),
-                    _ => Style::default(),
-                })
-            }
-        }
-        f.render_widget(gauge, gauge_chunks[idx]);
-    }
-
-    let help_paragraph = Paragraph::new(help_text.replace('\"', "") + "\nL: Reset Current Menu")
-        .style(Style::default().fg(Color::Cyan));
-    f.render_widget(help_paragraph, help_chunk);
-}
-
-/// Run
-pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
-    let app_tabs = &app.tabs;
-    let tab_selected = app_tabs.state.selected().unwrap();
-    let mut span_selected = Spans::default();
-
-    let titles: Vec<Spans> = app_tabs
-        .items
-        .iter()
-        .cloned()
-        .enumerate()
-        .map(|(idx, tab)| {
-            if idx == tab_selected {
-                span_selected = Spans::from(format!("> {}", tab));
-                Spans::from(format!("> {}", tab))
-            } else {
-                Spans::from(format!("  {}", tab))
-            }
-        })
-        .collect();
-    // There is only enough room to display 3 tabs of text
-    // So lets replace tabs not near the selected with "..."
-    let all_windows: Vec<&[Spans]> = titles
-        .windows(3)
-        .filter(|w| w.contains(&titles[tab_selected]))
-        .collect();
-    let first_window = all_windows[0];
-    let mut titles: Vec<Spans> = titles
-        .iter()
-        .cloned()
-        .map(
-            // Converts all tabs not in the window to "..."
-            |t| {
-                if first_window.contains(&t) {
-                    t
-                } else {
-                    Spans::from("...".to_owned())
-                }
-            },
-        )
-        .collect();
-    // Don't keep consecutive "..." tabs
-    titles.dedup();
-    // Now that the size of the titles vector has changed, need to re-locate the selected tab
-    let tab_selected_deduped: usize = titles
-        .iter()
-        .cloned()
-        .position(|span| span == span_selected)
-        .unwrap_or(0);
-
-    let tabs = Tabs::new(titles)
-        .block(Block::default().title(Spans::from(Span::styled(
-            "Ultimate Training Modpack Menu",
-            Style::default().fg(Color::LightRed),
-        ))))
-        .style(Style::default().fg(Color::White))
-        .highlight_style(Style::default().fg(Color::Yellow))
-        .divider("|")
-        .select(tab_selected_deduped);
-
-    let vertical_chunks = Layout::default()
-        .direction(Direction::Vertical)
-        .constraints(
-            [
-                Constraint::Length(2),
-                Constraint::Max(10),
-                Constraint::Length(2),
-            ]
-            .as_ref(),
-        )
-        .split(f.size());
-
-    // Prevent overflow by adding a length constraint of NX_TUI_WIDTH
-    // Need to add a second constraint since the .expand_to_fill() method
-    // is not publicly exposed, and the attribute defaults to true.
-    // https://github.com/fdehau/tui-rs/blob/v0.19.0/src/layout.rs#L121
-    let vertical_chunks: Vec<Rect> = vertical_chunks
-        .iter()
-        .map(|chunk| {
-            Layout::default()
-                .direction(Direction::Horizontal)
-                .constraints(
-                    [
-                        Constraint::Length(NX_TUI_WIDTH), // Width of the TUI terminal
-                        Constraint::Min(0),               // Fill the remainder margin
-                    ]
-                    .as_ref(),
-                )
-                .split(*chunk)[0]
-        })
-        .collect();
-
-    let list_chunks = Layout::default()
-        .direction(Direction::Horizontal)
-        .constraints(
-            (0..NUM_LISTS)
-                .into_iter()
-                .map(|_idx| Constraint::Percentage((100.0 / NUM_LISTS as f32) as u16))
-                .collect::<Vec<Constraint>>()
-                .as_ref(),
-        )
-        .split(vertical_chunks[1]);
-
-    f.render_widget(tabs, vertical_chunks[0]);
-
-    match app.page {
-        AppPage::SUBMENU => render_submenu_page(f, app, list_chunks, vertical_chunks[2]),
-        AppPage::SLIDER => render_slider_page(f, app, vertical_chunks[1], vertical_chunks[2]),
-        AppPage::TOGGLE => render_toggle_page(f, app, list_chunks, vertical_chunks[2]),
-        AppPage::CONFIRMATION => todo!(),
-    }
-}
+pub const NX_SUBMENU_ROWS: usize = 8;
+pub const NX_SUBMENU_COLUMNS: usize = 4;
diff --git a/training_mod_tui/src/list.rs b/training_mod_tui/src/list.rs
deleted file mode 100644
index 6742095..0000000
--- a/training_mod_tui/src/list.rs
+++ /dev/null
@@ -1,209 +0,0 @@
-use tui::widgets::ListState;
-
-pub struct MultiStatefulList<T> {
-    pub lists: Vec<StatefulList<T>>,
-    pub state: usize,
-    pub total_len: usize,
-}
-
-impl<T: Clone> MultiStatefulList<T> {
-    pub fn selected_list_item(&mut self) -> &mut T {
-        let (list_section, list_idx) = self.idx_to_list_idx(self.state);
-        &mut self.lists[list_section].items[list_idx]
-    }
-
-    pub fn idx_to_list_idx(&self, idx: usize) -> (usize, usize) {
-        self.idx_to_list_idx_opt(idx).unwrap_or((0, 0))
-    }
-
-    pub fn idx_to_list_idx_opt(&self, idx: usize) -> Option<(usize, usize)> {
-        for list_section in 0..self.lists.len() {
-            let list_section_min_idx =
-                (self.total_len as f32 / self.lists.len() as f32).ceil() as usize * list_section;
-            let list_section_max_idx = std::cmp::min(
-                (self.total_len as f32 / self.lists.len() as f32).ceil() as usize
-                    * (list_section + 1),
-                self.total_len,
-            );
-            if (list_section_min_idx..list_section_max_idx).contains(&idx) {
-                return Some((list_section, idx - list_section_min_idx));
-            }
-        }
-
-        None
-    }
-
-    fn list_idx_to_idx(&self, list_idx: (usize, usize)) -> usize {
-        let list_section = list_idx.0;
-        let mut list_idx = list_idx.1;
-        for list_section in 0..list_section {
-            list_idx += self.lists[list_section].items.len();
-        }
-        list_idx
-    }
-
-    pub fn with_items(items: Vec<T>, num_lists: usize) -> MultiStatefulList<T> {
-        let lists = (0..num_lists)
-            .map(|list_section| {
-                // Try to evenly chunk
-                let list_size = (items.len() as f32 / num_lists as f32).ceil() as usize;
-                let list_section_min_idx = std::cmp::min(list_size * list_section, items.len());
-                let list_section_max_idx =
-                    std::cmp::min(list_size * (list_section + 1), items.len());
-                let mut state = ListState::default();
-                if list_section == 0 {
-                    // Enforce state as first of list
-                    state.select(Some(0));
-                }
-                StatefulList {
-                    state,
-                    items: items[list_section_min_idx..list_section_max_idx].to_vec(),
-                }
-            })
-            .collect();
-        let total_len = items.len();
-        MultiStatefulList {
-            lists,
-            total_len,
-            state: 0,
-        }
-    }
-
-    pub fn next(&mut self) {
-        let (list_section, _) = self.idx_to_list_idx(self.state);
-        let (next_list_section, next_list_idx) = self.idx_to_list_idx(self.state + 1);
-
-        if list_section != next_list_section {
-            self.lists[list_section].unselect();
-        }
-        let state = if self.state + 1 >= self.total_len {
-            (0, 0)
-        } else {
-            (next_list_section, next_list_idx)
-        };
-
-        self.lists[state.0].state.select(Some(state.1));
-        self.state = self.list_idx_to_idx(state);
-    }
-
-    pub fn previous(&mut self) {
-        let (list_section, _) = self.idx_to_list_idx(self.state);
-        let mut last_list_section = self.lists.len() - 1;
-        let mut last_list_size = self.lists[last_list_section].items.len();
-
-        while last_list_size == 0 {
-            last_list_section -= 1;
-            last_list_size = self.lists[last_list_section].items.len();
-        }
-
-        let last_list_idx = last_list_size - 1;
-
-        self.lists[list_section].unselect();
-        let state = if self.state == 0 {
-            (last_list_section, last_list_idx)
-        } else {
-            let (prev_list_section, prev_list_idx) = self.idx_to_list_idx(self.state - 1);
-            (prev_list_section, prev_list_idx)
-        };
-
-        self.lists[state.0].state.select(Some(state.1));
-        self.state = self.list_idx_to_idx(state);
-    }
-
-    pub fn next_list(&mut self) {
-        let (list_section, list_idx) = self.idx_to_list_idx(self.state);
-        let mut next_list_section = (list_section + 1) % self.lists.len();
-        let mut next_list_len = self.lists[next_list_section].items.len();
-        while next_list_len == 0 {
-            next_list_section = (next_list_section + 1) % self.lists.len();
-            next_list_len = self.lists[next_list_section].items.len();
-        }
-        let next_list_idx = if list_idx > next_list_len - 1 {
-            next_list_len - 1
-        } else {
-            list_idx
-        };
-
-        if list_section != next_list_section {
-            self.lists[list_section].unselect();
-        }
-        let state = (next_list_section, next_list_idx);
-
-        self.lists[state.0].state.select(Some(state.1));
-        self.state = self.list_idx_to_idx(state);
-    }
-
-    pub fn previous_list(&mut self) {
-        let (list_section, list_idx) = self.idx_to_list_idx(self.state);
-        let mut prev_list_section = if list_section == 0 {
-            self.lists.len() - 1
-        } else {
-            list_section - 1
-        };
-
-        let mut prev_list_len = self.lists[prev_list_section].items.len();
-        while prev_list_len == 0 {
-            prev_list_section -= 1;
-            prev_list_len = self.lists[prev_list_section].items.len();
-        }
-        let prev_list_idx = if list_idx > prev_list_len - 1 {
-            prev_list_len - 1
-        } else {
-            list_idx
-        };
-
-        if list_section != prev_list_section {
-            self.lists[list_section].unselect();
-        }
-        let state = (prev_list_section, prev_list_idx);
-
-        self.lists[state.0].state.select(Some(state.1));
-        self.state = self.list_idx_to_idx(state);
-    }
-}
-
-pub struct StatefulList<T> {
-    pub state: ListState,
-    pub items: Vec<T>,
-}
-
-impl<T> StatefulList<T> {
-    pub fn with_items(items: Vec<T>) -> StatefulList<T> {
-        let mut state = ListState::default();
-        // Enforce state as first of list
-        state.select(Some(0));
-        StatefulList { state, items }
-    }
-
-    pub fn next(&mut self) {
-        let i = match self.state.selected() {
-            Some(i) => {
-                if i >= self.items.len() - 1 {
-                    0
-                } else {
-                    i + 1
-                }
-            }
-            None => 0,
-        };
-        self.state.select(Some(i));
-    }
-
-    pub fn previous(&mut self) {
-        let i = match self.state.selected() {
-            Some(i) => {
-                if i == 0 {
-                    self.items.len() - 1
-                } else {
-                    i - 1
-                }
-            }
-            None => 0,
-        };
-        self.state.select(Some(i));
-    }
-
-    pub fn unselect(&mut self) {
-        self.state.select(None);
-    }
-}
diff --git a/training_mod_tui/src/main.rs b/training_mod_tui/src/main.rs
deleted file mode 100644
index e1ecd1a..0000000
--- a/training_mod_tui/src/main.rs
+++ /dev/null
@@ -1,360 +0,0 @@
-#[cfg(feature = "has_terminal")]
-use crossterm::{
-    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
-    execute,
-    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
-};
-
-use std::error::Error;
-#[cfg(feature = "has_terminal")]
-use std::{
-    io,
-    time::{Duration, Instant},
-};
-#[cfg(feature = "has_terminal")]
-use tui::backend::CrosstermBackend;
-use tui::Terminal;
-
-use training_mod_consts::*;
-
-fn test_backend_setup(
-    ui_menu: UiMenu,
-    menu_defaults: (UiMenu, String),
-) -> Result<
-    (
-        Terminal<training_mod_tui::TestBackend>,
-        training_mod_tui::App,
-    ),
-    Box<dyn Error>,
-> {
-    let app = training_mod_tui::App::new(ui_menu, menu_defaults);
-    let backend = tui::backend::TestBackend::new(120, 15);
-    let terminal = Terminal::new(backend)?;
-    let mut state = tui::widgets::ListState::default();
-    state.select(Some(1));
-
-    Ok((terminal, app))
-}
-
-#[test]
-fn test_set_airdodge() -> Result<(), Box<dyn Error>> {
-    let menu;
-    let mut prev_menu;
-    let menu_defaults;
-    unsafe {
-        prev_menu = MENU.clone();
-        menu = ui_menu(MENU);
-        menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
-    }
-
-    let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
-    // Enter Mash Section
-    app.next_tab();
-    // Enter Mash Toggles
-    app.on_a();
-    // Set Mash Airdodge
-    app.on_a();
-    let menu_json = app.get_menu_selections();
-    let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
-    let menu = menu_struct.menu;
-    let _ = menu_struct.defaults_menu;
-    prev_menu.mash_state.toggle(Action::AIR_DODGE);
-    assert_eq!(
-        serde_json::to_string(&prev_menu).unwrap(),
-        serde_json::to_string(&menu).unwrap()
-    );
-
-    Ok(())
-}
-
-#[test]
-fn test_ensure_menu_retains_selections() -> Result<(), Box<dyn Error>> {
-    let menu;
-    let prev_menu;
-    let menu_defaults;
-    unsafe {
-        prev_menu = MENU.clone();
-        menu = ui_menu(MENU);
-        menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
-    }
-
-    let (_terminal, app) = test_backend_setup(menu, menu_defaults)?;
-    let menu_json = app.get_menu_selections();
-    let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
-    let menu = menu_struct.menu;
-    let _ = menu_struct.defaults_menu;
-    // At this point, we didn't change the menu at all; we should still see all the same options.
-    assert_eq!(
-        serde_json::to_string(&prev_menu).unwrap(),
-        serde_json::to_string(&menu).unwrap()
-    );
-
-    Ok(())
-}
-
-#[test]
-fn test_save_and_reset_defaults() -> Result<(), Box<dyn Error>> {
-    let menu;
-    let mut prev_menu;
-    let menu_defaults;
-    unsafe {
-        prev_menu = MENU.clone();
-        menu = ui_menu(MENU);
-        menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
-    }
-
-    let (_terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
-
-    // Enter Mash Section
-    app.next_tab();
-    // Enter Mash Toggles
-    app.on_a();
-    // Set Mash Airdodge
-    app.on_a();
-    // Return to submenu selection
-    app.on_b();
-    // Save Defaults
-    app.save_defaults();
-    // Enter Mash Toggles again
-    app.on_a();
-    // Unset Mash Airdodge
-    app.on_a();
-
-    let menu_json = app.get_menu_selections();
-    let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
-    let menu = menu_struct.menu;
-    let defaults_menu = menu_struct.defaults_menu;
-
-    assert_eq!(
-        serde_json::to_string(&prev_menu).unwrap(),
-        serde_json::to_string(&menu).unwrap(),
-        "The menu should be the same as how we started"
-    );
-    prev_menu.mash_state.toggle(Action::AIR_DODGE);
-    assert_eq!(
-        serde_json::to_string(&prev_menu).unwrap(),
-        serde_json::to_string(&defaults_menu).unwrap(),
-        "The defaults menu should have Mash Airdodge"
-    );
-
-    // Reset current menu alone to defaults
-    app.reset_current_submenu();
-    let menu_json = app.get_menu_selections();
-    let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
-    let menu = menu_struct.menu;
-    let _ = menu_struct.defaults_menu;
-
-    assert_eq!(
-        serde_json::to_string(&prev_menu).unwrap(),
-        serde_json::to_string(&menu).unwrap(),
-        "The menu should have Mash Airdodge"
-    );
-
-    // Enter Mash Section
-    app.next_tab();
-    // Enter Mash Toggles
-    app.on_a();
-    // Unset Mash Airdodge
-    app.on_a();
-    // Return to submenu selection
-    app.on_b();
-    // Go down to Followup Toggles
-    app.on_down();
-    // Enter Followup Toggles
-    app.on_a();
-    // Go down and set Jump
-    app.on_down();
-    app.on_a();
-    // Return to submenu selection
-    app.on_b();
-    // Save defaults
-    app.save_defaults();
-    // Go back in and unset Jump
-    app.on_a();
-    app.on_down();
-    app.on_a();
-    // Return to submenu selection
-    app.on_b();
-    // Reset all to defaults
-    app.reset_all_submenus();
-    let menu_json = app.get_menu_selections();
-    let menu_struct = serde_json::from_str::<MenuJsonStruct>(&menu_json).unwrap();
-    let menu = menu_struct.menu;
-    let _ = menu_struct.defaults_menu;
-
-    prev_menu.mash_state.toggle(Action::AIR_DODGE);
-    prev_menu.follow_up.toggle(Action::JUMP);
-    assert_eq!(
-        serde_json::to_string(&prev_menu).unwrap(),
-        serde_json::to_string(&menu).unwrap(),
-        "The menu should have Mash Airdodge off and Followup Jump on"
-    );
-
-    Ok(())
-}
-
-fn _get_frame_buffer(
-    mut terminal: Terminal<training_mod_tui::TestBackend>,
-    mut app: training_mod_tui::App,
-) -> Result<String, Box<dyn Error>> {
-    let frame_res = terminal.draw(|f| training_mod_tui::ui(f, &mut app))?;
-    let mut full_frame_buffer = String::new();
-    for (i, cell) in frame_res.buffer.content().iter().enumerate() {
-        full_frame_buffer.push_str(&cell.symbol);
-        if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
-            full_frame_buffer.push_str("\n");
-        }
-    }
-    full_frame_buffer.push_str("\n");
-
-    Ok(full_frame_buffer)
-}
-
-#[test]
-fn test_toggle_naming() -> Result<(), Box<dyn Error>> {
-    let menu;
-    let mut prev_menu;
-    let menu_defaults;
-    unsafe {
-        prev_menu = MENU.clone();
-        menu = ui_menu(MENU);
-        menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
-    }
-
-    let (mut terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
-    // Enter Mash Section
-    app.next_tab();
-    // Enter Mash Toggles
-    app.on_a();
-    // Set Mash Airdodge
-    app.on_a();
-
-    let frame_buffer = _get_frame_buffer(terminal, app)?;
-    assert!(frame_buffer.contains("Airdodge"));
-
-    Ok(())
-}
-
-fn main() -> Result<(), Box<dyn Error>> {
-    let args: Vec<String> = std::env::args().collect();
-    let inputs = args.get(1);
-    let menu;
-    let menu_defaults;
-
-    unsafe {
-        menu = ui_menu(MENU);
-        menu_defaults = (ui_menu(MENU), serde_json::to_string(&MENU).unwrap());
-    }
-
-    #[cfg(not(feature = "has_terminal"))]
-    {
-        let (mut terminal, mut app) = test_backend_setup(menu, menu_defaults)?;
-        if inputs.is_some() {
-            inputs
-                .unwrap()
-                .split(",")
-                .for_each(|input| match input.to_uppercase().as_str() {
-                    "X" => app.save_defaults(),
-                    "Y" => app.reset_current_submenu(),
-                    "Z" => app.reset_all_submenus(),
-                    "L" => app.previous_tab(),
-                    "R" => app.next_tab(),
-                    "A" => app.on_a(),
-                    "B" => app.on_b(),
-                    "UP" => app.on_up(),
-                    "DOWN" => app.on_down(),
-                    "LEFT" => app.on_left(),
-                    "RIGHT" => app.on_right(),
-                    _ => {}
-                })
-        }
-        let frame_res = terminal.draw(|f| training_mod_tui::ui(f, &mut app))?;
-        let menu_json = app.get_menu_selections();
-
-        for (i, cell) in frame_res.buffer.content().iter().enumerate() {
-            print!("{}", cell.symbol);
-            if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
-                println!();
-            }
-        }
-        println!();
-
-        println!("Menu:\n{menu_json}");
-    }
-
-    #[cfg(feature = "has_terminal")]
-    {
-        let app = training_mod_tui::App::new(menu, menu_defaults);
-
-        // setup terminal
-        enable_raw_mode()?;
-        let mut stdout = io::stdout();
-        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
-        let backend = CrosstermBackend::new(stdout);
-        let mut terminal = Terminal::new(backend)?;
-
-        let tick_rate = Duration::from_millis(250);
-        let res = run_app(&mut terminal, app, tick_rate);
-
-        // restore terminal
-        disable_raw_mode()?;
-        execute!(
-            terminal.backend_mut(),
-            LeaveAlternateScreen,
-            DisableMouseCapture
-        )?;
-        terminal.show_cursor()?;
-
-        if let Err(err) = res {
-            println!("{:?}", err)
-        } else {
-            println!("JSONs: {:#?}", res.as_ref().unwrap());
-            unsafe {
-                let menu = serde_json::from_str::<MenuJsonStruct>(&res.as_ref().unwrap()).unwrap();
-                println!("menu: {:#?}", menu);
-            }
-        }
-    }
-
-    Ok(())
-}
-
-#[cfg(feature = "has_terminal")]
-fn run_app<B: tui::backend::Backend>(
-    terminal: &mut Terminal<B>,
-    mut app: training_mod_tui::App,
-    tick_rate: Duration,
-) -> io::Result<String> {
-    let mut last_tick = Instant::now();
-    loop {
-        terminal.draw(|f| training_mod_tui::ui(f, &mut app).clone())?;
-        let menu_json = app.get_menu_selections();
-
-        let timeout = tick_rate
-            .checked_sub(last_tick.elapsed())
-            .unwrap_or_else(|| Duration::from_secs(0));
-
-        if crossterm::event::poll(timeout)? {
-            if let Event::Key(key) = event::read()? {
-                match key.code {
-                    KeyCode::Char('q') => return Ok(menu_json),
-                    KeyCode::Char('x') => app.save_defaults(),
-                    KeyCode::Char('p') => app.reset_current_submenu(),
-                    KeyCode::Char('o') => app.reset_all_submenus(),
-                    KeyCode::Char('r') => app.next_tab(),
-                    KeyCode::Char('l') => app.previous_tab(),
-                    KeyCode::Left => app.on_left(),
-                    KeyCode::Right => app.on_right(),
-                    KeyCode::Down => app.on_down(),
-                    KeyCode::Up => app.on_up(),
-                    KeyCode::Enter => app.on_a(),
-                    KeyCode::Backspace => app.on_b(),
-                    _ => {}
-                }
-            }
-        }
-        if last_tick.elapsed() >= tick_rate {
-            last_tick = Instant::now();
-        }
-    }
-}
diff --git a/training_mod_tui/src/structures/mod.rs b/training_mod_tui/src/structures/mod.rs
new file mode 100644
index 0000000..d464c84
--- /dev/null
+++ b/training_mod_tui/src/structures/mod.rs
@@ -0,0 +1,6 @@
+mod stateful_list;
+mod stateful_slider;
+mod stateful_table;
+pub use stateful_list::*;
+pub use stateful_slider::*;
+pub use stateful_table::*;
diff --git a/training_mod_tui/src/structures/stateful_list.rs b/training_mod_tui/src/structures/stateful_list.rs
new file mode 100644
index 0000000..fbc47bf
--- /dev/null
+++ b/training_mod_tui/src/structures/stateful_list.rs
@@ -0,0 +1,124 @@
+use ratatui::widgets::ListState;
+use serde::ser::{Serialize, SerializeSeq, Serializer};
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub struct StatefulList<T: Serialize> {
+    pub state: ListState,
+    pub items: Vec<T>,
+}
+
+impl<T: Serialize> IntoIterator for StatefulList<T> {
+    type Item = T;
+    type IntoIter = std::vec::IntoIter<Self::Item>;
+    fn into_iter(self) -> Self::IntoIter {
+        self.items.into_iter()
+    }
+}
+
+impl<T: Serialize> StatefulList<T> {
+    pub fn new() -> StatefulList<T> {
+        StatefulList {
+            state: ListState::default(),
+            items: Vec::new(),
+        }
+    }
+
+    pub fn with_items(items: Vec<T>) -> StatefulList<T> {
+        let mut state = ListState::default();
+        // Enforce state as first of list
+        state.select(Some(0));
+        StatefulList { state, items }
+    }
+
+    pub fn push(&mut self, item: T) {
+        self.items.push(item);
+    }
+
+    pub fn next(&mut self) {
+        let i = match self.state.selected() {
+            Some(i) => {
+                if i >= self.items.len() - 1 {
+                    0
+                } else {
+                    i + 1
+                }
+            }
+            None => 0,
+        };
+        self.state.select(Some(i));
+    }
+
+    pub fn previous(&mut self) {
+        let i = match self.state.selected() {
+            Some(i) => {
+                if i == 0 {
+                    self.items.len() - 1
+                } else {
+                    i - 1
+                }
+            }
+            None => 0,
+        };
+        self.state.select(Some(i));
+    }
+
+    pub fn unselect(&mut self) {
+        self.state.select(None);
+    }
+
+    pub fn get_selected(&mut self) -> Option<&mut T> {
+        if let Some(selected_index) = self.state.selected() {
+            Some(&mut self.items[selected_index])
+        } else {
+            None
+        }
+    }
+
+    pub fn get_before_selected(&mut self) -> Option<&mut T> {
+        let len = self.items.len();
+        if let Some(selected_index) = self.state.selected() {
+            if selected_index == 0 {
+                Some(&mut self.items[len - 1])
+            } else {
+                Some(&mut self.items[selected_index - 1])
+            }
+        } else {
+            None
+        }
+    }
+
+    pub fn get_after_selected(&mut self) -> Option<&mut T> {
+        let len = self.items.len();
+        if let Some(selected_index) = self.state.selected() {
+            if selected_index == len - 1 {
+                Some(&mut self.items[0])
+            } else {
+                Some(&mut self.items[selected_index + 1])
+            }
+        } else {
+            None
+        }
+    }
+}
+
+impl<T: Serialize> Serialize for StatefulList<T> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut seq = serializer.serialize_seq(Some(self.items.len()))?;
+        for e in self.items.iter() {
+            seq.serialize_element(e)?;
+        }
+        seq.end()
+    }
+}
+
+impl<T: Serialize> StatefulList<T> {
+    pub fn iter(&self) -> impl Iterator<Item = &T> + '_ {
+        self.items.iter()
+    }
+    pub fn iter_mut(&mut self) -> std::slice::IterMut<T> {
+        self.items.iter_mut()
+    }
+}
diff --git a/training_mod_tui/src/structures/stateful_slider.rs b/training_mod_tui/src/structures/stateful_slider.rs
new file mode 100644
index 0000000..fa47ee2
--- /dev/null
+++ b/training_mod_tui/src/structures/stateful_slider.rs
@@ -0,0 +1,146 @@
+use serde::{Serialize, Serializer};
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub enum SliderState {
+    LowerHover,
+    UpperHover,
+    LowerSelected,
+    UpperSelected,
+    None,
+}
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct StatefulSlider {
+    pub state: SliderState,
+    pub lower: u32,
+    pub upper: u32,
+    pub min: u32,
+    pub max: u32,
+    pub incr_amount_slow: u32,
+    pub incr_amount_fast: u32,
+}
+
+impl StatefulSlider {
+    pub fn new() -> StatefulSlider {
+        StatefulSlider {
+            state: SliderState::LowerHover,
+            lower: 0,
+            upper: 150,
+            min: 0,
+            max: 150,
+            incr_amount_slow: 1,
+            incr_amount_fast: 10,
+        }
+    }
+
+    pub fn increment_selected_slow(&mut self) {
+        match self.state {
+            SliderState::LowerSelected => {
+                self.lower = self
+                    .lower
+                    .saturating_add(self.incr_amount_slow)
+                    .min(self.upper); // Don't allow lower > upper
+            }
+            SliderState::UpperSelected => {
+                self.upper = self
+                    .upper
+                    .saturating_add(self.incr_amount_slow)
+                    .min(self.max); // Don't allow upper > max
+            }
+            _ => {}
+        }
+    }
+
+    pub fn increment_selected_fast(&mut self) {
+        match self.state {
+            SliderState::LowerSelected => {
+                self.lower = self
+                    .lower
+                    .saturating_add(self.incr_amount_fast)
+                    .min(self.upper); // Don't allow lower > upper
+            }
+            SliderState::UpperSelected => {
+                self.upper = self
+                    .upper
+                    .saturating_add(self.incr_amount_fast)
+                    .min(self.max); // Don't allow upper > max
+            }
+            _ => {}
+        }
+    }
+
+    pub fn decrement_selected_slow(&mut self) {
+        match self.state {
+            SliderState::LowerSelected => {
+                self.lower = self
+                    .lower
+                    .saturating_sub(self.incr_amount_slow)
+                    .max(self.min); // Don't allow lower < min
+            }
+            SliderState::UpperSelected => {
+                self.upper = self
+                    .upper
+                    .saturating_sub(self.incr_amount_slow)
+                    .max(self.lower); // Don't allow upper < lower
+            }
+            _ => {}
+        }
+    }
+
+    pub fn decrement_selected_fast(&mut self) {
+        match self.state {
+            SliderState::LowerSelected => {
+                self.lower = self
+                    .lower
+                    .saturating_sub(self.incr_amount_fast)
+                    .max(self.min); // Don't allow lower < min
+            }
+            SliderState::UpperSelected => {
+                self.upper = self
+                    .upper
+                    .saturating_sub(self.incr_amount_fast)
+                    .max(self.lower); // Don't allow upper < lower
+            }
+            _ => {}
+        }
+    }
+
+    pub fn select_deselect(&mut self) {
+        self.state = match self.state {
+            SliderState::LowerHover => SliderState::LowerSelected,
+            SliderState::LowerSelected => SliderState::LowerHover,
+            SliderState::UpperHover => SliderState::UpperSelected,
+            SliderState::UpperSelected => SliderState::UpperHover,
+            SliderState::None => SliderState::None,
+        }
+    }
+
+    pub fn deselect(&mut self) {
+        self.state = match self.state {
+            SliderState::LowerSelected => SliderState::LowerHover,
+            SliderState::UpperSelected => SliderState::UpperHover,
+            _ => self.state,
+        }
+    }
+
+    pub fn switch_hover(&mut self) {
+        self.state = match self.state {
+            SliderState::LowerHover => SliderState::UpperHover,
+            SliderState::UpperHover => SliderState::LowerHover,
+            _ => self.state,
+        }
+    }
+
+    pub fn is_handle_selected(&mut self) -> bool {
+        self.state == SliderState::LowerSelected || self.state == SliderState::UpperSelected
+    }
+}
+
+impl Serialize for StatefulSlider {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        [self.lower, self.upper].serialize(serializer)
+    }
+}
diff --git a/training_mod_tui/src/structures/stateful_table.rs b/training_mod_tui/src/structures/stateful_table.rs
new file mode 100644
index 0000000..de20f03
--- /dev/null
+++ b/training_mod_tui/src/structures/stateful_table.rs
@@ -0,0 +1,274 @@
+use ratatui::widgets::*;
+use serde::{Serialize, Serializer};
+
+/// Allows a snake-filled table of arbitrary size
+/// The final row does not need to be filled
+/// [ a , b , c , d ]
+/// [ e, f, g, h, i ]
+/// [ j, k          ]
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub struct StatefulTable<T: Clone + Serialize> {
+    pub state: TableState,
+    pub items: Vec<Vec<Option<T>>>,
+    pub rows: usize,
+    pub cols: usize,
+}
+
+// Size-related functions
+impl<T: Clone + Serialize> StatefulTable<T> {
+    pub fn len(&self) -> usize {
+        self.items
+            .iter()
+            .map(|row| {
+                row.iter()
+                    .map(|item| if item.is_some() { 1 } else { 0 })
+                    .sum::<usize>()
+            })
+            .sum()
+    }
+    pub fn full_len(&self) -> usize {
+        self.rows * self.cols
+    }
+    pub fn as_vec(&self) -> Vec<T> {
+        let mut v = Vec::with_capacity(self.len());
+        for row in self.items.iter() {
+            for item in row.iter() {
+                if let Some(i) = item {
+                    v.push(i.clone());
+                }
+            }
+        }
+        v
+    }
+}
+
+// Initalization Functions
+impl<T: Clone + Serialize> StatefulTable<T> {
+    pub fn new(rows: usize, cols: usize) -> Self {
+        Self {
+            state: TableState::default().with_selected(Some(TableSelection::default())),
+            items: vec![vec![None; cols]; rows],
+            rows: rows,
+            cols: cols,
+        }
+    }
+    pub fn with_items(rows: usize, cols: usize, v: Vec<T>) -> Self {
+        let mut table: Self = Self::new(rows, cols);
+        if v.len() > rows * cols {
+            panic!(
+                "Cannot create StatefulTable; too many items for size {}x{}: {}",
+                rows,
+                cols,
+                v.len()
+            );
+        } else {
+            for (i, item) in v.iter().enumerate() {
+                table.items[i.div_euclid(cols)][i.rem_euclid(cols)] = Some(item.clone());
+            }
+            table
+        }
+    }
+}
+
+// State Functions
+impl<T: Clone + Serialize> StatefulTable<T> {
+    pub fn select(&mut self, row: usize, col: usize) {
+        assert!(col < self.cols);
+        assert!(row < self.rows);
+        self.state.select(Some(TableSelection::Cell { row, col }));
+    }
+
+    pub fn get_selected(&mut self) -> Option<&mut T> {
+        self.items[self.state.selected_row().unwrap()][self.state.selected_col().unwrap()].as_mut()
+    }
+
+    pub fn get(&self, row: usize, column: usize) -> Option<&T> {
+        if row >= self.rows || column >= self.cols {
+            None
+        } else {
+            self.items[row][column].as_ref()
+        }
+    }
+
+    pub fn get_mut(&mut self, row: usize, column: usize) -> Option<&mut T> {
+        if row >= self.rows || column >= self.cols {
+            None
+        } else {
+            self.items[row][column].as_mut()
+        }
+    }
+
+    pub fn get_by_idx(&self, idx: usize) -> Option<&T> {
+        let row = idx.div_euclid(self.cols);
+        let col = idx.rem_euclid(self.cols);
+        self.get(row, col)
+    }
+    pub fn get_by_idx_mut(&mut self, idx: usize) -> Option<&mut T> {
+        let row = idx.div_euclid(self.cols);
+        let col = idx.rem_euclid(self.cols);
+        self.get_mut(row, col)
+    }
+
+    pub fn next_row(&mut self) {
+        let next_row = match self.state.selected_row() {
+            Some(row) => {
+                if row == self.items.len() - 1 {
+                    0
+                } else {
+                    row + 1
+                }
+            }
+
+            None => 0,
+        };
+        self.state.select_row(Some(next_row));
+    }
+
+    pub fn next_row_checked(&mut self) {
+        self.next_row();
+        if self.get_selected().is_none() {
+            self.next_row_checked();
+        }
+    }
+
+    pub fn prev_row(&mut self) {
+        let prev_row = match self.state.selected_row() {
+            Some(row) => {
+                if row == 0 {
+                    self.items.len() - 1
+                } else {
+                    row - 1
+                }
+            }
+            None => 0,
+        };
+
+        self.state.select_row(Some(prev_row));
+    }
+
+    pub fn prev_row_checked(&mut self) {
+        self.prev_row();
+        if self.get_selected().is_none() {
+            self.prev_row_checked();
+        }
+    }
+
+    pub fn next_col(&mut self) {
+        // Assumes that all rows are the same width
+        let next_col = match self.state.selected_col() {
+            Some(col) => {
+                if col == self.items[0].len() - 1 {
+                    0
+                } else {
+                    col + 1
+                }
+            }
+            None => 0,
+        };
+        self.state.select_col(Some(next_col));
+    }
+
+    pub fn next_col_checked(&mut self) {
+        self.next_col();
+        if self.get_selected().is_none() {
+            self.next_col_checked();
+        }
+    }
+
+    pub fn prev_col(&mut self) {
+        let prev_col = match self.state.selected_col() {
+            Some(col) => {
+                if col == 0 {
+                    self.items[0].len() - 1
+                } else {
+                    col - 1
+                }
+            }
+            None => 0,
+        };
+        self.state.select_col(Some(prev_col));
+    }
+
+    pub fn prev_col_checked(&mut self) {
+        self.prev_col();
+        self.carriage_return();
+    }
+
+    /// If the selected cell is None, move selection to the left until you get Some.
+    /// No-op if the selected cell is Some.
+    /// For example, a 2x3 table with 4 elements would shift the selection from 1,2 to 1,0
+    ///
+    /// [ a ,  b ,  c ]
+    /// [ d ,  e , [ ]]
+    ///
+    ///        |
+    ///        V
+    ///
+    /// [ a ,  b ,  c ]
+    /// [[d],    ,    ]
+    pub fn carriage_return(&mut self) {
+        assert!(
+            self.items[self.state.selected_row().unwrap()]
+                .iter()
+                .any(|x| x.is_some()),
+            "Carriage return called on an empty row!"
+        );
+        if self.get_selected().is_none() {
+            self.prev_col();
+            self.carriage_return();
+        }
+    }
+}
+
+impl<T: Clone + Serialize> Serialize for StatefulTable<T> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let flat: Vec<T> = self.as_vec();
+        flat.serialize(serializer)
+    }
+}
+
+// Implement .iter() for StatefulTable
+pub struct StatefulTableIterator<'a, T: Clone + Serialize> {
+    stateful_table: &'a StatefulTable<T>,
+    index: usize,
+}
+
+impl<'a, T: Clone + Serialize> Iterator for StatefulTableIterator<'a, T> {
+    type Item = &'a T;
+    fn next(&mut self) -> Option<Self::Item> {
+        self.index += 1;
+        self.stateful_table.get_by_idx(self.index - 1)
+    }
+}
+
+impl<T: Clone + Serialize> StatefulTable<T> {
+    pub fn iter(&self) -> StatefulTableIterator<T> {
+        StatefulTableIterator {
+            stateful_table: self,
+            index: 0,
+        }
+    }
+}
+
+pub struct StatefulTableIteratorMut<'a, T: Clone + Serialize> {
+    inner: std::iter::Flatten<std::slice::IterMut<'a, Vec<Option<T>>>>,
+}
+
+impl<'a, T: Clone + Serialize> Iterator for StatefulTableIteratorMut<'a, T> {
+    type Item = &'a mut Option<T>;
+    fn next(&mut self) -> Option<Self::Item> {
+        self.inner.next()
+    }
+}
+
+impl<'a, T: Clone + Serialize + 'a> StatefulTable<T> {
+    pub fn iter_mut(&'a mut self) -> StatefulTableIteratorMut<T> {
+        StatefulTableIteratorMut {
+            inner: self.items.iter_mut().flatten(),
+        }
+    }
+}
diff --git a/training_mod_tui/tests/test_stateful_list.rs b/training_mod_tui/tests/test_stateful_list.rs
new file mode 100644
index 0000000..c766198
--- /dev/null
+++ b/training_mod_tui/tests/test_stateful_list.rs
@@ -0,0 +1,182 @@
+use ratatui::widgets::ListState;
+use training_mod_tui_2::StatefulList;
+
+fn initialize_list(selected: Option<usize>) -> StatefulList<u8> {
+    StatefulList {
+        state: initialize_state(selected),
+        items: vec![10, 20, 30, 40],
+    }
+}
+
+fn initialize_state(selected: Option<usize>) -> ListState {
+    let mut state = ListState::default();
+    state.select(selected);
+    state
+}
+
+#[test]
+fn stateful_list_test_new() {
+    let l = initialize_list(None);
+    assert_eq!(
+        l,
+        StatefulList {
+            state: ListState::default(),
+            items: vec![10, 20, 30, 40],
+        }
+    );
+}
+
+#[test]
+fn stateful_list_with_items() {
+    let l = initialize_list(Some(0));
+    let m = StatefulList::<u8>::with_items(vec![10, 20, 30, 40]);
+    assert_eq!(l, m);
+}
+
+#[test]
+fn stateful_list_next() {
+    let mut l = initialize_list(Some(0));
+    let mut state = ListState::default();
+    state.select(Some(0));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 10));
+    l.next();
+    state.select(Some(1));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 20));
+    l.next();
+    state.select(Some(2));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 30));
+    l.next();
+    state.select(Some(3));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 40));
+    l.next();
+    state.select(Some(0));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 10));
+}
+
+#[test]
+fn stateful_list_prev() {
+    let mut l = initialize_list(Some(0));
+    let mut state = ListState::default();
+    state.select(Some(0));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 10));
+    l.previous();
+    state.select(Some(3));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 40));
+    l.previous();
+    state.select(Some(2));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 30));
+    l.previous();
+    state.select(Some(1));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 20));
+    l.previous();
+    state.select(Some(0));
+    assert_eq!(l.state, state);
+    assert_eq!(l.get_selected(), Some(&mut 10));
+}
+
+#[test]
+fn stateful_list_unselect() {
+    let mut l = initialize_list(Some(0));
+    let state = ListState::default();
+    l.unselect();
+    assert_eq!(l.state, state);
+    l.unselect();
+    assert_eq!(l.state, state);
+}
+
+#[test]
+fn stateful_list_get_selected() {
+    let mut l = initialize_list(None);
+    assert_eq!(l.get_selected(), None);
+    l.state.select(Some(0));
+    assert_eq!(l.get_selected(), Some(&mut 10));
+    l.state.select(Some(1));
+    assert_eq!(l.get_selected(), Some(&mut 20));
+    l.state.select(Some(2));
+    assert_eq!(l.get_selected(), Some(&mut 30));
+    l.state.select(Some(3));
+    assert_eq!(l.get_selected(), Some(&mut 40));
+}
+
+#[test]
+fn stateful_list_get_before_selected() {
+    let mut l = initialize_list(None);
+    assert_eq!(l.get_before_selected(), None);
+    l.state.select(Some(0));
+    assert_eq!(l.get_before_selected(), Some(&mut 40));
+    l.state.select(Some(1));
+    assert_eq!(l.get_before_selected(), Some(&mut 10));
+    l.state.select(Some(2));
+    assert_eq!(l.get_before_selected(), Some(&mut 20));
+    l.state.select(Some(3));
+    assert_eq!(l.get_before_selected(), Some(&mut 30));
+}
+
+#[test]
+fn stateful_list_get_after_selected() {
+    let mut l = initialize_list(None);
+    assert_eq!(l.get_after_selected(), None);
+    l.state.select(Some(0));
+    assert_eq!(l.get_after_selected(), Some(&mut 20));
+    l.state.select(Some(1));
+    assert_eq!(l.get_after_selected(), Some(&mut 30));
+    l.state.select(Some(2));
+    assert_eq!(l.get_after_selected(), Some(&mut 40));
+    l.state.select(Some(3));
+    assert_eq!(l.get_after_selected(), Some(&mut 10));
+}
+
+#[test]
+fn stateful_list_serialize() {
+    let l = initialize_list(Some(2));
+    let l_json = serde_json::to_string(&l).unwrap();
+    assert_eq!(&l_json, "[10,20,30,40]");
+}
+
+#[test]
+fn stateful_list_iter() {
+    let l = initialize_list(Some(2));
+    let mut l_iter = l.iter();
+    assert_eq!(l_iter.next(), Some(&10));
+    assert_eq!(l_iter.next(), Some(&20));
+    assert_eq!(l_iter.next(), Some(&30));
+    assert_eq!(l_iter.next(), Some(&40));
+    assert_eq!(l_iter.next(), None);
+    assert_eq!(l_iter.next(), None);
+}
+
+#[test]
+fn stateful_list_iter_mut() {
+    let mut l = initialize_list(Some(2));
+    let mut l_iter_mut = l.iter_mut();
+    assert_eq!(l_iter_mut.next(), Some(&mut 10));
+    assert_eq!(l_iter_mut.next(), Some(&mut 20));
+    assert_eq!(l_iter_mut.next(), Some(&mut 30));
+    assert_eq!(l_iter_mut.next(), Some(&mut 40));
+    assert_eq!(l_iter_mut.next(), None);
+    assert_eq!(l_iter_mut.next(), None);
+}
+
+#[test]
+fn stateful_list_push() {
+    let mut l = initialize_list(None);
+    l.push(5);
+    l.push(6);
+    l.push(7);
+    assert_eq!(
+        l,
+        StatefulList {
+            state: ListState::default(),
+            items: vec![10, 20, 30, 40, 5, 6, 7],
+        }
+    );
+}
diff --git a/training_mod_tui/tests/test_stateful_slider.rs b/training_mod_tui/tests/test_stateful_slider.rs
new file mode 100644
index 0000000..8acbed0
--- /dev/null
+++ b/training_mod_tui/tests/test_stateful_slider.rs
@@ -0,0 +1,288 @@
+use training_mod_tui_2::{SliderState, StatefulSlider};
+
+fn initialize_slider(state: SliderState) -> StatefulSlider {
+    StatefulSlider {
+        state: state,
+        lower: 0,
+        upper: 150,
+        min: 0,
+        max: 150,
+        incr_amount_slow: 1,
+        incr_amount_fast: 10,
+    }
+}
+
+#[test]
+fn stateful_slider_new() {
+    let s = initialize_slider(SliderState::LowerHover);
+    assert_eq!(s, StatefulSlider::new());
+}
+
+#[test]
+fn stateful_slider_increment_selected_slow() {
+    let mut s = initialize_slider(SliderState::LowerHover);
+    s.upper = 50;
+
+    // Check LowerHover: no increment
+    s.increment_selected_slow();
+    assert_eq!(s.lower, 0);
+
+    // Check LowerSelected: lower increments
+    s.state = SliderState::LowerSelected;
+    s.increment_selected_slow();
+    assert_eq!(s.lower, 1);
+
+    // Check LowerSelected: lower can't go above upper
+    s.lower = s.upper;
+    s.increment_selected_slow();
+    assert_eq!(s.lower, s.upper);
+
+    // Check UpperHover: no increment
+    s.lower = 5;
+    s.upper = 50;
+    s.state = SliderState::UpperHover;
+    s.increment_selected_slow();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, 50);
+
+    // Check UpperSelected: upper increments
+    s.state = SliderState::UpperSelected;
+    s.increment_selected_slow();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, 51);
+
+    // Check UpperSelected: upper can't go above max
+    s.upper = s.max;
+    s.increment_selected_slow();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, s.max);
+}
+
+#[test]
+fn stateful_slider_increment_selected_fast() {
+    let mut s = initialize_slider(SliderState::LowerHover);
+    s.upper = 50;
+
+    // Check LowerHover: no increment
+    s.increment_selected_fast();
+    assert_eq!(s.lower, 0);
+
+    // Check LowerSelected: lower increments
+    s.state = SliderState::LowerSelected;
+    s.increment_selected_fast();
+    assert_eq!(s.lower, 10);
+
+    // Check LowerSelected: lower can't go above upper
+    s.lower = s.upper;
+    s.increment_selected_fast();
+    assert_eq!(s.lower, s.upper);
+
+    // Check UpperHover: no increment
+    s.lower = 5;
+    s.upper = 50;
+    s.state = SliderState::UpperHover;
+    s.increment_selected_fast();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, 50);
+
+    // Check UpperSelected: upper increments
+    s.state = SliderState::UpperSelected;
+    s.increment_selected_fast();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, 60);
+
+    // Check UpperSelected: upper can't go above max
+    s.upper = s.max;
+    s.increment_selected_fast();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, s.max);
+}
+
+#[test]
+fn stateful_slider_decrement_selected_slow() {
+    let mut s = initialize_slider(SliderState::LowerHover);
+    s.min = 4;
+    s.lower = 5;
+    s.upper = 50;
+
+    // Check LowerHover: no decrement
+    s.decrement_selected_slow();
+    assert_eq!(s.lower, 5);
+
+    // Check LowerSelected: lower decrements
+    s.state = SliderState::LowerSelected;
+    s.decrement_selected_slow();
+    assert_eq!(s.lower, 4);
+
+    // Check LowerSelected: lower can't go below min
+    s.lower = s.min;
+    s.decrement_selected_slow();
+    assert_eq!(s.lower, s.min);
+
+    // Check LowerSelected: lower can't go below 0
+    s.min = 0;
+    s.lower = 0;
+    s.decrement_selected_slow();
+    assert_eq!(s.lower, 0);
+
+    // Check UpperHover: no decrement
+    s.lower = 5;
+    s.upper = 50;
+    s.state = SliderState::UpperHover;
+    s.decrement_selected_slow();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, 50);
+
+    // Check UpperSelected: upper decrements
+    s.state = SliderState::UpperSelected;
+    s.decrement_selected_slow();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, 49);
+
+    // Check UpperSelected: upper can't go below lower
+    s.upper = s.lower;
+    s.decrement_selected_slow();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, s.lower);
+}
+
+#[test]
+fn stateful_slider_decrement_selected_fast() {
+    let mut s = initialize_slider(SliderState::LowerHover);
+    s.min = 20;
+    s.lower = 35;
+    s.upper = 50;
+
+    // Check LowerHover: no decrement
+    s.decrement_selected_fast();
+    assert_eq!(s.lower, 35);
+
+    // Check LowerSelected: lower decrements
+    s.state = SliderState::LowerSelected;
+    s.decrement_selected_fast();
+    assert_eq!(s.lower, 25);
+
+    // Check LowerSelected: lower can't go below min
+    s.lower = s.min;
+    s.decrement_selected_fast();
+    assert_eq!(s.lower, s.min);
+
+    // Check LowerSelected: lower can't go below 0
+    s.min = 0;
+    s.lower = 0;
+    s.decrement_selected_fast();
+    assert_eq!(s.lower, 0);
+
+    // Check UpperHover: no decrement
+    s.lower = 5;
+    s.upper = 50;
+    s.state = SliderState::UpperHover;
+    s.decrement_selected_fast();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, 50);
+
+    // Check UpperSelected: upper decrements
+    s.state = SliderState::UpperSelected;
+    s.decrement_selected_fast();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, 40);
+
+    // Check UpperSelected: upper can't go below lower
+    s.upper = s.lower;
+    s.decrement_selected_fast();
+    assert_eq!(s.lower, 5);
+    assert_eq!(s.upper, s.lower);
+}
+
+#[test]
+fn stateful_slider_select_deselect() {
+    let mut s = initialize_slider(SliderState::LowerHover);
+    assert_eq!(s.state, SliderState::LowerHover);
+    s.select_deselect();
+    assert_eq!(s.state, SliderState::LowerSelected);
+    s.select_deselect();
+    assert_eq!(s.state, SliderState::LowerHover);
+    s.state = SliderState::UpperHover;
+    s.select_deselect();
+    assert_eq!(s.state, SliderState::UpperSelected);
+    s.select_deselect();
+    assert_eq!(s.state, SliderState::UpperHover);
+    s.state = SliderState::None;
+    s.select_deselect();
+    assert_eq!(s.state, SliderState::None);
+}
+
+#[test]
+fn stateful_slider_deselect() {
+    let mut s = initialize_slider(SliderState::LowerHover);
+    s.deselect();
+    assert_eq!(s.state, SliderState::LowerHover);
+
+    s.state = SliderState::LowerSelected;
+    s.deselect();
+    assert_eq!(s.state, SliderState::LowerHover);
+
+    s.state = SliderState::UpperHover;
+    s.deselect();
+    assert_eq!(s.state, SliderState::UpperHover);
+
+    s.state = SliderState::UpperSelected;
+    s.deselect();
+    assert_eq!(s.state, SliderState::UpperHover);
+
+    s.state = SliderState::None;
+    s.deselect();
+    assert_eq!(s.state, SliderState::None);
+}
+
+#[test]
+fn stateful_slider_switch_hover() {
+    let mut s = initialize_slider(SliderState::LowerHover);
+    s.switch_hover();
+    assert_eq!(s.state, SliderState::UpperHover);
+
+    s.state = SliderState::LowerSelected;
+    s.switch_hover();
+    assert_eq!(s.state, SliderState::LowerSelected);
+
+    s.state = SliderState::UpperHover;
+    s.switch_hover();
+    assert_eq!(s.state, SliderState::LowerHover);
+
+    s.state = SliderState::UpperSelected;
+    s.switch_hover();
+    assert_eq!(s.state, SliderState::UpperSelected);
+
+    s.state = SliderState::None;
+    s.switch_hover();
+    assert_eq!(s.state, SliderState::None);
+}
+
+#[test]
+fn stateful_slider_is_handle_selected() {
+    let mut s = initialize_slider(SliderState::LowerHover);
+    assert_eq!(s.is_handle_selected(), false);
+
+    s.state = SliderState::LowerSelected;
+    assert_eq!(s.is_handle_selected(), true);
+
+    s.state = SliderState::UpperHover;
+    assert_eq!(s.is_handle_selected(), false);
+
+    s.state = SliderState::UpperSelected;
+    assert_eq!(s.is_handle_selected(), true);
+
+    s.state = SliderState::None;
+    assert_eq!(s.is_handle_selected(), false);
+}
+
+#[test]
+fn stateful_slider_serialize() {
+    let mut s = initialize_slider(SliderState::LowerHover);
+    let s_json = serde_json::to_string(&s).unwrap();
+    assert_eq!(&s_json, "[0,150]");
+    s.lower = 25;
+    s.upper = 75;
+    let s_json = serde_json::to_string(&s).unwrap();
+    assert_eq!(&s_json, "[25,75]");
+}
diff --git a/training_mod_tui/tests/test_stateful_table.rs b/training_mod_tui/tests/test_stateful_table.rs
new file mode 100644
index 0000000..3e4ad3b
--- /dev/null
+++ b/training_mod_tui/tests/test_stateful_table.rs
@@ -0,0 +1,342 @@
+use ratatui::widgets::{TableSelection, TableState};
+use training_mod_tui_2::StatefulTable;
+
+fn initialize_table(row: usize, col: usize) -> StatefulTable<u8> {
+    let mut s = StatefulTable::with_items(2, 3, vec![0, 1, 2, 3, 4]);
+    s.select(row, col);
+    s
+}
+
+fn tablestate_with(row: usize, col: usize) -> TableState {
+    TableState::default().with_selected(Some(TableSelection::Cell { row, col }))
+}
+
+#[test]
+fn stateful_table_next_col_full() {
+    let mut t = initialize_table(0, 0);
+    assert_eq!(t.get_selected(), Some(&mut 0));
+    t.next_col();
+    assert_eq!(t.get_selected(), Some(&mut 1));
+    t.next_col();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.next_col();
+    assert_eq!(t.get_selected(), Some(&mut 0));
+}
+
+#[test]
+fn stateful_table_next_col_checked_full() {
+    let mut t = initialize_table(0, 0);
+    assert_eq!(t.get_selected(), Some(&mut 0));
+    t.next_col_checked();
+    assert_eq!(t.get_selected(), Some(&mut 1));
+    t.next_col_checked();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.next_col_checked();
+    assert_eq!(t.get_selected(), Some(&mut 0));
+}
+
+#[test]
+fn stateful_table_prev_col_full() {
+    let mut t = initialize_table(0, 0);
+    assert_eq!(t.get_selected(), Some(&mut 0));
+    t.prev_col();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.prev_col();
+    assert_eq!(t.get_selected(), Some(&mut 1));
+    t.prev_col();
+    assert_eq!(t.get_selected(), Some(&mut 0));
+}
+
+#[test]
+fn stateful_table_prev_col_checked_full() {
+    let mut t = initialize_table(0, 0);
+    assert_eq!(t.get_selected(), Some(&mut 0));
+    t.prev_col_checked();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.prev_col_checked();
+    assert_eq!(t.get_selected(), Some(&mut 1));
+    t.prev_col_checked();
+    assert_eq!(t.get_selected(), Some(&mut 0));
+}
+
+#[test]
+fn stateful_table_next_col_short() {
+    let mut t = initialize_table(1, 0);
+    assert_eq!(t.get_selected(), Some(&mut 3));
+    t.next_col();
+    assert_eq!(t.get_selected(), Some(&mut 4));
+    t.next_col();
+    assert_eq!(t.get_selected(), None);
+    t.next_col();
+    assert_eq!(t.get_selected(), Some(&mut 3));
+}
+
+#[test]
+fn stateful_table_next_col_checked_short() {
+    let mut t = initialize_table(1, 0);
+    assert_eq!(t.get_selected(), Some(&mut 3));
+    t.next_col_checked();
+    assert_eq!(t.get_selected(), Some(&mut 4));
+    t.next_col_checked();
+    assert_eq!(t.get_selected(), Some(&mut 3));
+}
+
+#[test]
+fn stateful_table_prev_col_short() {
+    let mut t = initialize_table(1, 0);
+    assert_eq!(t.get_selected(), Some(&mut 3));
+    t.prev_col();
+    assert_eq!(t.get_selected(), None);
+    t.prev_col();
+    assert_eq!(t.get_selected(), Some(&mut 4));
+    t.prev_col();
+    assert_eq!(t.get_selected(), Some(&mut 3));
+}
+
+#[test]
+fn stateful_table_carriage_return_none() {
+    let mut t = initialize_table(1, 2);
+    t.carriage_return();
+    assert_eq!(t.state, tablestate_with(1, 1));
+}
+
+#[test]
+fn stateful_table_carriage_return_some() {
+    let mut t = initialize_table(1, 1);
+    t.carriage_return();
+    assert_eq!(t.state, tablestate_with(1, 1));
+}
+
+#[test]
+fn stateful_table_table_with_items() {
+    let items: Vec<u8> = vec![0, 1, 2, 3, 4];
+    let t: StatefulTable<u8> = StatefulTable::with_items(2, 3, items);
+    let u = initialize_table(0, 0);
+    assert_eq!(t, u);
+}
+
+#[test]
+fn stateful_table_get_selected() {
+    let mut t = initialize_table(1, 1);
+    assert_eq!(t.get_selected(), Some(&mut 4));
+}
+
+#[test]
+fn stateful_table_get() {
+    let t = initialize_table(1, 1);
+    assert_eq!(t.get(0, 0), Some(&0));
+    assert_eq!(t.get(0, 1), Some(&1));
+    assert_eq!(t.get(0, 2), Some(&2));
+    assert_eq!(t.get(1, 0), Some(&3));
+    assert_eq!(t.get(1, 1), Some(&4));
+    assert_eq!(t.get(1, 2), None);
+    assert_eq!(t.get(10, 0), None);
+    assert_eq!(t.get(0, 10), None);
+}
+
+#[test]
+fn stateful_table_get_by_idx() {
+    let t = initialize_table(1, 1);
+    assert_eq!(t.get_by_idx(0), Some(&0));
+    assert_eq!(t.get_by_idx(1), Some(&1));
+    assert_eq!(t.get_by_idx(2), Some(&2));
+    assert_eq!(t.get_by_idx(3), Some(&3));
+    assert_eq!(t.get_by_idx(4), Some(&4));
+    assert_eq!(t.get_by_idx(5), None);
+    assert_eq!(t.get_by_idx(50), None);
+}
+
+#[test]
+fn stateful_table_len() {
+    let t = initialize_table(1, 1);
+    assert_eq!(t.len(), 5);
+}
+
+#[test]
+fn stateful_table_full_len() {
+    let t = initialize_table(0, 0);
+    assert_eq!(t.full_len(), 6);
+}
+
+#[test]
+fn stateful_table_serialize() {
+    let t = initialize_table(1, 1);
+    let t_ser = serde_json::to_string(&t).unwrap();
+    assert_eq!(&t_ser, "[0,1,2,3,4]");
+}
+
+#[test]
+fn stateful_table_new() {
+    let t: StatefulTable<u8> = StatefulTable::new(2, 3);
+    let u: StatefulTable<u8> = StatefulTable::with_items(2, 3, vec![]);
+    let v: StatefulTable<u8> = StatefulTable {
+        state: tablestate_with(0, 0),
+        items: vec![vec![None; 3]; 2],
+        rows: 2,
+        cols: 3,
+    };
+    assert_eq!(t, u);
+    assert_eq!(t, v);
+}
+
+#[test]
+fn stateful_table_with_items() {
+    let t: StatefulTable<u8> = StatefulTable::with_items(2, 3, vec![1, 2]);
+    let u: StatefulTable<u8> = StatefulTable {
+        state: tablestate_with(0, 0),
+        items: vec![vec![Some(1), Some(2), None], vec![None; 3]],
+        rows: 2,
+        cols: 3,
+    };
+    assert_eq!(t, u);
+}
+
+#[test]
+fn stateful_table_select() {
+    let mut t = initialize_table(0, 0);
+    assert_eq!(t.get_selected(), Some(&mut 0));
+    t.select(0, 1);
+    assert_eq!(t.get_selected(), Some(&mut 1));
+    t.select(0, 2);
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.select(1, 0);
+    assert_eq!(t.get_selected(), Some(&mut 3));
+    t.select(1, 1);
+    assert_eq!(t.get_selected(), Some(&mut 4));
+    t.select(1, 2);
+    assert_eq!(t.get_selected(), None);
+}
+
+#[test]
+fn stateful_table_get_mut() {
+    let mut t = initialize_table(1, 1);
+    assert_eq!(t.get_mut(0, 0), Some(&mut 0));
+    assert_eq!(t.get_mut(0, 1), Some(&mut 1));
+    assert_eq!(t.get_mut(0, 2), Some(&mut 2));
+    assert_eq!(t.get_mut(1, 0), Some(&mut 3));
+    assert_eq!(t.get_mut(1, 1), Some(&mut 4));
+    assert_eq!(t.get_mut(1, 2), None);
+    assert_eq!(t.get_mut(10, 0), None);
+    assert_eq!(t.get_mut(0, 10), None);
+}
+
+#[test]
+fn stateful_table_get_by_idx_mut() {
+    let mut t = initialize_table(1, 1);
+    assert_eq!(t.get_by_idx_mut(0), Some(&mut 0));
+    assert_eq!(t.get_by_idx_mut(1), Some(&mut 1));
+    assert_eq!(t.get_by_idx_mut(2), Some(&mut 2));
+    assert_eq!(t.get_by_idx_mut(3), Some(&mut 3));
+    assert_eq!(t.get_by_idx_mut(4), Some(&mut 4));
+    assert_eq!(t.get_by_idx_mut(5), None);
+    assert_eq!(t.get_by_idx_mut(50), None);
+}
+
+#[test]
+fn stateful_table_next_row_full() {
+    let mut t = initialize_table(0, 0);
+    assert_eq!(t.get_selected(), Some(&mut 0));
+    t.next_row();
+    assert_eq!(t.get_selected(), Some(&mut 3));
+    t.next_row();
+    assert_eq!(t.get_selected(), Some(&mut 0));
+}
+
+#[test]
+fn stateful_table_next_row_short() {
+    let mut t = initialize_table(0, 2);
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.next_row();
+    assert_eq!(t.get_selected(), None);
+    t.next_row();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+}
+
+#[test]
+fn stateful_table_next_row_checked_full() {
+    let mut t = initialize_table(0, 0);
+    assert_eq!(t.get_selected(), Some(&mut 0));
+    t.next_row_checked();
+    assert_eq!(t.get_selected(), Some(&mut 3));
+    t.next_row_checked();
+    assert_eq!(t.get_selected(), Some(&mut 0));
+}
+
+#[test]
+fn stateful_table_next_row_checked_short() {
+    let mut t = initialize_table(0, 2);
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.next_row_checked();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.next_row_checked();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+}
+
+#[test]
+fn stateful_table_prev_row_full() {
+    let mut t = initialize_table(0, 0);
+    assert_eq!(t.get_selected(), Some(&mut 0));
+    t.prev_row();
+    assert_eq!(t.get_selected(), Some(&mut 3));
+    t.prev_row();
+    assert_eq!(t.get_selected(), Some(&mut 0));
+}
+
+#[test]
+fn stateful_table_prev_row_short() {
+    let mut t = initialize_table(0, 2);
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.prev_row();
+    assert_eq!(t.get_selected(), None);
+    t.prev_row();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+}
+
+#[test]
+fn stateful_table_prev_row_checked_full() {
+    let mut t = initialize_table(0, 0);
+    assert_eq!(t.get_selected(), Some(&mut 0));
+    t.prev_row_checked();
+    assert_eq!(t.get_selected(), Some(&mut 3));
+    t.prev_row_checked();
+    assert_eq!(t.get_selected(), Some(&mut 0));
+}
+
+#[test]
+fn stateful_table_prev_row_checked_short() {
+    let mut t = initialize_table(0, 2);
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.prev_row_checked();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+    t.prev_row_checked();
+    assert_eq!(t.get_selected(), Some(&mut 2));
+}
+
+#[test]
+fn stateful_table_iter() {
+    let t = initialize_table(0, 0);
+    let mut t_iter = t.iter();
+    assert_eq!(t_iter.next(), Some(&0));
+    assert_eq!(t_iter.next(), Some(&1));
+    assert_eq!(t_iter.next(), Some(&2));
+    assert_eq!(t_iter.next(), Some(&3));
+    assert_eq!(t_iter.next(), Some(&4));
+    assert_eq!(t_iter.next(), None);
+    assert_eq!(t_iter.next(), None);
+}
+
+#[test]
+fn stateful_table_iter_mut() {
+    let mut t = initialize_table(0, 0);
+    for item in t.iter_mut().filter(|item| item.is_some()) {
+        *item = Some(item.unwrap() + 10);
+    }
+    let mut t_iter = t.iter();
+    assert_eq!(t_iter.next(), Some(&10));
+    assert_eq!(t_iter.next(), Some(&11));
+    assert_eq!(t_iter.next(), Some(&12));
+    assert_eq!(t_iter.next(), Some(&13));
+    assert_eq!(t_iter.next(), Some(&14));
+    assert_eq!(t_iter.next(), None);
+    assert_eq!(t_iter.next(), None);
+}
diff --git a/training_mod_tui/tests/test_submenu.rs b/training_mod_tui/tests/test_submenu.rs
new file mode 100644
index 0000000..a453d92
--- /dev/null
+++ b/training_mod_tui/tests/test_submenu.rs
@@ -0,0 +1,502 @@
+use ratatui::widgets::{TableSelection, TableState};
+use training_mod_tui_2::*;
+
+fn make_toggle<'a>(v: u8) -> Toggle<'a> {
+    Toggle {
+        title: "Title",
+        value: v,
+        max: 4,
+    }
+}
+
+fn make_toggle_table_multiple<'a>(
+    rows: usize,
+    cols: usize,
+    num: usize,
+) -> StatefulTable<Toggle<'a>> {
+    // [ (0)  1  2 ]
+    // [  3        ]
+    let v: Vec<Toggle> = (0..num).map(|v| make_toggle(v as u8)).collect();
+    StatefulTable::with_items(rows, cols, v)
+}
+
+fn make_toggle_table_single<'a>(rows: usize, cols: usize, num: usize) -> StatefulTable<Toggle<'a>> {
+    // [ (1)  0  0 ]
+    // [  0        ]
+    let v: Vec<Toggle> = (0..num).map(|_| make_toggle(0)).collect();
+    let mut t = StatefulTable::with_items(rows, cols, v);
+    t.items[0][0] = Some(make_toggle(1));
+    t
+}
+
+fn initialize_submenu<'a>(submenu_type: SubMenuType) -> SubMenu<'a> {
+    match submenu_type {
+        SubMenuType::ToggleSingle => SubMenu {
+            title: "Single Option Menu",
+            id: "single_option",
+            help_text: "A Single Option",
+            submenu_type: submenu_type,
+            toggles: make_toggle_table_single(2, 3, 4),
+            slider: None,
+        },
+        SubMenuType::ToggleMultiple => SubMenu {
+            title: "Multi Option Menu",
+            id: "multi_option",
+            help_text: "Multiple Options",
+            submenu_type: submenu_type,
+            toggles: make_toggle_table_multiple(2, 3, 4),
+            slider: None,
+        },
+        SubMenuType::Slider => SubMenu {
+            title: "Slider Menu",
+            id: "slider",
+            help_text: "A Double-ended Slider",
+            submenu_type: submenu_type,
+            toggles: make_toggle_table_multiple(0, 0, 0),
+            slider: Some(StatefulSlider::new()),
+        },
+        SubMenuType::None => {
+            panic!()
+        }
+    }
+}
+
+#[test]
+fn submenu_serialize() {
+    let submenu = initialize_submenu(SubMenuType::ToggleSingle);
+    let json = serde_json::to_string(&submenu).unwrap();
+    assert_eq!(&json, "[1,0,0,0]");
+
+    let submenu = initialize_submenu(SubMenuType::ToggleMultiple);
+    let json = serde_json::to_string(&submenu).unwrap();
+    assert_eq!(&json, "[0,1,2,3]");
+
+    let submenu = initialize_submenu(SubMenuType::Slider);
+    let json = serde_json::to_string(&submenu).unwrap();
+    assert_eq!(&json, "[0,150]");
+}
+
+#[test]
+fn submenu_selected_toggle() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
+    let mut t = make_toggle(1);
+    assert_eq!(submenu.selected_toggle(), &mut t);
+    t = make_toggle(0);
+    submenu.toggles.next_col();
+    assert_eq!(submenu.selected_toggle(), &mut t);
+
+    let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
+    let mut t = make_toggle(0);
+    assert_eq!(submenu.selected_toggle(), &mut t);
+    t = make_toggle(1);
+    submenu.toggles.next_col();
+    assert_eq!(submenu.selected_toggle(), &mut t);
+    t = make_toggle(2);
+    submenu.toggles.next_col();
+    assert_eq!(submenu.selected_toggle(), &mut t);
+}
+
+#[test]
+fn submenu_update_from_vec() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+    submenu.update_from_vec(vec![0, 0, 1, 0]);
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+
+    let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(3)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+    submenu.update_from_vec(vec![1, 1, 0, 4]);
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(4)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+
+    let mut submenu = initialize_submenu(SubMenuType::Slider);
+    let mut slider = StatefulSlider::new();
+    assert_eq!(submenu.slider, Some(slider));
+    slider.lower = 5;
+    submenu.update_from_vec(vec![5, 150]);
+    assert_eq!(submenu.slider, Some(slider));
+    slider.upper = 75;
+    submenu.update_from_vec(vec![5, 75]);
+    assert_eq!(submenu.slider, Some(slider));
+}
+
+#[test]
+fn submenu_single_on_a() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
+    submenu.toggles.select(1, 0);
+    submenu.on_a();
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+    submenu.on_a();
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+    submenu.toggles.select(0, 0);
+    submenu.on_a();
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+}
+
+#[test]
+fn submenu_multiple_on_a() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
+    submenu.toggles.select(1, 0);
+    submenu.on_a();
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(4)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+    submenu.on_a();
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+    submenu.toggles.select(0, 0);
+    submenu.on_a();
+    assert_eq!(submenu.toggles.items[0][0], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[0][1], Some(make_toggle(1)));
+    assert_eq!(submenu.toggles.items[0][2], Some(make_toggle(2)));
+    assert_eq!(submenu.toggles.items[1][0], Some(make_toggle(0)));
+    assert_eq!(submenu.toggles.items[1][1], None);
+    assert_eq!(submenu.toggles.items[1][2], None);
+}
+
+#[test]
+fn submenu_slider_on_a() {
+    let mut submenu = initialize_submenu(SubMenuType::Slider);
+    assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover);
+    submenu.on_a();
+    assert_eq!(submenu.slider.unwrap().state, SliderState::LowerSelected);
+    submenu.on_a();
+    assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover);
+    submenu.slider = Some(StatefulSlider {
+        state: SliderState::UpperHover,
+        ..submenu.slider.unwrap()
+    });
+    assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover);
+    submenu.on_a();
+    assert_eq!(submenu.slider.unwrap().state, SliderState::UpperSelected);
+    submenu.on_a();
+    assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover);
+}
+
+#[test]
+fn submenu_slider_on_b_selected() {
+    let mut submenu = initialize_submenu(SubMenuType::Slider);
+    submenu.slider = Some(StatefulSlider {
+        state: SliderState::LowerSelected,
+        ..submenu.slider.unwrap()
+    });
+    submenu.on_b();
+    assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover);
+    submenu.on_b();
+    assert_eq!(submenu.slider.unwrap().state, SliderState::LowerHover);
+    submenu.slider = Some(StatefulSlider {
+        state: SliderState::UpperSelected,
+        ..submenu.slider.unwrap()
+    });
+    submenu.on_b();
+    assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover);
+    submenu.on_b();
+    assert_eq!(submenu.slider.unwrap().state, SliderState::UpperHover);
+}
+
+#[test]
+fn submenu_single_on_up() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
+    let mut state = TableState::default();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_up();
+    state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_up();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+
+    submenu.toggles.select(0, 2);
+    state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_up();
+    assert_eq!(submenu.toggles.state, state);
+}
+
+#[test]
+fn submenu_multiple_on_up() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
+    let mut state = TableState::default();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_up();
+    state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_up();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+
+    submenu.toggles.select(0, 2);
+    state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_up();
+    assert_eq!(submenu.toggles.state, state);
+}
+#[test]
+fn submenu_single_on_down() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
+    let mut state = TableState::default();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_down();
+    state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_down();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+
+    submenu.toggles.select(0, 2);
+    state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_down();
+    assert_eq!(submenu.toggles.state, state);
+}
+
+#[test]
+fn submenu_multiple_on_down() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
+    let mut state = TableState::default();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_down();
+    state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_down();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+
+    submenu.toggles.select(0, 2);
+    state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
+    assert_eq!(submenu.toggles.state, state);
+    submenu.on_down();
+    assert_eq!(submenu.toggles.state, state);
+}
+
+#[test]
+fn submenu_single_on_left() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
+    let mut state = TableState::default();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1)));
+    submenu.on_left();
+    state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+    submenu.on_left();
+    state.select(Some(TableSelection::Cell { row: 0, col: 1 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+
+    submenu.toggles.select(1, 0);
+    submenu.on_left();
+    state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+    submenu.on_left();
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+}
+
+#[test]
+fn submenu_multiple_on_left() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
+    let mut state = TableState::default();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+    submenu.on_left();
+    state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(2)));
+    submenu.on_left();
+    state.select(Some(TableSelection::Cell { row: 0, col: 1 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1)));
+
+    submenu.toggles.select(1, 0);
+    submenu.on_left();
+    state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3)));
+    submenu.on_left();
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3)));
+}
+
+#[test]
+fn submenu_slider_on_left() {
+    let mut submenu = initialize_submenu(SubMenuType::Slider);
+    let mut state = SliderState::LowerHover;
+    assert_eq!(submenu.slider.unwrap().state, state);
+    state = SliderState::UpperHover;
+    submenu.on_left();
+    assert_eq!(submenu.slider.unwrap().state, state);
+
+    submenu.slider = Some(StatefulSlider {
+        state: SliderState::LowerSelected,
+        lower: 1,
+        ..submenu.slider.unwrap()
+    });
+    state = SliderState::LowerSelected;
+    submenu.on_left();
+    assert_eq!(submenu.slider.unwrap().state, state);
+    assert_eq!(submenu.slider.unwrap().lower, 0);
+    assert_eq!(submenu.slider.unwrap().upper, 150);
+    submenu.on_left();
+    assert_eq!(submenu.slider.unwrap().state, state);
+    assert_eq!(submenu.slider.unwrap().lower, 0);
+    assert_eq!(submenu.slider.unwrap().upper, 150);
+
+    submenu.slider = Some(StatefulSlider {
+        state: SliderState::UpperSelected,
+        lower: 99,
+        upper: 100,
+        ..submenu.slider.unwrap()
+    });
+    state = SliderState::UpperSelected;
+    submenu.on_left();
+    assert_eq!(submenu.slider.unwrap().state, state);
+    assert_eq!(submenu.slider.unwrap().lower, 99);
+    assert_eq!(submenu.slider.unwrap().upper, 99);
+    submenu.on_left();
+    assert_eq!(submenu.slider.unwrap().state, state);
+    assert_eq!(submenu.slider.unwrap().lower, 99);
+    assert_eq!(submenu.slider.unwrap().upper, 99);
+}
+
+#[test]
+fn submenu_single_on_right() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleSingle);
+    let mut state = TableState::default();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1)));
+    submenu.on_right();
+    state.select(Some(TableSelection::Cell { row: 0, col: 1 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+    submenu.on_right();
+    state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+
+    submenu.toggles.select(1, 0);
+    submenu.on_right();
+    state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+    submenu.on_right();
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+}
+
+#[test]
+fn submenu_multiple_on_right() {
+    let mut submenu = initialize_submenu(SubMenuType::ToggleMultiple);
+    let mut state = TableState::default();
+    state.select(Some(TableSelection::Cell { row: 0, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(0)));
+    submenu.on_right();
+    state.select(Some(TableSelection::Cell { row: 0, col: 1 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(1)));
+    submenu.on_right();
+    state.select(Some(TableSelection::Cell { row: 0, col: 2 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(2)));
+
+    submenu.toggles.select(1, 0);
+    submenu.on_right();
+    state.select(Some(TableSelection::Cell { row: 1, col: 0 }));
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3)));
+    submenu.on_right();
+    assert_eq!(submenu.toggles.state, state);
+    assert_eq!(submenu.toggles.get_selected(), Some(&mut make_toggle(3)));
+}
+
+#[test]
+fn submenu_slider_on_right() {
+    let mut submenu = initialize_submenu(SubMenuType::Slider);
+    let mut state = SliderState::LowerHover;
+    assert_eq!(submenu.slider.unwrap().state, state);
+    state = SliderState::UpperHover;
+    submenu.on_right();
+    assert_eq!(submenu.slider.unwrap().state, state);
+
+    submenu.slider = Some(StatefulSlider {
+        state: SliderState::LowerSelected,
+        lower: 10,
+        upper: 11,
+        ..submenu.slider.unwrap()
+    });
+    state = SliderState::LowerSelected;
+    submenu.on_right();
+    assert_eq!(submenu.slider.unwrap().state, state);
+    assert_eq!(submenu.slider.unwrap().lower, 11);
+    assert_eq!(submenu.slider.unwrap().upper, 11);
+    submenu.on_right();
+    assert_eq!(submenu.slider.unwrap().state, state);
+    assert_eq!(submenu.slider.unwrap().lower, 11);
+    assert_eq!(submenu.slider.unwrap().upper, 11);
+
+    submenu.slider = Some(StatefulSlider {
+        state: SliderState::UpperSelected,
+        lower: 100,
+        upper: 149,
+        ..submenu.slider.unwrap()
+    });
+    state = SliderState::UpperSelected;
+    submenu.on_right();
+    assert_eq!(submenu.slider.unwrap().state, state);
+    assert_eq!(submenu.slider.unwrap().lower, 100);
+    assert_eq!(submenu.slider.unwrap().upper, 150);
+    submenu.on_right();
+    assert_eq!(submenu.slider.unwrap().state, state);
+    assert_eq!(submenu.slider.unwrap().lower, 100);
+    assert_eq!(submenu.slider.unwrap().upper, 150);
+}
diff --git a/training_mod_tui/tests/test_toggle.rs b/training_mod_tui/tests/test_toggle.rs
new file mode 100644
index 0000000..adb07f8
--- /dev/null
+++ b/training_mod_tui/tests/test_toggle.rs
@@ -0,0 +1,44 @@
+use training_mod_tui_2::Toggle;
+
+#[test]
+fn toggle_serialize() {
+    let t = Toggle {
+        title: "Title",
+        value: 5,
+        max: 10,
+    };
+    let json = serde_json::to_string(&t).unwrap();
+    assert_eq!(json, "5");
+}
+
+#[test]
+fn toggle_increment() {
+    let mut t = Toggle {
+        title: "Title",
+        value: 5,
+        max: 10,
+    };
+    t.increment();
+    assert_eq!(t.value, 6);
+    t.value = 9;
+    t.increment();
+    assert_eq!(t.value, 10);
+    t.increment();
+    assert_eq!(t.value, 0);
+}
+
+#[test]
+fn toggle_decrement() {
+    let mut t = Toggle {
+        title: "Title",
+        value: 5,
+        max: 10,
+    };
+    t.decrement();
+    assert_eq!(t.value, 4);
+    t.value = 1;
+    t.decrement();
+    assert_eq!(t.value, 0);
+    t.decrement();
+    assert_eq!(t.value, 10);
+}
diff --git a/training_mod_tui/training_mod_tui.iml b/training_mod_tui/training_mod_tui.iml
deleted file mode 100644
index 2fecef3..0000000
--- a/training_mod_tui/training_mod_tui.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="RUST_MODULE" version="4">
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
-    <exclude-output />
-    <content url="file://$MODULE_DIR$">
-      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
-      <excludeFolder url="file://$MODULE_DIR$/target" />
-    </content>
-    <orderEntry type="inheritedJdk" />
-    <orderEntry type="sourceFolder" forTests="false" />
-  </component>
-</module>
\ No newline at end of file