aboutsummaryrefslogtreecommitdiff
path: root/src/pane.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-30 01:53:55 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-30 01:55:36 +0300
commit8df1639048529b383358000879b5ee078a90b9f9 (patch)
tree7b0088246c3cc0834ed04cf20bf9726645bdb040 /src/pane.rs
parent817a1562db9156e34f13487344ea147618f9070b (diff)
fix: use login shells for macos panes
refs #350
Diffstat (limited to 'src/pane.rs')
-rw-r--r--src/pane.rs231
1 files changed, 225 insertions, 6 deletions
diff --git a/src/pane.rs b/src/pane.rs
index 885fb11d..97aeb9cb 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -1,5 +1,6 @@
use std::cell::Cell;
-use std::io::{Read, Write};
+use std::io::{self, Read, Write};
+use std::path::Path;
use std::sync::{
atomic::{AtomicBool, AtomicU16, AtomicU32, Ordering},
Arc, Mutex,
@@ -637,6 +638,96 @@ fn pane_shell_from(configured_shell: &str, env_shell: Option<String>) -> String
.unwrap_or_else(|| "/bin/sh".into())
}
+#[derive(Clone, Copy)]
+pub(crate) struct PaneShellConfig<'a> {
+ pub(crate) default_shell: &'a str,
+ pub(crate) mode: crate::config::ShellModeConfig,
+}
+
+impl<'a> PaneShellConfig<'a> {
+ pub(crate) fn new(default_shell: &'a str, mode: crate::config::ShellModeConfig) -> Self {
+ Self {
+ default_shell,
+ mode,
+ }
+ }
+}
+
+fn shell_mode_uses_login_shell(
+ mode: crate::config::ShellModeConfig,
+ target_is_macos: bool,
+) -> bool {
+ match mode {
+ crate::config::ShellModeConfig::Auto => target_is_macos,
+ crate::config::ShellModeConfig::Login => true,
+ crate::config::ShellModeConfig::NonLogin => false,
+ }
+}
+
+fn is_executable_file(path: &Path) -> bool {
+ let Ok(metadata) = path.metadata() else {
+ return false;
+ };
+ if !metadata.is_file() {
+ return false;
+ }
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ metadata.permissions().mode() & 0o111 != 0
+ }
+ #[cfg(not(unix))]
+ {
+ true
+ }
+}
+
+fn resolve_shell_for_login_mode(shell: &str) -> io::Result<String> {
+ if shell.contains(std::path::MAIN_SEPARATOR) {
+ let path = Path::new(shell);
+ return is_executable_file(path)
+ .then(|| shell.to_string())
+ .ok_or_else(|| {
+ io::Error::new(
+ io::ErrorKind::NotFound,
+ format!("login shell {shell:?} is not executable"),
+ )
+ });
+ }
+
+ std::env::var_os("PATH")
+ .and_then(|path| {
+ std::env::split_paths(&path)
+ .map(|dir| dir.join(shell))
+ .find(|candidate| is_executable_file(candidate))
+ })
+ .and_then(|path| path.into_os_string().into_string().ok())
+ .ok_or_else(|| {
+ io::Error::new(
+ io::ErrorKind::NotFound,
+ format!("login shell {shell:?} was not found on PATH"),
+ )
+ })
+}
+
+fn pane_shell_command_builder_for_target(
+ shell_config: PaneShellConfig<'_>,
+ target_is_macos: bool,
+) -> io::Result<CommandBuilder> {
+ let shell = pane_shell(shell_config.default_shell);
+ if shell_mode_uses_login_shell(shell_config.mode, target_is_macos) {
+ let mut cmd = CommandBuilder::new_default_prog();
+ cmd.env("SHELL", resolve_shell_for_login_mode(&shell)?);
+ Ok(cmd)
+ } else {
+ Ok(CommandBuilder::new(&shell))
+ }
+}
+
+fn pane_shell_command_builder(shell_config: PaneShellConfig<'_>) -> io::Result<CommandBuilder> {
+ pane_shell_command_builder_for_target(shell_config, cfg!(target_os = "macos"))
+}
+
#[cfg(unix)]
fn duplicate_fd(fd: std::os::fd::RawFd) -> std::io::Result<std::os::fd::RawFd> {
let duplicated = unsafe { libc::dup(fd) };
@@ -909,7 +1000,7 @@ impl PaneRuntime {
cwd: std::path::PathBuf,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
- default_shell: &str,
+ shell_config: PaneShellConfig<'_>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<AtomicBool>,
@@ -921,7 +1012,7 @@ impl PaneRuntime {
cwd,
scrollback_limit_bytes,
host_terminal_theme,
- default_shell,
+ shell_config,
None,
events,
render_notify,
@@ -936,14 +1027,13 @@ impl PaneRuntime {
cwd: std::path::PathBuf,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
- default_shell: &str,
+ shell_config: PaneShellConfig<'_>,
initial_history_ansi: Option<&str>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<AtomicBool>,
) -> std::io::Result<Self> {
- let shell = pane_shell(default_shell);
- let mut cmd = CommandBuilder::new(&shell);
+ let mut cmd = pane_shell_command_builder(shell_config)?;
cmd.cwd(cwd);
cmd.env(crate::HERDR_ENV_VAR, crate::HERDR_ENV_VALUE);
apply_pane_terminal_env(&mut cmd);
@@ -2294,6 +2384,135 @@ mod tests {
}
#[test]
+ fn shell_mode_auto_uses_login_shell_only_on_macos() {
+ assert!(shell_mode_uses_login_shell(
+ crate::config::ShellModeConfig::Auto,
+ true
+ ));
+ assert!(!shell_mode_uses_login_shell(
+ crate::config::ShellModeConfig::Auto,
+ false
+ ));
+ assert!(shell_mode_uses_login_shell(
+ crate::config::ShellModeConfig::Login,
+ false
+ ));
+ assert!(!shell_mode_uses_login_shell(
+ crate::config::ShellModeConfig::NonLogin,
+ true
+ ));
+ }
+
+ #[test]
+ fn login_shell_builder_uses_default_prog_with_resolved_shell_env() {
+ let cmd = pane_shell_command_builder_for_target(
+ PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::Login),
+ false,
+ )
+ .unwrap();
+ assert!(cmd.is_default_prog());
+ assert_eq!(
+ cmd.get_env("SHELL").and_then(std::ffi::OsStr::to_str),
+ Some("/bin/sh")
+ );
+ }
+
+ #[test]
+ fn auto_shell_builder_uses_login_shell_on_macos_target() {
+ let cmd = pane_shell_command_builder_for_target(
+ PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::Auto),
+ true,
+ )
+ .unwrap();
+ assert!(cmd.is_default_prog());
+ assert_eq!(
+ cmd.get_env("SHELL").and_then(std::ffi::OsStr::to_str),
+ Some("/bin/sh")
+ );
+ }
+
+ #[test]
+ fn auto_shell_builder_keeps_direct_shell_on_non_macos_target() {
+ let cmd = pane_shell_command_builder_for_target(
+ PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::Auto),
+ false,
+ )
+ .unwrap();
+ assert!(!cmd.is_default_prog());
+ assert_eq!(cmd.get_argv(), &[std::ffi::OsString::from("/bin/sh")]);
+ }
+
+ #[test]
+ fn login_shell_builder_rejects_missing_shell_instead_of_falling_back() {
+ let err = pane_shell_command_builder_for_target(
+ PaneShellConfig::new(
+ "/__herdr_missing_shell__",
+ crate::config::ShellModeConfig::Login,
+ ),
+ false,
+ )
+ .unwrap_err();
+ assert_eq!(err.kind(), io::ErrorKind::NotFound);
+ }
+
+ #[test]
+ fn login_shell_builder_resolves_bare_shell_names_from_path() {
+ let _lock = crate::integration::integration_env_lock();
+ let base = std::env::temp_dir().join(format!(
+ "herdr-login-shell-path-{}-{}",
+ std::process::id(),
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ let bin = base.join("bin");
+ std::fs::create_dir_all(&bin).unwrap();
+ let shell = bin.join("fake-shell");
+ std::fs::write(&shell, "#!/bin/sh\nexit 0\n").unwrap();
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ std::fs::set_permissions(&shell, std::fs::Permissions::from_mode(0o755)).unwrap();
+ }
+ let original_path = std::env::var_os("PATH");
+ std::env::set_var("PATH", &bin);
+
+ let cmd = pane_shell_command_builder_for_target(
+ PaneShellConfig::new("fake-shell", crate::config::ShellModeConfig::Login),
+ false,
+ )
+ .unwrap();
+
+ assert!(cmd.is_default_prog());
+ assert_eq!(
+ cmd.get_env("SHELL").and_then(std::ffi::OsStr::to_str),
+ shell.to_str()
+ );
+ match original_path {
+ Some(path) => std::env::set_var("PATH", path),
+ None => std::env::remove_var("PATH"),
+ }
+ let _ = std::fs::remove_dir_all(base);
+ }
+
+ #[test]
+ fn login_shell_resolution_preserves_shell_paths() {
+ assert_eq!(resolve_shell_for_login_mode("/bin/sh").unwrap(), "/bin/sh");
+ }
+
+ #[test]
+ fn non_login_shell_builder_execs_resolved_shell_directly() {
+ let cmd = pane_shell_command_builder(PaneShellConfig::new(
+ "/bin/sh",
+ crate::config::ShellModeConfig::NonLogin,
+ ))
+ .unwrap();
+ assert!(!cmd.is_default_prog());
+ assert_eq!(cmd.get_argv(), &[std::ffi::OsString::from("/bin/sh")]);
+ }
+
+ #[test]
fn pane_terminal_identity_overrides_outer_terminal_env() {
let output = capture_shell_output("printf '%s\\n%s\\n' \"$TERM\" \"$COLORTERM\"", &[]);
assert_eq!(output, "xterm-256color\ntruecolor\n");