From 276c59e8bfcaf69d7438f5ce72da41c4708006a5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Aug 2026 13:01:54 +0200 Subject: [PATCH] Allow processing for hints and messages in the info file --- rustlings-macros/src/lib.rs | 12 ++---------- src/app_state.rs | 19 +++++++++---------- src/cargo_toml.rs | 4 ++-- src/dev/check.rs | 2 +- src/exercise.rs | 2 +- src/info_file.rs | 32 +++++++++++++++----------------- src/main.rs | 17 +++++++---------- src/watch/state.rs | 8 +++++++- 8 files changed, 44 insertions(+), 52 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index 40eebc16b9..db758d5945 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -16,16 +16,8 @@ struct InfoFile<'a> { #[proc_macro] pub fn include_files(_: TokenStream) -> TokenStream { - // Remove `\r` on Windows - let info_file = String::from_utf8( - include_bytes!("../info.toml") - .iter() - .copied() - .filter(|c| *c != b'\r') - .collect(), - ) - .expect("Failed to parse `info.toml` as UTF8"); - let exercises = toml::de::from_str::(&info_file) + let info_file = include_str!("../info.toml"); + let exercises = toml::de::from_str::(info_file) .expect("Failed to parse `info.toml`") .exercises; diff --git a/src/app_state.rs b/src/app_state.rs index 27c08b1d93..e1d0e0b868 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -54,7 +54,7 @@ pub struct AppState { exercises: Vec, // Cache the number of done exercises to avoid iterating over all exercises every time. n_done: u32, - final_message: &'static str, + final_message: String, state_file: File, // Preallocated buffer for reading and writing the state file. file_buf: Vec, @@ -66,8 +66,8 @@ pub struct AppState { impl AppState { pub fn new( - exercise_infos: Vec, - final_message: &'static str, + exercise_infos: Vec>, + final_message: String, editor: Option, vs_code_term: bool, ) -> Result<(Self, StateFileStatus)> { @@ -111,13 +111,12 @@ impl AppState { Exercise { name: exercise_info.name, dir: exercise_info.dir, - // Leaking for `Editor::open`. - // Leaking is fine since the app state exists until the end of the program. + // LEAKING: For `Editor::open`. The app state is used until the end of the program. path: exercise_info.path().leak(), canonical_path, test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, - hint: exercise_info.hint.trim_ascii(), + hint: exercise_info.hint, // Updated below. done: false, } @@ -549,9 +548,9 @@ impl AppState { clear_terminal(stdout)?; stdout.write_all(FINISH_LINE.as_bytes())?; - let final_message = self.final_message.trim_ascii(); + let final_message = self.final_message.as_bytes().trim_ascii(); if !final_message.is_empty() { - stdout.write_all(final_message.as_bytes())?; + stdout.write_all(final_message)?; stdout.write_all(b"\n")?; } @@ -617,7 +616,7 @@ mod tests { canonical_path: None, test: false, strict_clippy: false, - hint: "", + hint: String::new(), done: false, } } @@ -628,7 +627,7 @@ mod tests { current_exercise_ind: 0, exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], n_done: 0, - final_message: "", + final_message: String::new(), state_file: tempfile::tempfile().unwrap(), file_buf: Vec::new(), official_exercises: true, diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index 9297da82da..2407745218 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -110,7 +110,7 @@ mod tests { dir: None, test: true, strict_clippy: true, - hint: "", + hint: String::new(), skip_check_unsolved: false, }, ExerciseInfo { @@ -118,7 +118,7 @@ mod tests { dir: Some("d"), test: false, strict_clippy: false, - hint: "", + hint: String::new(), skip_check_unsolved: false, }, ]; diff --git a/src/dev/check.rs b/src/dev/check.rs index 042b2b5034..58c2a174af 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -383,7 +383,7 @@ pub fn check(require_solutions: bool) -> Result<()> { check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?; } - // Leaking is fine since they are used until the end of the program. + // LEAKING: Used until the end of the program. let cmd_runner = Box::leak(Box::new(CmdRunner::build()?)); let info_file = Box::leak(Box::new(info_file)); diff --git a/src/exercise.rs b/src/exercise.rs index b969c69ad2..7732e1a26e 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -73,7 +73,7 @@ pub struct Exercise { pub canonical_path: Option, pub test: bool, pub strict_clippy: bool, - pub hint: &'static str, + pub hint: String, pub done: bool, } diff --git a/src/info_file.rs b/src/info_file.rs index 26bb1a2ccd..da4086aa3e 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -6,11 +6,11 @@ use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; /// Deserialized from the `info.toml` file. #[derive(Deserialize)] -pub struct ExerciseInfo { +pub struct ExerciseInfo<'a> { /// Exercise's unique name. - pub name: &'static str, + pub name: &'a str, /// Exercise's directory name inside the `exercises/` directory. - pub dir: Option<&'static str>, + pub dir: Option<&'a str>, /// Run `cargo test` on the exercise. #[serde(default = "default_true")] pub test: bool, @@ -18,7 +18,7 @@ pub struct ExerciseInfo { #[serde(default)] pub strict_clippy: bool, /// The exercise's hint to be shown to the user on request. - pub hint: &'static str, + pub hint: String, /// The exercise is already solved. Ignore it when checking that all exercises are unsolved. #[serde(default)] pub skip_check_unsolved: bool, @@ -27,7 +27,7 @@ const fn default_true() -> bool { true } -impl ExerciseInfo { +impl ExerciseInfo<'_> { /// Path to the exercise file starting with the `exercises/` directory. pub fn path(&self) -> String { let mut path = if let Some(dir) = self.dir { @@ -53,7 +53,7 @@ impl ExerciseInfo { } } -impl RunnableExercise for ExerciseInfo { +impl RunnableExercise for ExerciseInfo<'_> { fn name(&self) -> &str { self.name } @@ -77,11 +77,14 @@ pub struct InfoFile { /// For possible breaking changes in the future for community exercises. pub format_version: u8, /// Shown to users when starting with the exercises. - pub welcome_message: Option<&'static str>, + #[serde(default)] + pub welcome_message: String, /// Shown to users after finishing all exercises. - pub final_message: Option<&'static str>, + #[serde(default)] + pub final_message: String, /// List of all exercises. - pub exercises: Vec, + #[serde(borrow)] + pub exercises: Vec>, } impl InfoFile { @@ -89,15 +92,10 @@ impl InfoFile { /// Community exercises: Parse the `info.toml` file in the current directory. pub fn parse() -> Result { // Read a local `info.toml` if it exists. - let slf = match fs::read("info.toml") { + let slf = match fs::read_to_string("info.toml") { Ok(file_content) => { - // Remove `\r` on Windows. - // Leaking is fine since the info file is used until the end of the program. - let file_content = - String::from_utf8(file_content.into_iter().filter(|c| *c != b'\r').collect()) - .context("Failed to parse `info.toml` as UTF8")? - .leak(); - toml::de::from_str::(file_content) + // LEAKING: The info file is used until the end of the program. + toml::de::from_str::(file_content.leak()) .context("Failed to parse the `info.toml` file")? } Err(e) => { diff --git a/src/main.rs b/src/main.rs index 8da36f7f57..623e8ed091 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,24 +70,22 @@ fn main() -> Result { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, - info_file.final_message.unwrap_or_default(), + info_file.final_message, editor, vs_code_term, )?; // Show the welcome message if the state file doesn't exist yet. - if let Some(welcome_message) = info_file.welcome_message { + let welcome_message = info_file.welcome_message.as_bytes().trim_ascii(); + if !welcome_message.is_empty() { match state_file_status { StateFileStatus::NotRead => { let mut stdout = io::stdout().lock(); clear_terminal(&mut stdout)?; let welcome_message = welcome_message.trim_ascii(); - write!( - stdout, - "{welcome_message}\n\n\ - Press ENTER to continue " - )?; + stdout.write_all(welcome_message)?; + stdout.write_all(b"\n\nPress ENTER to continue ")?; press_enter_prompt(&mut stdout)?; clear_terminal(&mut stdout)?; // Flush to be able to show errors occurring before printing a newline to stdout. @@ -106,8 +104,7 @@ fn main() -> Result { let notify_exercise_names = if args.manual_run { None } else { - // For the notify event handler thread. - // Leaking is fine since the slice is used until the end of the program. + // LEAKING: For the notify event handler thread. The slice is used until the end of the program. Some( &*app_state .exercises() @@ -176,7 +173,7 @@ fn main() -> Result { current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?; stdout.write_all(b"\n\nHint:\n")?; - stdout.write_all(current_exercise.hint.as_bytes())?; + stdout.write_all(current_exercise.hint.as_bytes().trim_ascii())?; stdout.write_all(b"\n")?; } // Handled in an earlier match. diff --git a/src/watch/state.rs b/src/watch/state.rs index 8bbdc58518..e47b73f816 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -224,7 +224,13 @@ impl<'a> WatchState<'a> { stdout.queue(ResetColor)?; stdout.write_all(b"\n")?; - stdout.write_all(self.app_state.current_exercise().hint.as_bytes())?; + stdout.write_all( + self.app_state + .current_exercise() + .hint + .as_bytes() + .trim_ascii(), + )?; stdout.write_all(b"\n\n")?; }