pnc 0.1.1

A fast, fully offline, terminal-only reverse Polish notation (RPN) calculator
use pnc_engine::clipboard::Clipboard;
use pnc_engine::error::PncError;
use std::io::Write;
use std::process::{Command, Stdio};

pub fn copy(text: &str) -> Result<(), String> {
    let tools: &[(&str, &[&str])] = &[
        ("xclip", &["-selection", "clipboard"]),
        ("xsel", &["--clipboard", "--input"]),
        ("wl-copy", &[]),
        ("pbcopy", &[]),
    ];
    for (tool, args) in tools {
        if let Ok(mut child) = Command::new(tool)
            .args(*args)
            .stdin(Stdio::piped())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
        {
            if let Some(stdin) = child.stdin.as_mut() {
                let _ = stdin.write_all(text.as_bytes());
            }
            if child.wait().map(|s| s.success()).unwrap_or(false) {
                return Ok(());
            }
        }
    }
    // OSC 52 fallback
    let encoded = base64_encode(text);
    let seq = format!("\x1b]52;c;{}\x07", encoded);
    let _ = write!(std::io::stdout(), "{}", seq);
    let _ = std::io::stdout().flush();
    Ok(())
}

pub fn paste() -> Result<String, String> {
    let tools: &[(&str, &[&str])] = &[
        ("xclip", &["-selection", "clipboard", "-o"]),
        ("xsel", &["--clipboard", "--output"]),
        ("wl-paste", &[]),
        ("pbpaste", &[]),
    ];
    for (tool, args) in tools {
        if let Ok(out) = Command::new(tool).args(*args).output() {
            if out.status.success() {
                return Ok(String::from_utf8_lossy(&out.stdout).to_string());
            }
        }
    }
    Err("clipboard unavailable".to_string())
}

fn base64_encode(input: &str) -> String {
    const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let bytes = input.as_bytes();
    let mut out = String::new();
    for chunk in bytes.chunks(3) {
        let b0 = chunk[0] as usize;
        let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
        let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
        out.push(TABLE[b0 >> 2] as char);
        out.push(TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char);
        if chunk.len() > 1 {
            out.push(TABLE[((b1 & 15) << 2) | (b2 >> 6)] as char);
        } else {
            out.push('=');
        }
        if chunk.len() > 2 {
            out.push(TABLE[b2 & 63] as char);
        } else {
            out.push('=');
        }
    }
    out
}

pub struct SystemClipboard;

impl Clipboard for SystemClipboard {
    fn get(&mut self) -> Result<String, PncError> {
        paste().map_err(|_| PncError::ClipboardUnavailable)
    }

    fn set(&mut self, text: String) -> Result<(), PncError> {
        copy(&text).map_err(|_| PncError::ClipboardUnavailable)
    }
}