Skip to main content

theme_engine_ffi/
lib.rs

1use std::ffi::{c_char, CStr};
2
3use theme_engine::{load_theme, Rgb, Style, Theme};
4
5/// Operation succeeded.
6pub const THEME_ENGINE_FFI_OK: i32 = 0;
7/// A required pointer argument was null.
8pub const THEME_ENGINE_FFI_ERR_NULL: i32 = 1;
9/// A C string argument was not valid UTF-8.
10pub const THEME_ENGINE_FFI_ERR_UTF8: i32 = 2;
11/// Theme loading or parsing failed.
12pub const THEME_ENGINE_FFI_ERR_THEME: i32 = 3;
13/// Requested style was not found.
14pub const THEME_ENGINE_FFI_ERR_NOT_FOUND: i32 = 4;
15
16/// Opaque C handle for a loaded theme.
17pub struct ThemeEngineTheme {
18    theme: Theme,
19}
20
21/// C ABI RGB triplet.
22#[repr(C)]
23#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
24pub struct ThemeEngineRgb {
25    pub r: u8,
26    pub g: u8,
27    pub b: u8,
28}
29
30/// C ABI style payload.
31#[repr(C)]
32#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
33pub struct ThemeEngineStyle {
34    pub has_fg: u8,
35    pub fg: ThemeEngineRgb,
36    pub has_bg: u8,
37    pub bg: ThemeEngineRgb,
38    pub bold: u8,
39    pub italic: u8,
40    pub underline: u8,
41}
42
43fn rgb_to_ffi(rgb: Rgb) -> ThemeEngineRgb {
44    ThemeEngineRgb {
45        r: rgb.r,
46        g: rgb.g,
47        b: rgb.b,
48    }
49}
50
51fn style_to_ffi(style: Style) -> ThemeEngineStyle {
52    let (has_fg, fg) = if let Some(color) = style.fg {
53        (1, rgb_to_ffi(color))
54    } else {
55        (0, ThemeEngineRgb::default())
56    };
57    let (has_bg, bg) = if let Some(color) = style.bg {
58        (1, rgb_to_ffi(color))
59    } else {
60        (0, ThemeEngineRgb::default())
61    };
62
63    ThemeEngineStyle {
64        has_fg,
65        fg,
66        has_bg,
67        bg,
68        bold: u8::from(style.bold),
69        italic: u8::from(style.italic),
70        underline: u8::from(style.underline),
71    }
72}
73
74unsafe fn parse_cstr<'a>(value: *const c_char) -> Result<&'a str, i32> {
75    if value.is_null() {
76        return Err(THEME_ENGINE_FFI_ERR_NULL);
77    }
78    // SAFETY: validated non-null above; caller promises valid C string lifetime.
79    let cstr = unsafe { CStr::from_ptr(value) };
80    cstr.to_str().map_err(|_| THEME_ENGINE_FFI_ERR_UTF8)
81}
82
83/// Frees a theme handle previously returned by this library.
84#[no_mangle]
85pub unsafe extern "C" fn theme_engine_theme_free(theme: *mut ThemeEngineTheme) {
86    if theme.is_null() {
87        return;
88    }
89    // SAFETY: pointer originated from Box::into_raw in this library.
90    let _ = unsafe { Box::from_raw(theme) };
91}
92
93/// Loads a built-in theme by name (for example `"tokyonight-dark"`).
94///
95/// Returns status code and writes a new handle to `out_theme` on success.
96#[no_mangle]
97pub unsafe extern "C" fn theme_engine_theme_load_builtin(
98    name: *const c_char,
99    out_theme: *mut *mut ThemeEngineTheme,
100) -> i32 {
101    if out_theme.is_null() {
102        return THEME_ENGINE_FFI_ERR_NULL;
103    }
104    // SAFETY: parse_cstr validates null and UTF-8.
105    let name = match unsafe { parse_cstr(name) } {
106        Ok(name) => name,
107        Err(code) => return code,
108    };
109
110    let theme = match load_theme(name) {
111        Ok(theme) => theme,
112        Err(_) => return THEME_ENGINE_FFI_ERR_THEME,
113    };
114
115    let boxed = Box::new(ThemeEngineTheme { theme });
116    // SAFETY: out_theme non-null checked above.
117    unsafe { *out_theme = Box::into_raw(boxed) };
118    THEME_ENGINE_FFI_OK
119}
120
121/// Loads a theme from a JSON string and returns a new handle.
122#[no_mangle]
123pub unsafe extern "C" fn theme_engine_theme_load_json(
124    json: *const c_char,
125    out_theme: *mut *mut ThemeEngineTheme,
126) -> i32 {
127    if out_theme.is_null() {
128        return THEME_ENGINE_FFI_ERR_NULL;
129    }
130    // SAFETY: parse_cstr validates null and UTF-8.
131    let json = match unsafe { parse_cstr(json) } {
132        Ok(json) => json,
133        Err(code) => return code,
134    };
135
136    let theme = match Theme::from_json_str(json) {
137        Ok(theme) => theme,
138        Err(_) => return THEME_ENGINE_FFI_ERR_THEME,
139    };
140
141    let boxed = Box::new(ThemeEngineTheme { theme });
142    // SAFETY: out_theme non-null checked above.
143    unsafe { *out_theme = Box::into_raw(boxed) };
144    THEME_ENGINE_FFI_OK
145}
146
147/// Resolves a syntax capture style (for example `"@keyword"`).
148#[no_mangle]
149pub unsafe extern "C" fn theme_engine_theme_resolve_capture(
150    theme: *const ThemeEngineTheme,
151    capture_name: *const c_char,
152    out_style: *mut ThemeEngineStyle,
153) -> i32 {
154    if theme.is_null() || out_style.is_null() {
155        return THEME_ENGINE_FFI_ERR_NULL;
156    }
157    // SAFETY: parse_cstr validates null and UTF-8.
158    let capture_name = match unsafe { parse_cstr(capture_name) } {
159        Ok(name) => name,
160        Err(code) => return code,
161    };
162    // SAFETY: theme pointer checked non-null above.
163    let theme = unsafe { &*theme };
164
165    let Some(style) = theme.theme.resolve(capture_name).copied() else {
166        return THEME_ENGINE_FFI_ERR_NOT_FOUND;
167    };
168    // SAFETY: out_style checked non-null above.
169    unsafe { *out_style = style_to_ffi(style) };
170    THEME_ENGINE_FFI_OK
171}
172
173/// Resolves a UI role style (for example `"statusline"` or `"tab_active"`).
174#[no_mangle]
175pub unsafe extern "C" fn theme_engine_theme_resolve_ui(
176    theme: *const ThemeEngineTheme,
177    role_name: *const c_char,
178    out_style: *mut ThemeEngineStyle,
179) -> i32 {
180    if theme.is_null() || out_style.is_null() {
181        return THEME_ENGINE_FFI_ERR_NULL;
182    }
183    // SAFETY: parse_cstr validates null and UTF-8.
184    let role_name = match unsafe { parse_cstr(role_name) } {
185        Ok(name) => name,
186        Err(code) => return code,
187    };
188    // SAFETY: theme pointer checked non-null above.
189    let theme = unsafe { &*theme };
190
191    let Some(style) = theme.theme.resolve_ui(role_name) else {
192        return THEME_ENGINE_FFI_ERR_NOT_FOUND;
193    };
194    // SAFETY: out_style checked non-null above.
195    unsafe { *out_style = style_to_ffi(style) };
196    THEME_ENGINE_FFI_OK
197}
198
199/// Returns default terminal foreground/background colors from the theme.
200///
201/// Writes `0/1` presence flags to `out_has_fg` and `out_has_bg`.
202/// If a flag is `1`, the corresponding `out_fg`/`out_bg` value is written.
203#[no_mangle]
204pub unsafe extern "C" fn theme_engine_theme_default_terminal_colors(
205    theme: *const ThemeEngineTheme,
206    out_has_fg: *mut u8,
207    out_fg: *mut ThemeEngineRgb,
208    out_has_bg: *mut u8,
209    out_bg: *mut ThemeEngineRgb,
210) -> i32 {
211    if theme.is_null() || out_has_fg.is_null() || out_has_bg.is_null() {
212        return THEME_ENGINE_FFI_ERR_NULL;
213    }
214    // SAFETY: theme pointer checked non-null above.
215    let theme = unsafe { &*theme };
216    let (fg, bg) = theme.theme.default_terminal_colors();
217
218    // SAFETY: out_has_* pointers checked non-null above.
219    unsafe {
220        *out_has_fg = u8::from(fg.is_some());
221        *out_has_bg = u8::from(bg.is_some());
222    }
223
224    if let Some(color) = fg {
225        if out_fg.is_null() {
226            return THEME_ENGINE_FFI_ERR_NULL;
227        }
228        // SAFETY: validated non-null when fg exists.
229        unsafe { *out_fg = rgb_to_ffi(color) };
230    }
231    if let Some(color) = bg {
232        if out_bg.is_null() {
233            return THEME_ENGINE_FFI_ERR_NULL;
234        }
235        // SAFETY: validated non-null when bg exists.
236        unsafe { *out_bg = rgb_to_ffi(color) };
237    }
238
239    THEME_ENGINE_FFI_OK
240}
241
242#[cfg(test)]
243mod tests {
244    use std::ffi::CString;
245    use std::ptr;
246
247    use super::{
248        theme_engine_theme_default_terminal_colors, theme_engine_theme_free,
249        theme_engine_theme_load_builtin, theme_engine_theme_resolve_ui, ThemeEngineRgb,
250        ThemeEngineStyle, ThemeEngineTheme, THEME_ENGINE_FFI_OK,
251    };
252
253    #[test]
254    fn ffi_load_and_resolve_ui_smoke() {
255        let name = CString::new("tokyonight-dark").expect("cstring failed");
256        let mut handle: *mut ThemeEngineTheme = ptr::null_mut();
257        // SAFETY: valid pointers and nul-terminated strings.
258        let code = unsafe { theme_engine_theme_load_builtin(name.as_ptr(), &mut handle) };
259        assert_eq!(code, THEME_ENGINE_FFI_OK);
260        assert!(!handle.is_null());
261
262        let role = CString::new("statusline").expect("cstring failed");
263        let mut out_style = ThemeEngineStyle::default();
264        // SAFETY: handle and output pointers are valid.
265        let code = unsafe { theme_engine_theme_resolve_ui(handle, role.as_ptr(), &mut out_style) };
266        assert_eq!(code, THEME_ENGINE_FFI_OK);
267        assert_eq!(out_style.has_fg, 1);
268
269        let mut has_fg = 0u8;
270        let mut has_bg = 0u8;
271        let mut fg = ThemeEngineRgb::default();
272        let mut bg = ThemeEngineRgb::default();
273        // SAFETY: handle and output pointers are valid.
274        let code = unsafe {
275            theme_engine_theme_default_terminal_colors(
276                handle,
277                &mut has_fg,
278                &mut fg,
279                &mut has_bg,
280                &mut bg,
281            )
282        };
283        assert_eq!(code, THEME_ENGINE_FFI_OK);
284        assert_eq!(has_fg, 1);
285        assert_eq!(has_bg, 1);
286
287        // SAFETY: handle was allocated by load function above.
288        unsafe { theme_engine_theme_free(handle) };
289    }
290}