entrust 0.6.0

A CLI password manager
Documentation
use crate::theme::color;
use arboard::Clipboard;
use clap::{Args, Subcommand};
use color_print::cprintln;
use std::borrow::Cow;
use std::io::{IsTerminal, Read, Write};
use std::process::{Command, Stdio};
use std::{env, io, thread};

#[derive(Args, Debug)]
pub struct ClipArgs {
    #[command(subcommand)]
    pub command: ClipSubcommand,
}

#[derive(Subcommand, Debug)]
pub enum ClipSubcommand {
    Copy,
    Clear {
        #[arg(short, long, default_value_t = 0)]
        delay_seconds: u64,
    },
}

pub fn run(args: ClipArgs) -> anyhow::Result<()> {
    match args.command {
        ClipSubcommand::Copy => copy_stdin(),
        ClipSubcommand::Clear { delay_seconds } => clear(delay_seconds, true),
    }
}

pub fn copy(content: Cow<'_, str>) -> anyhow::Result<()> {
    if cfg!(target_os = "linux") {
        copy_in_new_process(&content)
    } else {
        copy_now(content)
    }
}

fn copy_stdin() -> anyhow::Result<()> {
    if io::stdin().is_terminal() {
        return Ok(());
    };
    let mut stdin = String::new();
    io::stdin().read_to_string(&mut stdin)?;

    #[cfg(target_os = "linux")]
    linux::copy_wait(stdin.into())?;
    #[cfg(not(target_os = "linux"))]
    copy_now(stdin.into())?;

    Ok(())
}

fn copy_now(content: Cow<str>) -> anyhow::Result<()> {
    Clipboard::new()?.set_text(content)?;
    Ok(())
}

#[cfg(target_os = "linux")]
mod linux {
    use arboard::{Clipboard, SetExtLinux};
    use std::borrow::Cow;
    use std::time::{Duration, Instant};

    pub fn copy_wait(content: Cow<str>) -> anyhow::Result<()> {
        Clipboard::new()?
            .set()
            .wait_until(Instant::now() + Duration::from_secs(5))
            .text(content)?;
        Ok(())
    }
}

fn copy_in_new_process(content: &str) -> anyhow::Result<()> {
    let current_exe = env::current_exe()?;
    let mut child = Command::new(current_exe)
        .arg("clip")
        .arg("copy")
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()?;
    let mut stdin = child.stdin.take().unwrap();
    stdin.write_all(content.as_bytes())?;
    drop(stdin);
    Ok(())
}

fn clear(delay_seconds: u64, only_if_stdin_matches: bool) -> anyhow::Result<()> {
    if delay_seconds > 0 {
        thread::sleep(std::time::Duration::from_secs(delay_seconds));
    };
    if !only_if_stdin_matches || io::stdin().is_terminal() {
        return clear_now();
    }
    let mut stdin = String::new();
    io::stdin().read_to_string(&mut stdin)?;
    let stdin_matches_current = || {
        Clipboard::new()
            .and_then(|mut c| c.get_text())
            .is_ok_and(|current| {
                let matches = current.as_str() == stdin.trim_end();
                #[cfg(feature = "tracing")]
                trace_match(stdin.as_str(), current.as_str(), matches);
                matches
            })
    };
    if stdin.is_empty() || stdin_matches_current() {
        clear_now()?;
    }
    Ok(())
}

#[cfg(feature = "tracing")]
fn trace_match(stdin: &str, current: &str, matches: bool) {
    tracing::info!("stdin matches clipboard: {matches}");
    if !matches {
        let matches_trimmed = current.trim() == stdin.trim();
        tracing::info!("stdin matches trimmed clipboard: {matches_trimmed}");
    }
}

fn clear_now() -> anyhow::Result<()> {
    #[cfg(feature = "tracing")]
    tracing::info!("clearing clipboard");
    copy("".into())
}

pub fn clear_in_new_process(content: &str, delay_seconds: u64) -> anyhow::Result<()> {
    let current_exe = env::current_exe()?;
    let mut child = Command::new(current_exe)
        .arg("clip")
        .arg("clear")
        .arg("-d")
        .arg(delay_seconds.to_string())
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()?;
    let mut stdin = child.stdin.take().unwrap();
    stdin.write_all(content.as_bytes())?;
    drop(stdin);
    if color() {
        cprintln!(
            "<bright-black>The clipboard will be cleared in {delay_seconds}s if it has not changed."
        );
    } else {
        println!("The clipboard will be cleared in {delay_seconds}s if it has not changed.");
    };

    Ok(())
}