1use std::ffi::{c_char, CStr};
2
3use theme_engine::{load_theme, Rgb, Style, Theme};
4
5pub const THEME_ENGINE_FFI_OK: i32 = 0;
7pub const THEME_ENGINE_FFI_ERR_NULL: i32 = 1;
9pub const THEME_ENGINE_FFI_ERR_UTF8: i32 = 2;
11pub const THEME_ENGINE_FFI_ERR_THEME: i32 = 3;
13pub const THEME_ENGINE_FFI_ERR_NOT_FOUND: i32 = 4;
15
16pub struct ThemeEngineTheme {
18 theme: Theme,
19}
20
21#[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#[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 let cstr = unsafe { CStr::from_ptr(value) };
80 cstr.to_str().map_err(|_| THEME_ENGINE_FFI_ERR_UTF8)
81}
82
83#[no_mangle]
85pub unsafe extern "C" fn theme_engine_theme_free(theme: *mut ThemeEngineTheme) {
86 if theme.is_null() {
87 return;
88 }
89 let _ = unsafe { Box::from_raw(theme) };
91}
92
93#[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 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 unsafe { *out_theme = Box::into_raw(boxed) };
118 THEME_ENGINE_FFI_OK
119}
120
121#[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 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 unsafe { *out_theme = Box::into_raw(boxed) };
144 THEME_ENGINE_FFI_OK
145}
146
147#[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 let capture_name = match unsafe { parse_cstr(capture_name) } {
159 Ok(name) => name,
160 Err(code) => return code,
161 };
162 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 unsafe { *out_style = style_to_ffi(style) };
170 THEME_ENGINE_FFI_OK
171}
172
173#[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 let role_name = match unsafe { parse_cstr(role_name) } {
185 Ok(name) => name,
186 Err(code) => return code,
187 };
188 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 unsafe { *out_style = style_to_ffi(style) };
196 THEME_ENGINE_FFI_OK
197}
198
199#[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 let theme = unsafe { &*theme };
216 let (fg, bg) = theme.theme.default_terminal_colors();
217
218 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 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 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 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 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 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 unsafe { theme_engine_theme_free(handle) };
289 }
290}