Skip to main content

flywheel/
ffi.rs

1//! C Foreign Function Interface (FFI) for Flywheel.
2//!
3//! This module provides a C-compatible API for using Flywheel from
4//! other programming languages. All functions are `extern "C"` with
5//! stable ABI.
6//!
7//! # Safety
8//!
9//! All functions that accept pointers require valid, non-null pointers.
10//! The caller is responsible for proper memory management of handles.
11//!
12//! # Example (C)
13//!
14//! ```c
15//! #include "flywheel.h"
16//!
17//! int main() {
18//!     FlywheelEngine* engine = flywheel_engine_new();
19//!     if (!engine) return 1;
20//!
21//!     flywheel_engine_draw_text(engine, 0, 0, "Hello from C!", 0xFFFFFF, 0x000000);
22//!     flywheel_engine_request_redraw(engine);
23//!
24//!     // Main loop...
25//!
26//!     flywheel_engine_destroy(engine);
27//!     return 0;
28//! }
29//! ```
30
31// FFI modules intentionally use unsafe and no_mangle
32#![allow(unsafe_op_in_unsafe_fn)]
33#![allow(clippy::not_unsafe_ptr_arg_deref)]
34
35use crate::actor::{Engine, InputEvent, KeyCode};
36use crate::buffer::{Cell, Rgb};
37use crate::layout::Rect;
38use crate::widget::{AppendResult, StreamWidget};
39use std::ffi::CStr;
40use std::os::raw::{c_char, c_int, c_uint};
41use std::ptr;
42
43// =============================================================================
44// Opaque Handle Types
45// =============================================================================
46
47/// Opaque handle to a Flywheel engine.
48pub struct FlywheelEngine(Engine);
49
50/// Opaque handle to a stream widget.
51pub struct FlywheelStream(StreamWidget);
52
53// =============================================================================
54// Result and Error Codes
55// =============================================================================
56
57/// Result codes for FFI functions.
58#[repr(C)]
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum FlywheelResult {
61    /// Operation succeeded.
62    Ok = 0,
63    /// Null pointer passed.
64    NullPointer = 1,
65    /// Invalid UTF-8 string.
66    InvalidUtf8 = 2,
67    /// I/O error.
68    IoError = 3,
69    /// Out of bounds.
70    OutOfBounds = 4,
71    /// Engine not running.
72    NotRunning = 5,
73}
74
75/// Input event type from polling.
76#[repr(C)]
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum FlywheelEventType {
79    /// No event available.
80    None = 0,
81    /// Key press event.
82    Key = 1,
83    /// Terminal resize event.
84    Resize = 2,
85    /// Error event.
86    Error = 3,
87    /// Shutdown event.
88    Shutdown = 4,
89}
90
91/// Key event data.
92#[repr(C)]
93#[derive(Debug, Clone, Copy)]
94pub struct FlywheelKeyEvent {
95    /// The character (for printable keys), or 0.
96    pub char_code: u32,
97    /// Special key code (see `FLYWHEEL_KEY_*` constants).
98    pub key_code: c_int,
99    /// Modifier flags.
100    pub modifiers: c_uint,
101}
102
103/// Resize event data.
104#[repr(C)]
105#[derive(Debug, Clone, Copy)]
106pub struct FlywheelResizeEvent {
107    /// New width.
108    pub width: u16,
109    /// New height.
110    pub height: u16,
111}
112
113/// Polled event structure.
114#[repr(C)]
115pub struct FlywheelEvent {
116    /// Event type.
117    pub event_type: FlywheelEventType,
118    /// Key event data (valid if `event_type` == Key).
119    pub key: FlywheelKeyEvent,
120    /// Resize event data (valid if `event_type` == Resize).
121    pub resize: FlywheelResizeEvent,
122}
123
124// Key code constants
125/// No special key.
126pub const FLYWHEEL_KEY_NONE: c_int = 0;
127/// Enter key.
128pub const FLYWHEEL_KEY_ENTER: c_int = 1;
129/// Escape key.
130pub const FLYWHEEL_KEY_ESCAPE: c_int = 2;
131/// Backspace key.
132pub const FLYWHEEL_KEY_BACKSPACE: c_int = 3;
133/// Tab key.
134pub const FLYWHEEL_KEY_TAB: c_int = 4;
135/// Left arrow.
136pub const FLYWHEEL_KEY_LEFT: c_int = 5;
137/// Right arrow.
138pub const FLYWHEEL_KEY_RIGHT: c_int = 6;
139/// Up arrow.
140pub const FLYWHEEL_KEY_UP: c_int = 7;
141/// Down arrow.
142pub const FLYWHEEL_KEY_DOWN: c_int = 8;
143/// Home key.
144pub const FLYWHEEL_KEY_HOME: c_int = 9;
145/// End key.
146pub const FLYWHEEL_KEY_END: c_int = 10;
147/// Page Up.
148pub const FLYWHEEL_KEY_PAGE_UP: c_int = 11;
149/// Page Down.
150pub const FLYWHEEL_KEY_PAGE_DOWN: c_int = 12;
151/// Delete key.
152pub const FLYWHEEL_KEY_DELETE: c_int = 13;
153
154// Modifier flags
155/// Shift modifier.
156pub const FLYWHEEL_MOD_SHIFT: c_uint = 1;
157/// Control modifier.
158pub const FLYWHEEL_MOD_CTRL: c_uint = 2;
159/// Alt modifier.
160pub const FLYWHEEL_MOD_ALT: c_uint = 4;
161/// Super/Command modifier.
162pub const FLYWHEEL_MOD_SUPER: c_uint = 8;
163
164// =============================================================================
165// Engine Functions
166// =============================================================================
167
168/// Create a new Flywheel engine with default configuration.
169///
170/// Returns NULL on failure.
171#[unsafe(no_mangle)]
172pub extern "C" fn flywheel_engine_new() -> *mut FlywheelEngine {
173    Engine::new().map_or(
174        ptr::null_mut(),
175        |engine| Box::into_raw(Box::new(FlywheelEngine(engine)))
176    )
177}
178
179/// Destroy a Flywheel engine.
180#[unsafe(no_mangle)]
181pub unsafe extern "C" fn flywheel_engine_destroy(engine: *mut FlywheelEngine) {
182    if !engine.is_null() {
183        drop(Box::from_raw(engine));
184    }
185}
186
187/// Get the terminal width.
188#[unsafe(no_mangle)]
189pub const unsafe extern "C" fn flywheel_engine_width(engine: *const FlywheelEngine) -> u16 {
190    if engine.is_null() {
191        return 0;
192    }
193    (*engine).0.width()
194}
195
196/// Get the terminal height.
197#[unsafe(no_mangle)]
198pub const unsafe extern "C" fn flywheel_engine_height(engine: *const FlywheelEngine) -> u16 {
199    if engine.is_null() {
200        return 0;
201    }
202    (*engine).0.height()
203}
204
205/// Check if the engine is still running.
206#[unsafe(no_mangle)]
207pub const unsafe extern "C" fn flywheel_engine_is_running(engine: *const FlywheelEngine) -> bool {
208    if engine.is_null() {
209        return false;
210    }
211    (*engine).0.is_running()
212}
213
214/// Stop the engine.
215#[unsafe(no_mangle)]
216pub unsafe extern "C" fn flywheel_engine_stop(engine: *mut FlywheelEngine) {
217    if !engine.is_null() {
218        (*engine).0.stop();
219    }
220}
221
222/// Poll for the next input event.
223#[unsafe(no_mangle)]
224pub unsafe extern "C" fn flywheel_engine_poll_event(
225    engine: *const FlywheelEngine,
226    event_out: *mut FlywheelEvent,
227) -> FlywheelEventType {
228    if engine.is_null() || event_out.is_null() {
229        return FlywheelEventType::None;
230    }
231
232    match (*engine).0.poll_input() {
233        Some(InputEvent::Key { code, modifiers }) => {
234            let (char_code, key_code) = convert_key_code(code);
235            let mods = convert_modifiers(modifiers);
236
237            (*event_out).event_type = FlywheelEventType::Key;
238            (*event_out).key = FlywheelKeyEvent {
239                char_code,
240                key_code,
241                modifiers: mods,
242            };
243            FlywheelEventType::Key
244        }
245        Some(InputEvent::Resize { width, height }) => {
246            (*event_out).event_type = FlywheelEventType::Resize;
247            (*event_out).resize = FlywheelResizeEvent { width, height };
248            FlywheelEventType::Resize
249        }
250        Some(InputEvent::Shutdown) => {
251            (*event_out).event_type = FlywheelEventType::Shutdown;
252            FlywheelEventType::Shutdown
253        }
254        Some(InputEvent::Error(_)) => {
255            (*event_out).event_type = FlywheelEventType::Error;
256            FlywheelEventType::Error
257        }
258        _ => {
259            (*event_out).event_type = FlywheelEventType::None;
260            FlywheelEventType::None
261        }
262    }
263}
264
265/// Handle a resize event.
266#[unsafe(no_mangle)]
267pub unsafe extern "C" fn flywheel_engine_handle_resize(
268    engine: *mut FlywheelEngine,
269    width: u16,
270    height: u16,
271) {
272    if !engine.is_null() {
273        (*engine).0.handle_resize(width, height);
274    }
275}
276
277/// Request a full redraw.
278#[unsafe(no_mangle)]
279pub unsafe extern "C" fn flywheel_engine_request_redraw(engine: *const FlywheelEngine) {
280    if !engine.is_null() {
281        (*engine).0.request_redraw();
282    }
283}
284
285/// Request a diff-based update.
286#[unsafe(no_mangle)]
287pub unsafe extern "C" fn flywheel_engine_request_update(engine: *const FlywheelEngine) {
288    if !engine.is_null() {
289        (*engine).0.request_update();
290    }
291}
292
293/// Begin a new frame.
294#[unsafe(no_mangle)]
295pub unsafe extern "C" fn flywheel_engine_begin_frame(engine: *mut FlywheelEngine) {
296    if !engine.is_null() {
297        (*engine).0.begin_frame();
298    }
299}
300
301/// End a frame and request update.
302#[unsafe(no_mangle)]
303pub unsafe extern "C" fn flywheel_engine_end_frame(engine: *mut FlywheelEngine) {
304    if !engine.is_null() {
305        (*engine).0.end_frame();
306    }
307}
308
309/// Set a cell at the given position.
310#[unsafe(no_mangle)]
311#[allow(clippy::cast_sign_loss)] // c_char may be signed
312pub unsafe extern "C" fn flywheel_engine_set_cell(
313    engine: *mut FlywheelEngine,
314    x: u16,
315    y: u16,
316    c: c_char,
317    fg: u32,
318    bg: u32,
319) {
320    if engine.is_null() {
321        return;
322    }
323    let cell = Cell::new(c as u8 as char)
324        .with_fg(Rgb::from_u32(fg))
325        .with_bg(Rgb::from_u32(bg));
326    (*engine).0.set_cell(x, y, cell);
327}
328
329/// Draw text at the given position.
330#[unsafe(no_mangle)]
331pub unsafe extern "C" fn flywheel_engine_draw_text(
332    engine: *mut FlywheelEngine,
333    x: u16,
334    y: u16,
335    text: *const c_char,
336    fg: u32,
337    bg: u32,
338) -> u16 {
339    if engine.is_null() || text.is_null() {
340        return 0;
341    }
342
343    let Ok(text_str) = CStr::from_ptr(text).to_str() else {
344        return 0;
345    };
346
347    (*engine)
348        .0
349        .draw_text(x, y, text_str, Rgb::from_u32(fg), Rgb::from_u32(bg))
350}
351
352/// Clear the entire buffer.
353#[unsafe(no_mangle)]
354pub unsafe extern "C" fn flywheel_engine_clear(engine: *mut FlywheelEngine) {
355    if !engine.is_null() {
356        (*engine).0.clear();
357    }
358}
359
360/// Fill a rectangle with a character.
361#[unsafe(no_mangle)]
362#[allow(clippy::cast_sign_loss)] // c_char may be signed
363pub unsafe extern "C" fn flywheel_engine_fill_rect(
364    engine: *mut FlywheelEngine,
365    x: u16,
366    y: u16,
367    width: u16,
368    height: u16,
369    c: c_char,
370    fg: u32,
371    bg: u32,
372) {
373    if engine.is_null() {
374        return;
375    }
376    let cell = Cell::new(c as u8 as char)
377        .with_fg(Rgb::from_u32(fg))
378        .with_bg(Rgb::from_u32(bg));
379    (*engine).0.fill_rect(Rect::new(x, y, width, height), cell);
380}
381
382// =============================================================================
383// Stream Widget Functions
384// =============================================================================
385
386/// Create a new stream widget.
387#[unsafe(no_mangle)]
388pub extern "C" fn flywheel_stream_new(x: u16, y: u16, width: u16, height: u16) -> *mut FlywheelStream {
389    let widget = StreamWidget::new(Rect::new(x, y, width, height));
390    Box::into_raw(Box::new(FlywheelStream(widget)))
391}
392
393/// Destroy a stream widget.
394#[unsafe(no_mangle)]
395pub unsafe extern "C" fn flywheel_stream_destroy(stream: *mut FlywheelStream) {
396    if !stream.is_null() {
397        drop(Box::from_raw(stream));
398    }
399}
400
401/// Append text to the stream widget.
402#[unsafe(no_mangle)]
403pub unsafe extern "C" fn flywheel_stream_append(
404    stream: *mut FlywheelStream,
405    text: *const c_char,
406) -> c_int {
407    if stream.is_null() || text.is_null() {
408        return -1;
409    }
410
411    let Ok(text_str) = CStr::from_ptr(text).to_str() else {
412        return -1;
413    };
414
415    match (*stream).0.append(text_str) {
416        AppendResult::FastPath { .. } => 1,
417        AppendResult::SlowPath { .. } | AppendResult::Empty => 0,
418    }
419}
420
421/// Render the stream widget to the engine's buffer.
422#[unsafe(no_mangle)]
423pub unsafe extern "C" fn flywheel_stream_render(
424    stream: *mut FlywheelStream,
425    engine: *mut FlywheelEngine,
426) {
427    if stream.is_null() || engine.is_null() {
428        return;
429    }
430    (*stream).0.render((*engine).0.buffer_mut());
431}
432
433/// Clear the stream widget content.
434#[unsafe(no_mangle)]
435pub unsafe extern "C" fn flywheel_stream_clear(stream: *mut FlywheelStream) {
436    if !stream.is_null() {
437        (*stream).0.clear();
438    }
439}
440
441/// Set the foreground color for subsequent text.
442#[unsafe(no_mangle)]
443pub unsafe extern "C" fn flywheel_stream_set_fg(stream: *mut FlywheelStream, color: u32) {
444    if !stream.is_null() {
445        (*stream).0.set_fg(Rgb::from_u32(color));
446    }
447}
448
449/// Set the background color for subsequent text.
450#[unsafe(no_mangle)]
451pub unsafe extern "C" fn flywheel_stream_set_bg(stream: *mut FlywheelStream, color: u32) {
452    if !stream.is_null() {
453        (*stream).0.set_bg(Rgb::from_u32(color));
454    }
455}
456
457/// Scroll the stream widget up.
458#[unsafe(no_mangle)]
459pub unsafe extern "C" fn flywheel_stream_scroll_up(stream: *mut FlywheelStream, lines: usize) {
460    if !stream.is_null() {
461        (*stream).0.scroll_up(lines);
462    }
463}
464
465/// Scroll the stream widget down.
466#[unsafe(no_mangle)]
467pub unsafe extern "C" fn flywheel_stream_scroll_down(stream: *mut FlywheelStream, lines: usize) {
468    if !stream.is_null() {
469        (*stream).0.scroll_down(lines);
470    }
471}
472
473// =============================================================================
474// Color Utilities
475// =============================================================================
476
477/// Create an RGB color from components.
478#[unsafe(no_mangle)]
479#[allow(clippy::cast_lossless)]
480pub const extern "C" fn flywheel_rgb(r: u8, g: u8, b: u8) -> u32 {
481    ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
482}
483
484// =============================================================================
485// Version Information
486// =============================================================================
487
488/// Get the Flywheel version string.
489#[unsafe(no_mangle)]
490pub extern "C" fn flywheel_version() -> *const c_char {
491    static VERSION: &[u8] = b"0.1.0\0";
492    VERSION.as_ptr().cast::<c_char>()
493}
494
495// =============================================================================
496// Helper Functions
497// =============================================================================
498
499const fn convert_key_code(code: KeyCode) -> (u32, c_int) {
500    match code {
501        KeyCode::Char(c) => (c as u32, FLYWHEEL_KEY_NONE),
502        KeyCode::Enter => (0, FLYWHEEL_KEY_ENTER),
503        KeyCode::Esc => (0, FLYWHEEL_KEY_ESCAPE),
504        KeyCode::Backspace => (0, FLYWHEEL_KEY_BACKSPACE),
505        KeyCode::Tab => (0, FLYWHEEL_KEY_TAB),
506        KeyCode::Left => (0, FLYWHEEL_KEY_LEFT),
507        KeyCode::Right => (0, FLYWHEEL_KEY_RIGHT),
508        KeyCode::Up => (0, FLYWHEEL_KEY_UP),
509        KeyCode::Down => (0, FLYWHEEL_KEY_DOWN),
510        KeyCode::Home => (0, FLYWHEEL_KEY_HOME),
511        KeyCode::End => (0, FLYWHEEL_KEY_END),
512        KeyCode::PageUp => (0, FLYWHEEL_KEY_PAGE_UP),
513        KeyCode::PageDown => (0, FLYWHEEL_KEY_PAGE_DOWN),
514        KeyCode::Delete => (0, FLYWHEEL_KEY_DELETE),
515        _ => (0, FLYWHEEL_KEY_NONE),
516    }
517}
518
519const fn convert_modifiers(mods: crate::actor::KeyModifiers) -> c_uint {
520    let mut result = 0;
521    if mods.shift {
522        result |= FLYWHEEL_MOD_SHIFT;
523    }
524    if mods.control {
525        result |= FLYWHEEL_MOD_CTRL;
526    }
527    if mods.alt {
528        result |= FLYWHEEL_MOD_ALT;
529    }
530    if mods.super_key {
531        result |= FLYWHEEL_MOD_SUPER;
532    }
533    result
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    #[test]
541    fn test_flywheel_rgb() {
542        assert_eq!(flywheel_rgb(255, 128, 64), 0xFF8040);
543        assert_eq!(flywheel_rgb(0, 0, 0), 0x000000);
544        assert_eq!(flywheel_rgb(255, 255, 255), 0xFFFFFF);
545    }
546
547    #[test]
548    fn test_flywheel_version() {
549        unsafe {
550            let version = flywheel_version();
551            let version_str = CStr::from_ptr(version).to_str().unwrap();
552            assert_eq!(version_str, "0.1.0");
553        }
554    }
555}