From 5c0073a9485c4226e58b657cb49628919a28a942 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 01:15:43 +0200 Subject: Tolerate changes in the state file --- src/watch/notify_event.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/watch/notify_event.rs (limited to 'src/watch/notify_event.rs') diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs new file mode 100644 index 0000000..0c8d669 --- /dev/null +++ b/src/watch/notify_event.rs @@ -0,0 +1,42 @@ +use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; +use std::{path::Path, sync::mpsc::Sender}; + +use super::WatchEvent; + +pub struct DebounceEventHandler { + pub tx: Sender, + pub exercise_paths: &'static [&'static Path], +} + +impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { + fn handle_event(&mut self, event: DebounceEventResult) { + let event = match event { + Ok(event) => { + let Some(exercise_ind) = event + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercise_paths + .iter() + .position(|path| event.path.ends_with(path)) + }) + .min() + else { + return; + }; + + WatchEvent::FileChange { exercise_ind } + } + Err(e) => WatchEvent::NotifyErr(e), + }; + + // An error occurs when the receiver is dropped. + // After dropping the receiver, the debouncer guard should also be dropped. + let _ = self.tx.send(event); + } +} -- cgit v1.2.3 From bee62c89de09fdd9823cba81e07f0f8528fe8ef9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 02:41:19 +0200 Subject: Add terminal links --- src/app_state.rs | 2 +- src/app_state/state_file.rs | 8 +++----- src/embedded.rs | 7 ++++++- src/exercise.rs | 34 +++++++++++++++++++++++++++++++--- src/info_file.rs | 10 ++++------ src/list/state.rs | 2 +- src/run.rs | 12 +++++++++--- src/watch.rs | 2 +- src/watch/notify_event.rs | 4 ++-- src/watch/state.rs | 6 +----- 10 files changed, 59 insertions(+), 28 deletions(-) (limited to 'src/watch/notify_event.rs') diff --git a/src/app_state.rs b/src/app_state.rs index 1a051b9..98c6384 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -38,7 +38,7 @@ impl AppState { // Leaking to be able to borrow in the watch mode `Table`. // Leaking is not a problem because the `AppState` instance lives until // the end of the program. - let path = Box::leak(exercise_info.path().into_boxed_path()); + let path = exercise_info.path().leak(); exercise_info.name.shrink_to_fit(); let name = exercise_info.name.leak(); diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs index 364a1fa..4e4a0e1 100644 --- a/src/app_state/state_file.rs +++ b/src/app_state/state_file.rs @@ -59,7 +59,7 @@ pub fn write(app_state: &AppState) -> Result<()> { exercises: ExercisesStateSerializer(&app_state.exercises), }; - let mut buf = Vec::with_capacity(1024); + let mut buf = Vec::with_capacity(4096); serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?; fs::write(STATE_FILE_NAME, buf) .with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?; @@ -69,8 +69,6 @@ pub fn write(app_state: &AppState) -> Result<()> { #[cfg(test)] mod tests { - use std::path::Path; - use crate::info_file::Mode; use super::*; @@ -81,14 +79,14 @@ mod tests { let exercises = [ Exercise { name: "1", - path: Path::new("exercises/1.rs"), + path: "exercises/1.rs", mode: Mode::Run, hint: String::new(), done: true, }, Exercise { name: "2", - path: Path::new("exercises/2.rs"), + path: "exercises/2.rs", mode: Mode::Test, hint: String::new(), done: false, diff --git a/src/embedded.rs b/src/embedded.rs index 1e2d677..866b12b 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -91,7 +91,12 @@ impl EmbeddedFiles { Ok(()) } - pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> { + pub fn write_exercise_to_disk

(&self, path: P, strategy: WriteStrategy) -> io::Result<()> + where + P: AsRef, + { + let path = path.as_ref(); + if let Some(file) = self .exercises_dir .files diff --git a/src/exercise.rs b/src/exercise.rs index c5ece5f..2ec8d97 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; +use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - path::Path, + fs, process::{Command, Output}, }; @@ -10,11 +11,32 @@ use crate::{ info_file::Mode, }; +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,) + } + } +} + pub struct Exercise { // Exercise's unique name pub name: &'static str, // Exercise's path - pub path: &'static Path, + pub path: &'static str, // The mode of the exercise pub mode: Mode, // The hint text associated with the exercise @@ -60,10 +82,16 @@ impl Exercise { .write_exercise_to_disk(self.path, WriteStrategy::Overwrite) .with_context(|| format!("Failed to reset the exercise {self}")) } + + pub fn terminal_link(&self) -> StyledContent> { + style(TerminalFileLink { path: self.path }) + .underlined() + .blue() + } } impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Display::fmt(&self.path.display(), f) + self.path.fmt(f) } } diff --git a/src/info_file.rs b/src/info_file.rs index dc97b92..2a45e02 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; -use std::{fs, path::PathBuf}; +use std::fs; // The mode of the exercise. #[derive(Deserialize, Copy, Clone)] @@ -28,14 +28,12 @@ pub struct ExerciseInfo { } impl ExerciseInfo { - pub fn path(&self) -> PathBuf { - let path = if let Some(dir) = &self.dir { + pub fn path(&self) -> String { + if let Some(dir) = &self.dir { format!("exercises/{dir}/{}.rs", self.name) } else { format!("exercises/{}.rs", self.name) - }; - - PathBuf::from(path) + } } } diff --git a/src/list/state.rs b/src/list/state.rs index 38391a4..2a1fef1 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -63,7 +63,7 @@ impl<'a> UiState<'a> { next, exercise_state, Span::raw(exercise.name), - Span::raw(exercise.path.to_string_lossy()), + Span::raw(exercise.path), ])) }); diff --git a/src/run.rs b/src/run.rs index 9c504b5..863b584 100644 --- a/src/run.rs +++ b/src/run.rs @@ -17,18 +17,24 @@ pub fn run(app_state: &mut AppState) -> Result<()> { if !output.status.success() { app_state.set_pending(app_state.current_exercise_ind())?; - bail!("Ran {} with errors", app_state.current_exercise()); + bail!( + "Ran {} with errors", + app_state.current_exercise().terminal_link(), + ); } stdout.write_fmt(format_args!( "{}{}\n", "✓ Successfully ran ".green(), - exercise.path.to_string_lossy().green(), + exercise.path.green(), ))?; match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), - ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()), + ExercisesProgress::Pending => println!( + "Next exercise: {}", + app_state.current_exercise().terminal_link(), + ), } Ok(()) diff --git a/src/watch.rs b/src/watch.rs index 58e829f..bab64ae 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -42,7 +42,7 @@ pub enum WatchExit { pub fn watch( app_state: &mut AppState, - exercise_paths: &'static [&'static Path], + exercise_paths: &'static [&'static str], ) -> Result { let (tx, rx) = channel(); let mut debouncer = new_debouncer( diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 0c8d669..fb9a8c0 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -1,11 +1,11 @@ use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; -use std::{path::Path, sync::mpsc::Sender}; +use std::sync::mpsc::Sender; use super::WatchEvent; pub struct DebounceEventHandler { pub tx: Sender, - pub exercise_paths: &'static [&'static Path], + pub exercise_paths: &'static [&'static str], } impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { diff --git a/src/watch/state.rs b/src/watch/state.rs index 6a97637..1a79573 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -136,11 +136,7 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise )?; self.writer.write_fmt(format_args!( "{progress_bar}Current exercise: {}\n", - self.app_state - .current_exercise() - .path - .to_string_lossy() - .bold(), + self.app_state.current_exercise().terminal_link(), ))?; self.show_prompt()?; -- cgit v1.2.3 From 14fe248b4bfc3c577be7deacc346a959c7c0cc47 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 14:44:12 +0200 Subject: Optimize the notify event handler --- src/main.rs | 8 ++++---- src/watch.rs | 6 +++--- src/watch/notify_event.rs | 21 +++++++++++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) (limited to 'src/watch/notify_event.rs') diff --git a/src/main.rs b/src/main.rs index a928504..7a142fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -144,23 +144,23 @@ fn main() -> Result<()> { match args.command { None => { - let notify_exercise_paths: Option<&'static [&'static str]> = if args.manual_run { + let notify_exercise_names = if args.manual_run { None } else { // For the the notify event handler thread. // Leaking is not a problem because the slice lives until the end of the program. Some( - app_state + &*app_state .exercises() .iter() - .map(|exercise| exercise.path) + .map(|exercise| exercise.name.as_bytes()) .collect::>() .leak(), ) }; loop { - match watch::watch(&mut app_state, notify_exercise_paths)? { + match watch::watch(&mut app_state, notify_exercise_names)? { WatchExit::Shutdown => break, // It is much easier to exit the watch mode, launch the list mode and then restart // the watch mode instead of trying to pause the watch threads and correct the diff --git a/src/watch.rs b/src/watch.rs index 1e22f59..2f4409a 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -42,19 +42,19 @@ pub enum WatchExit { pub fn watch( app_state: &mut AppState, - notify_exercise_paths: Option<&'static [&'static str]>, + notify_exercise_names: Option<&'static [&'static [u8]]>, ) -> Result { let (tx, rx) = channel(); let mut manual_run = false; // Prevent dropping the guard until the end of the function. // Otherwise, the file watcher exits. - let _debouncer_guard = if let Some(exercise_paths) = notify_exercise_paths { + let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let mut debouncer = new_debouncer( Duration::from_millis(500), DebounceEventHandler { tx: tx.clone(), - exercise_paths, + exercise_names, }, ) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index fb9a8c0..f66a834 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -5,7 +5,7 @@ use super::WatchEvent; pub struct DebounceEventHandler { pub tx: Sender, - pub exercise_paths: &'static [&'static str], + pub exercise_names: &'static [&'static [u8]], } impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { @@ -15,15 +15,24 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { let Some(exercise_ind) = event .iter() .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { + if event.kind != DebouncedEventKind::Any { return None; } - self.exercise_paths + let file_name = event.path.file_name()?.to_str()?.as_bytes(); + + if file_name.len() < 4 { + return None; + } + let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3); + + if ext != b".rs" { + return None; + } + + self.exercise_names .iter() - .position(|path| event.path.ends_with(path)) + .position(|exercise_name| *exercise_name == file_name_without_ext) }) .min() else { -- cgit v1.2.3 From 17a2d42ffd868e2049c91d7d1adbecd7f9958020 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 16:44:48 +0200 Subject: Better variable naming --- src/watch/notify_event.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'src/watch/notify_event.rs') diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index f66a834..a224377 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -9,17 +9,17 @@ pub struct DebounceEventHandler { } impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { - fn handle_event(&mut self, event: DebounceEventResult) { - let event = match event { - Ok(event) => { - let Some(exercise_ind) = event + fn handle_event(&mut self, input_event: DebounceEventResult) { + let output_event = match input_event { + Ok(input_event) => { + let Some(exercise_ind) = input_event .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any { + .filter_map(|input_event| { + if input_event.kind != DebouncedEventKind::Any { return None; } - let file_name = event.path.file_name()?.to_str()?.as_bytes(); + let file_name = input_event.path.file_name()?.to_str()?.as_bytes(); if file_name.len() < 4 { return None; @@ -46,6 +46,6 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { // An error occurs when the receiver is dropped. // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.tx.send(event); + let _ = self.tx.send(output_event); } } -- cgit v1.2.3 From c8481d35c120ff99213e6ed73ba889e51cac10c5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 14 May 2024 01:49:22 +0200 Subject: Done documentation --- src/progress_bar.rs | 3 +++ src/run.rs | 6 +----- src/watch.rs | 5 +++-- src/watch/notify_event.rs | 5 +++-- src/watch/state.rs | 9 +++++---- 5 files changed, 15 insertions(+), 13 deletions(-) (limited to 'src/watch/notify_event.rs') diff --git a/src/progress_bar.rs b/src/progress_bar.rs index d6962b8..4a54170 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -12,6 +12,7 @@ const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; const PROGRESS_EXCEEDS_MAX_ERR: &str = "The progress of the progress bar is higher than the maximum"; +/// Terminal progress bar to be used when not using Ratataui. pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result { use crossterm::style::Stylize; @@ -54,6 +55,8 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result Result> { use ratatui::style::Stylize; diff --git a/src/run.rs b/src/run.rs index ac97414..36899b9 100644 --- a/src/run.rs +++ b/src/run.rs @@ -41,11 +41,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), - ExercisesProgress::CurrentPending => println!( - "Current exercise: {}", - app_state.current_exercise().terminal_link(), - ), - ExercisesProgress::NewPending => println!( + ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!( "Next exercise: {}", app_state.current_exercise().terminal_link(), ), diff --git a/src/watch.rs b/src/watch.rs index 2fbc533..88a1230 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -14,7 +14,7 @@ use std::{ use crate::app_state::{AppState, ExercisesProgress}; use self::{ - notify_event::DebounceEventHandler, + notify_event::NotifyEventHandler, state::WatchState, terminal_event::{terminal_event_handler, InputEvent}, }; @@ -40,6 +40,7 @@ pub enum WatchExit { List, } +/// `notify_exercise_names` as None activates the manual run mode. pub fn watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, @@ -52,7 +53,7 @@ pub fn watch( let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let mut debouncer = new_debouncer( Duration::from_millis(200), - DebounceEventHandler { + NotifyEventHandler { tx: tx.clone(), exercise_names, }, diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index a224377..7471640 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -3,12 +3,13 @@ use std::sync::mpsc::Sender; use super::WatchEvent; -pub struct DebounceEventHandler { +pub struct NotifyEventHandler { pub tx: Sender, + /// Used to report which exercise was modified. pub exercise_names: &'static [&'static [u8]], } -impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { +impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { fn handle_event(&mut self, input_event: DebounceEventResult) { let output_event = match input_event { Ok(input_event) => { diff --git a/src/watch/state.rs b/src/watch/state.rs index abd21fb..14c3f01 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,7 +1,7 @@ use anyhow::Result; use crossterm::{ style::{style, Stylize}, - terminal::size, + terminal, }; use std::io::{self, StdoutLock, Write}; @@ -84,6 +84,7 @@ impl<'a> WatchState<'a> { self.run_current_exercise() } + /// Move on to the next exercise if the current one is done. pub fn next_exercise(&mut self) -> Result { if self.done_status == DoneStatus::Pending { return Ok(ExercisesProgress::CurrentPending); @@ -113,7 +114,7 @@ impl<'a> WatchState<'a> { } pub fn render(&mut self) -> Result<()> { - // Prevent having the first line shifted. + // Prevent having the first line shifted if clearing wasn't successful. self.writer.write_all(b"\n")?; clear_terminal(&mut self.writer)?; @@ -145,11 +146,11 @@ When you are done experimenting, enter `n` to move on to the next exercise 🦀" writeln!( self.writer, "A solution file can be found at {}\n", - style(TerminalFileLink(solution_path)).underlined().green() + style(TerminalFileLink(solution_path)).underlined().green(), )?; } - let line_width = size()?.0; + let line_width = terminal::size()?.0; let progress_bar = progress_bar( self.app_state.n_done(), self.app_state.exercises().len() as u16, -- cgit v1.2.3