diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app_state.rs | 68 | ||||
| -rw-r--r-- | src/embedded.rs | 147 | ||||
| -rw-r--r-- | src/exercise.rs | 29 | ||||
| -rw-r--r-- | src/info_file.rs | 29 | ||||
| -rw-r--r-- | src/init.rs | 4 | ||||
| -rw-r--r-- | src/main.rs | 1 | ||||
| -rw-r--r-- | src/run.rs | 14 | ||||
| -rw-r--r-- | src/terminal_link.rs | 23 | ||||
| -rw-r--r-- | src/watch/state.rs | 35 |
9 files changed, 219 insertions, 131 deletions
diff --git a/src/app_state.rs b/src/app_state.rs index 09de2a3..33d3de2 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -11,11 +11,7 @@ use std::{ process::{Command, Stdio}, }; -use crate::{ - embedded::{WriteStrategy, EMBEDDED_FILES}, - exercise::Exercise, - info_file::ExerciseInfo, -}; +use crate::{embedded::EMBEDDED_FILES, exercise::Exercise, info_file::ExerciseInfo, DEBUG_PROFILE}; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -100,10 +96,15 @@ impl AppState { exercise_info.name.shrink_to_fit(); let name = exercise_info.name.leak(); + let dir = exercise_info.dir.map(|mut dir| { + dir.shrink_to_fit(); + &*dir.leak() + }); let hint = exercise_info.hint.trim().to_owned(); Exercise { + dir, name, path, mode: exercise_info.mode, @@ -181,10 +182,16 @@ impl AppState { Ok(()) } - fn reset_path(&self, path: &str) -> Result<()> { + fn reset(&self, ind: usize, dir_name: Option<&str>, path: &str) -> Result<()> { if self.official_exercises { return EMBEDDED_FILES - .write_exercise_to_disk(path, WriteStrategy::Overwrite) + .write_exercise_to_disk( + ind, + dir_name.context( + "Official exercises must be nested in the `exercises` directory", + )?, + path, + ) .with_context(|| format!("Failed to reset the exercise {path}")); } @@ -209,11 +216,11 @@ impl AppState { } pub fn reset_current_exercise(&mut self) -> Result<&'static str> { - let path = self.current_exercise().path; self.set_pending(self.current_exercise_ind)?; - self.reset_path(path)?; + let exercise = self.current_exercise(); + self.reset(self.current_exercise_ind, exercise.dir, exercise.path)?; - Ok(path) + Ok(exercise.path) } pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> { @@ -221,11 +228,11 @@ impl AppState { bail!(BAD_INDEX_ERR); } - let path = self.exercises[exercise_ind].path; self.set_pending(exercise_ind)?; - self.reset_path(path)?; + let exercise = &self.exercises[exercise_ind]; + self.reset(exercise_ind, exercise.dir, exercise.path)?; - Ok(path) + Ok(exercise.path) } fn next_pending_exercise_ind(&self) -> Option<usize> { @@ -250,6 +257,41 @@ impl AppState { } } + pub fn current_solution_path(&self) -> Result<Option<String>> { + if DEBUG_PROFILE { + return Ok(None); + } + + let current_exercise = self.current_exercise(); + + if self.official_exercises { + let dir_name = current_exercise + .dir + .context("Official exercises must be nested in the `exercises` directory")?; + let solution_path = format!("solutions/{dir_name}/{}.rs", current_exercise.name); + + EMBEDDED_FILES.write_solution_to_disk( + self.current_exercise_ind, + dir_name, + &solution_path, + )?; + + Ok(Some(solution_path)) + } else { + let solution_path = if let Some(dir) = current_exercise.dir { + format!("solutions/{dir}/{}.rs", current_exercise.name) + } else { + format!("solutions/{}.rs", current_exercise.name) + }; + + if Path::new(&solution_path).exists() { + return Ok(Some(solution_path)); + } + + Ok(None) + } + } + pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> { let exercise = &mut self.exercises[self.current_exercise_ind]; if !exercise.done { diff --git a/src/embedded.rs b/src/embedded.rs index eae3099..d7952a1 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -1,9 +1,11 @@ +use anyhow::{bail, Context, Error, Result}; use std::{ - fs::{create_dir, File, OpenOptions}, + fs::{create_dir, create_dir_all, OpenOptions}, io::{self, Write}, - path::Path, }; +use crate::info_file::ExerciseInfo; + pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); #[derive(Clone, Copy)] @@ -13,107 +15,110 @@ pub enum WriteStrategy { } impl WriteStrategy { - fn open<P: AsRef<Path>>(self, path: P) -> io::Result<File> { - match self { + fn write(self, path: &str, content: &[u8]) -> Result<()> { + let file = match self { Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path), Self::Overwrite => OpenOptions::new() .create(true) .write(true) .truncate(true) .open(path), - } - } -} - -struct EmbeddedFile { - path: &'static str, - content: &'static [u8], -} + }; -impl EmbeddedFile { - fn write_to_disk(&self, strategy: WriteStrategy) -> io::Result<()> { - strategy.open(self.path)?.write_all(self.content) + file.context("Failed to open the file `{path}` in write mode")? + .write_all(content) + .context("Failed to write the file {path}") } } -struct EmbeddedFlatDir { - path: &'static str, - readme: EmbeddedFile, - content: &'static [EmbeddedFile], +struct ExerciseFiles { + exercise: &'static [u8], + solution: &'static [u8], } -impl EmbeddedFlatDir { - fn init_on_disk(&self) -> io::Result<()> { - let path = Path::new(self.path); +struct ExerciseDir { + name: &'static str, + readme: &'static [u8], +} - if let Err(e) = create_dir(path) { - if e.kind() != io::ErrorKind::AlreadyExists { - return Err(e); +impl ExerciseDir { + fn init_on_disk(&self) -> Result<()> { + let path_prefix = "exercises/"; + let readme_path_postfix = "/README.md"; + let mut dir_path = + String::with_capacity(path_prefix.len() + self.name.len() + readme_path_postfix.len()); + dir_path.push_str(path_prefix); + dir_path.push_str(self.name); + + if let Err(e) = create_dir(&dir_path) { + if e.kind() == io::ErrorKind::AlreadyExists { + return Ok(()); } + + return Err( + Error::from(e).context(format!("Failed to create the directory {dir_path}")) + ); } - self.readme.write_to_disk(WriteStrategy::Overwrite) - } -} + let readme_path = { + dir_path.push_str(readme_path_postfix); + dir_path + }; + WriteStrategy::Overwrite.write(&readme_path, self.readme)?; -struct ExercisesDir { - readme: EmbeddedFile, - files: &'static [EmbeddedFile], - dirs: &'static [EmbeddedFlatDir], + Ok(()) + } } pub struct EmbeddedFiles { - exercises_dir: ExercisesDir, + exercise_files: &'static [ExerciseFiles], + exercise_dirs: &'static [ExerciseDir], } impl EmbeddedFiles { - pub fn init_exercises_dir(&self) -> io::Result<()> { - create_dir("exercises")?; - - self.exercises_dir - .readme - .write_to_disk(WriteStrategy::IfNotExists)?; + pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { + create_dir("exercises").context("Failed to create the directory `exercises`")?; - for file in self.exercises_dir.files { - file.write_to_disk(WriteStrategy::IfNotExists)?; - } + WriteStrategy::IfNotExists.write( + "exercises/README.md", + include_bytes!("../exercises/README.md"), + )?; - for dir in self.exercises_dir.dirs { + for dir in self.exercise_dirs { dir.init_on_disk()?; + } - for file in dir.content { - file.write_to_disk(WriteStrategy::IfNotExists)?; - } + for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) { + WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?; } Ok(()) } - pub fn write_exercise_to_disk<P>(&self, path: P, strategy: WriteStrategy) -> io::Result<()> - where - P: AsRef<Path>, - { - let path = path.as_ref(); - - if let Some(file) = self - .exercises_dir - .files - .iter() - .find(|file| Path::new(file.path) == path) - { - return file.write_to_disk(strategy); - } - - for dir in self.exercises_dir.dirs { - if let Some(file) = dir.content.iter().find(|file| Path::new(file.path) == path) { - dir.init_on_disk()?; - return file.write_to_disk(strategy); - } - } + pub fn write_exercise_to_disk( + &self, + exercise_ind: usize, + dir_name: &str, + path: &str, + ) -> Result<()> { + let Some(dir) = self.exercise_dirs.iter().find(|dir| dir.name == dir_name) else { + bail!("`{dir_name}` not found in the embedded directories"); + }; + + dir.init_on_disk()?; + WriteStrategy::Overwrite.write(path, self.exercise_files[exercise_ind].exercise) + } - Err(io::Error::new( - io::ErrorKind::NotFound, - format!("{} not found in the embedded files", path.display()), - )) + pub fn write_solution_to_disk( + &self, + exercise_ind: usize, + dir_name: &str, + path: &str, + ) -> Result<()> { + let dir_path = format!("solutions/{dir_name}"); + create_dir_all(&dir_path) + .with_context(|| format!("Failed to create the directory {dir_path}"))?; + + WriteStrategy::Overwrite.write(path, self.exercise_files[exercise_ind].solution) } } diff --git a/src/exercise.rs b/src/exercise.rs index e85efe4..45ac208 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -2,35 +2,14 @@ use anyhow::{Context, Result}; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - fs, path::Path, process::{Command, Output}, }; -use crate::{info_file::Mode, DEBUG_PROFILE}; - -pub struct TerminalFileLink<'a> { - path: &'a str, -} - -impl<'a> Display for TerminalFileLink<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if let Ok(Some(canonical_path)) = fs::canonicalize(self.path) - .as_deref() - .map(|path| path.to_str()) - { - write!( - f, - "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", - canonical_path, self.path, - ) - } else { - write!(f, "{}", self.path,) - } - } -} +use crate::{info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE}; pub struct Exercise { + pub dir: Option<&'static str>, // Exercise's unique name pub name: &'static str, // Exercise's path @@ -84,9 +63,7 @@ impl Exercise { } pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> { - style(TerminalFileLink { path: self.path }) - .underlined() - .blue() + style(TerminalFileLink(self.path)).underlined().blue() } } diff --git a/src/info_file.rs b/src/info_file.rs index 879609e..f344464 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -1,6 +1,8 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; -use std::fs; +use std::{fs, io::ErrorKind}; + +use crate::DEBUG_PROFILE; // The mode of the exercise. #[derive(Deserialize, Copy, Clone)] @@ -46,18 +48,27 @@ pub struct InfoFile { } impl InfoFile { + fn from_embedded() -> Result<Self> { + toml_edit::de::from_str(include_str!("../info.toml")) + .context("Failed to parse the embedded `info.toml` file") + } + pub fn parse() -> Result<Self> { + if DEBUG_PROFILE { + return Self::from_embedded(); + } + // Read a local `info.toml` if it exists. - let slf: Self = match fs::read_to_string("info.toml") { - Ok(file_content) => toml_edit::de::from_str(&file_content) + let slf = match fs::read_to_string("info.toml") { + Ok(file_content) => toml_edit::de::from_str::<Self>(&file_content) .context("Failed to parse the `info.toml` file")?, - Err(e) => match e.kind() { - std::io::ErrorKind::NotFound => { - toml_edit::de::from_str(include_str!("../info.toml")) - .context("Failed to parse the embedded `info.toml` file")? + Err(e) => { + if e.kind() == ErrorKind::NotFound { + return Self::from_embedded(); } - _ => return Err(Error::from(e).context("Failed to read the `info.toml` file")), - }, + + return Err(Error::from(e).context("Failed to read the `info.toml` file")); + } }; if slf.exercises.is_empty() { diff --git a/src/init.rs b/src/init.rs index f210db7..f1a9509 100644 --- a/src/init.rs +++ b/src/init.rs @@ -24,11 +24,11 @@ pub fn init() -> Result<()> { set_current_dir("rustlings") .context("Failed to change the current directory to `rustlings`")?; + let info_file = InfoFile::parse()?; EMBEDDED_FILES - .init_exercises_dir() + .init_exercises_dir(&info_file.exercises) .context("Failed to initialize the `rustlings/exercises` directory")?; - let info_file = InfoFile::parse()?; let current_cargo_toml = include_str!("../dev/Cargo.toml"); // Skip the first line (comment). let newline_ind = current_cargo_toml diff --git a/src/main.rs b/src/main.rs index 9ff218a..790fff6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod init; mod list; mod progress_bar; mod run; +mod terminal_link; mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; @@ -1,8 +1,11 @@ use anyhow::{bail, Result}; -use crossterm::style::Stylize; +use crossterm::style::{style, Stylize}; use std::io::{self, Write}; -use crate::app_state::{AppState, ExercisesProgress}; +use crate::{ + app_state::{AppState, ExercisesProgress}, + terminal_link::TerminalFileLink, +}; pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); @@ -29,6 +32,13 @@ pub fn run(app_state: &mut AppState) -> Result<()> { exercise.path.green(), ))?; + if let Some(solution_path) = app_state.current_solution_path()? { + println!( + "\nA solution file can be found at {}\n", + style(TerminalFileLink(&solution_path)).underlined().green(), + ); + } + match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), ExercisesProgress::Pending => println!( diff --git a/src/terminal_link.rs b/src/terminal_link.rs new file mode 100644 index 0000000..c9e6bce --- /dev/null +++ b/src/terminal_link.rs @@ -0,0 +1,23 @@ +use std::{ + fmt::{self, Display, Formatter}, + fs, +}; + +pub struct TerminalFileLink<'a>(pub &'a str); + +impl<'a> Display for TerminalFileLink<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if let Ok(Some(canonical_path)) = fs::canonicalize(self.0) + .as_deref() + .map(|path| path.to_str()) + { + write!( + f, + "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", + canonical_path, self.0, + ) + } else { + write!(f, "{}", self.0) + } + } +} diff --git a/src/watch/state.rs b/src/watch/state.rs index c0f6c53..5f4abf3 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,6 +1,6 @@ use anyhow::Result; use crossterm::{ - style::Stylize, + style::{style, Stylize}, terminal::{size, Clear, ClearType}, ExecutableCommand, }; @@ -9,15 +9,22 @@ use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, progress_bar::progress_bar, + terminal_link::TerminalFileLink, }; +enum DoneStatus { + DoneWithSolution(String), + DoneWithoutSolution, + Pending, +} + pub struct WatchState<'a> { writer: StdoutLock<'a>, app_state: &'a mut AppState, stdout: Option<Vec<u8>>, stderr: Option<Vec<u8>>, show_hint: bool, - show_done: bool, + done_status: DoneStatus, manual_run: bool, } @@ -31,7 +38,7 @@ impl<'a> WatchState<'a> { stdout: None, stderr: None, show_hint: false, - show_done: false, + done_status: DoneStatus::Pending, manual_run, } } @@ -49,13 +56,18 @@ impl<'a> WatchState<'a> { if output.status.success() { self.stderr = None; - self.show_done = true; + self.done_status = + if let Some(solution_path) = self.app_state.current_solution_path()? { + DoneStatus::DoneWithSolution(solution_path) + } else { + DoneStatus::DoneWithoutSolution + }; } else { self.app_state .set_pending(self.app_state.current_exercise_ind())?; self.stderr = Some(output.stderr); - self.show_done = false; + self.done_status = DoneStatus::Pending; } self.render() @@ -67,7 +79,7 @@ impl<'a> WatchState<'a> { } pub fn next_exercise(&mut self) -> Result<ExercisesProgress> { - if !self.show_done { + if matches!(self.done_status, DoneStatus::Pending) { self.writer .write_all(b"The current exercise isn't done yet\n")?; self.show_prompt()?; @@ -84,7 +96,7 @@ impl<'a> WatchState<'a> { self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?; } - if self.show_done { + if !matches!(self.done_status, DoneStatus::Pending) { self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; } @@ -124,7 +136,7 @@ impl<'a> WatchState<'a> { ))?; } - if self.show_done { + if !matches!(self.done_status, DoneStatus::Pending) { self.writer.write_fmt(format_args!( "{}\n\n", "Exercise done ✓ @@ -134,6 +146,13 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise ))?; } + if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { + self.writer.write_fmt(format_args!( + "A solution file can be found at {}\n\n", + style(TerminalFileLink(solution_path)).underlined().green() + ))?; + } + let line_width = size()?.0; let progress_bar = progress_bar( self.app_state.n_done(), |
