3.1 KiB · text History aea77cc
use crate::PplError;
use clap::{Args, ValueEnum};
use directories::BaseDirs;
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::PathBuf;

const MARKER: &str = "# ppl motd";

#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum Shell {
    Bash,
    Zsh,
    Fish,
}

#[derive(Args, Debug)]
pub struct ShellArgs {
    /// Shell to install the motd hook for (defaults to $SHELL)
    #[arg(value_enum)]
    pub shell: Option<Shell>,
    /// Print the snippet instead of writing it to the rc file
    #[arg(long)]
    pub print: bool,
}

impl Shell {
    fn detect() -> Option<Shell> {
        let shell = std::env::var("SHELL").ok()?;
        match shell.rsplit('/').next()? {
            "bash" => Some(Shell::Bash),
            "zsh" => Some(Shell::Zsh),
            "fish" => Some(Shell::Fish),
            _ => None,
        }
    }

    fn rc_path(&self) -> Option<PathBuf> {
        let base = BaseDirs::new()?;
        Some(match self {
            Shell::Bash => base.home_dir().join(".bashrc"),
            Shell::Zsh => std::env::var_os("ZDOTDIR")
                .map(PathBuf::from)
                .unwrap_or_else(|| base.home_dir().to_path_buf())
                .join(".zshrc"),
            Shell::Fish => base.config_dir().join("fish").join("config.fish"),
        })
    }

    /// Only run in interactive shells, and only if ppl is on the PATH, so
    /// scp/rsync sessions and uninstalls don't break the shell.
    fn snippet(&self) -> &'static str {
        match self {
            Shell::Bash => "[[ $- == *i* ]] && command -v ppl >/dev/null 2>&1 && ppl motd",
            Shell::Zsh => "[[ -o interactive ]] && (( $+commands[ppl] )) && ppl motd",
            Shell::Fish => "if status is-interactive; and command -q ppl; ppl motd; end",
        }
    }
}

pub fn do_shell(args: &ShellArgs) -> Result<(), PplError> {
    let shell = args.shell.or_else(Shell::detect).ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            "could not detect shell from $SHELL, pass one of: bash, zsh, fish",
        )
    })?;
    let entry = format!("{}\n{}\n", MARKER, shell.snippet());

    if args.print {
        print!("{}", entry);
        return Ok(());
    }

    let path = shell
        .rc_path()
        .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "could not find home directory"))?;

    let existing = match fs::read_to_string(&path) {
        Ok(s) => s,
        Err(e) if e.kind() == io::ErrorKind::NotFound => String::new(),
        Err(e) => return Err(e.into()),
    };
    if existing.contains(MARKER) {
        println!("motd already installed in {}", path.display());
        return Ok(());
    }

    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let mut file = OpenOptions::new().create(true).append(true).open(&path)?;
    let sep = if existing.is_empty() || existing.ends_with("\n\n") {
        ""
    } else if existing.ends_with('\n') {
        "\n"
    } else {
        "\n\n"
    };
    write!(file, "{}{}", sep, entry)?;
    println!("motd installed in {}", path.display());
    Ok(())
}