Compare commits

...

7 commits

Author SHA1 Message Date
mo8it 01e6732e4d Improve resetting 2024-04-18 12:41:17 +02:00
mo8it f04089b8bc Only take a reference 2024-04-18 11:40:54 +02:00
mo8it 2566f9aaf6 Place mods under all imports 2024-04-18 11:31:08 +02:00
mo8it 1eac00e89a Disable init command during development 2024-04-18 11:28:28 +02:00
mo8it 2e9b9a9f13 Move constant 2024-04-18 11:21:39 +02:00
mo8it 9f5be60b40 Use git stash to reset third-party exercises 2024-04-18 11:20:51 +02:00
mo8it d64836f317 Avoid an unneeded syscall 2024-04-18 01:49:32 +02:00
9 changed files with 113 additions and 61 deletions

View file

@ -7,9 +7,15 @@ use crossterm::{
use std::{ use std::{
fs::{self, File}, fs::{self, File},
io::{Read, StdoutLock, Write}, io::{Read, StdoutLock, Write},
path::Path,
process::{Command, Stdio},
}; };
use crate::{exercise::Exercise, info_file::ExerciseInfo, FENISH_LINE}; use crate::{
embedded::{WriteStrategy, EMBEDDED_FILES},
exercise::Exercise,
info_file::ExerciseInfo,
};
const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
@ -31,6 +37,7 @@ pub struct AppState {
n_done: u16, n_done: u16,
final_message: String, final_message: String,
file_buf: Vec<u8>, file_buf: Vec<u8>,
official_exercises: bool,
} }
impl AppState { impl AppState {
@ -111,6 +118,7 @@ impl AppState {
n_done: 0, n_done: 0,
final_message, final_message,
file_buf: Vec::with_capacity(2048), file_buf: Vec::with_capacity(2048),
official_exercises: !Path::new("info.toml").exists(),
}; };
let state_file_status = slf.update_from_file(); let state_file_status = slf.update_from_file();
@ -172,6 +180,53 @@ impl AppState {
Ok(()) Ok(())
} }
fn reset_path(&self, path: &str) -> Result<()> {
if self.official_exercises {
return EMBEDDED_FILES
.write_exercise_to_disk(path, WriteStrategy::Overwrite)
.with_context(|| format!("Failed to reset the exercise {path}"));
}
let output = Command::new("git")
.arg("stash")
.arg("push")
.arg("--")
.arg(path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.output()
.with_context(|| format!("Failed to run `git stash push -- {path}`"))?;
if !output.status.success() {
bail!(
"`git stash push -- {path}` didn't run successfully: {}",
String::from_utf8_lossy(&output.stderr),
);
}
Ok(())
}
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)?;
Ok(path)
}
pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> {
if exercise_ind >= self.exercises.len() {
bail!(BAD_INDEX_ERR);
}
let path = self.exercises[exercise_ind].path;
self.set_pending(exercise_ind)?;
self.reset_path(path)?;
Ok(path)
}
fn next_pending_exercise_ind(&self) -> Option<usize> { fn next_pending_exercise_ind(&self) -> Option<usize> {
if self.current_exercise_ind == self.exercises.len() - 1 { if self.current_exercise_ind == self.exercises.len() - 1 {
// The last exercise is done. // The last exercise is done.
@ -275,3 +330,25 @@ All exercises seem to be done.
Recompiling and running all exercises to make sure that all of them are actually done. Recompiling and running all exercises to make sure that all of them are actually done.
"; ";
const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! |
+-------------------------- ------------------------+
\\/\x1b[31m
\x1b[0m
";

View file

@ -1,6 +1,8 @@
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
use clap::Subcommand; use clap::Subcommand;
use crate::DEVELOPING_OFFICIAL_RUSTLINGS;
mod check; mod check;
mod init; mod init;
mod update; mod update;
@ -15,7 +17,13 @@ pub enum DevCommands {
impl DevCommands { impl DevCommands {
pub fn run(self) -> Result<()> { pub fn run(self) -> Result<()> {
match self { match self {
DevCommands::Init => init::init().context(INIT_ERR), DevCommands::Init => {
if DEVELOPING_OFFICIAL_RUSTLINGS {
bail!("Disabled while developing the official Rustlings");
}
init::init().context(INIT_ERR)
}
DevCommands::Check => check::check(), DevCommands::Check => check::check(),
DevCommands::Update => update::update(), DevCommands::Update => update::update(),
} }

View file

@ -79,7 +79,7 @@ fn unexpected_file(path: &Path) -> Error {
anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `exercises` directory", path.display()) anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `exercises` directory", path.display())
} }
fn check_exercise_dir_files(info_file_paths: hashbrown::HashSet<PathBuf>) -> Result<()> { fn check_exercise_dir_files(info_file_paths: &hashbrown::HashSet<PathBuf>) -> Result<()> {
for entry in read_dir("exercises").context("Failed to open the `exercises` directory")? { for entry in read_dir("exercises").context("Failed to open the `exercises` directory")? {
let entry = entry.context("Failed to read the `exercises` directory")?; let entry = entry.context("Failed to read the `exercises` directory")?;
@ -131,7 +131,7 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> {
} }
let info_file_paths = check_info_file_exercises(info_file)?; let info_file_paths = check_info_file_exercises(info_file)?;
check_exercise_dir_files(info_file_paths)?; check_exercise_dir_files(&info_file_paths)?;
Ok(()) Ok(())
} }

View file

@ -47,14 +47,12 @@ impl EmbeddedFlatDir {
let path = Path::new(self.path); let path = Path::new(self.path);
if let Err(e) = create_dir(path) { if let Err(e) = create_dir(path) {
if !path.is_dir() { if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e); return Err(e);
} }
} }
self.readme.write_to_disk(WriteStrategy::Overwrite)?; self.readme.write_to_disk(WriteStrategy::Overwrite)
Ok(())
} }
} }

View file

@ -7,11 +7,7 @@ use std::{
process::{Command, Output}, process::{Command, Output},
}; };
use crate::{ use crate::{info_file::Mode, DEVELOPING_OFFICIAL_RUSTLINGS};
embedded::{WriteStrategy, EMBEDDED_FILES},
info_file::Mode,
DEVELOPING_OFFICIAL_RUSTLINGS,
};
pub struct TerminalFileLink<'a> { pub struct TerminalFileLink<'a> {
path: &'a str, path: &'a str,
@ -87,12 +83,6 @@ impl Exercise {
} }
} }
pub fn reset(&self) -> Result<()> {
EMBEDDED_FILES
.write_exercise_to_disk(self.path, WriteStrategy::Overwrite)
.with_context(|| format!("Failed to reset the exercise {self}"))
}
pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> { pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> {
style(TerminalFileLink { path: self.path }) style(TerminalFileLink { path: self.path })
.underlined() .underlined()

View file

@ -7,12 +7,12 @@ use crossterm::{
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use std::io; use std::io;
mod state;
use crate::app_state::AppState; use crate::app_state::AppState;
use self::state::{Filter, UiState}; use self::state::{Filter, UiState};
mod state;
pub fn list(app_state: &mut AppState) -> Result<()> { pub fn list(app_state: &mut AppState) -> Result<()> {
let mut stdout = io::stdout().lock(); let mut stdout = io::stdout().lock();
stdout.execute(EnterAlternateScreen)?; stdout.execute(EnterAlternateScreen)?;

View file

@ -217,23 +217,22 @@ impl<'a> UiState<'a> {
return Ok(self); return Ok(self);
}; };
let (ind, exercise) = self let ind = self
.app_state .app_state
.exercises() .exercises()
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(ind, exercise)| match self.filter { .filter_map(|(ind, exercise)| match self.filter {
Filter::Done => exercise.done.then_some((ind, exercise)), Filter::Done => exercise.done.then_some(ind),
Filter::Pending => (!exercise.done).then_some((ind, exercise)), Filter::Pending => (!exercise.done).then_some(ind),
Filter::None => Some((ind, exercise)), Filter::None => Some(ind),
}) })
.nth(selected) .nth(selected)
.context("Invalid selection index")?; .context("Invalid selection index")?;
exercise.reset()?; let exercise_path = self.app_state.reset_exercise_by_ind(ind)?;
self.message self.message
.write_fmt(format_args!("The exercise {exercise} has been reset!"))?; .write_fmt(format_args!("The exercise {exercise_path} has been reset"))?;
self.app_state.set_pending(ind)?;
Ok(self.with_updated_rows()) Ok(self.with_updated_rows())
} }

View file

@ -11,6 +11,8 @@ use std::{
process::exit, process::exit,
}; };
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
mod app_state; mod app_state;
mod dev; mod dev;
mod embedded; mod embedded;
@ -22,8 +24,6 @@ mod progress_bar;
mod run; mod run;
mod watch; mod watch;
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
const CURRENT_FORMAT_VERSION: u8 = 1; const CURRENT_FORMAT_VERSION: u8 = 1;
const DEVELOPING_OFFICIAL_RUSTLINGS: bool = { const DEVELOPING_OFFICIAL_RUSTLINGS: bool = {
#[allow(unused_assignments, unused_mut)] #[allow(unused_assignments, unused_mut)]
@ -79,6 +79,10 @@ fn main() -> Result<()> {
match args.command { match args.command {
Some(Subcommands::Init) => { Some(Subcommands::Init) => {
if DEVELOPING_OFFICIAL_RUSTLINGS {
bail!("Disabled while developing the official Rustlings");
}
return init::init().context("Initialization failed"); return init::init().context("Initialization failed");
} }
Some(Subcommands::Dev(dev_command)) => return dev_command.run(), Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
@ -154,10 +158,8 @@ fn main() -> Result<()> {
} }
Some(Subcommands::Reset { name }) => { Some(Subcommands::Reset { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
let exercise = app_state.current_exercise(); let exercise_path = app_state.reset_current_exercise()?;
exercise.reset()?; println!("The exercise {exercise_path} has been reset");
println!("The exercise {exercise} has been reset!");
app_state.set_pending(app_state.current_exercise_ind())?;
} }
Some(Subcommands::Hint { name }) => { Some(Subcommands::Hint { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
@ -190,25 +192,3 @@ const PRE_INIT_MSG: &str = r"
The `exercises` directory wasn't found in the current directory. The `exercises` directory wasn't found in the current directory.
If you are just starting with Rustlings, run the command `rustlings init` to initialize it."; If you are just starting with Rustlings, run the command `rustlings init` to initialize it.";
const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! |
+-------------------------- ------------------------+
\\/\x1b[31m
\x1b[0m
";

View file

@ -11,10 +11,6 @@ use std::{
time::Duration, time::Duration,
}; };
mod notify_event;
mod state;
mod terminal_event;
use crate::app_state::{AppState, ExercisesProgress}; use crate::app_state::{AppState, ExercisesProgress};
use self::{ use self::{
@ -23,6 +19,10 @@ use self::{
terminal_event::{terminal_event_handler, InputEvent}, terminal_event::{terminal_event_handler, InputEvent},
}; };
mod notify_event;
mod state;
mod terminal_event;
enum WatchEvent { enum WatchEvent {
Input(InputEvent), Input(InputEvent),
FileChange { exercise_ind: usize }, FileChange { exercise_ind: usize },