ratatui_ffi 0.2.6

C ABI bindings for Ratatui (Rust TUI) to consume from C/C#/etc.
Documentation
use crate::*;
use std::ffi::CString;

#[no_mangle]
pub extern "C" fn ratatui_headless_render_frame(
    width: u16,
    height: u16,
    cmds: *const FfiDrawCmd,
    len: usize,
    out_text_utf8: *mut *mut c_char,
) -> bool {
    if cmds.is_null() || out_text_utf8.is_null() {
        return false;
    }
    let area = Rect {
        x: 0,
        y: 0,
        width,
        height,
    };
    let mut buf = Buffer::empty(area);
    let Some(slice) = slice_checked(cmds, len, "headless_render_frame(slice)") else {
        return false;
    };
    for cmd in slice.iter() {
        crate::ffi::render::render_cmd_to_buffer(cmd, &mut buf);
    }
    let mut s = String::new();
    for y in 0..height {
        for x in 0..width {
            s.push_str(buf[(x, y)].symbol());
        }
        if y + 1 < height {
            s.push('\n');
        }
    }
    match CString::new(s) {
        Ok(cstr) => {
            unsafe {
                *out_text_utf8 = cstr.into_raw();
            }
            true
        }
        Err(_) => false,
    }
}

#[no_mangle]
pub extern "C" fn ratatui_headless_render_frame_styles(
    width: u16,
    height: u16,
    cmds: *const FfiDrawCmd,
    len: usize,
    out_text_utf8: *mut *mut c_char,
) -> bool {
    if cmds.is_null() || out_text_utf8.is_null() {
        return false;
    }
    let area = Rect {
        x: 0,
        y: 0,
        width,
        height,
    };
    let mut buf = Buffer::empty(area);
    let Some(slice) = slice_checked(cmds, len, "headless_render_frame_styles(slice)") else {
        return false;
    };
    for cmd in slice.iter() {
        crate::ffi::render::render_cmd_to_buffer(cmd, &mut buf);
    }
    let mut s = String::new();
    for y in 0..height {
        for x in 0..width {
            let cell = &buf[(x, y)];
            let st = cell.style();
            let fg = st.fg.unwrap_or(Color::Reset);
            let bg = st.bg.unwrap_or(Color::Reset);
            let mods = st.add_modifier | st.sub_modifier;
            let to_hex = |c: Color| -> u8 {
                match c {
                    Color::Black => 0x01,
                    Color::Red => 0x02,
                    Color::Green => 0x03,
                    Color::Yellow => 0x04,
                    Color::Blue => 0x05,
                    Color::Magenta => 0x06,
                    Color::Cyan => 0x07,
                    Color::Gray => 0x08,
                    Color::DarkGray => 0x09,
                    Color::LightRed => 0x0A,
                    Color::LightGreen => 0x0B,
                    Color::LightYellow => 0x0C,
                    Color::LightBlue => 0x0D,
                    Color::LightMagenta => 0x0E,
                    Color::LightCyan => 0x0F,
                    Color::White => 0x10,
                    _ => 0x00,
                }
            };
            s.push_str(&format!(
                "{:02X}{:02X}{:04X}",
                to_hex(fg),
                to_hex(bg),
                mods.bits()
            ));
            if x + 1 < width {
                s.push(' ');
            }
        }
        if y + 1 < height {
            s.push('\n');
        }
    }
    match CString::new(s) {
        Ok(cstr) => {
            unsafe {
                *out_text_utf8 = cstr.into_raw();
            }
            true
        }
        Err(_) => false,
    }
}

#[no_mangle]
pub extern "C" fn ratatui_headless_render_frame_styles_ex(
    width: u16,
    height: u16,
    cmds: *const FfiDrawCmd,
    len: usize,
    out_text_utf8: *mut *mut c_char,
) -> bool {
    if cmds.is_null() || out_text_utf8.is_null() {
        return false;
    }
    let area = Rect {
        x: 0,
        y: 0,
        width,
        height,
    };
    let mut buf = Buffer::empty(area);
    let Some(slice) = slice_checked(cmds, len, "headless_render_frame_styles_ex(slice)") else {
        return false;
    };
    for cmd in slice.iter() {
        crate::ffi::render::render_cmd_to_buffer(cmd, &mut buf);
    }
    let mut s = String::new();
    for y in 0..height {
        for x in 0..width {
            let cell = &buf[(x, y)];
            let st = cell.style();
            let fg = st.fg.unwrap_or(Color::Reset);
            let bg = st.bg.unwrap_or(Color::Reset);
            let mods = st.add_modifier | st.sub_modifier;
            s.push_str(&format!(
                "{:08X}{:08X}{:04X}",
                color_to_u32(fg),
                color_to_u32(bg),
                mods.bits()
            ));
            if x + 1 < width {
                s.push(' ');
            }
        }
        if y + 1 < height {
            s.push('\n');
        }
    }
    match CString::new(s) {
        Ok(cstr) => {
            unsafe {
                *out_text_utf8 = cstr.into_raw();
            }
            true
        }
        Err(_) => false,
    }
}

#[no_mangle]
pub extern "C" fn ratatui_headless_render_frame_cells(
    width: u16,
    height: u16,
    cmds: *const FfiDrawCmd,
    len: usize,
    out_cells: *mut FfiCellInfo,
    cap: usize,
) -> usize {
    if cmds.is_null() || out_cells.is_null() || cap == 0 {
        return 0;
    }
    let area = Rect {
        x: 0,
        y: 0,
        width,
        height,
    };
    let mut buf = Buffer::empty(area);
    let Some(slice) = slice_checked(cmds, len, "headless_render_frame_cells(slice)") else {
        return 0;
    };
    for cmd in slice.iter() {
        crate::ffi::render::render_cmd_to_buffer(cmd, &mut buf);
    }
    let total = (width as usize) * (height as usize);
    let n = total.min(cap);
    let mut idx = 0usize;
    for y in 0..height as usize {
        for x in 0..width as usize {
            if idx >= n {
                break;
            }
            let cell = &buf[(x as u16, y as u16)];
            let ch = cell.symbol().chars().next().map(|c| c as u32).unwrap_or(0);
            let st = cell.style();
            let fg = color_to_u32(st.fg.unwrap_or(Color::Reset));
            let bg = color_to_u32(st.bg.unwrap_or(Color::Reset));
            let mods = (st.add_modifier | st.sub_modifier).bits();
            unsafe { *out_cells.add(idx) = FfiCellInfo { ch, fg, bg, mods } };
            idx += 1;
        }
    }
    n
}