diff --git a/src/editor.rs b/src/editor.rs index d22c7c7..864fd73 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -272,11 +272,53 @@ fn platform_fallback_editor() -> &'static str { } } +fn is_wsl() -> bool { + static CACHED: std::sync::OnceLock = std::sync::OnceLock::new(); + *CACHED.get_or_init(|| { + if std::env::var("WSL_DISTRO_NAME") + .map(|s| !s.is_empty()) + .unwrap_or(false) + { + return true; + } + std::fs::read_to_string("/proc/sys/kernel/osrelease") + .map(|s| s.to_lowercase().contains("microsoft")) + .unwrap_or(false) + }) +} + +pub(crate) enum LaunchStrategy { + SpawnAndAssume(Command), + RunAndCheck(Command), +} + +fn silence(cmd: &mut Command) { + cmd.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); +} + +impl LaunchStrategy { + fn run(self) -> bool { + match self { + LaunchStrategy::SpawnAndAssume(mut cmd) => { + silence(&mut cmd); + cmd.spawn().is_ok() + } + LaunchStrategy::RunAndCheck(mut cmd) => { + silence(&mut cmd); + cmd.status().map(|s| s.success()).unwrap_or(false) + } + } + } +} + pub(crate) fn try_new_tab_command( editor: &str, file: &Path, emulator: &TerminalEmulator, -) -> Option { + wsl: bool, +) -> Option { let (bin, args) = split_editor_cmd(editor); let file_str = file.display().to_string(); @@ -292,7 +334,7 @@ pub(crate) fn try_new_tab_command( cmd.arg(a); } cmd.arg(&file_str); - Some(cmd) + Some(LaunchStrategy::RunAndCheck(cmd)) } TerminalEmulator::GnomeTerminal => { let mut cmd = Command::new("gnome-terminal"); @@ -304,7 +346,7 @@ pub(crate) fn try_new_tab_command( cmd.arg(a); } cmd.arg(&file_str); - Some(cmd) + Some(LaunchStrategy::SpawnAndAssume(cmd)) } TerminalEmulator::MacTerminal(ref tp) => { let app_name = if tp == "Apple_Terminal" { @@ -327,9 +369,15 @@ pub(crate) fn try_new_tab_command( }; let mut cmd = Command::new("osascript"); cmd.arg("-e").arg(script); - Some(cmd) + Some(LaunchStrategy::SpawnAndAssume(cmd)) } TerminalEmulator::WindowsTerminal => { + let is_windows_bin = Path::new(bin) + .extension() + .is_some_and(|e| e.eq_ignore_ascii_case("exe")); + if wsl && !is_windows_bin { + return None; + } let mut cmd = Command::new("wt"); cmd.arg("new-tab") .arg("--title") @@ -339,7 +387,7 @@ pub(crate) fn try_new_tab_command( cmd.arg(a); } cmd.arg(&file_str); - Some(cmd) + Some(LaunchStrategy::SpawnAndAssume(cmd)) } TerminalEmulator::Termux | TerminalEmulator::Unknown => None, } @@ -361,24 +409,18 @@ pub(crate) fn open_in_editor( match kind { EditorKind::Gui => { let exec = which(bin).unwrap_or_else(|| PathBuf::from(bin)); - Command::new(&exec) - .args(&args) - .arg(file) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .map_err(|e| format!("{bin}: {e}"))?; + let mut cmd = Command::new(&exec); + cmd.args(&args).arg(file); + silence(&mut cmd); + cmd.spawn().map_err(|e| format!("{bin}: {e}"))?; Ok(EditorResult::Opened) } EditorKind::Terminal => { - if let Some(mut cmd) = try_new_tab_command(editor, file, emulator) { - cmd.stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - if cmd.spawn().is_ok() { - return Ok(EditorResult::Opened); - } + if try_new_tab_command(editor, file, emulator, is_wsl()) + .map(LaunchStrategy::run) + .unwrap_or(false) + { + return Ok(EditorResult::Opened); } Ok(EditorResult::NeedsSameTerminal) } diff --git a/src/main.rs b/src/main.rs index 305c24d..cbdda3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,7 +37,7 @@ pub(crate) use config::{config_path, LeafConfig}; #[cfg(test)] pub(crate) use editor::{ binary_name, classify, expand_editor_placeholders, resolve_editor, selection_modifier_label, - split_editor_cmd, try_new_tab_command, EditorKind, TerminalEmulator, + split_editor_cmd, try_new_tab_command, EditorKind, LaunchStrategy, TerminalEmulator, }; #[cfg(test)] pub(crate) use markdown::toc::{normalize_toc, toc_levels, TocEntry}; diff --git a/src/tests/editor.rs b/src/tests/editor.rs index 5c3d180..af50f1c 100644 --- a/src/tests/editor.rs +++ b/src/tests/editor.rs @@ -164,11 +164,67 @@ fn classify_windows_path_with_spaces() { fn mac_tab_script(editor: &str, file: &str, term_program: &str) -> String { let emulator = TerminalEmulator::MacTerminal(term_program.to_string()); - let cmd = try_new_tab_command(editor, Path::new(file), &emulator).unwrap(); + let strategy = try_new_tab_command(editor, Path::new(file), &emulator, false).unwrap(); + let cmd = match strategy { + LaunchStrategy::SpawnAndAssume(cmd) => cmd, + LaunchStrategy::RunAndCheck(_) => panic!("expected SpawnAndAssume for MacTerminal"), + }; let args: Vec<_> = cmd.get_args().collect(); args[1].to_str().unwrap().to_string() } +#[test] +fn try_new_tab_command_windows_terminal_wsl_linux_editor_returns_none() { + let emulator = TerminalEmulator::WindowsTerminal; + let strategy = try_new_tab_command("nano", Path::new("/tmp/test.md"), &emulator, true); + assert!(strategy.is_none()); +} + +#[test] +fn try_new_tab_command_windows_terminal_wsl_windows_editor_returns_spawn() { + let emulator = TerminalEmulator::WindowsTerminal; + let strategy = + try_new_tab_command("notepad.exe", Path::new("/tmp/test.md"), &emulator, true).unwrap(); + assert!(matches!(strategy, LaunchStrategy::SpawnAndAssume(_))); +} + +#[test] +fn try_new_tab_command_windows_terminal_non_wsl_returns_spawn() { + let emulator = TerminalEmulator::WindowsTerminal; + let strategy = + try_new_tab_command("nano", Path::new("/tmp/test.md"), &emulator, false).unwrap(); + assert!(matches!(strategy, LaunchStrategy::SpawnAndAssume(_))); +} + +#[test] +fn try_new_tab_command_kitty_returns_run_and_check() { + let emulator = TerminalEmulator::Kitty; + let strategy = + try_new_tab_command("nano", Path::new("/tmp/test.md"), &emulator, false).unwrap(); + assert!(matches!(strategy, LaunchStrategy::RunAndCheck(_))); +} + +#[test] +fn try_new_tab_command_gnome_returns_spawn() { + let emulator = TerminalEmulator::GnomeTerminal; + let strategy = try_new_tab_command("vim", Path::new("/tmp/test.md"), &emulator, false).unwrap(); + assert!(matches!(strategy, LaunchStrategy::SpawnAndAssume(_))); +} + +#[test] +fn try_new_tab_command_termux_returns_none() { + let emulator = TerminalEmulator::Termux; + let strategy = try_new_tab_command("nano", Path::new("/tmp/test.md"), &emulator, false); + assert!(strategy.is_none()); +} + +#[test] +fn try_new_tab_command_unknown_returns_none() { + let emulator = TerminalEmulator::Unknown; + let strategy = try_new_tab_command("nano", Path::new("/tmp/test.md"), &emulator, false); + assert!(strategy.is_none()); +} + #[test] fn new_tab_command_apple_terminal_has_printf() { let script = mac_tab_script("nano", "/tmp/test.md", "Apple_Terminal");