ratatui_ffi 0.2.6

C ABI bindings for Ratatui (Rust TUI) to consume from C/C#/etc.
Documentation
use crate::*;
use ratatui::prelude::{Alignment, Line, Span};
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::Padding as RtPadding;
use ratatui::widgets::{Block, BorderType as RtBorderType, Borders};

pub fn spans_from_ffi<'a>(spans: *const FfiSpan, len: usize) -> Option<Vec<Span<'static>>> {
    if spans.is_null() {
        return None;
    }
    // Sanity limit to avoid OOM if a bogus length arrives from FFI.
    // Typical TUI usage keeps this comfortably small.
    const MAX_SPANS: usize = 1_000_000;
    if len > MAX_SPANS {
        eprintln!(
            "ratatui_ffi spans_from_ffi: excessive len={} (>{}), dropping",
            len, MAX_SPANS
        );
        log_line(&format!(
            "spans_from_ffi: excessive len={}, dropping",
            len
        ));
        return None;
    }
    // Validate pointer alignment and computed slice size.
    let Some(slice) = crate::slice_checked(spans, len, "spans_from_ffi") else {
        return None;
    };
    let mut out: Vec<Span<'static>> = Vec::with_capacity(len);
    for s in slice.iter() {
        if s.text_utf8.is_null() {
            continue;
        }
        let c = unsafe { std::ffi::CStr::from_ptr(s.text_utf8) };
        if let Ok(txt) = c.to_str() {
            out.push(Span::styled(txt.to_string(), style_from_ffi(s.style)));
        }
    }
    Some(out)
}

pub fn build_block_from_adv(
    borders_bits: u8,
    border_type: u32,
    pad_l: u16,
    pad_t: u16,
    pad_r: u16,
    pad_b: u16,
    title_spans: *const FfiSpan,
    title_len: usize,
) -> Block<'static> {
    let mut block = Block::default().borders(borders_from_bits(borders_bits));
    block = block.padding(RtPadding {
        left: pad_l,
        right: pad_r,
        top: pad_t,
        bottom: pad_b,
    });
    block = match border_type {
        1 => block.border_type(RtBorderType::Rounded),
        2 => block.border_type(RtBorderType::Double),
        3 => block.border_type(RtBorderType::Thick),
        4 => block.border_type(RtBorderType::QuadrantInside),
        5 => block.border_type(RtBorderType::QuadrantOutside),
        _ => block.border_type(RtBorderType::Plain),
    };
    if let Some(sp) = spans_from_ffi(title_spans, title_len) {
        block = block.title(Line::from(sp));
    }
    block
}

pub fn apply_block_title_alignment(b: Block<'static>, align_code: u32) -> Block<'static> {
    let align = match align_code {
        1 => Alignment::Center,
        2 => Alignment::Right,
        _ => Alignment::Left,
    };
    b.title_alignment(align)
}

bitflags::bitflags! {
    #[repr(transparent)]
    pub struct FfiBorders: u8 {
        const NONE   = 0;
        const LEFT   = 1<<0;
        const RIGHT  = 1<<1;
        const TOP    = 1<<2;
        const BOTTOM = 1<<3;
    }
}

pub fn borders_from_bits(bits: u8) -> Borders {
    let fb = FfiBorders::from_bits_truncate(bits);
    let mut b = Borders::NONE;
    if fb.contains(FfiBorders::LEFT) {
        b |= Borders::LEFT;
    }
    if fb.contains(FfiBorders::RIGHT) {
        b |= Borders::RIGHT;
    }
    if fb.contains(FfiBorders::TOP) {
        b |= Borders::TOP;
    }
    if fb.contains(FfiBorders::BOTTOM) {
        b |= Borders::BOTTOM;
    }
    b
}

pub fn color_from_u32(c: u32) -> Option<Color> {
    if c == 0 {
        return None;
    }
    if (c & 0x8000_0000) != 0 {
        let r = ((c >> 16) & 0xFF) as u8;
        let g = ((c >> 8) & 0xFF) as u8;
        let b = (c & 0xFF) as u8;
        return Some(Color::Rgb(r, g, b));
    }
    if (c & 0x4000_0000) != 0 {
        let idx = (c & 0xFF) as u8;
        return Some(Color::Indexed(idx));
    }
    match c {
        1 => Some(Color::Black),
        2 => Some(Color::Red),
        3 => Some(Color::Green),
        4 => Some(Color::Yellow),
        5 => Some(Color::Blue),
        6 => Some(Color::Magenta),
        7 => Some(Color::Cyan),
        8 => Some(Color::Gray),
        9 => Some(Color::DarkGray),
        10 => Some(Color::LightRed),
        11 => Some(Color::LightGreen),
        12 => Some(Color::LightYellow),
        13 => Some(Color::LightBlue),
        14 => Some(Color::LightMagenta),
        15 => Some(Color::LightCyan),
        16 => Some(Color::White),
        _ => None,
    }
}

pub fn color_to_u32(c: Color) -> u32 {
    match c {
        Color::Reset => 0,
        Color::Black => 1,
        Color::Red => 2,
        Color::Green => 3,
        Color::Yellow => 4,
        Color::Blue => 5,
        Color::Magenta => 6,
        Color::Cyan => 7,
        Color::Gray => 8,
        Color::DarkGray => 9,
        Color::LightRed => 10,
        Color::LightGreen => 11,
        Color::LightYellow => 12,
        Color::LightBlue => 13,
        Color::LightMagenta => 14,
        Color::LightCyan => 15,
        Color::White => 16,
        Color::Indexed(i) => 0x4000_0000 | (i as u32),
        Color::Rgb(r, g, b) => 0x8000_0000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32),
    }
}

pub fn style_from_ffi(s: FfiStyle) -> Style {
    let mut st = Style::default();
    if let Some(fg) = color_from_u32(s.fg) {
        st = st.fg(fg);
    }
    if let Some(bg) = color_from_u32(s.bg) {
        st = st.bg(bg);
    }
    let mods = FfiStyleMods::from_bits_truncate(s.mods);
    if mods.contains(FfiStyleMods::BOLD) {
        st = st.add_modifier(Modifier::BOLD);
    }
    if mods.contains(FfiStyleMods::ITALIC) {
        st = st.add_modifier(Modifier::ITALIC);
    }
    if mods.contains(FfiStyleMods::UNDERLINE) {
        st = st.add_modifier(Modifier::UNDERLINED);
    }
    if mods.contains(FfiStyleMods::DIM) {
        st = st.add_modifier(Modifier::DIM);
    }
    if mods.contains(FfiStyleMods::CROSSED) {
        st = st.add_modifier(Modifier::CROSSED_OUT);
    }
    if mods.contains(FfiStyleMods::REVERSED) {
        st = st.add_modifier(Modifier::REVERSED);
    }
    if mods.contains(FfiStyleMods::RAPIDBLINK) {
        st = st.add_modifier(Modifier::RAPID_BLINK);
    }
    if mods.contains(FfiStyleMods::SLOWBLINK) {
        st = st.add_modifier(Modifier::SLOW_BLINK);
    }
    if mods.contains(FfiStyleMods::HIDDEN) {
        st = st.add_modifier(Modifier::HIDDEN);
    }
    st
}