From bee62c89de09fdd9823cba81e07f0f8528fe8ef9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 02:41:19 +0200 Subject: [PATCH] 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(-) diff --git a/src/app_state.rs b/src/app_state.rs index 1a051b97..98c63842 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 364a1fa3..4e4a0e15 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 1e2d6770..866b12b8 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 c5ece5f5..2ec8d979 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 dc97b926..2a45e02d 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 38391a49..2a1fef18 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 9c504b53..863b584e 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 58e829f3..bab64ae1 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 0c8d6692..fb9a8c05 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 6a97637b..1a79573f 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()?;