jk 0.2.7

A jj-native terminal UI for Jujutsu
use std::io::{self, Write};

pub fn copy_command_line(command_line: &str) -> String {
    match write_terminal_clipboard(command_line) {
        Ok(()) => "copied command".to_owned(),
        Err(error) => format!("copy failed: {error}"),
    }
}

fn write_terminal_clipboard(text: &str) -> io::Result<()> {
    let sequence = osc52_sequence(text);
    let mut stdout = io::stdout();
    stdout.write_all(sequence.as_bytes())?;
    stdout.flush()
}

fn osc52_sequence(text: &str) -> String {
    format!("\u{1b}]52;c;{}\u{7}", base64_encode(text.as_bytes()))
}

fn base64_encode(bytes: &[u8]) -> String {
    const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let mut encoded = String::with_capacity(bytes.len().div_ceil(3) * 4);

    for chunk in bytes.chunks(3) {
        let first = chunk[0];
        let second = *chunk.get(1).unwrap_or(&0);
        let third = *chunk.get(2).unwrap_or(&0);
        let value = (u32::from(first) << 16) | (u32::from(second) << 8) | u32::from(third);

        encoded.push(TABLE[((value >> 18) & 0x3f) as usize] as char);
        encoded.push(TABLE[((value >> 12) & 0x3f) as usize] as char);
        if chunk.len() > 1 {
            encoded.push(TABLE[((value >> 6) & 0x3f) as usize] as char);
        } else {
            encoded.push('=');
        }
        if chunk.len() > 2 {
            encoded.push(TABLE[(value & 0x3f) as usize] as char);
        } else {
            encoded.push('=');
        }
    }

    encoded
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn base64_encode_handles_padding() {
        assert_eq!(base64_encode(b""), "");
        assert_eq!(base64_encode(b"j"), "ag==");
        assert_eq!(base64_encode(b"jj"), "amo=");
        assert_eq!(base64_encode(b"jj undo"), "amogdW5kbw==");
    }

    #[test]
    fn osc52_sequence_wraps_encoded_clipboard_payload() {
        assert_eq!(osc52_sequence("jj undo"), "\u{1b}]52;c;amogdW5kbw==\u{7}");
    }
}