From 10ebff5a6fad37b366e5d6bbb189340645ce2cf0 Mon Sep 17 00:00:00 2001
From: Austin Traver <austintraver@gmail.com>
Date: Tue, 15 Aug 2023 19:37:35 -0700
Subject: [PATCH 1/4] Refactor string representation for buttons and menus
 (#592)

* Refactor string representation for buttons and menus

* Pass test cases

* Update help text
---
 src/common/menu.rs                 |   3 +-
 src/lib.rs                         |  39 +--
 src/training/ui/menu.rs            |  10 +-
 training_mod_consts/src/lib.rs     | 544 +++++++++++++++--------------
 training_mod_consts/src/options.rs | 106 ++++++
 training_mod_tui/src/lib.rs        | 107 +++---
 training_mod_tui/src/main.rs       |  10 +-
 7 files changed, 462 insertions(+), 357 deletions(-)

diff --git a/src/common/menu.rs b/src/common/menu.rs
index c689629..023f76e 100644
--- a/src/common/menu.rs
+++ b/src/common/menu.rs
@@ -5,7 +5,6 @@ use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use skyline::nn::hid::GetNpadStyleSet;
 use training_mod_consts::MenuJsonStruct;
-
 use training_mod_tui::AppPage;
 
 use crate::common::button_config::button_mapping;
@@ -95,7 +94,7 @@ enum DirectionButton {
 }
 
 lazy_static! {
-    pub static ref QUICK_MENU_APP: Mutex<training_mod_tui::App<'static>> = Mutex::new(
+    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),
diff --git a/src/lib.rs b/src/lib.rs
index a3bf0df..f6f77fb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -106,54 +106,25 @@ pub fn main() {
 
     unsafe {
         notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
-        notification(
-            "Open Menu".to_string(),
-            MENU.menu_open
-                .to_vec()
-                .iter()
-                .map(|button| button.as_str().unwrap())
-                .intersperse(" + ")
-                .collect(),
-            120,
-        );
+        notification("Open Menu".to_string(), MENU.menu_open.to_string(), 120);
         notification(
             "Save State".to_string(),
-            MENU.save_state_save
-                .to_vec()
-                .iter()
-                .map(|button| button.as_str().unwrap())
-                .intersperse(" + ")
-                .collect(),
+            MENU.save_state_save.to_string(),
             120,
         );
         notification(
             "Load State".to_string(),
-            MENU.save_state_load
-                .to_vec()
-                .iter()
-                .map(|button| button.as_str().unwrap())
-                .intersperse(" + ")
-                .collect(),
+            MENU.save_state_load.to_string(),
             120,
         );
         notification(
             "Input Record".to_string(),
-            MENU.input_record
-                .to_vec()
-                .iter()
-                .map(|button| button.as_str().unwrap())
-                .intersperse(" + ")
-                .collect(),
+            MENU.input_record.to_string(),
             120,
         );
         notification(
             "Input Playback".to_string(),
-            MENU.input_playback
-                .to_vec()
-                .iter()
-                .map(|button| button.as_str().unwrap())
-                .intersperse(" + ")
-                .collect(),
+            MENU.input_playback.to_string(),
             120,
         );
     }
diff --git a/src/training/ui/menu.rs b/src/training/ui/menu.rs
index 12b624c..2cb2191 100644
--- a/src/training/ui/menu.rs
+++ b/src/training/ui/menu.rs
@@ -105,7 +105,7 @@ unsafe fn render_submenu_page(app: &App, root_pane: &Pane) {
             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);
+            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.
@@ -128,7 +128,7 @@ unsafe fn render_submenu_page(app: &App, root_pane: &Pane) {
                     .find_pane_by_name_recursive("FooterTxt")
                     .unwrap()
                     .as_textbox()
-                    .set_text_string(submenu.help_text);
+                    .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);
@@ -250,7 +250,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);
+    header.set_text_string(title.as_str());
     let min_button = slider_pane
         .find_pane_by_name_recursive("MinButton")
         .unwrap()
@@ -411,7 +411,7 @@ pub unsafe fn draw(root_pane: &Pane) {
     } else {
         tab_selected + 1
     };
-    let tab_titles = [prev_tab, tab_selected, next_tab].map(|idx| app_tabs[idx]);
+    let tab_titles = [prev_tab, tab_selected, next_tab].map(|idx| app_tabs[idx].clone());
 
     let is_gcc = (*common::menu::P1_CONTROLLER_STYLE.data_ptr()) == ControllerStyle::GCController;
     let button_mapping = if is_gcc {
@@ -471,7 +471,7 @@ 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]);
+        help_pane.set_text_string(tab_titles[idx].as_str());
     });
     [
         (save_defaults_key, "SaveDefaults", "Save Defaults"),
diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs
index 77d6d1d..2fa9718 100644
--- a/training_mod_consts/src/lib.rs
+++ b/training_mod_consts/src/lib.rs
@@ -1,3 +1,4 @@
+#![feature(iter_intersperse)]
 #[macro_use]
 extern crate bitflags;
 
@@ -111,8 +112,8 @@ pub enum SubMenuType {
 }
 
 impl SubMenuType {
-    pub fn from_str(s: &str) -> SubMenuType {
-        match s {
+    pub fn from_string(s: &String) -> SubMenuType {
+        match s.as_str() {
             "toggle" => SubMenuType::TOGGLE,
             "slider" => SubMenuType::SLIDER,
             _ => panic!("Unexpected SubMenuType!"),
@@ -203,25 +204,25 @@ pub struct Slider {
 }
 
 #[derive(Clone, Serialize)]
-pub struct Toggle<'a> {
+pub struct Toggle {
     pub toggle_value: u32,
-    pub toggle_title: &'a str,
+    pub toggle_title: String,
     pub checked: bool,
 }
 
 #[derive(Clone, Serialize)]
-pub struct SubMenu<'a> {
-    pub submenu_title: &'a str,
-    pub submenu_id: &'a str,
-    pub help_text: &'a str,
+pub struct SubMenu {
+    pub submenu_title: String,
+    pub submenu_id: String,
+    pub help_text: String,
     pub is_single_option: bool,
-    pub toggles: Vec<Toggle<'a>>,
+    pub toggles: Vec<Toggle>,
     pub slider: Option<Slider>,
-    pub _type: &'a str,
+    pub _type: String,
 }
 
-impl<'a> SubMenu<'a> {
-    pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: &'a str, checked: bool) {
+impl SubMenu {
+    pub fn add_toggle(&mut self, toggle_value: u32, toggle_title: String, checked: bool) {
         self.toggles.push(Toggle {
             toggle_value,
             toggle_title,
@@ -229,12 +230,12 @@ impl<'a> SubMenu<'a> {
         });
     }
     pub fn new_with_toggles<T: ToggleTrait>(
-        submenu_title: &'a str,
-        submenu_id: &'a str,
-        help_text: &'a str,
+        submenu_title: String,
+        submenu_id: String,
+        help_text: String,
         is_single_option: bool,
         initial_value: &u32,
-    ) -> SubMenu<'a> {
+    ) -> SubMenu {
         let mut instance = SubMenu {
             submenu_title: submenu_title,
             submenu_id: submenu_id,
@@ -242,15 +243,15 @@ impl<'a> SubMenu<'a> {
             is_single_option: is_single_option,
             toggles: Vec::new(),
             slider: None,
-            _type: "toggle",
+            _type: "toggle".to_string(),
         };
 
         let values = T::to_toggle_vals();
-        let titles = T::to_toggle_strs();
+        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], checked);
+            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) {
@@ -259,12 +260,12 @@ impl<'a> SubMenu<'a> {
         instance
     }
     pub fn new_with_slider<S: SliderTrait>(
-        submenu_title: &'a str,
-        submenu_id: &'a str,
-        help_text: &'a str,
+        submenu_title: String,
+        submenu_id: String,
+        help_text: String,
         initial_lower_value: &u32,
         initial_upper_value: &u32,
-    ) -> SubMenu<'a> {
+    ) -> SubMenu {
         let min_max = S::get_limits();
         SubMenu {
             submenu_title: submenu_title,
@@ -278,31 +279,31 @@ impl<'a> SubMenu<'a> {
                 abs_min: min_max.0,
                 abs_max: min_max.1,
             }),
-            _type: "slider",
+            _type: "slider".to_string(),
         }
     }
 }
 
 #[derive(Serialize, Clone)]
-pub struct Tab<'a> {
-    pub tab_id: &'a str,
-    pub tab_title: &'a str,
-    pub tab_submenus: Vec<SubMenu<'a>>,
+pub struct Tab {
+    pub tab_id: String,
+    pub tab_title: String,
+    pub tab_submenus: Vec<SubMenu>,
 }
 
-impl<'a> Tab<'a> {
+impl Tab {
     pub fn add_submenu_with_toggles<T: ToggleTrait>(
         &mut self,
-        submenu_title: &'a str,
-        submenu_id: &'a str,
-        help_text: &'a str,
+        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,
-            submenu_id,
-            help_text,
+            submenu_title.to_string(),
+            submenu_id.to_string(),
+            help_text.to_string(),
             is_single_option,
             initial_value,
         ));
@@ -310,16 +311,16 @@ impl<'a> Tab<'a> {
 
     pub fn add_submenu_with_slider<S: SliderTrait>(
         &mut self,
-        submenu_title: &'a str,
-        submenu_id: &'a str,
-        help_text: &'a str,
+        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,
-            submenu_id,
-            help_text,
+            submenu_title.to_string(),
+            submenu_id.to_string(),
+            help_text.to_string(),
             initial_lower_value,
             initial_upper_value,
         ))
@@ -327,542 +328,555 @@ impl<'a> Tab<'a> {
 }
 
 #[derive(Serialize, Clone)]
-pub struct UiMenu<'a> {
-    pub tabs: Vec<Tab<'a>>,
+pub struct UiMenu {
+    pub tabs: Vec<Tab>,
 }
 
-pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu<'static> {
+pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu {
     let mut overall_menu = UiMenu { tabs: Vec::new() };
 
     let mut mash_tab = Tab {
-        tab_id: "mash",
-        tab_title: "Mash Settings",
+        tab_id: "mash".to_string(),
+        tab_title: "Mash Settings".to_string(),
         tab_submenus: Vec::new(),
     };
     mash_tab.add_submenu_with_toggles::<Action>(
-        "Mash Toggles",
-        "mash_state",
-        "Mash Toggles: Actions to be performed as soon as possible",
+        "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",
-        "follow_up",
-        "Followup Toggles: Actions to be performed after a Mash option",
+        "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",
-        "mash_triggers",
-        "Mash triggers: Configure what causes the CPU to perform a Mash option",
+        "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",
-        "attack_angle",
-        "Attack Angle: For attacks that can be angled, such as some forward tilts",
+        "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",
-        "throw_state",
-        "Throw Options: Throw to be performed when a grab is landed",
+        "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",
-        "throw_delay",
-        "Throw Delay: How many frames to delay the throw option",
+        "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",
-        "pummel_delay",
-        "Pummel Delay: How many frames after a grab to wait before starting to pummel",
+        "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",
-        "falling_aerials",
-        "Falling Aerials: Should aerials be performed when rising or when falling",
+        "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",
-        "full_hop",
-        "Full Hop: Should the CPU perform a full hop or a short hop",
+        "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",
-        "aerial_delay",
-        "Aerial Delay: How long to delay a Mash aerial attack",
+        "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",
-        "fast_fall",
-        "Fast Fall: Should the CPU fastfall during a jump",
+        "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",
-        "fast_fall_delay",
-        "Fast Fall Delay: How many frames the CPU should delay their fastfall",
+        "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",
-        "oos_offset",
-        "OoS Offset: How many times the CPU shield can be hit before performing a Mash option",
+        "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",
-        "reaction_time",
-        "Reaction Time: How many frames to delay before performing a mash option",
+        "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",
-        tab_title: "Override Settings",
+        tab_id: "override".to_string(),
+        tab_title: "Override Settings".to_string(),
         tab_submenus: Vec::new(),
     };
     override_tab.add_submenu_with_toggles::<Action>(
-        "Ledge Neutral Getup",
-        "ledge_neutral_override",
-        "Neutral Getup Override: Mash Actions to be performed after a Neutral Getup from ledge",
+        "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",
-        "ledge_roll_override",
-        "Ledge Roll Override: Mash Actions to be performed after a Roll Getup from ledge",
+        "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",
-        "ledge_jump_override",
-        "Ledge Jump Override: Mash Actions to be performed after a Jump Getup from ledge",
+        "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",
-        "ledge_attack_override",
-        "Ledge Attack Override: Mash Actions to be performed after a Getup Attack from ledge",
+        "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",
-        "tech_action_override",
-        "Tech Action Override: Mash Actions to be performed after any tech 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",
-        "clatter_override",
-        "Clatter Override: Mash Actions to be performed after leaving a clatter situation (grab, bury, etc)",
+        "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",
-        "tumble_override",
-        "Tumble Override: Mash Actions to be performed after exiting a tumble state",
+        "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",
-        "hitstun_override",
-        "Hitstun Override: Mash Actions to be performed after exiting a hitstun state",
+        "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",
-        "parry_override",
-        "Parry Override: Mash Actions to be performed after a parry",
+        "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",
-        "shieldstun_override",
-        "Shieldstun Override: Mash Actions to be performed after exiting a shieldstun state",
+        "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",
-        "footstool_override",
-        "Footstool Override: Mash Actions to be performed after exiting a footstool state",
+        "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",
-        "landing_override",
-        "Landing Override: Mash Actions to be performed after landing on the ground",
+        "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",
-        "trump_override",
-        "Ledge Trump Override: Mash Actions to be performed after leaving a ledgetrump state",
+        "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",
-        tab_title: "Defensive Settings",
+        tab_id: "defensive".to_string(),
+        tab_title: "Defensive Settings".to_string(),
         tab_submenus: Vec::new(),
     };
     defensive_tab.add_submenu_with_toggles::<Direction>(
-        "Airdodge Direction",
-        "air_dodge_dir",
-        "Airdodge Direction: Direction to angle airdodges",
+        "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",
-        "di_state",
-        "DI Direction: Direction to angle the directional influence during hitlag",
+        "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",
-        "sdi_state",
-        "SDI Direction: Direction to angle the smash directional influence during hitlag",
+        "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",
-        "sdi_strength",
-        "SDI Strength: Relative strength of the smash directional influence inputs",
+        "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",
-        "clatter_strength",
-        "Clatter Strength: Configure how rapidly the CPU will mash out of grabs, buries, etc.",
+        "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",
-        "ledge_state",
-        "Ledge Options: Actions to be taken when on the ledge",
+        "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",
-        "ledge_delay",
-        "Ledge Delay: How many frames to delay the ledge option",
+        "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",
-        "tech_state",
-        "Tech Options: Actions to take when slammed into a hard surface",
+        "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",
-        "miss_tech_state",
-        "Mistech Options: Actions to take after missing a tech",
+        "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",
-        "shield_state",
-        "Shield Toggles: CPU Shield Behavior",
+        "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",
-        "shield_tilt",
-        "Shield Tilt: Direction to tilt the shield",
+        "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",
-        "crouch",
-        "Crouch: Have the CPU crouch when on the ground",
+        "Crouch".to_string(),
+        "crouch".to_string(),
+        "Crouch: Have the CPU crouch when on the ground".to_string(),
         true,
         &(menu.crouch as u32),
     );
     overall_menu.tabs.push(defensive_tab);
 
     let mut save_state_tab = Tab {
-        tab_id: "save_state",
-        tab_title: "Save States",
+        tab_id: "save_state".to_string(),
+        tab_title: "Save States".to_string(),
         tab_submenus: Vec::new(),
     };
     save_state_tab.add_submenu_with_toggles::<SaveStateMirroring>(
-        "Mirroring",
-        "save_state_mirroring",
-        "Mirroring: Flips save states in the left-right direction across the stage center",
+        "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",
-        "save_state_autoload",
-        "Auto Save States: Load save state when any fighter dies",
+        "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)",
-        "save_damage_cpu",
-        "Save Damage: Should save states retain CPU damage",
+        "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)",
-        "save_damage_limits_cpu",
-        "Limits on random damage to apply to the CPU when loading a save state",
+        "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)",
-        "save_damage_player",
-        "Save Damage: Should save states retain player damage",
+        "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)",
-        "save_damage_limits_player",
-        "Limits on random damage to apply to the player when loading a save state",
+        "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",
-        "save_state_enable",
-        "Save States: Enable save states! Save a state with Shield+Down Taunt, load it with Shield+Up Taunt.",
+        "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",
-        "save_state_slot",
-        "Save State Slot: Save and load states from different slots.",
+        "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 as u32),
     );
     save_state_tab.add_submenu_with_toggles::<OnOff>(
-        "Randomize Slots",
-        "randomize_slots",
-        "Randomize Slots: Randomize slot when loading save state.",
+        "Randomize Slots".to_string(),
+        "randomize_slots".to_string(),
+        "Randomize Slots: Randomize slot when loading save state.".to_string(),
         true,
         &(menu.randomize_slots as u32),
     );
     save_state_tab.add_submenu_with_toggles::<CharacterItem>(
-        "Character Item",
-        "character_item",
-        "Character Item: The item to give to the player's fighter when loading a save state",
+        "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",
-        "buff_state",
-        "Buff Options: Buff(s) to be applied to the respective fighters when loading a save state",
+        "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",
-        "save_state_playback",
-        "Save State Playback: Choose which slots to playback input recording upon loading a save state",
+        "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",
-        tab_title: "Misc Settings",
+        tab_id: "misc".to_string(),
+        tab_title: "Misc Settings".to_string(),
         tab_submenus: Vec::new(),
     };
     misc_tab.add_submenu_with_toggles::<OnOff>(
-        "Frame Advantage",
-        "frame_advantage",
-        "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable",
+        "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",
-        "hitbox_vis",
-        "Hitbox Visualization: Display a visual representation for active hitboxes (hides other visual effects)",
+        "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::<Delay>(
-        "Input Delay",
-        "input_delay",
-        "Input Delay: Frames to delay player inputs by",
+        "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",
-        "stage_hazards",
-        "Stage Hazards: Turn stage hazards on/off",
+        "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",
-        "hud",
-        "HUD: Show/hide elements of the UI",
+        "HUD".to_string(),
+        "hud".to_string(),
+        "HUD: Show/hide elements of the UI".to_string(),
         true,
         &(menu.hud as u32),
     );
     overall_menu.tabs.push(misc_tab);
 
     let mut input_tab = Tab {
-        tab_id: "input",
-        tab_title: "Input Recording",
+        tab_id: "input".to_string(),
+        tab_title: "Input Recording".to_string(),
         tab_submenus: Vec::new(),
     };
     input_tab.add_submenu_with_toggles::<RecordSlot>(
-        "Recording Slot",
-        "recording_slot",
-        "Recording Slot: Choose which slot to record into",
+        "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",
-        "record_trigger",
-        "Recording Trigger: Whether to begin recording via button combination (Default: Attack+Left Taunt) or upon loading a Save State",
+        "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::<RecordingFrames>(
-        "Recording Frames",
-        "recording_frames",
-        "Recording Frames: Number of frames to record for in the current slot",
+        "Recording Frames".to_string(),
+        "recording_frames".to_string(),
+        "Recording Frames: Number of frames to record for in the current slot".to_string(),
         true,
         &(menu.recording_frames as u32),
     );
     input_tab.add_submenu_with_toggles::<PlaybackSlot>(
-        "Playback Button Combination",
-        "playback_button_combination",
-        "Playback Button Combination: Choose which slots to playback input recording upon pressing button combination (Default: Attack+Right Taunt)",
+        "Playback Button Combination".to_string(),
+        "playback_button_combination".to_string(),
+        format!("Playback Button Combination: Choose which slots to playback input recording upon pressing button combination ({})", menu.input_playback.combination_string()),
         false,
         &(menu.playback_button_combination.bits() as u32),
     );
     input_tab.add_submenu_with_toggles::<HitstunPlayback>(
-        "Playback Hitstun Timing",
-        "hitstun_playback",
-        "Playback Hitstun Timing: When to begin playing back inputs when a hitstun mash trigger occurs",
+        "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",
-        "playback_mash",
-        "Playback Mash Interrupt: End input playback when a mash trigger occurs",
+        "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",
-        "playback_loop",
-        "Playback Loop: Repeat triggered input playbacks indefinitely",
+        "Playback Loop".to_string(),
+        "playback_loop".to_string(),
+        "Playback Loop: Repeat triggered input playbacks indefinitely".to_string(),
         true,
         &(menu.playback_loop as u32),
     );
     input_tab.add_submenu_with_toggles::<OnOff>(
-        "Recording Crop",
-        "recording_crop",
-        "Recording Crop: Remove neutral input frames at the end of your recording",
+        "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),
     );
     overall_menu.tabs.push(input_tab);
 
     let mut button_tab = Tab {
-        tab_id: "button",
-        tab_title: "Button Config",
+        tab_id: "button".to_string(),
+        tab_title: "Button Config".to_string(),
         tab_submenus: Vec::new(),
     };
     button_tab.add_submenu_with_toggles::<ButtonConfig>(
-        "Menu Open",
-        "menu_open",
-        "Menu Open: Hold: Hold any one button and press the others to trigger",
+        "Menu Open".to_string(),
+        "menu_open".to_string(),
+        "Menu Open: Hold: Hold any one button and press the others to trigger".to_string(),
         false,
         &(menu.menu_open.bits() as u32),
     );
     button_tab.add_submenu_with_toggles::<ButtonConfig>(
-        "Save State Save",
-        "save_state_save",
-        "Save State Save: Hold any one button and press the others to trigger",
+        "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",
-        "save_state_load",
-        "Save State Load: Hold any one button and press the others to trigger",
+        "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",
-        "input_record",
-        "Input Record: Hold any one button and press the others to trigger",
+        "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",
-        "input_playback",
-        "Input Playback: Hold any one button and press the others to trigger",
+        "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),
     );
diff --git a/training_mod_consts/src/options.rs b/training_mod_consts/src/options.rs
index ce7df62..9b54a0f 100644
--- a/training_mod_consts/src/options.rs
+++ b/training_mod_consts/src/options.rs
@@ -3,6 +3,7 @@ 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;
 
@@ -21,6 +22,7 @@ fn log_2(x: u32) -> u32 {
 pub trait ToggleTrait {
     fn to_toggle_strs() -> Vec<&'static str>;
     fn to_toggle_vals() -> Vec<u32>;
+    fn to_toggle_strings() -> Vec<String>;
 }
 
 pub trait SliderTrait {
@@ -81,6 +83,11 @@ macro_rules! extra_bitflag_impls {
                 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()
+            }
         }
     }
 }
@@ -346,6 +353,12 @@ impl Shield {
     }
 }
 
+impl fmt::Display for Shield {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str().unwrap())
+    }
+}
+
 impl ToggleTrait for Shield {
     fn to_toggle_strs() -> Vec<&'static str> {
         Shield::iter().map(|i| i.as_str().unwrap_or("")).collect()
@@ -354,6 +367,9 @@ impl ToggleTrait for Shield {
     fn to_toggle_vals() -> Vec<u32> {
         Shield::iter().map(|i| i as u32).collect()
     }
+    fn to_toggle_strings() -> Vec<String> {
+        Shield::iter().map(|i| i.to_string()).collect()
+    }
 }
 
 // Save State Mirroring
@@ -377,6 +393,12 @@ impl SaveStateMirroring {
     }
 }
 
+impl fmt::Display for SaveStateMirroring {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str().unwrap())
+    }
+}
+
 impl ToggleTrait for SaveStateMirroring {
     fn to_toggle_strs() -> Vec<&'static str> {
         SaveStateMirroring::iter()
@@ -387,6 +409,10 @@ 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)]
@@ -420,6 +446,9 @@ 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! {
@@ -1006,6 +1035,16 @@ 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()
+    }
+}
+
+impl fmt::Display for SdiFrequency {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str().unwrap())
+    }
 }
 
 #[repr(u32)]
@@ -1049,6 +1088,16 @@ 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()
+    }
+}
+
+impl fmt::Display for ClatterFrequency {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str().unwrap())
+    }
 }
 
 /// Item Selections
@@ -1114,6 +1163,16 @@ 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()
+    }
+}
+
+impl fmt::Display for CharacterItem {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str().unwrap())
+    }
 }
 
 bitflags! {
@@ -1251,6 +1310,16 @@ impl ToggleTrait for SaveStateSlot {
     fn to_toggle_vals() -> Vec<u32> {
         SaveStateSlot::iter().map(|i| i as u32).collect()
     }
+
+    fn to_toggle_strings() -> Vec<String> {
+        SaveStateSlot::iter().map(|i| i.to_string()).collect()
+    }
+}
+
+impl fmt::Display for SaveStateSlot {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str().unwrap())
+    }
 }
 
 // Input Recording Slot
@@ -1298,6 +1367,16 @@ 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()
+    }
+}
+
+impl fmt::Display for RecordSlot {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str().unwrap())
+    }
 }
 
 // Input Playback Slot
@@ -1371,6 +1450,16 @@ 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()
+    }
+}
+
+impl fmt::Display for HitstunPlayback {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str().unwrap())
+    }
 }
 
 // Input Recording Trigger Type
@@ -1462,6 +1551,16 @@ impl ToggleTrait for RecordingFrames {
     fn to_toggle_vals() -> Vec<u32> {
         RecordingFrames::iter().map(|i| i as u32).collect()
     }
+
+    fn to_toggle_strings() -> Vec<String> {
+        RecordingFrames::iter().map(|i| i.to_string()).collect()
+    }
+}
+
+impl fmt::Display for RecordingFrames {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str().unwrap())
+    }
 }
 
 bitflags! {
@@ -1508,6 +1607,13 @@ impl ButtonConfig {
             _ => return None,
         })
     }
+    pub fn combination_string(&self) -> String {
+        self.to_vec()
+            .iter()
+            .map(|button| button.as_str().unwrap())
+            .intersperse(" + ")
+            .collect::<String>()
+    }
 }
 
 extra_bitflag_impls! {ButtonConfig}
diff --git a/training_mod_tui/src/lib.rs b/training_mod_tui/src/lib.rs
index 3cd4045..0ca02a1 100644
--- a/training_mod_tui/src/lib.rs
+++ b/training_mod_tui/src/lib.rs
@@ -35,27 +35,29 @@ pub enum AppPage {
 /// 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<'a> {
-    pub tabs: StatefulList<&'a str>,
-    pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
-    pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
+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<'a>, String),
+    pub default_menu: (UiMenu, String),
 }
 
-impl<'a> App<'a> {
-    pub fn new(menu: UiMenu<'a>, default_menu: (UiMenu<'a>, String)) -> App<'a> {
+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,
+                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).collect()),
+            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(),
@@ -82,7 +84,7 @@ impl<'a> App<'a> {
 
         let toggles = selected_sub_menu.toggles.clone();
         let slider = selected_sub_menu.slider.clone();
-        match SubMenuType::from_str(self.sub_menu_selected()._type) {
+        match SubMenuType::from_string(&self.sub_menu_selected()._type) {
             SubMenuType::TOGGLE => {
                 self.selected_sub_menu_toggles = MultiStatefulList::with_items(
                     toggles,
@@ -117,13 +119,13 @@ impl<'a> App<'a> {
     /// Returns the currently selected SubMenu struct
     ///
     /// {
-    ///   submenu_title: &'a str,
-    ///   submenu_id: &'a str,
-    ///   help_text: &'a str,
+    ///   submenu_title: String,
+    ///   submenu_id: String,
+    ///   help_text: String,
     ///   is_single_option: bool,
     ///   toggles: Vec<Toggle<'a>>,
     ///   slider: Option<Slider>,
-    ///   _type: &'a str,
+    ///   _type: String,
     /// }
     fn sub_menu_selected(&self) -> &SubMenu {
         let (list_section, list_idx) = self
@@ -141,7 +143,7 @@ impl<'a> App<'a> {
     /// Toggles: calls next()
     /// Slider: Swaps between MinHover and MaxHover
     pub fn sub_menu_next(&mut self) {
-        match SubMenuType::from_str(self.sub_menu_selected()._type) {
+        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,
@@ -157,7 +159,7 @@ impl<'a> App<'a> {
     ///     * Swaps between MinHover and MaxHover
     ///     * Increments the selected_min/max if possible
     pub fn sub_menu_next_list(&mut self) {
-        match SubMenuType::from_str(self.sub_menu_selected()._type) {
+        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,
@@ -185,7 +187,7 @@ impl<'a> App<'a> {
     /// Toggles: calls previous()
     /// Slider: Swaps between MinHover and MaxHover
     pub fn sub_menu_previous(&mut self) {
-        match SubMenuType::from_str(self.sub_menu_selected()._type) {
+        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,
@@ -201,7 +203,7 @@ impl<'a> App<'a> {
     ///     * Swaps between MinHover and MaxHover
     ///     * Decrements the selected_min/max if possible
     pub fn sub_menu_previous_list(&mut self) {
-        match SubMenuType::from_str(self.sub_menu_selected()._type) {
+        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,
@@ -232,11 +234,13 @@ impl<'a> App<'a> {
     /// 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) -> (&str, &str, Vec<(Vec<(bool, &str)>, ListState)>) {
+    pub fn sub_menu_strs_and_states(
+        &self,
+    ) -> (String, String, Vec<(Vec<(bool, String)>, ListState)>) {
         (
-            self.sub_menu_selected().submenu_title,
-            self.sub_menu_selected().help_text,
-            match SubMenuType::from_str(self.sub_menu_selected()._type) {
+            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
@@ -246,7 +250,7 @@ impl<'a> App<'a> {
                             toggle_list
                                 .items
                                 .iter()
-                                .map(|toggle| (toggle.checked, toggle.toggle_title))
+                                .map(|toggle| (toggle.checked, toggle.toggle_title.clone()))
                                 .collect(),
                             toggle_list.state.clone(),
                         )
@@ -264,16 +268,16 @@ impl<'a> App<'a> {
     /// 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) -> (&str, &str, &DoubleEndedGauge) {
-        let slider = match SubMenuType::from_str(self.sub_menu_selected()._type) {
+    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,
-            self.sub_menu_selected().help_text,
+            self.sub_menu_selected().submenu_title.clone(),
+            self.sub_menu_selected().help_text.clone(),
             slider,
         )
     }
@@ -298,7 +302,7 @@ impl<'a> App<'a> {
             .get_mut(list_idx)
             .unwrap();
         if self.page == AppPage::SUBMENU {
-            match SubMenuType::from_str(selected_sub_menu._type) {
+            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;
@@ -307,7 +311,7 @@ impl<'a> App<'a> {
                 SubMenuType::TOGGLE => self.page = AppPage::TOGGLE,
             }
         } else {
-            match SubMenuType::from_str(selected_sub_menu._type) {
+            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;
@@ -404,7 +408,7 @@ impl<'a> App<'a> {
             .items
             .get_mut(list_idx)
             .unwrap();
-        match SubMenuType::from_str(selected_sub_menu._type) {
+        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;
@@ -455,10 +459,11 @@ impl<'a> App<'a> {
             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;
+            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).unwrap() = default_json_value.get(id).unwrap().clone();
+            *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());
         }
@@ -587,7 +592,7 @@ impl<'a> App<'a> {
         .unwrap()
     }
 
-    pub fn submenu_ids(&self) -> Vec<&str> {
+    pub fn submenu_ids(&self) -> Vec<String> {
         return self
             .menu_items
             .values()
@@ -599,10 +604,10 @@ impl<'a> App<'a> {
                         sub_stateful_list
                             .items
                             .iter()
-                            .map(|submenu| submenu.submenu_id)
+                            .map(|submenu| submenu.submenu_id.clone())
                     })
             })
-            .collect::<Vec<&str>>();
+            .collect::<Vec<String>>();
     }
 }
 
@@ -627,9 +632,9 @@ fn render_submenu_page<B: Backend>(
             .iter()
             .map(|i| {
                 let lines = vec![Spans::from(if stateful_list.state.selected().is_some() {
-                    i.submenu_title.to_owned()
+                    i.submenu_title.clone()
                 } else {
-                    "   ".to_owned() + i.submenu_title
+                    format!("   {}", i.submenu_title.clone())
                 })];
                 ListItem::new(lines).style(Style::default().fg(Color::White))
             })
@@ -650,14 +655,18 @@ fn render_submenu_page<B: Backend>(
 
         let mut state = stateful_list.state.clone();
         if state.selected().is_some() {
-            item_help = Some(stateful_list.items[state.selected().unwrap()].help_text);
+            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("").replace('\"', "")
+        item_help.unwrap_or("".to_string()).replace('\"', "")
             + "\nZL/ZR: Next tab | X: Save Defaults | R: Reset All Menus",
     )
     .style(Style::default().fg(Color::Cyan));
@@ -677,14 +686,20 @@ pub fn render_toggle_page<B: Backend>(
         let values_items: Vec<ListItem> = sub_menu_str
             .iter()
             .map(|s| {
-                ListItem::new(vec![Spans::from(
-                    (if s.0 { "X " } else { "  " }).to_owned() + s.1,
-                )])
+                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 } else { "" }))
+            .block(Block::default().title(if list_section == 0 {
+                title.clone()
+            } else {
+                "".to_string()
+            }))
             .start_corner(Corner::TopLeft)
             .highlight_style(
                 Style::default()
@@ -795,10 +810,10 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
         .enumerate()
         .map(|(idx, tab)| {
             if idx == tab_selected {
-                span_selected = Spans::from("> ".to_owned() + tab);
-                Spans::from("> ".to_owned() + tab)
+                span_selected = Spans::from(format!("> {}", tab));
+                Spans::from(format!("> {}", tab))
             } else {
-                Spans::from("  ".to_owned() + tab)
+                Spans::from(format!("  {}", tab))
             }
         })
         .collect();
diff --git a/training_mod_tui/src/main.rs b/training_mod_tui/src/main.rs
index df0220c..a10e722 100644
--- a/training_mod_tui/src/main.rs
+++ b/training_mod_tui/src/main.rs
@@ -17,17 +17,17 @@ use tui::Terminal;
 
 use training_mod_consts::*;
 
-fn test_backend_setup<'a>(
-    ui_menu: UiMenu<'a>,
-    menu_defaults: (UiMenu<'a>, String),
+fn test_backend_setup(
+    ui_menu: UiMenu,
+    menu_defaults: (UiMenu, String),
 ) -> Result<
     (
         Terminal<training_mod_tui::TestBackend>,
-        training_mod_tui::App<'a>,
+        training_mod_tui::App,
     ),
     Box<dyn Error>,
 > {
-    let app = training_mod_tui::App::<'a>::new(ui_menu, menu_defaults);
+    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();

From 6f1e4af332a375e37968fcb452436125bc388cef Mon Sep 17 00:00:00 2001
From: jugeeya <jugeeya@live.com>
Date: Tue, 15 Aug 2023 23:57:38 -0700
Subject: [PATCH 2/4] Don't set CPU controls outside of training

---
 src/training/input_record.rs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/training/input_record.rs b/src/training/input_record.rs
index b59b9e2..7625be6 100644
--- a/src/training/input_record.rs
+++ b/src/training/input_record.rs
@@ -423,6 +423,10 @@ pub unsafe fn handle_final_input_mapping(player_idx: i32, out: *mut MappedInputs
 #[skyline::hook(offset = 0x2da180)] // After cpu controls are assigned from ai calls
 unsafe fn set_cpu_controls(p_data: *mut *mut u8) {
     call_original!(p_data);
+    if !is_training_mode() {
+        return;
+    }
+    
     let controller_data = *p_data.add(1) as *mut ControlModuleInternal;
     let _controller_no = (*controller_data).controller_index;
 

From e78ab92d4bf3eca93fb25ef403956c2513dc829f Mon Sep 17 00:00:00 2001
From: Austin Traver <austintraver@gmail.com>
Date: Wed, 16 Aug 2023 06:15:11 -0700
Subject: [PATCH 3/4] Fix soft-freeze on loading Quickplay lobby (#593)

* Fix soft-freeze on loading Quickplay lobby

* Formatting

---------

Co-authored-by: jugeeya <jugeeya@live.com>
---
 src/training/input_record.rs | 22 ++++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/src/training/input_record.rs b/src/training/input_record.rs
index 7625be6..bd79d54 100644
--- a/src/training/input_record.rs
+++ b/src/training/input_record.rs
@@ -1,15 +1,20 @@
-use crate::common::button_config;
-use crate::common::consts::{FighterId, HitstunPlayback, OnOff, RecordTrigger};
-use crate::common::input::*;
-use crate::common::{get_module_accessor, is_in_hitstun, is_in_shieldstun, MENU};
-use crate::training::mash;
-use crate::training::ui::notifications::{clear_notifications, color_notification};
+use std::cmp::Ordering;
+
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use skyline::nn::ui2d::ResColor;
 use smash::app::{lua_bind::*, utility, BattleObjectModuleAccessor};
 use smash::lib::lua_const::*;
-use std::cmp::Ordering;
+
+use InputRecordState::*;
+use PossessionState::*;
+
+use crate::common::consts::{FighterId, HitstunPlayback, OnOff, RecordTrigger};
+use crate::common::input::*;
+use crate::common::{button_config, is_training_mode};
+use crate::common::{get_module_accessor, is_in_hitstun, is_in_shieldstun, MENU};
+use crate::training::mash;
+use crate::training::ui::notifications::{clear_notifications, color_notification};
 
 #[derive(PartialEq, Debug)]
 pub enum InputRecordState {
@@ -43,9 +48,6 @@ pub enum StartingStatus {
     Other,
 }
 
-use InputRecordState::*;
-use PossessionState::*;
-
 const STICK_NEUTRAL: f32 = 0.2;
 const STICK_CLAMP_MULTIPLIER: f32 = 1.0 / 120.0; // 120.0 = CLAMP_MAX
 const FINAL_RECORD_MAX: usize = 600; // Maximum length for input recording sequences (capacity)

From 0f808320c334b0e46d627ba009ea74970abb675f Mon Sep 17 00:00:00 2001
From: jugeeya <jugeeya@live.com>
Date: Wed, 16 Aug 2023 06:29:14 -0700
Subject: [PATCH 4/4] Fix formatting issue (#594)

---
 src/training/input_record.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/training/input_record.rs b/src/training/input_record.rs
index bd79d54..07e4fd3 100644
--- a/src/training/input_record.rs
+++ b/src/training/input_record.rs
@@ -428,7 +428,7 @@ unsafe fn set_cpu_controls(p_data: *mut *mut u8) {
     if !is_training_mode() {
         return;
     }
-    
+
     let controller_data = *p_data.add(1) as *mut ControlModuleInternal;
     let _controller_no = (*controller_data).controller_index;