pub const ALT_SCREEN_ENV_VAR: &str = "CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN";
pub const ALT_SCREEN_DEFAULT: &str = "1";
pub const ALT_SCREEN_ENABLED: &str = "0";
pub fn configured_env(alternate_screen: bool) -> Vec<(String, String)> {
let value = if alternate_screen {
ALT_SCREEN_ENABLED
} else {
ALT_SCREEN_DEFAULT
};
vec![(ALT_SCREEN_ENV_VAR.to_owned(), value.to_owned())]
}
pub fn configured_shell_assignments(alternate_screen: bool) -> String {
let configured = configured_env(alternate_screen);
MANAGED_DEFAULTS
.iter()
.map(
|d| match configured.iter().find(|(name, _)| name == d.env_var) {
Some((name, value)) => format!("{name}={value}"),
None => d.shell_assignment.to_owned(),
},
)
.collect::<Vec<_>>()
.join(" ")
}
pub fn managed_shell_assignments() -> String {
configured_shell_assignments(configured_alternate_screen())
}
pub fn configured_alternate_screen() -> bool {
configured_alternate_screen_in(operator_config_root().as_deref())
}
pub fn operator_config_root() -> Option<std::path::PathBuf> {
trusty_common::crate_config::crate_config_dir(crate::core::trusty_tools_config::CRATE_NAME)
}
pub fn configured_alternate_screen_in(crate_config_root: Option<&std::path::Path>) -> bool {
match crate_config_root {
Some(root) => configured_alternate_screen_at(root),
None => tmux_option_fallback(),
}
}
fn tmux_option_fallback() -> bool {
use crate::core::trusty_tools_config::{TrustyToolsConfig, resolve_tmux_options};
resolve_tmux_options(&TrustyToolsConfig::default()).alternate_screen
}
pub fn configured_alternate_screen_at(crate_config_root: &std::path::Path) -> bool {
use crate::core::trusty_tools_config::{TrustyToolsConfig, resolve_tmux_options};
let path = crate_config_root.join(trusty_common::crate_config::CONFIG_FILE);
match trusty_common::crate_config::load_at::<TrustyToolsConfig>(&path) {
Ok(config) => resolve_tmux_options(&config.unwrap_or_default()).alternate_screen,
Err(err) => {
let fallback = tmux_option_fallback();
tracing::warn!(
path = %path.display(),
"#8405: cannot read tmux.alternate_screen ({err}); the launch proceeds with \
alternate_screen={fallback}, the value the tmux alternate-screen option \
falls back to"
);
fallback
}
}
}
pub fn apply_configured_to_command(cmd: &mut std::process::Command, alternate_screen: bool) {
for (name, value) in configured_env(alternate_screen) {
cmd.env(name, value);
}
apply_default_to_command(cmd);
}
pub const ALT_SCREEN_SHELL_ASSIGNMENT: &str =
"CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=\"${CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN-1}\"";
pub const MOUSE_ENV_VAR: &str = "CLAUDE_CODE_DISABLE_MOUSE";
pub const MOUSE_DEFAULT: &str = "1";
pub const MOUSE_SHELL_ASSIGNMENT: &str =
"CLAUDE_CODE_DISABLE_MOUSE=\"${CLAUDE_CODE_DISABLE_MOUSE-1}\"";
struct ManagedDefault {
env_var: &'static str,
default: &'static str,
shell_assignment: &'static str,
}
const MANAGED_DEFAULTS: &[ManagedDefault] = &[
ManagedDefault {
env_var: ALT_SCREEN_ENV_VAR,
default: ALT_SCREEN_DEFAULT,
shell_assignment: ALT_SCREEN_SHELL_ASSIGNMENT,
},
ManagedDefault {
env_var: MOUSE_ENV_VAR,
default: MOUSE_DEFAULT,
shell_assignment: MOUSE_SHELL_ASSIGNMENT,
},
];
pub fn apply_default_to_command(cmd: &mut std::process::Command) {
apply_default_when_unset(cmd, |name| std::env::var_os(name).is_some());
}
pub fn apply_default_when_unset(cmd: &mut std::process::Command, is_set: impl Fn(&str) -> bool) {
for entry in MANAGED_DEFAULTS {
let explicit = cmd.get_envs().any(|(name, _)| name == entry.env_var);
if explicit || is_set(entry.env_var) {
continue;
}
cmd.env(entry.env_var, entry.default);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_assignment_pins_the_defaulting_form() {
assert_eq!(
ALT_SCREEN_SHELL_ASSIGNMENT,
"CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=\"${CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN-1}\""
);
assert!(
!ALT_SCREEN_SHELL_ASSIGNMENT.contains(":-"),
"`:-` would override an operator value of empty string"
);
}
#[test]
fn shell_assignment_names_the_variable_and_the_default() {
assert_eq!(
ALT_SCREEN_SHELL_ASSIGNMENT,
format!("{ALT_SCREEN_ENV_VAR}=\"${{{ALT_SCREEN_ENV_VAR}-{ALT_SCREEN_DEFAULT}}}\"")
);
}
#[test]
fn mouse_shell_assignment_pins_the_defaulting_form() {
assert_eq!(
MOUSE_SHELL_ASSIGNMENT,
"CLAUDE_CODE_DISABLE_MOUSE=\"${CLAUDE_CODE_DISABLE_MOUSE-1}\""
);
assert!(
!MOUSE_SHELL_ASSIGNMENT.contains(":-"),
"`:-` would override an operator value of empty string"
);
}
#[test]
fn mouse_shell_assignment_names_the_variable_and_the_default() {
assert_eq!(
MOUSE_SHELL_ASSIGNMENT,
format!("{MOUSE_ENV_VAR}=\"${{{MOUSE_ENV_VAR}-{MOUSE_DEFAULT}}}\"")
);
}
fn override_for(cmd: &std::process::Command, name: &str) -> Option<Option<String>> {
cmd.get_envs()
.find(|(k, _)| *k == name)
.map(|(_, v)| v.map(|v| v.to_string_lossy().into_owned()))
}
#[test]
fn command_defaults_apply_when_every_variable_is_unset() {
let mut cmd = std::process::Command::new("claude");
apply_default_when_unset(&mut cmd, |_| false);
assert_eq!(
override_for(&cmd, ALT_SCREEN_ENV_VAR),
Some(Some(ALT_SCREEN_DEFAULT.to_string())),
"an unset alternate-screen variable must be provisioned with the tm default"
);
assert_eq!(
override_for(&cmd, MOUSE_ENV_VAR),
Some(Some(MOUSE_DEFAULT.to_string())),
"an unset mouse-capture variable must be provisioned with the tm default"
);
}
#[test]
fn command_defaults_yield_to_an_operator_value() {
let mut cmd = std::process::Command::new("claude");
apply_default_when_unset(&mut cmd, |_| true);
assert_eq!(
override_for(&cmd, ALT_SCREEN_ENV_VAR),
None,
"a value the launch already carries must reach claude untouched"
);
assert_eq!(
override_for(&cmd, MOUSE_ENV_VAR),
None,
"a value the launch already carries must reach claude untouched"
);
}
#[test]
fn command_defaults_yield_per_variable_independently() {
let mut cmd = std::process::Command::new("claude");
apply_default_when_unset(&mut cmd, |name| name == ALT_SCREEN_ENV_VAR);
assert_eq!(
override_for(&cmd, ALT_SCREEN_ENV_VAR),
None,
"the variable the launch already carries must reach claude untouched"
);
assert_eq!(
override_for(&cmd, MOUSE_ENV_VAR),
Some(Some(MOUSE_DEFAULT.to_string())),
"the variable the launch does NOT carry must still be provisioned"
);
}
#[test]
fn command_defaults_never_replace_an_explicit_assignment() {
let mut cmd = std::process::Command::new("claude");
for (name, value) in configured_env(true) {
cmd.env(name, value);
}
apply_default_when_unset(&mut cmd, |_| false);
assert_eq!(
override_for(&cmd, ALT_SCREEN_ENV_VAR),
Some(Some(ALT_SCREEN_ENABLED.to_string())),
"the default must not overwrite the configured fullscreen renderer"
);
assert_eq!(
override_for(&cmd, MOUSE_ENV_VAR),
Some(Some(MOUSE_DEFAULT.to_string())),
"a variable with no explicit assignment still gets the tm default"
);
}
#[test]
fn configured_env_forces_the_fullscreen_renderer_when_enabled() {
assert_eq!(
configured_env(true),
vec![(ALT_SCREEN_ENV_VAR.to_owned(), "0".to_owned())]
);
}
#[test]
fn configured_env_forces_the_classic_renderer_when_disabled() {
assert_eq!(
configured_env(false),
vec![(ALT_SCREEN_ENV_VAR.to_owned(), "1".to_owned())]
);
}
#[test]
fn configured_shell_assignments_assign_the_renderer_in_both_directions() {
assert_eq!(
configured_shell_assignments(true),
format!("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=0 {MOUSE_SHELL_ASSIGNMENT}")
);
assert_eq!(
configured_shell_assignments(false),
format!("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 {MOUSE_SHELL_ASSIGNMENT}")
);
}
fn write_config(root: &std::path::Path, yaml: &str) {
std::fs::create_dir_all(root).expect("create root");
std::fs::write(root.join("config.yaml"), yaml).expect("write config");
}
#[test]
fn configured_alternate_screen_reads_the_named_root() {
let dir = tempfile::tempdir().expect("tempdir");
write_config(dir.path(), "tmux:\n alternate_screen: true\n");
assert!(configured_alternate_screen_at(dir.path()));
write_config(dir.path(), "tmux:\n alternate_screen: false\n");
assert!(!configured_alternate_screen_at(dir.path()));
}
#[test]
fn configured_alternate_screen_defaults_off_without_a_config() {
let dir = tempfile::tempdir().expect("tempdir");
assert!(!configured_alternate_screen_at(dir.path()));
}
#[test]
fn configured_alternate_screen_warns_and_falls_back_on_a_malformed_config() {
let dir = tempfile::tempdir().expect("tempdir");
write_config(dir.path(), "tmux:\n alternate_screen: [not, a, bool\n");
let (value, logged) = capture_warnings(|| configured_alternate_screen_at(dir.path()));
let tmux_option = crate::core::trusty_tools_config::resolve_tmux_options(
&crate::core::trusty_tools_config::TrustyToolsConfig::default(),
)
.alternate_screen;
assert_eq!(value, tmux_option);
let path = dir.path().join("config.yaml");
assert!(logged.contains(&path.display().to_string()), "{logged}");
assert!(
logged.contains("YAML"),
"the warning must carry the error: {logged}"
);
}
#[test]
fn configured_command_assigns_the_renderer_over_the_pane() {
for (alternate_screen, want) in [(true, "0"), (false, "1")] {
let mut cmd = std::process::Command::new("claude");
apply_configured_to_command(&mut cmd, alternate_screen);
assert_eq!(
override_for(&cmd, ALT_SCREEN_ENV_VAR),
Some(Some(want.to_string()))
);
}
}
fn capture_warnings<T>(f: impl FnOnce() -> T) -> (T, String) {
use tracing_subscriber::layer::SubscriberExt;
crate::test_support::enable_event_capture();
let buffer = trusty_common::log_buffer::LogBuffer::new(64);
let subscriber = tracing_subscriber::registry().with(
trusty_common::log_buffer::LogBufferLayer::new(buffer.clone()),
);
let value = tracing::subscriber::with_default(subscriber, f);
(value, buffer.tail(64).join("\n"))
}
}