elio 1.3.0

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
use anyhow::{Context, Result};
use base64::Engine as _;
use ratatui::layout::Rect;
use std::{
    fs::File,
    io::{Read, Write as _},
    path::Path,
};

pub(super) fn place_terminal_image_with_konsole_protocol(
    path: &Path,
    area: Rect,
) -> Result<Vec<u8>> {
    let mut out = Vec::new();
    let _ = write!(
        out,
        "\x1b[{};{}H",
        area.y.saturating_add(1),
        area.x.saturating_add(1)
    );
    out.extend(build_konsole_upload_sequence(
        path,
        konsole_image_id(),
        area,
    )?);
    Ok(out)
}

pub(super) fn clear_terminal_images_with_konsole_protocol() -> Result<Vec<u8>> {
    Ok(build_konsole_clear_sequence(konsole_image_id())
        .as_bytes()
        .to_vec())
}

fn build_konsole_upload_sequence(path: &Path, id: u32, area: Rect) -> Result<Vec<u8>> {
    let mut file = File::open(path)
        .with_context(|| format!("failed to open Konsole preview image {}", path.display()))?;
    let total = file
        .metadata()
        .with_context(|| format!("failed to stat Konsole preview image {}", path.display()))?
        .len() as usize;
    if total == 0 {
        anyhow::bail!("Konsole preview image {} is empty", path.display());
    }

    let mut sent = 0usize;
    let mut chunk = vec![0u8; 3 * 4096 / 4];
    let mut out = Vec::new();
    while sent < total {
        let remaining = total.saturating_sub(sent);
        let chunk_len = remaining.min(chunk.len());
        file.read_exact(&mut chunk[..chunk_len])
            .with_context(|| format!("failed to read Konsole preview image {}", path.display()))?;
        sent += chunk_len;
        let more = sent < total;
        let payload = base64::engine::general_purpose::STANDARD.encode(&chunk[..chunk_len]);
        if sent == chunk_len {
            write!(
                out,
                "\u{1b}_Ga=T,q=2,f=100,i={id},p=1,c={},r={},C=1,m={};{payload}\u{1b}\\",
                area.width.max(1),
                area.height.max(1),
                if more { 1 } else { 0 },
            )?;
        } else {
            write!(
                out,
                "\u{1b}_Gm={};{payload}\u{1b}\\",
                if more { 1 } else { 0 },
            )?;
        }
    }
    Ok(out)
}

fn build_konsole_clear_sequence(id: u32) -> String {
    format!("\u{1b}_Ga=d,d=I,i={id},p=1,q=2\u{1b}\\")
}

fn konsole_image_id() -> u32 {
    std::process::id() % (0xff_ffff + 1)
}

#[cfg(test)]
mod tests {
    use super::*;
    use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
    use image::ImageFormat;
    use std::{
        fs,
        path::{Path, PathBuf},
        time::{SystemTime, UNIX_EPOCH},
    };

    fn temp_root(label: &str) -> PathBuf {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time should be after unix epoch")
            .as_nanos();
        std::env::temp_dir().join(format!("elio-konsole-inline-image-{label}-{unique}"))
    }

    fn write_test_raster_image(path: &Path, format: ImageFormat, width: u32, height: u32) {
        let image =
            image::DynamicImage::ImageRgba8(image::RgbaImage::from_fn(width, height, |x, y| {
                image::Rgba([(x % 255) as u8, (y % 255) as u8, 0x80, 0xff])
            }));
        image
            .save_with_format(path, format)
            .expect("test raster image should save");
    }

    #[test]
    fn build_konsole_upload_sequence_uses_direct_placement_mode() {
        let root = temp_root("konsole-upload-sequence");
        fs::create_dir_all(&root).expect("failed to create temp root");
        let path = root.join("demo.png");
        write_test_raster_image(&path, ImageFormat::Png, 24, 16);
        let payload = fs::read(&path).expect("png payload should exist");
        let id = 42_u32;
        let area = Rect {
            x: 10,
            y: 4,
            width: 30,
            height: 20,
        };

        let sequence = String::from_utf8(
            build_konsole_upload_sequence(&path, id, area)
                .expect("Konsole upload sequence should build"),
        )
        .expect("Konsole upload sequence should be utf8");

        assert!(sequence.starts_with("\u{1b}_G"));
        assert!(sequence.contains("a=T"));
        assert!(sequence.contains("q=2"));
        assert!(sequence.contains("f=100"));
        assert!(sequence.contains(&format!("i={id}")));
        assert!(sequence.contains("p=1"));
        assert!(sequence.contains("c=30"));
        assert!(sequence.contains("r=20"));
        assert!(sequence.contains("C=1"));
        assert!(sequence.contains("m=0"));
        assert!(!sequence.contains("U=1"));
        assert!(sequence.contains(&BASE64_STANDARD.encode(payload)));

        fs::remove_dir_all(root).expect("failed to remove temp root");
    }

    #[test]
    fn place_konsole_terminal_image_prefixes_cursor_move() {
        let root = temp_root("konsole-cursor-prefix");
        fs::create_dir_all(&root).expect("failed to create temp root");
        let path = root.join("demo.png");
        write_test_raster_image(&path, ImageFormat::Png, 16, 16);

        let output = String::from_utf8(
            place_terminal_image_with_konsole_protocol(
                &path,
                Rect {
                    x: 10,
                    y: 4,
                    width: 8,
                    height: 6,
                },
            )
            .expect("Konsole placement should build"),
        )
        .expect("Konsole placement should be utf8");

        assert!(output.starts_with("\x1b[5;11H\x1b_G"));

        fs::remove_dir_all(root).expect("failed to remove temp root");
    }

    #[test]
    fn clear_konsole_uses_targeted_delete_sequence() {
        let sequence = String::from_utf8(
            clear_terminal_images_with_konsole_protocol()
                .expect("Konsole clear sequence should build"),
        )
        .expect("Konsole clear sequence should be utf8");

        assert_eq!(
            sequence,
            format!("\u{1b}_Ga=d,d=I,i={},p=1,q=2\u{1b}\\", konsole_image_id())
        );
    }
}