use std::io::{Read, Write};
use std::sync::{Arc, Mutex};
use anyhow::Result;
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
#[derive(Debug)]
pub struct Capture {
pub output: String,
pub exit_code: u32,
}
impl Capture {
pub fn visible_text(&self) -> String {
strip_ansi(&self.output)
}
}
pub fn run_capture(cmd: CommandBuilder, size: PtySize, input: Option<&[u8]>) -> Result<Capture> {
let pty = native_pty_system();
let pair = pty.openpty(size)?;
let mut child = pair.slave.spawn_command(cmd)?;
let mut reader = pair.master.try_clone_reader()?;
let writer = Arc::new(Mutex::new(pair.master.take_writer()?));
if let Some(bytes) = input {
let mut w = writer.lock().expect("writer mutex");
w.write_all(bytes)?;
w.flush()?;
}
drop(pair.slave);
let writer_for_reader = Arc::clone(&writer);
let reader_thread = std::thread::spawn(move || {
let mut buf = Vec::new();
let mut chunk = [0u8; 4096];
loop {
match reader.read(&mut chunk) {
Ok(0) => break, Ok(n) => {
let slice = &chunk[..n];
buf.extend_from_slice(slice);
answer_terminal_queries(slice, &writer_for_reader);
}
Err(_) => break,
}
}
buf
});
let status = child.wait()?;
drop(pair.master);
let buf = reader_thread
.join()
.map_err(|_| anyhow::anyhow!("le thread de lecture ConPTY a paniqué"))?;
Ok(Capture {
output: String::from_utf8_lossy(&buf).into_owned(),
exit_code: status.exit_code(),
})
}
fn answer_terminal_queries(data: &[u8], writer: &Arc<Mutex<Box<dyn Write + Send>>>) {
let reply = query_reply(data);
if !reply.is_empty()
&& let Ok(mut w) = writer.lock()
{
let _ = w.write_all(&reply);
let _ = w.flush();
}
}
fn query_reply(data: &[u8]) -> Vec<u8> {
let mut reply = Vec::new();
if contains(data, b"\x1b[6n") {
reply.extend_from_slice(b"\x1b[1;1R");
}
if contains(data, b"\x1b[5n") {
reply.extend_from_slice(b"\x1b[0n");
}
reply
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || haystack.len() < needle.len() {
return false;
}
haystack.windows(needle.len()).any(|w| w == needle)
}
fn strip_ansi(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == 0x1b && i + 1 < bytes.len() {
match bytes[i + 1] {
b'[' => {
i += 2;
while i < bytes.len() && !(0x40..=0x7e).contains(&bytes[i]) {
i += 1;
}
i += 1; }
b']' => {
i += 2;
while i < bytes.len() && bytes[i] != 0x07 {
if bytes[i] == 0x1b && i + 1 < bytes.len() && bytes[i + 1] == b'\\' {
i += 1;
break;
}
i += 1;
}
i += 1;
}
_ => {
i += 2;
}
}
} else {
let ch_len = utf8_len(bytes[i]);
let end = (i + ch_len).min(bytes.len());
if let Ok(chunk) = std::str::from_utf8(&bytes[i..end]) {
out.push_str(chunk);
}
i = end;
}
}
out
}
fn utf8_len(first: u8) -> usize {
match first {
0x00..=0x7f => 1,
0xc0..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf7 => 4,
_ => 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_ansi_retire_les_sequences_csi() {
let s = "\x1b[31mrouge\x1b[0m et normal";
assert_eq!(strip_ansi(s), "rouge et normal");
}
#[test]
fn strip_ansi_retire_les_sequences_osc() {
let s = "\x1b]0;titre\x07contenu";
assert_eq!(strip_ansi(s), "contenu");
}
#[test]
fn strip_ansi_preserve_utf8() {
let s = "caf\u{e9} \u{1f680}"; assert_eq!(strip_ansi(s), "café 🚀");
}
#[test]
fn dsr_curseur_donne_une_reponse_cpr() {
assert_eq!(query_reply(b"\x1b[6n"), b"\x1b[1;1R");
}
#[test]
fn dsr_curseur_detecte_dans_un_flux() {
let flux = b"avant\x1b[6napres";
assert_eq!(query_reply(flux), b"\x1b[1;1R");
}
#[test]
fn dsr_etat_appareil_donne_ok() {
assert_eq!(query_reply(b"\x1b[5n"), b"\x1b[0n");
}
#[test]
fn flux_sans_requete_ne_repond_rien() {
assert!(query_reply(b"du texte normal\r\n").is_empty());
}
#[test]
fn contains_fonctionne() {
assert!(contains(b"abcdef", b"cde"));
assert!(!contains(b"abc", b"xyz"));
assert!(!contains(b"ab", b"abcdef"));
}
}