diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-20 18:55:41 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-20 18:55:41 +0300 |
| commit | 836fd7e585af15cc1ccb0d4f04586ffec28d2a3c (patch) | |
| tree | a3bf07e7af7ffd884a38fce3d65f8c2243bc0615 /src/remote.rs | |
| parent | be66f33d2acde30d25794bcead991e5e7b7eaff8 (diff) | |
feat: add keybinding v2
refs #154
refs #201
refs #202
refs #219
Diffstat (limited to 'src/remote.rs')
| -rw-r--r-- | src/remote.rs | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/src/remote.rs b/src/remote.rs index 5ab7e6fd..089772b4 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -284,6 +284,7 @@ fn prepare_remote_herdr(target: &str) -> io::Result<RemoteHerdr> { ))); } warn_if_remote_bin_not_on_path(target)?; + maybe_copy_local_keybindings_to_remote(target, &remote_herdr)?; Ok(remote_herdr) } @@ -652,6 +653,131 @@ fn confirm_remote_install( Ok(()) } +fn maybe_copy_local_keybindings_to_remote( + target: &str, + remote_herdr: &RemoteHerdr, +) -> io::Result<()> { + let Some(config_toml) = local_keybindings_config_toml()? else { + return Ok(()); + }; + let remote_config_path = remote_config_path(target, remote_herdr)?; + if remote_path_exists(target, &remote_config_path)? { + return Ok(()); + } + if !confirm_remote_keybindings_copy(target)? { + return Ok(()); + } + upload_remote_config(target, &remote_config_path, config_toml.as_bytes()) +} + +fn local_keybindings_config_toml() -> io::Result<Option<String>> { + let path = crate::config::config_path(); + if !path.exists() { + return Ok(None); + } + let content = fs::read_to_string(&path)?; + Ok(local_keybindings_config_toml_from_str(&content)) +} + +fn local_keybindings_config_toml_from_str(content: &str) -> Option<String> { + let mut value = content.parse::<toml::Value>().ok()?; + let root = value.as_table_mut()?; + let mut keys = root.remove("keys")?.as_table()?.clone(); + keys.remove("command"); + if keys.is_empty() { + return None; + } + + let mut out = toml::map::Map::new(); + out.insert("keys".to_string(), toml::Value::Table(keys)); + toml::to_string_pretty(&toml::Value::Table(out)).ok() +} + +fn remote_config_path(target: &str, remote_herdr: &RemoteHerdr) -> io::Result<String> { + let command = format!("{} --help", remote_herdr.shell_path); + let output = ssh_output(target, &command)?; + if !output.status.success() { + return Err(command_failed("remote config path probe failed", &output)); + } + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .lines() + .find_map(|line| line.trim().strip_prefix("Config: ").map(str::to_string)) + .ok_or_else(|| io::Error::other("remote config path probe did not print a Config line")) +} + +fn remote_path_exists(target: &str, path: &str) -> io::Result<bool> { + let command = format!("test -e {}", shell_quote(path)); + let output = ssh_output(target, &command)?; + Ok(output.status.success()) +} + +fn confirm_remote_keybindings_copy(target: &str) -> io::Result<bool> { + if !io::stdin().is_terminal() { + return Ok(false); + } + eprintln!("remote Herdr config is not present on {target}."); + eprintln!( + "Herdr can copy your local [keys] settings so the remote server uses the same keybindings." + ); + eprintln!("Custom command keybindings are not copied because they run on the remote host."); + eprint!("Copy local Herdr keybindings to {target}? [Y/n] "); + io::stderr().flush()?; + + let mut answer = String::new(); + io::stdin().read_line(&mut answer)?; + let answer = answer.trim().to_ascii_lowercase(); + Ok(!(answer == "n" || answer == "no")) +} + +fn upload_remote_config(target: &str, path: &str, content: &[u8]) -> io::Result<()> { + let script = format!( + r#"dest={} +dir="${{dest%/*}}" +mkdir -p "$dir" +umask 077 +tmp="${{dest}}.tmp.$$" +cat > "$tmp" +mv "$tmp" "$dest" +"#, + shell_quote(path) + ); + + let mut child = Command::new("ssh") + .arg("-T") + .arg(target) + .arg(format!("sh -eu -c {}", shell_quote(&script))) + .stdin(Stdio::piped()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .map_err(|err| { + io::Error::new( + err.kind(), + format!("failed to start ssh config upload: {err}"), + ) + })?; + + let copy_result = if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(content) + } else { + Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "ssh config upload stdin missing", + )) + }; + let status = child.wait()?; + copy_result?; + + if status.success() { + Ok(()) + } else { + Err(io::Error::other(format!( + "remote config upload exited with {status}" + ))) + } +} + fn install_remote_herdr( target: &str, remote_herdr: &RemoteHerdr, @@ -1096,6 +1222,43 @@ mod tests { } #[test] + fn local_keybindings_config_extracts_only_keys_without_commands() { + let toml = r#" +[theme] +name = "one-dark" + +[keys] +prefix = "ctrl+a" +new_tab = ["prefix+c", "ctrl+alt+n"] +next_tab = "prefix+n" + +[keys.indexed] +tabs = "ctrl" + +[[keys.command]] +key = "prefix+g" +type = "pane" +command = "lazygit" +"#; + + let copied = local_keybindings_config_toml_from_str(toml).expect("copied key config"); + assert!(copied.contains("[keys]")); + assert!(copied.contains("prefix = \"ctrl+a\"")); + assert!(copied.contains("prefix+c")); + assert!(copied.contains("ctrl+alt+n")); + assert!(copied.contains("[keys.indexed]")); + assert!(copied.contains("tabs = \"ctrl\"")); + assert!(!copied.contains("one-dark")); + assert!(!copied.contains("lazygit")); + assert!(!copied.contains("[[keys.command]]")); + } + + #[test] + fn local_keybindings_config_returns_none_without_keys() { + assert!(local_keybindings_config_toml_from_str("[theme]\nname = \"one-dark\"\n").is_none()); + } + + #[test] fn remote_bridge_command_uses_installed_binary() { let remote_herdr = RemoteHerdr::for_platform(RemotePlatform { os: "linux", |
