Skip to main content

rsnaker/graphics/menus/retro_parameter_table/
customized_with_edit.rs

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