#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command as StdCommand;
use tokio::process::Command as TokioCommand;
use crate::{
cmd_line::CmdLineSettings,
utils::{expand_tilde, handle_wslpaths},
};
#[cfg(target_os = "macos")]
const FORKED_FROM_TTY_ENV_VAR: &str = "NEOVIDE_FORKED_FROM_TTY";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OpenArgs {
pub files_to_open: Vec<String>,
pub tabs: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OpenMode {
None,
Startup,
Args(OpenArgs),
}
#[derive(Debug, Clone)]
struct CommandSpec {
program: String,
args: Vec<String>,
#[cfg(target_os = "windows")]
creation_flags: Option<u32>,
}
impl CommandSpec {
fn new(program: impl Into<String>, args: Vec<String>) -> Self {
Self {
program: program.into(),
args,
#[cfg(target_os = "windows")]
creation_flags: None,
}
}
#[cfg(target_os = "windows")]
fn with_creation_flags(mut self, flags: u32) -> Self {
self.creation_flags = Some(flags);
self
}
}
pub fn create_blocking_nvim_command(cmdline_settings: &CmdLineSettings, embed: bool) -> StdCommand {
let (bin, args) = build_nvim_command_parts(cmdline_settings, embed, OpenMode::Startup);
let cwd = command_cwd(cmdline_settings, None);
let spec = create_command_spec(&bin, &args, cmdline_settings, cwd.as_deref());
let mut cmd = std_command_from_spec(spec);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
cmd
}
pub fn create_tokio_nvim_command(
cmdline_settings: &CmdLineSettings,
embed: bool,
cwd: Option<&Path>,
mode: OpenMode,
) -> TokioCommand {
let (bin, args) = build_nvim_command_parts(cmdline_settings, embed, mode);
let cwd = command_cwd(cmdline_settings, cwd);
let spec = create_command_spec(&bin, &args, cmdline_settings, cwd.as_deref());
let mut cmd = tokio_command_from_spec(spec);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
cmd
}
fn command_cwd(settings: &CmdLineSettings, cwd: Option<&Path>) -> Option<PathBuf> {
cwd.map(Path::to_path_buf).or_else(|| {
settings.chdir.as_deref().map(|dir| {
if dir.starts_with('~') { PathBuf::from(expand_tilde(dir)) } else { PathBuf::from(dir) }
})
})
}
fn build_nvim_command_parts(
cmdline_settings: &CmdLineSettings,
embed: bool,
mode: OpenMode,
) -> (String, Vec<String>) {
let bin = cmdline_settings.neovim_bin.clone().unwrap_or_else(|| "nvim".to_owned());
let mut args = cmdline_settings.neovim_args.clone();
if embed {
append_embed_arg(&mut args);
}
args.extend(build_open_args(cmdline_settings, mode));
(bin, args)
}
fn build_open_args(cmdline_settings: &CmdLineSettings, open_mode: OpenMode) -> Vec<String> {
let (files_to_open, tabs) = match open_mode {
OpenMode::None => return Vec::new(),
OpenMode::Startup => (cmdline_settings.files_to_open.clone(), cmdline_settings.tabs),
OpenMode::Args(args) => (args.files_to_open, args.tabs),
};
tabs.then(|| "-p".to_string())
.into_iter()
.chain(handle_wslpaths(files_to_open, cmdline_settings.wsl))
.collect()
}
fn append_embed_arg(args: &mut Vec<String>) {
if !args.iter().any(|arg| arg == "--embed") {
args.push("--embed".to_string());
}
}
fn tokio_command_from_spec(spec: CommandSpec) -> TokioCommand {
let CommandSpec {
program,
args,
#[cfg(target_os = "windows")]
creation_flags,
} = spec;
let mut result = TokioCommand::new(program);
result.args(&args);
#[cfg(target_os = "windows")]
if let Some(flags) = creation_flags {
result.creation_flags(flags);
}
result
}
fn std_command_from_spec(spec: CommandSpec) -> StdCommand {
let CommandSpec {
program,
args,
#[cfg(target_os = "windows")]
creation_flags,
} = spec;
let mut result = StdCommand::new(program);
result.args(&args);
#[cfg(target_os = "windows")]
if let Some(flags) = creation_flags {
result.creation_flags(flags);
}
result
}
#[cfg(target_os = "macos")]
fn launched_from_desktop() -> bool {
if std::env::var_os(FORKED_FROM_TTY_ENV_VAR).is_some() {
return false;
}
use rustix::process;
matches!(process::getppid(), Some(ppid) if ppid.is_init())
}
#[cfg(target_os = "macos")]
fn create_command_spec(
command: &str,
args: &[String],
_cmdline_settings: &CmdLineSettings,
cwd: Option<&Path>,
) -> CommandSpec {
use uzers::os::unix::UserExt;
if !launched_from_desktop() {
CommandSpec::new(command, args.to_vec())
} else {
let user = uzers::get_user_by_uid(uzers::get_current_uid()).unwrap();
let shell = user.shell();
CommandSpec::new(
"/usr/bin/login",
vec![
"-fpq".to_string(),
user.name().to_str().unwrap().to_string(),
shell.to_str().unwrap().to_string(),
"-c".to_string(),
build_login_shell_command(command, args, cwd),
],
)
}
}
#[cfg(target_os = "macos")]
fn build_login_shell_command(command: &str, args: &[String], cwd: Option<&Path>) -> String {
let quoted_command = shlex::try_join(
std::iter::once(command).chain(args.iter().map(std::string::String::as_str)),
)
.expect("Failed to join command");
match cwd {
Some(dir) => {
let cwd_path = dir.to_string_lossy();
let cwd = shlex::try_quote(cwd_path.as_ref()).expect("Failed to quote cwd");
format!("cd {cwd} && exec {quoted_command}")
}
None => format!("exec {quoted_command}"),
}
}
#[cfg(target_os = "windows")]
fn create_command_spec(
command: &str,
args: &[String],
cmdline_settings: &CmdLineSettings,
_cwd: Option<&Path>,
) -> CommandSpec {
let spec = if cfg!(target_os = "windows") && cmdline_settings.wsl {
let args =
shlex::try_join(args.iter().map(|s| s.as_ref())).expect("Failed to join arguments");
CommandSpec::new(
"wsl",
vec![
"$SHELL".to_string(),
"-l".to_string(),
"-c".to_string(),
format!("{command} {args}"),
],
)
} else {
CommandSpec::new(command, args.to_vec())
};
spec.with_creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0)
}
#[cfg(target_os = "linux")]
fn create_command_spec(
command: &str,
args: &[String],
_cmdline_settings: &CmdLineSettings,
_cwd: Option<&Path>,
) -> CommandSpec {
CommandSpec::new(command, args.to_vec())
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use crate::{cmd_line::handle_command_line_arguments, settings::Settings};
use super::*;
fn parse_cmdline_settings(args: &[&str]) -> CmdLineSettings {
let settings = Settings::new();
let args = args.iter().map(|arg| arg.to_string()).collect();
handle_command_line_arguments(args, &settings).expect("Could not parse arguments");
settings.get::<CmdLineSettings>()
}
#[test]
fn build_nvim_command_parts_places_embed_before_auto_open_args() {
let cmdline_settings =
parse_cmdline_settings(&["neovide", "./foo.txt", "./bar.md", "--grid=420x240"]);
let (_, args) = build_nvim_command_parts(&cmdline_settings, true, OpenMode::Startup);
assert_eq!(args, vec!["--embed", "-p", "./foo.txt", "./bar.md"]);
}
#[test]
fn build_nvim_command_parts_skips_auto_open_args_when_requested() {
let cmdline_settings =
parse_cmdline_settings(&["neovide", "./foo.txt", "./bar.md", "--grid=420x240"]);
let (_, args) = build_nvim_command_parts(&cmdline_settings, true, OpenMode::None);
assert_eq!(args, vec!["--embed"]);
}
#[test]
fn build_nvim_command_parts_uses_route_auto_open_args() {
let cmdline_settings =
parse_cmdline_settings(&["neovide", "./foo.txt", "./bar.md", "--grid=420x240"]);
let open_args = OpenArgs { files_to_open: vec!["/tmp/project".to_string()], tabs: false };
let (_, args) =
build_nvim_command_parts(&cmdline_settings, true, OpenMode::Args(open_args));
assert_eq!(args, vec!["--embed", "/tmp/project"]);
}
#[test]
fn build_nvim_command_parts_preserves_launcher_args_before_embed() {
let cmdline_settings = parse_cmdline_settings(&[
"neovide",
"--no-tabs",
"--neovim-bin",
"ssh",
"--",
"my-server",
"nvim",
]);
let (bin, args) = build_nvim_command_parts(&cmdline_settings, true, OpenMode::Startup);
assert_eq!(bin, "ssh");
assert_eq!(args, vec!["my-server", "nvim", "--embed"]);
}
#[test]
fn command_cwd_prefers_override() {
let cmdline_settings = parse_cmdline_settings(&["neovide", "--chdir", "/random/path"]);
assert_eq!(
command_cwd(&cmdline_settings, Some(Path::new("/route/cwd"))),
Some(PathBuf::from("/route/cwd"))
);
}
#[test]
fn command_cwd_falls_back_to_cmdline_setting() {
let cmdline_settings = parse_cmdline_settings(&["neovide", "--chdir", "/random/path"]);
assert_eq!(command_cwd(&cmdline_settings, None), Some(PathBuf::from("/random/path")));
}
#[test]
fn command_cwd_expands_tilde_in_cmdline_setting() {
let cmdline_settings = parse_cmdline_settings(&["neovide", "--chdir", "~"]);
assert_eq!(command_cwd(&cmdline_settings, None), Some(PathBuf::from(expand_tilde("~"))));
}
#[test]
fn command_cwd_expands_tilde_subpath_in_cmdline_setting() {
let cmdline_settings =
parse_cmdline_settings(&["neovide", "--chdir", "~/some/other/project"]);
assert_eq!(
command_cwd(&cmdline_settings, None),
Some(PathBuf::from(expand_tilde("~/some/other/project")))
);
}
#[cfg(target_os = "macos")]
#[test]
fn build_login_shell_command_preserves_cwd() {
let command = build_login_shell_command(
"/bin/nvim",
&["--embed".to_string(), "-p".to_string(), "/path/to/project/file.txt".to_string()],
Some(Path::new("/path/to/new/cwd")),
);
assert_eq!(
command,
"cd /path/to/new/cwd && exec /bin/nvim --embed -p /path/to/project/file.txt"
);
}
#[cfg(target_os = "macos")]
#[test]
fn build_login_shell_command_skips_cd_without_override() {
let command = build_login_shell_command(
"/bin/nvim",
&["--embed".to_string(), "-p".to_string(), "/path/to/project/file.txt".to_string()],
None,
);
assert_eq!(command, "exec /bin/nvim --embed -p /path/to/project/file.txt");
}
}