Fall back to xterm-256color when the client TERM isn't on the server
dosh propagates the client's TERM, but a server lacking that terminal's terminfo (e.g. xterm-ghostty, xterm-kitty) made tmux/vim fail with "missing or unsuitable terminal". build_shell_command now keeps the requested TERM only when its terminfo exists on this host, otherwise falls back to xterm-256color so remote apps always work. Affects both the in-process and holder-spawned shells. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+65
-2
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
||||
use portable_pty::{Child, CommandBuilder, MasterPty, NativePtySystem, PtySize, PtySystem};
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -148,16 +148,65 @@ pub fn spawn_pty_session(
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether this host has a terminfo entry for `term`, searching the standard
|
||||
/// ncurses directories. Both the legacy single-letter (`x/xterm`) and the
|
||||
/// hashed (`78/xterm`) subdirectory layouts are checked.
|
||||
fn terminfo_available(term: &str) -> bool {
|
||||
let Some(first) = term.chars().next() else {
|
||||
return false;
|
||||
};
|
||||
let letter = first.to_string();
|
||||
let hashed = format!("{:x}", first as u32);
|
||||
let mut dirs: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(t) = std::env::var("TERMINFO")
|
||||
&& !t.is_empty()
|
||||
{
|
||||
dirs.push(PathBuf::from(t));
|
||||
}
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
dirs.push(home.join(".terminfo"));
|
||||
}
|
||||
if let Ok(td) = std::env::var("TERMINFO_DIRS") {
|
||||
for d in td.split(':').filter(|d| !d.is_empty()) {
|
||||
dirs.push(PathBuf::from(d));
|
||||
}
|
||||
}
|
||||
for d in [
|
||||
"/etc/terminfo",
|
||||
"/lib/terminfo",
|
||||
"/usr/share/terminfo",
|
||||
"/usr/lib/terminfo",
|
||||
"/usr/share/lib/terminfo",
|
||||
] {
|
||||
dirs.push(PathBuf::from(d));
|
||||
}
|
||||
dirs.iter()
|
||||
.any(|dir| dir.join(&letter).join(term).exists() || dir.join(&hashed).join(term).exists())
|
||||
}
|
||||
|
||||
/// Build the [`CommandBuilder`] for a dosh shell, identically for the in-process
|
||||
/// `spawn_pty_session` and the out-of-process holder, so a persistent session's
|
||||
/// environment matches a non-persistent one.
|
||||
pub fn build_shell_command(shell: &str, env: &[(String, String)]) -> CommandBuilder {
|
||||
let mut cmd = CommandBuilder::new(shell);
|
||||
cmd.env("TERM", "xterm-256color");
|
||||
cmd.env("COLORTERM", "truecolor");
|
||||
for (name, value) in env {
|
||||
cmd.env(name, value);
|
||||
}
|
||||
// The client's TERM is propagated, but a server that lacks that terminal's
|
||||
// terminfo entry (e.g. xterm-ghostty, xterm-kitty) breaks ncurses apps like
|
||||
// tmux/vim with "missing or unsuitable terminal". Keep the requested TERM
|
||||
// only when this host actually has its terminfo; otherwise fall back to a
|
||||
// universally available entry so remote apps always work.
|
||||
let term = match env
|
||||
.iter()
|
||||
.find(|(n, _)| n == "TERM")
|
||||
.map(|(_, v)| v.as_str())
|
||||
{
|
||||
Some(requested) if terminfo_available(requested) => requested.to_string(),
|
||||
_ => "xterm-256color".to_string(),
|
||||
};
|
||||
cmd.env("TERM", term);
|
||||
cmd.env("SHELL", shell);
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
cmd.env("HOME", home.as_os_str());
|
||||
@@ -235,3 +284,17 @@ fn spawn_reader_thread(
|
||||
.context("spawn pty reader")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn terminfo_available_detects_known_and_unknown() {
|
||||
// A near-universal entry should be present on any host with ncurses.
|
||||
assert!(terminfo_available("xterm") || terminfo_available("xterm-256color"));
|
||||
// Bogus / empty names must report missing so we fall back.
|
||||
assert!(!terminfo_available("definitely-not-a-real-terminal-xyz"));
|
||||
assert!(!terminfo_available(""));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user