use std::path::Path;
use crate::error::Result;
use crate::infra::{env::Environment, fs, paths};
use crate::model::{
ActivationPolicy, ActivationReport, Availability, FailureKind, Operation, Shell,
};
use crate::service::{
FailureContext, failure, manual_activation_report, zsh_target_is_autoloadable,
};
use crate::{Error, shell};
pub(crate) fn execute(
env: &Environment,
shell: Shell,
program_name: &str,
) -> Result<ActivationReport> {
paths::validate_program_name(program_name)?;
let target_path = paths::default_install_path(env, &shell, program_name)
.map_err(|error| map_resolve_error(&shell, error))?;
shell::detect_default(env, &shell, program_name, &target_path)
.map_err(|error| map_detect_error(&shell, &target_path, error))
}
pub(crate) fn execute_at_path(
env: &Environment,
shell: Shell,
program_name: &str,
target_path: &Path,
) -> Result<ActivationReport> {
paths::validate_program_name(program_name)?;
match shell {
Shell::Fish => {
if path_matches_default_target(env, &Shell::Fish, program_name, target_path) {
shell::detect_default(env, &shell, program_name, target_path)
.map_err(|error| map_detect_error(&shell, target_path, error))
} else {
manual_custom_detection_report(&shell, program_name, target_path)
.map_err(|error| map_detect_error(&shell, target_path, error))
}
}
Shell::Bash => {
let report = shell::detect_default(env, &shell, program_name, target_path)
.map_err(|error| map_detect_error(&shell, target_path, error))?;
if path_matches_default_target(env, &Shell::Bash, program_name, target_path)
|| report.availability != Availability::ManualActionRequired
{
Ok(report)
} else {
manual_custom_detection_report(&shell, program_name, target_path)
.map_err(|error| map_detect_error(&shell, target_path, error))
}
}
Shell::Zsh => {
if !zsh_target_is_autoloadable(program_name, target_path) {
return manual_custom_detection_report(&shell, program_name, target_path)
.map_err(|error| map_detect_error(&shell, target_path, error));
}
let report = shell::detect_default(env, &shell, program_name, target_path)
.map_err(|error| map_detect_error(&shell, target_path, error))?;
if path_matches_default_target(env, &Shell::Zsh, program_name, target_path)
|| report.availability != Availability::ManualActionRequired
{
Ok(report)
} else {
manual_custom_detection_report(&shell, program_name, target_path)
.map_err(|error| map_detect_error(&shell, target_path, error))
}
}
_ => shell::detect_default(env, &shell, program_name, target_path)
.map_err(|error| map_detect_error(&shell, target_path, error)),
}
}
fn path_matches_default_target(
env: &Environment,
shell: &Shell,
program_name: &str,
target_path: &Path,
) -> bool {
paths::default_install_path(env, shell, program_name)
.map(|default_path| default_path == target_path)
.unwrap_or(false)
}
fn manual_custom_detection_report(
shell: &Shell,
program_name: &str,
target_path: &Path,
) -> Result<ActivationReport> {
let installed = fs::file_exists(target_path);
let mut report = manual_activation_report(
shell,
program_name,
target_path,
true,
ActivationPolicy::Manual,
)?;
report.availability = if installed {
Availability::Unknown
} else {
Availability::ManualActionRequired
};
report.reason = Some(if installed {
format!(
"Completion file `{}` is installed at a custom path, but shellcomp could not confirm managed activation for it.",
target_path.display()
)
} else {
format!(
"Completion file `{}` is not installed.",
target_path.display()
)
});
Ok(report)
}
fn map_resolve_error(shell: &Shell, error: Error) -> Error {
match error {
Error::MissingHome => failure(
FailureContext {
operation: Operation::DetectActivation,
shell,
target_path: None,
affected_locations: Vec::new(),
kind: FailureKind::MissingHome,
},
"Could not resolve the managed completion path because HOME is not set.",
Some(
"Set HOME for the current process so shellcomp can resolve the default managed path."
.to_owned(),
),
),
Error::UnsupportedShell(unsupported) => failure(
FailureContext {
operation: Operation::DetectActivation,
shell: &unsupported,
target_path: None,
affected_locations: Vec::new(),
kind: FailureKind::UnsupportedShell,
},
format!(
"Shell `{unsupported}` is not implemented in the current production support set."
),
None,
),
other => other,
}
}
fn map_detect_error(shell: &Shell, target_path: &std::path::Path, error: Error) -> Error {
match error {
Error::MissingHome => failure(
FailureContext {
operation: Operation::DetectActivation,
shell,
target_path: Some(target_path),
affected_locations: vec![target_path.to_path_buf()],
kind: FailureKind::MissingHome,
},
format!(
"Could not resolve the managed {} startup file because HOME is not set.",
shell
),
Some(
"Set HOME for the current process or inspect activation manually for the target completion file."
.to_owned(),
),
),
Error::Io { path, .. } | Error::InvalidUtf8File { path } => failure(
FailureContext {
operation: Operation::DetectActivation,
shell,
target_path: Some(target_path),
affected_locations: vec![target_path.to_path_buf(), path.clone()],
kind: FailureKind::ProfileUnavailable,
},
format!("Could not inspect the managed {} activation state.", shell),
Some(
"Review the relevant shell startup file manually, or re-run install to restore managed wiring."
.to_owned(),
),
),
Error::NonUtf8Path { path } => failure(
FailureContext {
operation: Operation::DetectActivation,
shell,
target_path: Some(target_path),
affected_locations: vec![target_path.to_path_buf(), path],
kind: FailureKind::InvalidTargetPath,
},
"The requested completion path could not be represented safely as UTF-8 for activation detection.",
Some(
"Move the completion file to a UTF-8 path or choose a UTF-8 path before asking shellcomp to inspect activation."
.to_owned(),
),
),
Error::ManagedBlockMissingEnd { path, .. } => failure(
FailureContext {
operation: Operation::DetectActivation,
shell,
target_path: Some(target_path),
affected_locations: vec![target_path.to_path_buf(), path.clone()],
kind: FailureKind::ProfileCorrupted,
},
format!(
"The managed {} activation block is malformed and could not be inspected safely.",
shell
),
Some(
"Repair or remove the malformed managed block manually, then re-run install."
.to_owned(),
),
),
other => other,
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::{execute, execute_at_path};
use crate::infra::env::Environment;
use crate::model::{ActivationMode, Availability, InstallRequest, Operation, Shell};
use crate::service::install;
#[test]
fn detect_reports_missing_completion() {
let temp_root = crate::tests::temp_dir("detect-missing");
let home = temp_root.join("home");
let env = Environment::test()
.with_var("HOME", &home)
.without_var("XDG_CONFIG_HOME")
.without_real_path_lookups();
let report = execute(&env, Shell::Fish, "tool").expect("detect should succeed");
assert_eq!(report.mode, ActivationMode::NativeDirectory);
assert_eq!(report.availability, Availability::ManualActionRequired);
}
#[test]
fn detect_reports_installed_zsh_completion() {
let temp_root = crate::tests::temp_dir("detect-zsh");
let home = temp_root.join("home");
let env = Environment::test()
.with_var("HOME", &home)
.without_var("ZDOTDIR")
.without_real_path_lookups();
install::execute(
&env,
InstallRequest {
shell: Shell::Zsh,
program_name: "tool",
script: b"#compdef tool\n",
path_override: None,
},
)
.expect("install should succeed");
let report = execute(&env, Shell::Zsh, "tool").expect("detect should succeed");
assert_eq!(report.mode, ActivationMode::ManagedRcBlock);
assert_eq!(report.availability, Availability::AvailableAfterSource);
}
#[test]
fn detect_fails_without_home_for_default_paths() {
let env = Environment::test()
.without_var("HOME")
.without_var("ZDOTDIR")
.without_real_path_lookups();
let error = execute(&env, Shell::Zsh, "tool").expect_err("detect should fail");
assert!(matches!(
error,
crate::Error::Failure(report) if report.kind == crate::FailureKind::MissingHome
));
}
#[test]
fn detect_returns_profile_corrupted_for_malformed_zsh_block() {
let temp_root = crate::tests::temp_dir("detect-zsh-corrupted");
let home = temp_root.join("home");
let completion_dir = home.join(".zfunc");
fs::create_dir_all(&completion_dir).expect("completion dir should be creatable");
fs::write(completion_dir.join("_tool"), b"#compdef tool\n")
.expect("completion file should be writable");
fs::write(
home.join(".zshrc"),
"# >>> shellcomp zsh tool >>>\nfpath=(~/.zfunc $fpath)\n",
)
.expect(".zshrc should be writable");
let env = Environment::test()
.with_var("HOME", &home)
.without_var("ZDOTDIR")
.without_real_path_lookups();
let error = execute(&env, Shell::Zsh, "tool").expect_err("detect should fail");
match error {
crate::Error::Failure(report) => {
assert_eq!(report.operation, Operation::DetectActivation);
assert_eq!(report.kind, crate::FailureKind::ProfileCorrupted);
assert_eq!(report.target_path, Some(completion_dir.join("_tool")));
assert!(
report
.affected_locations
.iter()
.any(|path| path.ends_with(".zshrc"))
);
assert!(report.next_step.is_some());
}
other => panic!("unexpected error variant: {other}"),
}
}
#[test]
fn detect_at_path_reports_unknown_for_custom_bash_path_without_managed_wiring() {
let temp_root = crate::tests::temp_dir("detect-custom-bash-manual");
let home = temp_root.join("home");
let target = temp_root.join("custom").join("tool.bash");
fs::create_dir_all(target.parent().expect("target should have a parent"))
.expect("target dir should be creatable");
fs::write(&target, "complete -F _tool tool\n").expect("target should be writable");
let env = Environment::test()
.with_var("HOME", &home)
.without_var("XDG_DATA_HOME")
.without_real_path_lookups();
let report =
execute_at_path(&env, Shell::Bash, "tool", &target).expect("detect should succeed");
assert_eq!(report.mode, ActivationMode::Manual);
assert_eq!(report.availability, Availability::Unknown);
}
#[test]
fn detect_at_path_reports_manual_for_non_autoloadable_zsh_target() {
let temp_root = crate::tests::temp_dir("detect-custom-zsh-manual");
let home = temp_root.join("home");
let target = temp_root.join("custom").join("tool.zsh");
fs::create_dir_all(target.parent().expect("target should have a parent"))
.expect("target dir should be creatable");
fs::write(&target, "#compdef tool\n").expect("target should be writable");
let env = Environment::test()
.with_var("HOME", &home)
.without_var("ZDOTDIR")
.without_real_path_lookups();
let report =
execute_at_path(&env, Shell::Zsh, "tool", &target).expect("detect should succeed");
assert_eq!(report.mode, ActivationMode::Manual);
assert_eq!(report.availability, Availability::Unknown);
}
#[cfg(unix)]
#[test]
fn detect_at_path_returns_structured_failure_for_non_utf8_path() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let temp_root = crate::tests::temp_dir("detect-non-utf8-path");
let target = temp_root.join(OsString::from_vec(b"tool-\xff.fish".to_vec()));
std::fs::write(&target, "complete -c tool -f\n").expect("target should be writable");
let env = Environment::test().without_real_path_lookups();
let error = execute_at_path(&env, Shell::Fish, "tool", &target)
.expect_err("detect should fail structurally");
match error {
crate::Error::Failure(report) => {
assert_eq!(report.operation, Operation::DetectActivation);
assert_eq!(report.kind, crate::FailureKind::InvalidTargetPath);
assert_eq!(report.target_path, Some(target));
assert!(report.next_step.is_some());
}
other => panic!("unexpected error variant: {other}"),
}
}
}