quinjet 0.0.50

A fast, live, keyboard-first Git source-control interface for the terminal
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use super::*;

pub(super) fn targets(shell: Shell) -> Result<Vec<Target>> {
    if shell == Shell::PowerShell {
        return powershell_targets();
    }
    targets_in(shell, &completion_dirs()?)
}

pub(super) fn targets_in(shell: Shell, directories: &CompletionDirs) -> Result<Vec<Target>> {
    match shell {
        Shell::Bash => Ok(vec![Target {
            script: directories.data.join("bash-completion/completions/quinjet"),
            profile: Some(ProfileIntegration {
                path: directories.home.join(".bashrc"),
                completion: None,
            }),
        }]),
        Shell::Elvish => {
            let root = directories.config.join("elvish");
            Ok(vec![Target {
                script: root.join("lib/quinjet.elv"),
                profile: Some(ProfileIntegration {
                    path: root.join("rc.elv"),
                    completion: Some("use quinjet".to_owned()),
                }),
            }])
        }
        Shell::Fish => Ok(vec![Target {
            script: directories.config.join("fish/completions/quinjet.fish"),
            profile: Some(ProfileIntegration {
                path: directories.config.join("fish/config.fish"),
                completion: None,
            }),
        }]),
        Shell::Zsh => {
            let functions = directories.zsh.join(".zfunc");
            let escaped = single_quote(&functions.to_string_lossy());
            Ok(vec![Target {
                script: functions.join("_quinjet"),
                profile: Some(ProfileIntegration {
                    path: directories.zsh.join(".zshrc"),
                    completion: Some(format!(
                        "fpath=('{escaped}' $fpath)\nautoload -Uz compinit\ncompinit"
                    )),
                }),
            }])
        }
        Shell::PowerShell => bail!("PowerShell profiles are resolved by PowerShell"),
        _ => bail!("this shell does not support generated completions"),
    }
}

pub(super) fn powershell_targets() -> Result<Vec<Target>> {
    let recorded = recorded_powershell_profiles()?;
    if !recorded.is_empty() {
        return Ok(recorded
            .iter()
            .map(|profile| powershell_target(profile))
            .collect());
    }
    let mut profiles = Vec::new();
    for program in ["pwsh", "powershell"] {
        let output = ProcessCommand::new(program)
            .args([
                "-NoLogo",
                "-NoProfile",
                "-NonInteractive",
                "-Command",
                "[Console]::Out.Write($PROFILE.CurrentUserAllHosts)",
            ])
            .stdin(Stdio::null())
            .stderr(Stdio::null())
            .output();
        let Ok(output) = output else {
            continue;
        };
        if !output.status.success() {
            continue;
        }
        let profile_text = String::from_utf8_lossy(&output.stdout);
        let profile = PathBuf::from(profile_text.trim());
        if profile.as_os_str().is_empty() || profiles.contains(&profile) {
            continue;
        }
        profiles.push(profile);
    }
    if profiles.is_empty() {
        bail!("could not locate a PowerShell profile")
    }
    let mut record = String::new();
    for profile in &profiles {
        record.push_str(&profile.to_string_lossy());
        record.push('\n');
    }
    write_file(&powershell_record()?, record.as_bytes())?;
    Ok(profiles
        .iter()
        .map(|profile| powershell_target(profile))
        .collect())
}

pub(super) fn powershell_target(profile: &Path) -> Target {
    let parent = profile.parent().unwrap_or(profile);
    let script = parent.join("quinjet-completions.ps1");
    let escaped = script.to_string_lossy().replace('\'', "''");
    Target {
        script,
        profile: Some(ProfileIntegration {
            path: profile.to_path_buf(),
            completion: Some(format!(". '{escaped}'")),
        }),
    }
}

pub(super) fn recorded_powershell_profiles() -> Result<Vec<PathBuf>> {
    let record = powershell_record()?;
    let contents = match fs::read_to_string(&record) {
        Ok(contents) => contents,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
        Err(error) => {
            return Err(error).with_context(|| format!("failed to read {}", record.display()));
        }
    };
    Ok(contents
        .lines()
        .filter(|line| !line.is_empty())
        .map(PathBuf::from)
        .collect())
}

pub(super) fn powershell_record() -> Result<PathBuf> {
    Ok(state_root()?.join("powershell-profiles"))
}

pub(super) fn shell_state(shell: Shell) -> Result<PathBuf> {
    Ok(state_root()?.join(format!("{shell}-installed")))
}

pub(super) fn shortcut_state() -> Result<PathBuf> {
    Ok(state_root()?.join("shortcut-installed"))
}

pub(super) fn shortcut_is_enabled() -> Result<bool> {
    let state = shortcut_state()?;
    match fs::read_to_string(&state) {
        Ok(contents) => Ok(contents != "removed\n"),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
        Err(error) => Err(error).with_context(|| format!("failed to read {}", state.display())),
    }
}

pub(super) fn state_root() -> Result<PathBuf> {
    #[cfg(windows)]
    if let Some(local) = env_path("LOCALAPPDATA") {
        return Ok(local.join("Quinjet/state"));
    }
    let home = env_path("HOME")
        .or_else(|| env_path("USERPROFILE"))
        .context("HOME is not set, so the completion state cannot be determined")?;
    Ok(env_path("XDG_STATE_HOME")
        .unwrap_or_else(|| home.join(".local/state"))
        .join("quinjet"))
}