qreader 0.3.0

A clipboard manager and file reader with emoji support and customizable fonts
use std::fs;

fn main() {
    println!("cargo:rerun-if-changed=Cargo.toml");
    println!("cargo:rerun-if-changed=README.md");
    println!("cargo:rerun-if-changed=CHANGELOG.md");

    sync_version_in_docs();

    #[cfg(windows)]
    embed_windows_icon();
}

/// Returns the display version string.
/// Strips the "+000" suffix when the build number is zero,
/// so "0.2.0+000" is displayed as "0.2.0".
fn get_display_version(version: &str) -> String {
    if let Some(stripped) = version.strip_suffix("+000") {
        stripped.to_string()
    } else {
        version.to_string()
    }
}

/// Synchronizes the version string from Cargo.toml into README.md and CHANGELOG.md.
/// Only writes files when the version actually changes to avoid unnecessary rebuilds.
fn sync_version_in_docs() {
    let version = match std::env::var("CARGO_PKG_VERSION") {
        Ok(v) => v,
        Err(_) => return,
    };
    let display_version = get_display_version(&version);

    update_readme(&display_version);
    update_changelog(&display_version);
}

/// Updates the version reference in README.md.
/// Replaces the title line `# QReader v<OLD>` with the current version.
fn update_readme(display_version: &str) {
    let path = "README.md";
    let content = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(_) => return,
    };

    let new_content = replace_version_in_line(&content, "# QReader v", display_version);

    if new_content != content {
        let _ = fs::write(path, &new_content);
    }
}

/// Updates the current version header in CHANGELOG.md.
/// Replaces the first `## [<OLD>]` entry (skipping `## [Unreleased]`) with the current version.
fn update_changelog(display_version: &str) {
    let path = "CHANGELOG.md";
    let content = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(_) => return,
    };

    // Replace the first versioned header (not [Unreleased]) with the current version
    let mut replaced = false;
    let new_content: String = content
        .lines()
        .map(|line| {
            if !replaced && line.starts_with("## [") && !line.starts_with("## [Unreleased]") {
                replaced = true;
                // Preserve the date part after `## [VERSION]`
                let after_bracket = line.find(']').map(|i| &line[i + 1..]).unwrap_or("");
                format!("## [{}]{}", display_version, after_bracket)
            } else {
                line.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join("\n");

    // Preserve trailing newline if original had one
    let new_content = if content.ends_with('\n') {
        format!("{}\n", new_content)
    } else {
        new_content
    };

    if new_content != content {
        let _ = fs::write(path, &new_content);
    }
}

/// Replaces `<prefix><OLD_VERSION>` with `<prefix><new_version>` in the first matching line.
fn replace_version_in_line(content: &str, prefix: &str, new_version: &str) -> String {
    let mut replaced = false;
    content
        .lines()
        .map(|line| {
            if !replaced && line.starts_with(prefix) {
                replaced = true;
                format!("{}{}", prefix, new_version)
            } else {
                line.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join("\n")
        + if content.ends_with('\n') { "\n" } else { "" }
}

// ── Windows icon embedding ────────────────────────────────────────────────────
// Generates a multi-size .ico file from the same pixel-drawing logic used in
// main.rs and embeds it into the .exe via winres so the taskbar, Explorer, and
// Alt+Tab all show the correct icon before the window is rendered.

/// Fills a rectangle in an RGBA pixel buffer.
#[cfg(windows)]
fn ico_fill_rect(buf: &mut [u8], size: u32, x1: u32, y1: u32, x2: u32, y2: u32, color: [u8; 4]) {
    for y in y1..y2.min(size) {
        for x in x1..x2.min(size) {
            let i = ((y * size + x) * 4) as usize;
            buf[i..i + 4].copy_from_slice(&color);
        }
    }
}

/// Applies anti-aliased rounded corners to a rectangle in an RGBA pixel buffer.
#[cfg(windows)]
fn ico_round_corners(buf: &mut [u8], size: u32, x1: u32, y1: u32, x2: u32, y2: u32, r: u32) {
    if r == 0 || x2 == 0 || y2 == 0 {
        return;
    }
    let rf = r as f32;
    for dy in 0..r + 3 {
        for dx in 0..r + 3 {
            let dist = ((dx as f32 - rf).powi(2) + (dy as f32 - rf).powi(2)).sqrt();
            // 3-pixel smooth transition: coverage goes from 1.0 at (rf-1.5) to 0.0 at (rf+1.5)
            let coverage = ((rf + 1.5 - dist) / 3.0).clamp(0.0, 1.0);
            if coverage >= 1.0 {
                continue;
            }
            let px_r = x2.saturating_sub(1).saturating_sub(dx);
            let py_b = y2.saturating_sub(1).saturating_sub(dy);
            for (px, py) in [
                (x1 + dx, y1 + dy),
                (px_r, y1 + dy),
                (x1 + dx, py_b),
                (px_r, py_b),
            ] {
                if px < size && py < size {
                    let i = ((py * size + px) * 4) as usize;
                    let new_a = (buf[i + 3] as f32 * coverage) as u8;
                    buf[i + 3] = new_a;
                    if new_a == 0 {
                        buf[i] = 0;
                        buf[i + 1] = 0;
                        buf[i + 2] = 0;
                    }
                }
            }
        }
    }
}

/// Generates the clipboard icon as RGBA pixels at the given square size.
/// Mirrors the drawing logic in `create_app_icon()` in main.rs.
#[cfg(windows)]
fn ico_generate_rgba(size: u32) -> Vec<u8> {
    let mut rgba = vec![0u8; (size * size * 4) as usize];
    let sc = |v: f32| (v * size as f32 / 24.0) as u32;

    let body_color = [74u8, 144, 226, 255];
    let clip_color = [38u8, 90, 168, 255];
    let line_color = [220u8, 235, 255, 210];

    let (bx1, by1, bx2, by2) = (sc(2.0), sc(3.5), sc(22.0), sc(23.0));
    ico_fill_rect(&mut rgba, size, bx1, by1, bx2, by2, body_color);
    ico_round_corners(&mut rgba, size, bx1, by1, bx2, by2, sc(2.5));

    let (cx1, cy1, cx2, cy2) = (sc(7.0), sc(0.5), sc(17.0), sc(6.5));
    ico_fill_rect(&mut rgba, size, cx1, cy1, cx2, cy2, clip_color);
    ico_round_corners(&mut rgba, size, cx1, cy1, cx2, cy2, sc(2.0));

    let hcx = sc(12.0) as i32;
    let hcy = sc(3.5) as i32;
    let hr = sc(1.5) as i32;
    for dy in -hr..=hr {
        for dx in -hr..=hr {
            if dx * dx + dy * dy <= hr * hr {
                let px = hcx + dx;
                let py = hcy + dy;
                if px >= 0 && py >= 0 && (px as u32) < size && (py as u32) < size {
                    let i = ((py as u32 * size + px as u32) * 4) as usize;
                    rgba[i..i + 4].copy_from_slice(&body_color);
                }
            }
        }
    }

    let lh = sc(1.5).max(3);
    ico_fill_rect(
        &mut rgba,
        size,
        sc(5.5),
        sc(10.5),
        sc(18.5),
        sc(10.5) + lh,
        line_color,
    );
    ico_fill_rect(
        &mut rgba,
        size,
        sc(5.5),
        sc(14.0),
        sc(18.5),
        sc(14.0) + lh,
        line_color,
    );
    ico_fill_rect(
        &mut rgba,
        size,
        sc(5.5),
        sc(17.5),
        sc(13.5),
        sc(17.5) + lh,
        line_color,
    );

    rgba
}

/// Encodes RGBA pixels as a BMP-in-ICO image (32 bpp, bottom-up, with AND mask).
#[cfg(windows)]
fn ico_rgba_to_bmp(rgba: &[u8], size: u32) -> Vec<u8> {
    let mut bmp = Vec::new();

    // BITMAPINFOHEADER (40 bytes)
    bmp.extend_from_slice(&40u32.to_le_bytes()); // biSize
    bmp.extend_from_slice(&(size as i32).to_le_bytes()); // biWidth
    bmp.extend_from_slice(&((size * 2) as i32).to_le_bytes()); // biHeight (×2 for ICO)
    bmp.extend_from_slice(&1u16.to_le_bytes()); // biPlanes
    bmp.extend_from_slice(&32u16.to_le_bytes()); // biBitCount
    bmp.extend_from_slice(&0u32.to_le_bytes()); // biCompression (BI_RGB)
    bmp.extend_from_slice(&0u32.to_le_bytes()); // biSizeImage
    bmp.extend_from_slice(&0i32.to_le_bytes()); // biXPelsPerMeter
    bmp.extend_from_slice(&0i32.to_le_bytes()); // biYPelsPerMeter
    bmp.extend_from_slice(&0u32.to_le_bytes()); // biClrUsed
    bmp.extend_from_slice(&0u32.to_le_bytes()); // biClrImportant

    // XOR mask: BGRA, bottom-up row order
    for y in (0..size).rev() {
        for x in 0..size {
            let i = ((y * size + x) * 4) as usize;
            bmp.push(rgba[i + 2]); // B
            bmp.push(rgba[i + 1]); // G
            bmp.push(rgba[i]); // R
            bmp.push(rgba[i + 3]); // A
        }
    }

    // AND mask: all zeros (alpha channel is used instead)
    let row_bytes = size.div_ceil(32) * 4;
    bmp.extend(vec![0u8; (row_bytes * size) as usize]);

    bmp
}

/// Builds a multi-size ICO file from the given pixel sizes.
#[cfg(windows)]
fn ico_build(sizes: &[u32]) -> Vec<u8> {
    let images: Vec<Vec<u8>> = sizes
        .iter()
        .map(|&s| ico_rgba_to_bmp(&ico_generate_rgba(s), s))
        .collect();

    let dir_offset = 6 + 16 * sizes.len();
    let mut ico = Vec::new();

    // ICONDIR header
    ico.extend_from_slice(&0u16.to_le_bytes()); // reserved
    ico.extend_from_slice(&1u16.to_le_bytes()); // type = icon
    ico.extend_from_slice(&(sizes.len() as u16).to_le_bytes());

    // ICONDIRENTRY for each size
    let mut data_offset = dir_offset as u32;
    for (i, &size) in sizes.iter().enumerate() {
        ico.push(if size >= 256 { 0 } else { size as u8 }); // width (0 = 256)
        ico.push(if size >= 256 { 0 } else { size as u8 }); // height
        ico.push(0); // color count (0 = true-color)
        ico.push(0); // reserved
        ico.extend_from_slice(&1u16.to_le_bytes()); // planes
        ico.extend_from_slice(&32u16.to_le_bytes()); // bit count
        ico.extend_from_slice(&(images[i].len() as u32).to_le_bytes()); // data size
        ico.extend_from_slice(&data_offset.to_le_bytes()); // data offset
        data_offset += images[i].len() as u32;
    }

    for img in &images {
        ico.extend_from_slice(img);
    }

    ico
}

/// Generates a .ico file and embeds it into the Windows executable via winres.
/// Called only on Windows builds; has no effect on other platforms.
#[cfg(windows)]
fn embed_windows_icon() {
    let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
    let ico_path = std::path::Path::new(&out_dir).join("app_icon.ico");

    let ico_data = ico_build(&[16, 32, 48, 256]);
    fs::write(&ico_path, &ico_data).expect("Failed to write app_icon.ico");

    let mut res = winres::WindowsResource::new();
    res.set_icon(ico_path.to_str().unwrap());
    res.compile().expect("Failed to compile Windows resources");
}