From 7d53dc4c95d6b27761f157f00b958c120104f066 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 30 Mar 2026 17:35:45 +0200 Subject: [PATCH 01/16] Update deps --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a65ecff7cf..4c2d6d75b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" [[package]] name = "foldhash" @@ -236,9 +236,9 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -306,9 +306,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "linux-raw-sys" @@ -345,9 +345,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", @@ -515,9 +515,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -564,9 +564,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -655,27 +655,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "unicode-ident" @@ -885,9 +885,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" [[package]] name = "wit-bindgen" From 7ed231604099c347823c53c3874a8bdba59ca87a Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 15:45:31 +0200 Subject: [PATCH 02/16] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f326b601c..8b9b356bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,14 @@ ## Unreleased +### Fixed + +- Fix integer overflow on big terminal widths [@gabfec](https://github.com/gabfec) +- Fix workspace detection on Windows [@senekor](https://github.com/senekor) + ### Changed +- Avoid initializing a nested Git repository [@senekor](https://github.com/senekor) - `vecs2`: Removed the use of `map` and `collect`, which are only taught later. ## 6.5.0 (2025-08-21) From c466d01da938029e12d14514e47deb17c4b4e726 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 17:19:11 +0200 Subject: [PATCH 03/16] Unify imports --- src/cli.rs | 41 +++++++++++++++++++++++++ src/dev.rs | 4 +-- src/list.rs | 7 +++-- src/list/state.rs | 3 +- src/main.rs | 61 +++++++++---------------------------- src/watch.rs | 3 +- src/watch/notify_event.rs | 2 +- src/watch/state.rs | 3 +- src/watch/terminal_event.rs | 2 +- 9 files changed, 66 insertions(+), 60 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000000..2bea554487 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,41 @@ +use clap::{Parser, Subcommand}; + +use crate::dev::DevCommand; + +/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code +#[derive(Parser)] +#[command(version)] +pub struct Args { + #[command(subcommand)] + pub command: Option, + /// Manually run the current exercise using `r` in the watch mode. + /// Only use this if Rustlings fails to detect exercise file changes. + #[arg(long)] + pub manual_run: bool, +} + +#[derive(Subcommand)] +pub enum Command { + /// Initialize the official Rustlings exercises + Init, + /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified + Run { + /// The name of the exercise + name: Option, + }, + /// Check all the exercises, marking them as done or pending accordingly. + CheckAll, + /// Reset a single exercise + Reset { + /// The name of the exercise + name: String, + }, + /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified + Hint { + /// The name of the exercise + name: Option, + }, + /// Commands for developing (community) Rustlings exercises + #[command(subcommand)] + Dev(DevCommand), +} diff --git a/src/dev.rs b/src/dev.rs index 41fddbeb98..f2be606646 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -7,7 +7,7 @@ mod new; mod update; #[derive(Subcommand)] -pub enum DevCommands { +pub enum DevCommand { /// Create a new project for community exercises New { /// The path to create the project in @@ -26,7 +26,7 @@ pub enum DevCommands { Update, } -impl DevCommands { +impl DevCommand { pub fn run(self) -> Result<()> { match self { Self::New { path, no_git } => { diff --git a/src/list.rs b/src/list.rs index a2eee9e16a..c60a5299ef 100644 --- a/src/list.rs +++ b/src/list.rs @@ -11,9 +11,10 @@ use crossterm::{ }; use std::io::{self, StdoutLock, Write}; -use crate::app_state::AppState; - -use self::state::{Filter, ListState}; +use crate::{ + app_state::AppState, + list::state::{Filter, ListState}, +}; mod scroll_state; mod state; diff --git a/src/list/state.rs b/src/list/state.rs index 58aa496167..4e097613f4 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -15,11 +15,10 @@ use std::{ use crate::{ app_state::AppState, exercise::Exercise, + list::scroll_state::ScrollState, term::{CountedWrite, MaxLenWriter, progress_bar}, }; -use super::scroll_state::ScrollState; - const COL_SPACING: usize = 2; const SELECTED_ROW_ATTRIBUTES: Attributes = Attributes::none() .with(Attribute::Reverse) diff --git a/src/main.rs b/src/main.rs index c39e862927..8f31703b03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, bail}; use app_state::StateFileStatus; -use clap::{Parser, Subcommand}; +use clap::Parser; use std::{ io::{self, IsTerminal, Write}, path::Path, @@ -8,10 +8,15 @@ use std::{ }; use term::{clear_terminal, press_enter_prompt}; -use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile}; +use crate::{ + app_state::AppState, + cli::{Args, Command}, + info_file::InfoFile, +}; mod app_state; mod cargo_toml; +mod cli; mod cmd; mod dev; mod embedded; @@ -25,44 +30,6 @@ mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; -/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code -#[derive(Parser)] -#[command(version)] -struct Args { - #[command(subcommand)] - command: Option, - /// Manually run the current exercise using `r` in the watch mode. - /// Only use this if Rustlings fails to detect exercise file changes. - #[arg(long)] - manual_run: bool, -} - -#[derive(Subcommand)] -enum Subcommands { - /// Initialize the official Rustlings exercises - Init, - /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified - Run { - /// The name of the exercise - name: Option, - }, - /// Check all the exercises, marking them as done or pending accordingly. - CheckAll, - /// Reset a single exercise - Reset { - /// The name of the exercise - name: String, - }, - /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified - Hint { - /// The name of the exercise - name: Option, - }, - /// Commands for developing (community) Rustlings exercises - #[command(subcommand)] - Dev(DevCommands), -} - fn main() -> Result { let args = Args::parse(); @@ -72,8 +39,8 @@ fn main() -> Result { 'priority_cmd: { match args.command { - Some(Subcommands::Init) => init::init().context("Initialization failed")?, - Some(Subcommands::Dev(dev_command)) => dev_command.run()?, + Some(Command::Init) => init::init().context("Initialization failed")?, + Some(Command::Dev(dev_command)) => dev_command.run()?, _ => break 'priority_cmd, } @@ -141,13 +108,13 @@ fn main() -> Result { watch::watch(&mut app_state, notify_exercise_names)?; } - Some(Subcommands::Run { name }) => { + Some(Command::Run { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } return run::run(&mut app_state); } - Some(Subcommands::CheckAll) => { + Some(Command::CheckAll) => { let mut stdout = io::stdout().lock(); if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? { if app_state.current_exercise().done { @@ -175,19 +142,19 @@ fn main() -> Result { app_state.render_final_message(&mut stdout)?; } - Some(Subcommands::Reset { name }) => { + Some(Command::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; let exercise_path = app_state.reset_current_exercise()?; println!("The exercise {exercise_path} has been reset"); } - Some(Subcommands::Hint { name }) => { + Some(Command::Hint { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } println!("{}", app_state.current_exercise().hint); } // Handled in an earlier match. - Some(Subcommands::Init | Subcommands::Dev(_)) => (), + Some(Command::Init | Command::Dev(_)) => (), } Ok(ExitCode::SUCCESS) diff --git a/src/watch.rs b/src/watch.rs index 3a56b4b65b..e0b5ccd01b 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -13,10 +13,9 @@ use std::{ use crate::{ app_state::{AppState, ExercisesProgress}, list, + watch::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}, }; -use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}; - mod notify_event; mod state; mod terminal_event; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 9c05f10dad..edd9c7207d 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -12,7 +12,7 @@ use std::{ time::Duration, }; -use super::{EXERCISE_RUNNING, WatchEvent}; +use crate::watch::{EXERCISE_RUNNING, WatchEvent}; const DEBOUNCE_DURATION: Duration = Duration::from_millis(200); diff --git a/src/watch/state.rs b/src/watch/state.rs index f93ea0cf90..1b285d6759 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -17,10 +17,9 @@ use crate::{ clear_terminal, exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line}, term::progress_bar, + watch::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler}, }; -use super::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler}; - const HEADING_ATTRIBUTES: Attributes = Attributes::none() .with(Attribute::Bold) .with(Attribute::Underlined); diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 439e47300a..4f0685b6e0 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -4,7 +4,7 @@ use std::sync::{ mpsc::{Receiver, Sender}, }; -use super::{EXERCISE_RUNNING, WatchEvent}; +use crate::watch::{EXERCISE_RUNNING, WatchEvent}; pub enum InputEvent { Next, From 95b6160b54120515bc538c0c88b40beb0552ab29 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 17:28:48 +0200 Subject: [PATCH 04/16] Don't manually inline --- src/app_state.rs | 7 ------- src/cmd.rs | 2 -- src/exercise.rs | 5 ----- src/info_file.rs | 5 ----- src/list/scroll_state.rs | 4 ---- src/list/state.rs | 5 ----- src/term.rs | 6 ------ src/watch.rs | 2 -- tests/integration_tests.rs | 5 ----- 9 files changed, 41 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 5722e607eb..411afe3667 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -180,37 +180,30 @@ impl AppState { Ok((slf, state_file_status)) } - #[inline] pub fn current_exercise_ind(&self) -> usize { self.current_exercise_ind } - #[inline] pub fn exercises(&self) -> &[Exercise] { &self.exercises } - #[inline] pub fn n_done(&self) -> u32 { self.n_done } - #[inline] pub fn n_pending(&self) -> u32 { self.exercises.len() as u32 - self.n_done } - #[inline] pub fn current_exercise(&self) -> &Exercise { &self.exercises[self.current_exercise_ind] } - #[inline] pub fn cmd_runner(&self) -> &CmdRunner { &self.cmd_runner } - #[inline] pub fn emit_file_links(&self) -> bool { self.emit_file_links } diff --git a/src/cmd.rs b/src/cmd.rs index b2c58f6abb..6442e449a3 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -126,7 +126,6 @@ pub struct CargoSubcommand<'out> { } impl CargoSubcommand<'_> { - #[inline] pub fn args<'arg, I>(&mut self, args: I) -> &mut Self where I: IntoIterator, @@ -136,7 +135,6 @@ impl CargoSubcommand<'_> { } /// The boolean in the returned `Result` is true if the command's exit status is success. - #[inline] pub fn run(self, description: &str) -> Result { run_cmd(self.cmd, description, self.output) } diff --git a/src/exercise.rs b/src/exercise.rs index c07a94e8b5..987428e635 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -158,7 +158,6 @@ pub trait RunnableExercise { /// Compile, check and run the exercise. /// The output is written to the `output` buffer after clearing it. - #[inline] fn run_exercise(&self, output: Option<&mut Vec>, cmd_runner: &CmdRunner) -> Result { self.run::(self.name(), output, cmd_runner) } @@ -201,22 +200,18 @@ pub trait RunnableExercise { } impl RunnableExercise for Exercise { - #[inline] fn name(&self) -> &str { self.name } - #[inline] fn dir(&self) -> Option<&str> { self.dir } - #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy } - #[inline] fn test(&self) -> bool { self.test } diff --git a/src/info_file.rs b/src/info_file.rs index 54a21a5c02..26bb1a2ccd 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -23,7 +23,6 @@ pub struct ExerciseInfo { #[serde(default)] pub skip_check_unsolved: bool, } -#[inline] const fn default_true() -> bool { true } @@ -55,22 +54,18 @@ impl ExerciseInfo { } impl RunnableExercise for ExerciseInfo { - #[inline] fn name(&self) -> &str { self.name } - #[inline] fn dir(&self) -> Option<&str> { self.dir } - #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy } - #[inline] fn test(&self) -> bool { self.test } diff --git a/src/list/scroll_state.rs b/src/list/scroll_state.rs index 2c02ed4f7e..299db568c9 100644 --- a/src/list/scroll_state.rs +++ b/src/list/scroll_state.rs @@ -19,7 +19,6 @@ impl ScrollState { } } - #[inline] pub fn offset(&self) -> usize { self.offset } @@ -41,7 +40,6 @@ impl ScrollState { .min(global_max_offset); } - #[inline] pub fn selected(&self) -> Option { self.selected } @@ -86,12 +84,10 @@ impl ScrollState { self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1))); } - #[inline] fn update_scroll_padding(&mut self) { self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding); } - #[inline] pub fn max_n_rows_to_display(&self) -> usize { self.max_n_rows_to_display } diff --git a/src/list/state.rs b/src/list/state.rs index 4e097613f4..4fcbd3c353 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -303,7 +303,6 @@ impl<'a> ListState<'a> { self.scroll_state.set_n_rows(n_rows); } - #[inline] pub fn filter(&self) -> Filter { self.filter } @@ -313,22 +312,18 @@ impl<'a> ListState<'a> { self.update_rows(); } - #[inline] pub fn select_next(&mut self) { self.scroll_state.select_next(); } - #[inline] pub fn select_previous(&mut self) { self.scroll_state.select_previous(); } - #[inline] pub fn select_first(&mut self) { self.scroll_state.select_first(); } - #[inline] pub fn select_last(&mut self) { self.scroll_state.select_last(); } diff --git a/src/term.rs b/src/term.rs index 8cab50055f..2467b45036 100644 --- a/src/term.rs +++ b/src/term.rs @@ -18,7 +18,6 @@ pub struct MaxLenWriter<'a, 'lock> { } impl<'a, 'lock> MaxLenWriter<'a, 'lock> { - #[inline] pub fn new(stdout: &'a mut StdoutLock<'lock>, max_len: usize) -> Self { Self { stdout, @@ -28,7 +27,6 @@ impl<'a, 'lock> MaxLenWriter<'a, 'lock> { } // Additional is for emojis that take more space. - #[inline] pub fn add_to_len(&mut self, additional: usize) { self.len += additional; } @@ -64,24 +62,20 @@ impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> { Ok(()) } - #[inline] fn stdout(&mut self) -> &mut StdoutLock<'lock> { self.stdout } } impl<'a> CountedWrite<'a> for StdoutLock<'a> { - #[inline] fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> { self.write_all(ascii) } - #[inline] fn write_str(&mut self, unicode: &str) -> io::Result<()> { self.write_all(unicode.as_bytes()) } - #[inline] fn stdout(&mut self) -> &mut StdoutLock<'a> { self } diff --git a/src/watch.rs b/src/watch.rs index e0b5ccd01b..ab96893ea3 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -27,7 +27,6 @@ static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false); pub struct InputPauseGuard(()); impl InputPauseGuard { - #[inline] pub fn scoped_pause() -> Self { EXERCISE_RUNNING.store(true, Relaxed); Self(()) @@ -35,7 +34,6 @@ impl InputPauseGuard { } impl Drop for InputPauseGuard { - #[inline] fn drop(&mut self) { EXERCISE_RUNNING.store(false, Relaxed); } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 91d0536e1e..bb1e398c27 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -19,19 +19,16 @@ struct Cmd<'a> { } impl<'a> Cmd<'a> { - #[inline] fn current_dir(&mut self, current_dir: &'a str) -> &mut Self { self.current_dir = Some(current_dir); self } - #[inline] fn args(&mut self, args: &'a [&'a str]) -> &mut Self { self.args = args; self } - #[inline] fn output(&mut self, output: Output<'a>) -> &mut Self { self.output = Some(output); self @@ -70,13 +67,11 @@ impl<'a> Cmd<'a> { } } - #[inline] #[track_caller] fn success(&self) { self.assert(true); } - #[inline] #[track_caller] fn fail(&self) { self.assert(false); From 4d97c31c0f748acda001e6f0f9c63261ec1d7afe Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 18:17:10 +0200 Subject: [PATCH 05/16] Add Zellij support --- src/app_state.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++- src/cli.rs | 13 +++-- src/main.rs | 1 + src/watch/state.rs | 11 ++-- tmp.txt | 1 + 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 tmp.txt diff --git a/src/app_state.rs b/src/app_state.rs index 411afe3667..31053f73ac 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Error, Result, bail}; use crossterm::{QueueableCommand, cursor, terminal}; +use serde::Deserialize; use std::{ collections::HashSet, env, @@ -11,7 +12,7 @@ use std::{ atomic::{AtomicUsize, Ordering::Relaxed}, mpsc, }, - thread, + thread::{self, JoinHandle}, }; use crate::{ @@ -49,6 +50,44 @@ pub enum CheckProgress { Pending, } +#[derive(Deserialize)] +struct Pane { + id: u32, +} + +#[must_use] +pub struct EditCmdJoinHandle(Option>>); + +fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { + // Remove newline + let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; + let id_str = str::from_utf8(b).ok()?; + + let (first, rest) = b.split_first()?; + let mut id = u32::from(first - b'0'); + + for c in rest { + id = 10 * id + u32::from(c - b'0'); + } + + Some((id_str.to_owned(), id)) +} + +fn close_pane(pane_id: &str) -> Result<()> { + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run `zellij action close-pane -p ID`")?; + + Ok(()) +} + pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -61,12 +100,15 @@ pub struct AppState { official_exercises: bool, cmd_runner: CmdRunner, emit_file_links: bool, + zellij: bool, + open_pane: Option<(String, u32, usize)>, } impl AppState { pub fn new( exercise_infos: Vec, final_message: &'static str, + zellij: bool, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -175,6 +217,8 @@ impl AppState { cmd_runner, // VS Code has its own file link handling emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"), + zellij, + open_pane: None, }; Ok((slf, state_file_status)) @@ -553,6 +597,86 @@ impl AppState { Ok(()) } + + pub fn close_pane(&mut self) -> Result<()> { + if let Some((pane_id_str, _, _)) = self.open_pane.take() { + close_pane(&pane_id_str)?; + } + + Ok(()) + } + + pub fn edit_cmd(&mut self) -> Result { + if !self.zellij { + return Ok(EditCmdJoinHandle(None)); + } + + let open_pane = self.open_pane.take(); + let current_exercise_ind = self.current_exercise_ind; + let mut edit_cmd = Command::new("zellij"); + edit_cmd + .arg("action") + .arg("edit") + .arg(&self.current_exercise().path) + .stdin(Stdio::null()) + .stderr(Stdio::null()); + + let handle = thread::Builder::new() + .spawn(move || { + if let Some((pane_id_str, pane_id, exercise_ind)) = open_pane { + if exercise_ind == current_exercise_ind { + // Check if the pane is still open + let mut output = Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .context("Failed to run `zellij action list-panes -j`")?; + + if !output.status.success() { + bail!("`zellij action list-panes -j` didn't exit successfully"); + } + + // Remove newline + output.stdout.pop(); + + let panes = serde_json::de::from_slice::>(&output.stdout) + .context( + "Failed to parse the output of `zellij action list-panes -j`", + )?; + + if panes.iter().any(|pane| pane.id == pane_id) { + return Ok((pane_id_str, pane_id)); + } + } else { + close_pane(&pane_id_str)?; + } + } + + let output = edit_cmd.output()?; + + if !output.status.success() { + bail!("Failed to open a new Zellij editor pane"); + } + + parse_pane_id(&output.stdout) + .context("Failed to parse the ID of the new Zellij pane") + }) + .context("Failed to spawn a thread to open and close Zellij panes")?; + + Ok(EditCmdJoinHandle(Some(handle))) + } + + pub fn join_edit_cmd(&mut self, handle: EditCmdJoinHandle) -> Result<()> { + if let Some(handle) = handle.0 { + let (pane_id_str, pane_id) = handle.join().unwrap()?; + self.open_pane = Some((pane_id_str, pane_id, self.current_exercise_ind)); + } + + Ok(()) + } } const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -608,6 +732,8 @@ mod tests { official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), emit_file_links: true, + zellij: false, + open_pane: None, }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/cli.rs b/src/cli.rs index 2bea554487..dbc31f9cc0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,28 +9,33 @@ pub struct Args { #[command(subcommand)] pub command: Option, /// Manually run the current exercise using `r` in the watch mode. - /// Only use this if Rustlings fails to detect exercise file changes. + /// Only use this if Rustlings fails to detect exercise file changes #[arg(long)] pub manual_run: bool, + /// Open the current exercise in a new Zellij pane and close the last one if exists + #[arg(long)] + pub zellij: bool, } #[derive(Subcommand)] pub enum Command { /// Initialize the official Rustlings exercises Init, - /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified + /// Run a single exercise. + /// Runs the next pending exercise if the exercise name is not specified Run { /// The name of the exercise name: Option, }, - /// Check all the exercises, marking them as done or pending accordingly. + /// Check all the exercises, marking them as done or pending accordingly CheckAll, /// Reset a single exercise Reset { /// The name of the exercise name: String, }, - /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified + /// Show a hint. + /// Shows the hint of the next pending exercise if the exercise name is not specified Hint { /// The name of the exercise name: Option, diff --git a/src/main.rs b/src/main.rs index 8f31703b03..29ec3d0168 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,7 @@ fn main() -> Result { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), + args.zellij, )?; // Show the welcome message if the state file doesn't exist yet. diff --git a/src/watch/state.rs b/src/watch/state.rs index 1b285d6759..9e7d1ddc54 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -78,14 +78,16 @@ impl<'a> WatchState<'a> { // Ignore any input until running the exercise is done. let _input_pause_guard = InputPauseGuard::scoped_pause(); - self.show_hint = false; - writeln!( stdout, "\nChecking the exercise `{}`. Please wait…", self.app_state.current_exercise().name, )?; + let edit_cmd_handle = self.app_state.edit_cmd()?; + + self.show_hint = false; + let success = self .app_state .current_exercise() @@ -105,7 +107,9 @@ impl<'a> WatchState<'a> { self.done_status = DoneStatus::Pending; } + self.app_state.join_edit_cmd(edit_cmd_handle)?; self.render(stdout)?; + Ok(()) } @@ -127,9 +131,10 @@ impl<'a> WatchState<'a> { match answer[0] { b'y' | b'Y' => { + self.app_state.close_pane()?; self.app_state.reset_current_exercise()?; - // The file watcher reruns the exercise otherwise. + // The file watcher reruns the exercise otherwise if self.manual_run { self.run_current_exercise(stdout)?; } diff --git a/tmp.txt b/tmp.txt new file mode 100644 index 0000000000..3aefeefc6f --- /dev/null +++ b/tmp.txt @@ -0,0 +1 @@ +226.867688ms \ No newline at end of file From c9ccedcff6a40f6b77c1b608ef0d3942215138c9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 14:01:34 +0200 Subject: [PATCH 06/16] Support VSCode and --edit-cmd as editor --- src/app_state.rs | 146 +++++++------------------------------------ src/cli.rs | 7 ++- src/editor.rs | 137 ++++++++++++++++++++++++++++++++++++++++ src/editor/zellij.rs | 62 ++++++++++++++++++ src/exercise.rs | 6 +- src/main.rs | 4 +- src/watch/state.rs | 6 +- 7 files changed, 236 insertions(+), 132 deletions(-) create mode 100644 src/editor.rs create mode 100644 src/editor/zellij.rs diff --git a/src/app_state.rs b/src/app_state.rs index 31053f73ac..bc1d520d28 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,9 +1,7 @@ use anyhow::{Context, Error, Result, bail}; use crossterm::{QueueableCommand, cursor, terminal}; -use serde::Deserialize; use std::{ collections::HashSet, - env, fs::{File, OpenOptions}, io::{Read, Seek, StdoutLock, Write}, path::{MAIN_SEPARATOR_STR, Path}, @@ -12,12 +10,13 @@ use std::{ atomic::{AtomicUsize, Ordering::Relaxed}, mpsc, }, - thread::{self, JoinHandle}, + thread, }; use crate::{ clear_terminal, cmd::CmdRunner, + editor::{Editor, EditorJoinHandle}, embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, @@ -50,44 +49,6 @@ pub enum CheckProgress { Pending, } -#[derive(Deserialize)] -struct Pane { - id: u32, -} - -#[must_use] -pub struct EditCmdJoinHandle(Option>>); - -fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { - // Remove newline - let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; - let id_str = str::from_utf8(b).ok()?; - - let (first, rest) = b.split_first()?; - let mut id = u32::from(first - b'0'); - - for c in rest { - id = 10 * id + u32::from(c - b'0'); - } - - Some((id_str.to_owned(), id)) -} - -fn close_pane(pane_id: &str) -> Result<()> { - Command::new("zellij") - .arg("action") - .arg("close-pane") - .arg("-p") - .arg(pane_id) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run `zellij action close-pane -p ID`")?; - - Ok(()) -} - pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -100,15 +61,14 @@ pub struct AppState { official_exercises: bool, cmd_runner: CmdRunner, emit_file_links: bool, - zellij: bool, - open_pane: Option<(String, u32, usize)>, + editor: Option, } impl AppState { pub fn new( exercise_infos: Vec, final_message: &'static str, - zellij: bool, + editor: Option, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -150,7 +110,9 @@ impl AppState { Exercise { name: exercise_info.name, dir: exercise_info.dir, - path: exercise_info.path(), + // Leaking for `Editor::open`. + // Leaking is fine since the app state exists until the end of the program. + path: exercise_info.path().leak(), canonical_path, test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, @@ -216,9 +178,8 @@ impl AppState { official_exercises: !Path::new("info.toml").exists(), cmd_runner, // VS Code has its own file link handling - emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"), - zellij, - open_pane: None, + emit_file_links: !matches!(editor, Some(Editor::VSCode)), + editor, }; Ok((slf, state_file_status)) @@ -376,9 +337,9 @@ impl AppState { pub fn reset_current_exercise(&mut self) -> Result<&str> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, &exercise.path)?; + self.reset(self.current_exercise_ind, exercise.path)?; - Ok(&exercise.path) + Ok(exercise.path) } // Reset the exercise by index and return its name. @@ -389,7 +350,7 @@ impl AppState { self.set_pending(exercise_ind)?; let exercise = &self.exercises[exercise_ind]; - self.reset(exercise_ind, &exercise.path)?; + self.reset(exercise_ind, exercise.path)?; Ok(exercise.name) } @@ -598,81 +559,23 @@ impl AppState { Ok(()) } - pub fn close_pane(&mut self) -> Result<()> { - if let Some((pane_id_str, _, _)) = self.open_pane.take() { - close_pane(&pane_id_str)?; + pub fn open_editor(&mut self) -> Result { + if let Some(editor) = self.editor.take() { + return editor.open(self.current_exercise_ind, self.current_exercise().path); } - Ok(()) + Ok(EditorJoinHandle::default()) } - pub fn edit_cmd(&mut self) -> Result { - if !self.zellij { - return Ok(EditCmdJoinHandle(None)); - } - - let open_pane = self.open_pane.take(); - let current_exercise_ind = self.current_exercise_ind; - let mut edit_cmd = Command::new("zellij"); - edit_cmd - .arg("action") - .arg("edit") - .arg(&self.current_exercise().path) - .stdin(Stdio::null()) - .stderr(Stdio::null()); - - let handle = thread::Builder::new() - .spawn(move || { - if let Some((pane_id_str, pane_id, exercise_ind)) = open_pane { - if exercise_ind == current_exercise_ind { - // Check if the pane is still open - let mut output = Command::new("zellij") - .arg("action") - .arg("list-panes") - .arg("-j") - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .context("Failed to run `zellij action list-panes -j`")?; - - if !output.status.success() { - bail!("`zellij action list-panes -j` didn't exit successfully"); - } - - // Remove newline - output.stdout.pop(); - - let panes = serde_json::de::from_slice::>(&output.stdout) - .context( - "Failed to parse the output of `zellij action list-panes -j`", - )?; + pub fn join_editor_handle(&mut self, handle: EditorJoinHandle) -> Result<()> { + self.editor = handle.join()?; - if panes.iter().any(|pane| pane.id == pane_id) { - return Ok((pane_id_str, pane_id)); - } - } else { - close_pane(&pane_id_str)?; - } - } - - let output = edit_cmd.output()?; - - if !output.status.success() { - bail!("Failed to open a new Zellij editor pane"); - } - - parse_pane_id(&output.stdout) - .context("Failed to parse the ID of the new Zellij pane") - }) - .context("Failed to spawn a thread to open and close Zellij panes")?; - - Ok(EditCmdJoinHandle(Some(handle))) + Ok(()) } - pub fn join_edit_cmd(&mut self, handle: EditCmdJoinHandle) -> Result<()> { - if let Some(handle) = handle.0 { - let (pane_id_str, pane_id) = handle.join().unwrap()?; - self.open_pane = Some((pane_id_str, pane_id, self.current_exercise_ind)); + pub fn close_editor(&mut self) -> Result<()> { + if let Some(editor) = &mut self.editor { + editor.close()?; } Ok(()) @@ -711,7 +614,7 @@ mod tests { Exercise { name: "0", dir: None, - path: String::from("exercises/0.rs"), + path: "exercises/0.rs", canonical_path: None, test: false, strict_clippy: false, @@ -732,8 +635,7 @@ mod tests { official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), emit_file_links: true, - zellij: false, - open_pane: None, + editor: None, }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/cli.rs b/src/cli.rs index dbc31f9cc0..8bbe25aa2c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,13 +8,14 @@ use crate::dev::DevCommand; pub struct Args { #[command(subcommand)] pub command: Option, + /// Open the current exercise by running the provided `EDIT_CMD EXERCISE_NAME`. + /// Ignored in VS Code + #[arg(long)] + pub edit_cmd: Option, /// Manually run the current exercise using `r` in the watch mode. /// Only use this if Rustlings fails to detect exercise file changes #[arg(long)] pub manual_run: bool, - /// Open the current exercise in a new Zellij pane and close the last one if exists - #[arg(long)] - pub zellij: bool, } #[derive(Subcommand)] diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 0000000000..be24e3eff6 --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,137 @@ +use std::{ + env, + process::{Command, Stdio}, + thread::{self, JoinHandle}, +}; + +use anyhow::{Context, Result, bail}; + +mod zellij; + +pub enum Editor { + VSCode, + Cmd(String, Vec), + Zellij(Option<(String, u32, usize)>), +} + +impl Editor { + pub fn new(cmd: Option) -> Option { + if env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode") { + return Some(Self::VSCode); + } + + if let Some(cmd) = cmd { + todo!() + } + + if env::var_os("ZELLIJ").is_some() { + return Some(Self::Zellij(None)); + } + + None + } + + pub fn open( + self, + exercise_ind: usize, + exercise_path: &'static str, + ) -> Result { + let handle = thread::Builder::new() + .spawn(move || match self { + Editor::VSCode => { + if !Command::new("code") + .arg(exercise_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run `code` to open the current exercise file")? + .success() + { + bail!("Failed to run `code PATH` to open the current exercise file"); + } + + Ok(Self::VSCode) + } + Editor::Cmd(program, args) => { + if !Command::new("code") + .arg(exercise_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run the command from `--edit-cmd`") + .is_ok_and(|status| status.success()) + { + bail!("Failed to run the command from `--edit-cmd`"); + } + + Ok(Self::Cmd(program, args)) + } + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { + if open_exercise_ind == exercise_ind { + if zellij::pane_open(pane_id)? { + return Ok(Self::Zellij(Some(( + pane_id_str, + pane_id, + exercise_ind, + )))); + } + } else { + zellij::close_pane(&pane_id_str)?; + } + } + + let output = Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .context("Failed to run `zellij`")?; + + if !output.status.success() { + bail!("Failed to open a new Zellij editor pane"); + } + + let (pane_id_str, pane_id) = zellij::parse_pane_id(&output.stdout) + .context("Failed to parse the ID of the new Zellij pane")?; + + Ok(Self::Zellij(Some((pane_id_str, pane_id, exercise_ind)))) + } + }) + .context("Failed to spawn a thread to open the editor")?; + + Ok(EditorJoinHandle(Some(handle))) + } + + pub fn close(&mut self) -> Result<()> { + match self { + Editor::VSCode | Editor::Cmd(_, _) => (), + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, _, _)) = open_pane.take() { + zellij::close_pane(&pane_id_str)?; + } + } + } + + Ok(()) + } +} + +#[must_use] +#[derive(Default)] +pub struct EditorJoinHandle(Option>>); + +impl EditorJoinHandle { + pub fn join(self) -> Result> { + if let Some(handle) = self.0 { + let editor = handle.join().unwrap()?; + return Ok(Some(editor)); + } + + Ok(None) + } +} diff --git a/src/editor/zellij.rs b/src/editor/zellij.rs new file mode 100644 index 0000000000..ffa906dc95 --- /dev/null +++ b/src/editor/zellij.rs @@ -0,0 +1,62 @@ +use std::process::{Command, Stdio}; + +use anyhow::{Context, Result, bail}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Pane { + id: u32, +} + +pub fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { + // Remove newline + let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; + let id_str = str::from_utf8(b).ok()?; + + let (first, rest) = b.split_first()?; + let mut id = u32::from(first - b'0'); + + for c in rest { + id = 10 * id + u32::from(c - b'0'); + } + + Some((id_str.to_owned(), id)) +} + +pub fn pane_open(pane_id: u32) -> Result { + let mut output = Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .context("Failed to run `zellij action list-panes -j`")?; + + if !output.status.success() { + bail!("`zellij action list-panes -j` didn't exit successfully"); + } + + // Remove newline + output.stdout.pop(); + + let panes = serde_json::de::from_slice::>(&output.stdout) + .context("Failed to parse the output of `zellij action list-panes -j`")?; + + Ok(panes.iter().any(|pane| pane.id == pane_id)) +} + +pub fn close_pane(pane_id: &str) -> Result<()> { + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run `zellij action close-pane -p ID`")?; + + Ok(()) +} diff --git a/src/exercise.rs b/src/exercise.rs index 987428e635..b969c69ad2 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -69,7 +69,7 @@ pub struct Exercise { pub name: &'static str, pub dir: Option<&'static str>, /// Path of the exercise file starting with the `exercises/` directory. - pub path: String, + pub path: &'static str, pub canonical_path: Option, pub test: bool, pub strict_clippy: bool, @@ -85,9 +85,9 @@ impl Exercise { ) -> io::Result<()> { file_path(writer, Color::Blue, |writer| { if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() { - terminal_file_link(writer, &self.path, canonical_path) + terminal_file_link(writer, self.path, canonical_path) } else { - writer.write_str(&self.path) + writer.write_str(self.path) } }) } diff --git a/src/main.rs b/src/main.rs index 29ec3d0168..1d6c98b128 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use term::{clear_terminal, press_enter_prompt}; use crate::{ app_state::AppState, cli::{Args, Command}, + editor::Editor, info_file::InfoFile, }; @@ -19,6 +20,7 @@ mod cargo_toml; mod cli; mod cmd; mod dev; +mod editor; mod embedded; mod exercise; mod info_file; @@ -61,7 +63,7 @@ fn main() -> Result { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), - args.zellij, + Editor::new(args.edit_cmd), )?; // Show the welcome message if the state file doesn't exist yet. diff --git a/src/watch/state.rs b/src/watch/state.rs index 9e7d1ddc54..8bbdc58518 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -84,7 +84,7 @@ impl<'a> WatchState<'a> { self.app_state.current_exercise().name, )?; - let edit_cmd_handle = self.app_state.edit_cmd()?; + let editor_handle = self.app_state.open_editor()?; self.show_hint = false; @@ -107,7 +107,7 @@ impl<'a> WatchState<'a> { self.done_status = DoneStatus::Pending; } - self.app_state.join_edit_cmd(edit_cmd_handle)?; + self.app_state.join_editor_handle(editor_handle)?; self.render(stdout)?; Ok(()) @@ -131,7 +131,7 @@ impl<'a> WatchState<'a> { match answer[0] { b'y' | b'Y' => { - self.app_state.close_pane()?; + self.app_state.close_editor()?; self.app_state.reset_current_exercise()?; // The file watcher reruns the exercise otherwise From dace3e39534f31adc2ef95a036fc8aee1fbaf004 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 16:12:49 +0200 Subject: [PATCH 07/16] Add run_cmd --- src/editor.rs | 63 ++++++++++++++++++++------------------------ src/editor/zellij.rs | 45 +++++++++++++------------------ 2 files changed, 47 insertions(+), 61 deletions(-) diff --git a/src/editor.rs b/src/editor.rs index be24e3eff6..af5c2fbea1 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -8,6 +8,25 @@ use anyhow::{Context, Result, bail}; mod zellij; +fn run_cmd(cmd: &mut Command) -> Result> { + let output = cmd + .stdin(Stdio::null()) + .output() + .with_context(|| format!("Failed to run the command {cmd:?}"))?; + + if !output.status.success() { + bail!( + "The command {cmd:?} didn't run successfully\n\n\ + stdout:\n{}\n\n\ + stderr:\n{}", + str::from_utf8(&output.stdout).unwrap_or_default(), + str::from_utf8(&output.stderr).unwrap_or_default(), + ); + } + + Ok(output.stdout) +} + pub enum Editor { VSCode, Cmd(String, Vec), @@ -39,32 +58,12 @@ impl Editor { let handle = thread::Builder::new() .spawn(move || match self { Editor::VSCode => { - if !Command::new("code") - .arg(exercise_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run `code` to open the current exercise file")? - .success() - { - bail!("Failed to run `code PATH` to open the current exercise file"); - } + run_cmd(Command::new("code").arg(exercise_path))?; Ok(Self::VSCode) } Editor::Cmd(program, args) => { - if !Command::new("code") - .arg(exercise_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run the command from `--edit-cmd`") - .is_ok_and(|status| status.success()) - { - bail!("Failed to run the command from `--edit-cmd`"); - } + run_cmd(Command::new(&program).args(&args).arg(exercise_path))?; Ok(Self::Cmd(program, args)) } @@ -83,20 +82,14 @@ impl Editor { } } - let output = Command::new("zellij") - .arg("action") - .arg("edit") - .arg(exercise_path) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .context("Failed to run `zellij`")?; - - if !output.status.success() { - bail!("Failed to open a new Zellij editor pane"); - } + let stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path), + )?; - let (pane_id_str, pane_id) = zellij::parse_pane_id(&output.stdout) + let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) .context("Failed to parse the ID of the new Zellij pane")?; Ok(Self::Zellij(Some((pane_id_str, pane_id, exercise_ind)))) diff --git a/src/editor/zellij.rs b/src/editor/zellij.rs index ffa906dc95..b628a682cf 100644 --- a/src/editor/zellij.rs +++ b/src/editor/zellij.rs @@ -1,8 +1,10 @@ -use std::process::{Command, Stdio}; +use std::process::Command; -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result}; use serde::Deserialize; +use crate::editor::run_cmd; + #[derive(Deserialize)] struct Pane { id: u32, @@ -24,39 +26,30 @@ pub fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { } pub fn pane_open(pane_id: u32) -> Result { - let mut output = Command::new("zellij") - .arg("action") - .arg("list-panes") - .arg("-j") - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .context("Failed to run `zellij action list-panes -j`")?; - - if !output.status.success() { - bail!("`zellij action list-panes -j` didn't exit successfully"); - } + let mut stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j"), + )?; // Remove newline - output.stdout.pop(); + stdout.pop(); - let panes = serde_json::de::from_slice::>(&output.stdout) + let panes = serde_json::de::from_slice::>(&stdout) .context("Failed to parse the output of `zellij action list-panes -j`")?; Ok(panes.iter().any(|pane| pane.id == pane_id)) } pub fn close_pane(pane_id: &str) -> Result<()> { - Command::new("zellij") - .arg("action") - .arg("close-pane") - .arg("-p") - .arg(pane_id) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run `zellij action close-pane -p ID`")?; + run_cmd( + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id), + )?; Ok(()) } From b48663030b492abf335a3338b5c8bd00613a0bdd Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 16:55:10 +0200 Subject: [PATCH 08/16] Add shlex --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/editor.rs | 17 ++++++++++++----- src/main.rs | 3 ++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c2d6d75b0..b8db4360e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,6 +485,7 @@ dependencies = [ "rustlings-macros", "serde", "serde_json", + "shlex", "tempfile", "toml", ] @@ -571,6 +572,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.18" diff --git a/Cargo.toml b/Cargo.toml index 068a3f49ee..192eeb616d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ notify = "8" rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" } serde_json = "1" serde.workspace = true +shlex = "1" toml.workspace = true [target.'cfg(not(windows))'.dependencies] diff --git a/src/editor.rs b/src/editor.rs index af5c2fbea1..6e7126056c 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -5,6 +5,7 @@ use std::{ }; use anyhow::{Context, Result, bail}; +use shlex::Shlex; mod zellij; @@ -34,20 +35,26 @@ pub enum Editor { } impl Editor { - pub fn new(cmd: Option) -> Option { + pub fn new(cmd: Option) -> Result> { if env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode") { - return Some(Self::VSCode); + return Ok(Some(Self::VSCode)); } if let Some(cmd) = cmd { - todo!() + let shlex = &mut Shlex::new(&cmd); + let program = shlex.next().context("Program missing in `--edit-cmd`")?; + let args = shlex.collect(); + if shlex.had_error { + bail!("Failed to parse the command in `--edit-cmd`"); + } + return Ok(Some(Self::Cmd(program, args))); } if env::var_os("ZELLIJ").is_some() { - return Some(Self::Zellij(None)); + return Ok(Some(Self::Zellij(None))); } - None + Ok(None) } pub fn open( diff --git a/src/main.rs b/src/main.rs index 1d6c98b128..7cd5c80b45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,10 +60,11 @@ fn main() -> Result { bail!(FORMAT_VERSION_HIGHER_ERR); } + let editor = Editor::new(args.edit_cmd)?; let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), - Editor::new(args.edit_cmd), + editor, )?; // Show the welcome message if the state file doesn't exist yet. From b0dc0140406ef00ce3eb41d0a9adb8fca8f1539b Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 17:24:43 +0200 Subject: [PATCH 09/16] Improve description of --edit-cmd --- src/cli.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 8bbe25aa2c..5830cbed79 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,8 +8,14 @@ use crate::dev::DevCommand; pub struct Args { #[command(subcommand)] pub command: Option, - /// Open the current exercise by running the provided `EDIT_CMD EXERCISE_NAME`. - /// Ignored in VS Code + /// Open the current exercise by running `EDIT_CMD EXERCISE_PATH`. + /// The command is not allowed to block (e.g. `vim`). + /// It should communicate with an editor in a different process. + /// `EDIT_CMD` can contain arguments like `--edit-cmd "PROGRAM -x --arg1"`. + /// The current exercise's path is added by Rustlings as the last argument. + /// `--edit-cmd` is ignored in VS Code. + /// + /// Example: `--edit-cmd "code"` (default behavior if running in a VS Code terminal) #[arg(long)] pub edit_cmd: Option, /// Manually run the current exercise using `r` in the watch mode. From bc0b4e9f9a716e577e80667b2b1521b55724d71b Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 17:27:35 +0200 Subject: [PATCH 10/16] Simplify Editor::open --- src/editor.rs | 62 ++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/src/editor.rs b/src/editor.rs index 6e7126056c..3f36e266be 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -58,49 +58,45 @@ impl Editor { } pub fn open( - self, + mut self, exercise_ind: usize, exercise_path: &'static str, ) -> Result { let handle = thread::Builder::new() - .spawn(move || match self { - Editor::VSCode => { - run_cmd(Command::new("code").arg(exercise_path))?; - - Ok(Self::VSCode) - } - Editor::Cmd(program, args) => { - run_cmd(Command::new(&program).args(&args).arg(exercise_path))?; - - Ok(Self::Cmd(program, args)) - } - Editor::Zellij(open_pane) => { - if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { - if open_exercise_ind == exercise_ind { - if zellij::pane_open(pane_id)? { - return Ok(Self::Zellij(Some(( - pane_id_str, - pane_id, - exercise_ind, - )))); + .spawn(move || { + match &mut self { + Editor::VSCode => { + run_cmd(Command::new("code").arg(exercise_path))?; + } + Editor::Cmd(program, args) => { + run_cmd(Command::new(program).args(args).arg(exercise_path))?; + } + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { + if *open_exercise_ind == exercise_ind { + if zellij::pane_open(*pane_id)? { + return Ok(self); + } + } else { + zellij::close_pane(pane_id_str)?; } - } else { - zellij::close_pane(&pane_id_str)?; } - } - let stdout = run_cmd( - Command::new("zellij") - .arg("action") - .arg("edit") - .arg(exercise_path), - )?; + let stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path), + )?; - let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) - .context("Failed to parse the ID of the new Zellij pane")?; + let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) + .context("Failed to parse the ID of the new Zellij pane")?; - Ok(Self::Zellij(Some((pane_id_str, pane_id, exercise_ind)))) + *open_pane = Some((pane_id_str, pane_id, exercise_ind)); + } } + + Ok(self) }) .context("Failed to spawn a thread to open the editor")?; From 95499f18dd7cb0811dd304acd051e493fa8661d6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:11:13 +0200 Subject: [PATCH 11/16] Close editor on quit --- src/main.rs | 1 + src/watch.rs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7cd5c80b45..652d1468a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,6 +111,7 @@ fn main() -> Result { }; watch::watch(&mut app_state, notify_exercise_names)?; + app_state.close_editor()?; } Some(Command::Run { name }) => { if let Some(name) = name { diff --git a/src/watch.rs b/src/watch.rs index ab96893ea3..857ccfde51 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -172,8 +172,7 @@ pub fn watch( watch_list_loop(app_state, notify_exercise_names) } -const QUIT_MSG: &[u8] = b" - +const QUIT_MSG: &[u8] = b"q\n We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again in this directory. "; From f403d9e1b68b601a5b6e9e57201f767b134e2e67 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:21:15 +0200 Subject: [PATCH 12/16] Show current exercise on hint command --- CHANGELOG.md | 4 ++++ src/main.rs | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9b356bea..b85a3e0bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Show the file link of the current exercise when running `rustlings hint` + ### Fixed - Fix integer overflow on big terminal widths [@gabfec](https://github.com/gabfec) diff --git a/src/main.rs b/src/main.rs index 652d1468a1..564e0719ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,15 @@ fn main() -> Result { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } - println!("{}", app_state.current_exercise().hint); + + let current_exercise = app_state.current_exercise(); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"Current exercise: ")?; + 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(b"\n")?; } // Handled in an earlier match. Some(Command::Init | Command::Dev(_)) => (), From 695f927893ae4fc1f1aaa629e2feb62a9cdcc78d Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:26:36 +0200 Subject: [PATCH 13/16] Show file link on reset command --- CHANGELOG.md | 2 +- src/app_state.rs | 6 ++---- src/main.rs | 9 +++++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b85a3e0bd4..ec74cf1064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Show the file link of the current exercise when running `rustlings hint` +- Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` ### Fixed diff --git a/src/app_state.rs b/src/app_state.rs index bc1d520d28..9980aeea63 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -334,12 +334,10 @@ impl AppState { Ok(()) } - pub fn reset_current_exercise(&mut self) -> Result<&str> { + pub fn reset_current_exercise(&mut self) -> Result<()> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, exercise.path)?; - - Ok(exercise.path) + self.reset(self.current_exercise_ind, exercise.path) } // Reset the exercise by index and return its name. diff --git a/src/main.rs b/src/main.rs index 564e0719ec..fb9766538a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -149,8 +149,13 @@ fn main() -> Result { } Some(Command::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; - let exercise_path = app_state.reset_current_exercise()?; - println!("The exercise {exercise_path} has been reset"); + app_state.reset_current_exercise()?; + + let current_exercise = app_state.current_exercise(); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"The exercise ")?; + current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?; + stdout.write_all(b" has been reset\n")?; } Some(Command::Hint { name }) => { if let Some(name) = name { From b5fbf59c0c79a78e06d0fffd9db86abf0774e0f6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:55:30 +0200 Subject: [PATCH 14/16] Check if editor program exists before choosing it --- CHANGELOG.md | 3 +++ src/app_state.rs | 3 ++- src/editor.rs | 35 +++++++++++++++++++++++------------ src/main.rs | 5 ++++- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec74cf1064..0d2f003f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Added +- Automatically open the current file if Rustlings is running in a VS Code terminal +- Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev) +- New argument `--edit-cmd` to communicate with an editor running in a different process to open the current exercise - Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` ### Fixed diff --git a/src/app_state.rs b/src/app_state.rs index 9980aeea63..78b9c20598 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -69,6 +69,7 @@ impl AppState { exercise_infos: Vec, final_message: &'static str, editor: Option, + vs_code_term: bool, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -178,7 +179,7 @@ impl AppState { official_exercises: !Path::new("info.toml").exists(), cmd_runner, // VS Code has its own file link handling - emit_file_links: !matches!(editor, Some(Editor::VSCode)), + emit_file_links: !vs_code_term, editor, }; diff --git a/src/editor.rs b/src/editor.rs index 3f36e266be..3c189c789d 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, env, process::{Command, Stdio}, thread::{self, JoinHandle}, @@ -28,16 +29,29 @@ fn run_cmd(cmd: &mut Command) -> Result> { Ok(output.stdout) } +fn program_exists(program: &str) -> bool { + Command::new(program) + .arg("--version") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()) +} + pub enum Editor { - VSCode, - Cmd(String, Vec), + Cmd(Cow<'static, str>, Vec), Zellij(Option<(String, u32, usize)>), } impl Editor { - pub fn new(cmd: Option) -> Result> { - if env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode") { - return Ok(Some(Self::VSCode)); + pub fn new(cmd: Option, vs_code_term: bool) -> Result> { + if vs_code_term { + for program in ["code", "codium"] { + if program_exists(program) { + return Ok(Some(Self::Cmd(Cow::Borrowed(program), Vec::new()))); + } + } } if let Some(cmd) = cmd { @@ -47,10 +61,10 @@ impl Editor { if shlex.had_error { bail!("Failed to parse the command in `--edit-cmd`"); } - return Ok(Some(Self::Cmd(program, args))); + return Ok(Some(Self::Cmd(Cow::Owned(program), args))); } - if env::var_os("ZELLIJ").is_some() { + if env::var_os("ZELLIJ").is_some() && program_exists("zellij") { return Ok(Some(Self::Zellij(None))); } @@ -65,11 +79,8 @@ impl Editor { let handle = thread::Builder::new() .spawn(move || { match &mut self { - Editor::VSCode => { - run_cmd(Command::new("code").arg(exercise_path))?; - } Editor::Cmd(program, args) => { - run_cmd(Command::new(program).args(args).arg(exercise_path))?; + run_cmd(Command::new(&**program).args(args).arg(exercise_path))?; } Editor::Zellij(open_pane) => { if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { @@ -105,7 +116,7 @@ impl Editor { pub fn close(&mut self) -> Result<()> { match self { - Editor::VSCode | Editor::Cmd(_, _) => (), + Editor::Cmd(_, _) => (), Editor::Zellij(open_pane) => { if let Some((pane_id_str, _, _)) = open_pane.take() { zellij::close_pane(&pane_id_str)?; diff --git a/src/main.rs b/src/main.rs index fb9766538a..0fa9b75d85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result, bail}; use app_state::StateFileStatus; use clap::Parser; use std::{ + env, io::{self, IsTerminal, Write}, path::Path, process::ExitCode, @@ -60,11 +61,13 @@ fn main() -> Result { bail!(FORMAT_VERSION_HIGHER_ERR); } - let editor = Editor::new(args.edit_cmd)?; + let vs_code_term = env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"); + let editor = Editor::new(args.edit_cmd, vs_code_term)?; let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), editor, + vs_code_term, )?; // Show the welcome message if the state file doesn't exist yet. From 432d1f84ea68d21e865215281764094a73236da0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 7 Apr 2026 00:07:41 +0200 Subject: [PATCH 15/16] Add --no-editor --- CHANGELOG.md | 1 + src/cli.rs | 4 ++++ src/main.rs | 7 ++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2f003f92..cdcdbb1412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Automatically open the current file if Rustlings is running in a VS Code terminal - Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev) +- New argument `--no-editor` to disable automatic opening of the current file in VS Code or Zellij - New argument `--edit-cmd` to communicate with an editor running in a different process to open the current exercise - Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` diff --git a/src/cli.rs b/src/cli.rs index 5830cbed79..153994be33 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,6 +8,10 @@ use crate::dev::DevCommand; pub struct Args { #[command(subcommand)] pub command: Option, + /// Disable automatic opening of the current file in VS Code or Zellij. + /// Ignores `--edit-cmd` + #[arg(long)] + pub no_editor: bool, /// Open the current exercise by running `EDIT_CMD EXERCISE_PATH`. /// The command is not allowed to block (e.g. `vim`). /// It should communicate with an editor in a different process. diff --git a/src/main.rs b/src/main.rs index 0fa9b75d85..8da36f7f57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,7 +62,12 @@ fn main() -> Result { } let vs_code_term = env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"); - let editor = Editor::new(args.edit_cmd, vs_code_term)?; + let editor = if args.no_editor { + None + } else { + Editor::new(args.edit_cmd, vs_code_term)? + }; + let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), From a307599b0bce21bd8c14741fba81d3d076f99b14 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 7 Apr 2026 00:15:24 +0200 Subject: [PATCH 16/16] Fix test --- tests/integration_tests.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bb1e398c27..d38d4e9309 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -4,7 +4,6 @@ use std::{ }; enum Output<'a> { - FullStdout(&'a [u8]), PartialStdout(&'a str), PartialStderr(&'a str), } @@ -47,9 +46,6 @@ impl<'a> Cmd<'a> { let output = cmd.output().unwrap(); match self.output { None => (), - Some(FullStdout(stdout)) => { - assert_eq!(output.stdout, stdout); - } Some(PartialStdout(stdout)) => { assert!(from_utf8(&output.stdout).unwrap().contains(stdout)); } @@ -129,7 +125,7 @@ fn hint() { Cmd::default() .current_dir("tests/test_exercises") .args(&["hint", "test_failure"]) - .output(FullStdout(b"The answer to everything: 42\n")) + .output(PartialStdout("\n\nHint:\nThe answer to everything: 42\n")) .success(); }