use std::path::Path;
use super::*;
use crate::layout::Direction;
use crate::procs;
impl App {
pub(super) fn ssh_follow_command(&self) -> Option<String> {
let pane = self.focused_pane()?;
let shell = pane.session.pid();
let fg = procs::foreground(shell).filter(|&fg| fg != shell)?;
let target = procs::ssh_target(&procs::cmdline(fg))?;
let remote_dir = pane
.session
.shell
.lock()
.remote_cwd
.clone()
.map(|(_, dir)| dir);
follow_command(&target, remote_dir.as_deref())
}
pub(super) fn split(&mut self, dir: Direction) {
let follow = if self.config.splits.follow_ssh {
self.ssh_follow_command()
} else {
None
};
let cwd = self.inherited_cwd();
match self.split_with(dir, cwd) {
Ok(id) => self.run_in(id, follow.as_deref()),
Err(e) => eprintln!("cyberterm: couldn't split: {e}"),
}
}
pub(super) fn split_local(&mut self, dir: Direction) {
let cwd = self.inherited_cwd();
if let Err(e) = self.split_with(dir, cwd) {
eprintln!("cyberterm: couldn't split: {e}");
}
}
}
fn follow_command(target: &procs::SshTarget, remote_dir: Option<&Path>) -> Option<String> {
if target.has_command {
return None;
}
let flags_with = |set: &str| {
target.options.iter().any(|o| {
o.starts_with('-') && !o.starts_with("--") && o[1..].chars().any(|c| set.contains(c))
})
};
if flags_with("NfW") {
return None;
}
let mut parts: Vec<String> = vec!["ssh".into()];
parts.extend(target.options.iter().map(|o| shell_quote(o)));
match remote_dir {
Some(dir) => {
if !flags_with("t") {
parts.push("-t".into());
}
parts.push(shell_quote(&target.destination));
let remote = format!(
"cd {} && exec \"$SHELL\" -l",
shell_quote(&dir.to_string_lossy())
);
parts.push(shell_quote(&remote));
}
None => parts.push(shell_quote(&target.destination)),
}
Some(parts.join(" "))
}
fn shell_quote(s: &str) -> String {
if !s.is_empty()
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || "/._+-:=@,%~".contains(c))
{
s.to_string()
} else {
format!("'{}'", s.replace('\'', r"'\''"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn target(argv: &str) -> procs::SshTarget {
let args: Vec<String> = argv.split_whitespace().map(str::to_string).collect();
procs::ssh_target(&args).unwrap()
}
#[test]
fn reconnects_with_the_same_options() {
assert_eq!(
follow_command(&target("ssh -p 2222 -i ~/.ssh/k me@box"), None).unwrap(),
"ssh -p 2222 -i ~/.ssh/k me@box"
);
}
#[test]
fn lands_in_the_remote_directory() {
let cmd = follow_command(&target("ssh box"), Some(Path::new("/srv/my app"))).unwrap();
assert_eq!(
cmd,
r#"ssh -t box 'cd '\''/srv/my app'\'' && exec "$SHELL" -l'"#
);
let out = std::process::Command::new("sh")
.arg("-c")
.arg(format!("set -- {}; printf '%s\\n' \"$3\"", &cmd[4..]))
.output()
.unwrap();
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
r#"cd '/srv/my app' && exec "$SHELL" -l"#
);
}
#[test]
fn skips_non_interactive_sessions() {
assert!(follow_command(&target("ssh box uptime"), None).is_none());
assert!(follow_command(&target("ssh -N -L 8080:localhost:80 box"), None).is_none());
assert!(follow_command(&target("ssh -fN box"), None).is_none());
assert!(
follow_command(&target("ssh -tt box"), Some(Path::new("/x")))
.is_some_and(|c| !c.contains(" -t box"))
);
}
}