diff --git a/CHANGELOG.md b/CHANGELOG.md index ec74cf10..0d2f003f 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 9980aeea..78b9c205 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 3f36e266..3c189c78 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 fb976653..0fa9b75d 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.