I'm starting out with Rust and decided to implement a game of Hangman. Can you please provide general feedback on the code?
I've already identified some lines (see comments [A], [B], [C]) where I cannot find a better way; feedback regarding these would be much appreciated as well.
use rand::seq::SliceRandom;
use std::io::stdin;
const MAX_ALLOWED_ERRORS: i32 = 7;
fn main() {
    println!("\nWelcome to Hangman!\n");
    let to_guess = generate_word();
    let mut display = str::repeat("_ ", to_guess.len());
    println!("{}", display);
    let mut nb_errors = 0;
    loop {
        let mut user_input = String::new();
        stdin().read_line(&mut user_input).expect("Could not read user input");
        match extract_and_capitalize_first_letter(&user_input) {
            Ok(user_guess) => {
                println!("You guessed: {}", user_guess);
                let (is_error, is_full, new_display) = compute_displayed_word(
                    &to_guess, &Some(display), &user_guess,
                );
                // [A] Is there a way to avoid this? Ideally, I would like
                //     to use the same variable for storing the result as
                //     the one passed as the argument
                display = new_display;
                if is_error {
                    nb_errors += 1;
                }
                if is_full {
                    println!("You win!");
                    break;
                }
            }
            Err(_) => {
                println!("Could not process your input");
                nb_errors += 1;
            }
        }
        println!("Error counter: {}", nb_errors);  // maybe later print a real hangman
        if nb_errors > MAX_ALLOWED_ERRORS {
            println!("You lose! The word was: {}", to_guess);
            break;
        }
        println!("{}", display);
    }
}
// return the string to display along with 2 booleans:
// 1. indicating if an error should be counted
// 2. indicating if the word is fully guessed
fn compute_displayed_word(
    word: &String,
    current_display: &Option<String>,
    guess: &char,
) -> (bool, bool, String) {
    let mut is_error = true;
    let mut is_full = true;
    // [B] Should I work with a Vec<char>, or is a mutable String ok?
    let mut new_display = "".to_string();
    for (i, letter) in word.chars().enumerate() {
        if letter.to_ascii_uppercase().eq(guess) {
            is_error = false;
            new_display.push(*guess);
        } else {
            let letter_to_display = match current_display {
                // [C] I couldn't find a way to not do the .chars().collect()
                //     at each iteration, apparently this is due to the
                //     absence of Copy implementation in Vec<char>
                Some(d) => d.chars().collect::<Vec<char>>()[i * 2],
                _ => '_'
            };
            if letter_to_display.eq(&'_') {
                is_full = false;
            }
            new_display.push(letter_to_display);
        }
        new_display.push(' ');
    }
    (is_error, is_full, new_display)
}
fn generate_word() -> String {
    let all_words = vec!["test", "cat", "dog", "controller", "operation", "jazz"];
    let chosen_word = all_words.choose(&mut rand::thread_rng());
    match chosen_word {
        Some(&s) => {
            return s.to_string();
        }
        None => panic!("Could not choose a word!")
    }
}
fn extract_and_capitalize_first_letter(s: &String) -> Result<char, String> {
    return match s.chars().nth(0) {
        Some(c) => {
            if !c.is_ascii_alphabetic() {
                return Err("Only alphabetic characters allowed".to_string());
            }
            Ok(c.to_ascii_uppercase())
        }
        None => Err("Empty string".to_string()),
    };
}
