rsnaker/graphics/menus/retro_parameter_table/
customized_with_edit.rs

1use crate::game_logic::game_options::{get_parameter_range, GameOptions, ONLY_FOR_CLI_PARAMETERS};
2use crate::graphics::menus::retro_parameter_table::generic_logic::{
3    get_default_action_input, ActionInputs, CellValue, FooterData, GenericMenu, RowData,
4    TableParameterAction,
5};
6use clap::CommandFactory;
7use crossterm::event::KeyCode;
8use ratatui::DefaultTerminal;
9
10pub fn setup_and_run_cli_table_parameters(
11    terminal: &mut DefaultTerminal,
12    options: &mut GameOptions,
13) {
14    let current_preset = options.load;
15    let data: Vec<RowData> = load_parameter_cli_in_table(options);
16    let mut actions = get_default_action_input();
17    actions.push(ActionInputs {
18        key: vec![KeyCode::Char('x'), KeyCode::Char('X')],
19        action: vec![
20            TableParameterAction::ApplyAndSave(options),
21            TableParameterAction::Quit,
22        ],
23    });
24    // Add presets 1 to 7
25    for i in 1..=7u16 {
26        // Convert the number 'i' (1-7) into its corresponding character ('1'-'7').
27        // unwrap() is safe here because 'i' is between 1 and 7 (base 10),
28        // which are valid digits for char::from_digit.
29        let key_char = char::from_digit(u32::from(i), 10).unwrap();
30
31        actions.push(ActionInputs {
32            // Key is the character representation of the preset number
33            key: vec![
34                KeyCode::Char(key_char),
35                KeyCode::Char(key_char.to_ascii_uppercase()),
36            ],
37            // The action loads the preset corresponding to 'i'
38            action: vec![TableParameterAction::LoadPreset(i, |preset| {
39                let footer_updated_data = Some(parameters_cli_get_footer_data(Some(preset)));
40                //Get the GameOption struct filled by toml preset file
41                if let Ok(game_options_from_preset) =
42                    &mut GameOptions::load_from_toml_preset(preset)
43                {
44                    (
45                        Some(load_parameter_cli_in_table(game_options_from_preset)),
46                        footer_updated_data,
47                    )
48                } else {
49                    //If no preset file, nothing to do except updating the preset for saving later
50                    (None, footer_updated_data)
51                }
52            })],
53        });
54    }
55    // Call Parameters screen and input management with the game options to modify
56    GenericMenu::new(
57        data,
58        &parameters_cli_get_headers(),
59        parameters_cli_get_footer_data(current_preset),
60        current_preset,
61    )
62    .run(actions, terminal);
63}
64
65#[must_use]
66fn load_parameter_cli_in_table(options: &mut GameOptions) -> Vec<RowData> {
67    let cmd = GameOptions::command();
68    let mut rows = vec![];
69    let mut arg_value;
70    //We get all the possible arguments for the game (not the ones for CLI tweaks)
71    //to have the same arguments tweakable with the in-game menu as the cli
72    for arg in cmd.get_arguments().filter(|arg| {
73        !ONLY_FOR_CLI_PARAMETERS
74            .iter()
75            .any(|arg_pattern| arg.get_long().unwrap().contains(arg_pattern))
76    }) {
77        //We want all the possible values for the argument to have a selectable list of them
78        let mut all_value_for_this_arg = vec![];
79        //For booleans and enums, use clap functionalities to get possible values
80        // get_possible_values() only works for boolean or enum
81        let pv_bool_enum = arg.get_possible_values();
82        if pv_bool_enum.is_empty() {
83            if let Some(range) = get_parameter_range(arg.get_long().unwrap()) {
84                all_value_for_this_arg.extend(range.map(|i| i.to_string()));
85            } else {
86                // If we are on Emoji String (the only no boolean, no range, no enum type there),
87                // if any other I have created a possible_value macro for each argument:
88                // use the emoji vector to get them:
89                all_value_for_this_arg.extend(GameOptions::emojis_iterator());
90                //Check if the user provides an existing emoji or a brand new one
91                add_unique_symbol(&mut all_value_for_this_arg, options.head_symbol.clone());
92                add_unique_symbol(&mut all_value_for_this_arg, options.body_symbol.clone());
93            }
94        } else {
95            //For booleans and enums,
96            all_value_for_this_arg
97                .extend(pv_bool_enum.into_iter().map(|v| v.get_name().to_string()));
98        }
99        // Set default value, from current value (default auto so if not set in CLI using default,
100        // and serde default for missing serialize value for not-to-be-serialized value,
101        // for others...your fault to no provides them :p)
102        let mut index = 0;
103        //TOML crate prefers _ vs. -
104        if let Some(default_value) = options
105            .to_structured_toml()
106            .get(&arg.get_long().unwrap().replace('-', "_"))
107        {
108            let mut default_str = default_value.to_string();
109            //TOML crate seems to love adding apostrophes and capitalize to string value
110            if default_str.contains('"') {
111                default_str = default_str.split('"').collect::<Vec<&str>>()[1].to_string();
112            }
113            index = all_value_for_this_arg
114                .iter()
115                .position(|v| v == &default_str)
116                .unwrap_or(0);
117        }
118        //index = values.iter().position(|v| v == &default_str).unwrap_or(0);
119        let arg_name = "--".to_string() + arg.get_long().unwrap();
120        arg_value = CellValue::new_with_options(arg_name, all_value_for_this_arg, index);
121
122        rows.push(RowData::new(vec![
123            arg_value,
124            CellValue::new(arg.get_long().unwrap().to_string()),
125            CellValue::new(
126                arg.get_help()
127                    .unwrap_or_else(|| {
128                        panic!("Missing help for argument: {}", arg.get_long().unwrap())
129                    })
130                    .to_string(),
131            ),
132        ]));
133    }
134    rows
135}
136#[must_use]
137fn parameters_cli_get_headers() -> Vec<String> {
138    vec![
139        "🎯 Value".to_string(),
140        "📋 Parameter".to_string(),
141        "📝 Description / super power".to_string(),
142    ]
143}
144/// Should add an action to the Footer Data (like apply, move, change value)
145#[must_use]
146pub fn parameters_cli_get_footer_data(current_preset: Option<u16>) -> Vec<FooterData> {
147    vec![
148        FooterData {
149            symbol: "Esc".into(),
150            text: "Quit".into(),
151            value: None,
152        },
153        FooterData {
154            symbol: "x".into(),
155            text: "Quit & Save".into(),
156            value: None,
157        },
158        FooterData {
159            symbol: "↕".into(),
160            text: "Move".into(),
161            value: None,
162        },
163        FooterData {
164            symbol: "← →".into(),
165            text: "Change value".into(),
166            value: None,
167        },
168        FooterData {
169            symbol: "1-7".into(),
170            text: "Load".into(),
171            value: current_preset,
172        },
173    ]
174}
175
176fn add_unique_symbol(collection: &mut Vec<String>, symbol: String) {
177    if !collection.contains(&symbol) {
178        collection.push(symbol);
179    }
180}