use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Channel {
Kitty,
Zmodem,
ScpHint,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct Env {
pub vars: HashMap<String, String>,
pub has_kitten: bool,
pub has_rz: bool,
}
impl Env {
pub(crate) fn current() -> Self {
Self {
vars: std::env::vars().collect(),
has_kitten: on_path("kitten"),
has_rz: on_path("rz"),
}
}
fn get(&self, key: &str) -> Option<&str> {
self.vars
.get(key)
.map(String::as_str)
.filter(|v| !v.is_empty())
}
fn has(&self, key: &str) -> bool {
self.get(key).is_some()
}
}
pub(crate) fn is_remote(env: &Env) -> bool {
crate::utils::drop_transfer::SSH_MARKERS
.iter()
.any(|k| env.has(k))
}
pub(crate) fn multiplexed(env: &Env) -> bool {
env.has("TMUX") || env.has("STY")
}
pub(crate) fn choose(env: &Env) -> Channel {
if multiplexed(env) {
return Channel::ScpHint;
}
if env.has_kitten && is_kitty(env) {
return Channel::Kitty;
}
if env.has_rz && zmodem_capable(env) {
return Channel::Zmodem;
}
Channel::ScpHint
}
fn is_kitty(env: &Env) -> bool {
env.get("TERM").is_some_and(|t| t.contains("kitty")) || env.has("KITTY_WINDOW_ID")
}
fn zmodem_capable(env: &Env) -> bool {
matches!(
env.get("TERM_PROGRAM"),
Some("iTerm.app") | Some("WezTerm") | Some("tabby")
) || env.has("ITERM_SESSION_ID")
}
pub(crate) fn scp_hint(env: &Env, client_path: &str, dest_dir: &str) -> String {
format!(
"scp {} {}:{}/",
shell_quote(client_path),
login_target(env),
dest_dir
)
}
fn login_target(env: &Env) -> String {
let here = env
.get("SSH_CONNECTION")
.and_then(|c| c.split_whitespace().nth(2))
.unwrap_or("<this-host>");
let user = env.get("USER").unwrap_or("<you>");
format!("{user}@{here}")
}
fn shell_quote(path: &str) -> String {
format!("'{}'", path.replace('\'', r"'\''"))
}
fn on_path(cmd: &str) -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| dir.join(cmd).is_file())
}
pub(crate) fn guidance(env: &Env, client_path: &str, dest_dir: &str) -> String {
if !is_remote(env) {
return format!("{client_path} does not exist here.");
}
let mut out = format!(
"{client_path} is on your machine, not this one. Run this ON YOUR MACHINE:\n {}",
scp_hint(env, client_path, dest_dir)
);
match choose(env) {
Channel::Kitty => {
out.push_str("\n(kitty can also transfer it in-band: see `kitten transfer --help`)")
}
Channel::Zmodem => {
out.push_str("\n(your terminal also answers zmodem: run `rz` here for a file picker)")
}
Channel::ScpHint if multiplexed(env) => out.push_str(
"\n(tmux/screen rewrites the escape stream, so in-band transfer is unavailable)",
),
Channel::ScpHint => {}
}
out.push_str(&format!(
"\nOr, with OpenCrabs on your machine too: run `opencrabs drop-agent` there and \
reconnect with `{}`; drops are then copied here on their own.",
crate::utils::drop_transfer::ssh_hint(
&login_target(env),
crate::utils::drop_transfer::DEFAULT_DROP_PORT
)
));
out
}