fusabi_tui/bindings/
mod.rs

1//! Bindings module - Fusabi VM integration
2//!
3//! This module provides the bridge between Fusabi scripts and the TUI library,
4//! exposing native functions that can be called from F# scripts.
5//!
6//! ## Native Functions
7//!
8//! Functions exposed to Fusabi scripts for:
9//! - Formatting utilities (numbers, bytes, time)
10//! - Widget specification builders (tables, canvas)
11//!
12//! ## Limitations
13//!
14//! Due to Fusabi's current `Rc<RefCell<T>>` design (not Send+Sync), full widget
15//! lifecycle management is not yet supported. Instead, this module provides:
16//!
17//! 1. **Formatting functions** - Pure functions for data formatting
18//! 2. **Widget specifications** - JSON-serializable structures that F# can build
19//!
20//! The Rust application is responsible for interpreting widget specifications
21//! and rendering them. This is a temporary workaround until Fusabi supports
22//! Send+Sync Engine instances.
23//!
24//! See `/home/beengud/raibid-labs/hibana/docs/FUSABI_SEND_SYNC_ISSUE_DRAFT.md`
25//! for details on the upstream limitation.
26//!
27//! ## Example
28//!
29//! ```rust
30//! use fusabi_tui::bindings::FusabiTuiModule;
31//! use fusabi::Engine;
32//!
33//! let mut engine = Engine::new();
34//! let module = FusabiTuiModule::new();
35//! module.register(&mut engine).unwrap();
36//!
37//! // Now F# scripts can call tui_format_number, tui_format_bytes, etc.
38//! ```
39
40use anyhow::Result;
41use fusabi::Engine;
42use fusabi::Value;
43use fusabi_vm::VmError;
44use std::cell::RefCell;
45use std::rc::Rc;
46
47use crate::formatting::{format_bytes, format_latency, format_number};
48use std::time::Duration;
49
50/// Widget specification types that can be serialized to/from JSON
51pub mod specs;
52
53/// Main module for registering TUI functions with the Fusabi VM
54#[derive(Clone)]
55pub struct FusabiTuiModule;
56
57impl FusabiTuiModule {
58    /// Create a new Fusabi TUI module
59    pub fn new() -> Self {
60        Self
61    }
62
63    /// Register native functions with the Fusabi VM
64    ///
65    /// Registers the following functions:
66    /// - `tui_format_number(i64) -> string` - Format large numbers (K/M/B)
67    /// - `tui_format_bytes(i64) -> string` - Format byte sizes (KB/MB/GB)
68    /// - `tui_format_latency(i64) -> string` - Format latency in microseconds
69    /// - `tui_format_duration(i64) -> string` - Format duration in seconds
70    ///
71    /// ## Example F# Usage
72    ///
73    /// ```fsharp
74    /// let count = 1500000L
75    /// let formatted = tui_format_number count  // Returns "1.50M"
76    ///
77    /// let size = 2048L
78    /// let formatted = tui_format_bytes size  // Returns "2.00 KB"
79    /// ```
80    pub fn register(&self, engine: &mut Engine) -> Result<()> {
81        // Register formatting functions as global Fusabi functions
82        // These are pure functions with no side effects, so they work
83        // despite the Send+Sync limitation.
84
85        // tui_format_number(n: int) -> string
86        engine.register_raw("tui_format_number", |_vm, args| {
87            if args.len() != 1 {
88                return Err(VmError::Runtime(
89                    "tui_format_number expects 1 argument: number (int)".into(),
90                ));
91            }
92            let num = args[0].as_int().ok_or(VmError::TypeMismatch {
93                expected: "int",
94                got: args[0].type_name(),
95            })? as u64;
96
97            let formatted = format_number(num);
98            Ok(Value::Str(formatted))
99        });
100
101        // tui_format_bytes(bytes: int) -> string
102        engine.register_raw("tui_format_bytes", |_vm, args| {
103            if args.len() != 1 {
104                return Err(VmError::Runtime(
105                    "tui_format_bytes expects 1 argument: bytes (int)".into(),
106                ));
107            }
108            let bytes = args[0].as_int().ok_or(VmError::TypeMismatch {
109                expected: "int",
110                got: args[0].type_name(),
111            })? as u64;
112
113            let formatted = format_bytes(bytes);
114            Ok(Value::Str(formatted))
115        });
116
117        // tui_format_latency(microseconds: int) -> string
118        engine.register_raw("tui_format_latency", |_vm, args| {
119            if args.len() != 1 {
120                return Err(VmError::Runtime(
121                    "tui_format_latency expects 1 argument: microseconds (int)".into(),
122                ));
123            }
124            let us = args[0].as_int().ok_or(VmError::TypeMismatch {
125                expected: "int",
126                got: args[0].type_name(),
127            })? as u64;
128
129            let formatted = format_latency(us);
130            Ok(Value::Str(formatted))
131        });
132
133        // tui_format_duration(seconds: int) -> string
134        engine.register_raw("tui_format_duration", |_vm, args| {
135            if args.len() != 1 {
136                return Err(VmError::Runtime(
137                    "tui_format_duration expects 1 argument: seconds (int)".into(),
138                ));
139            }
140            let secs = args[0].as_int().ok_or(VmError::TypeMismatch {
141                expected: "int",
142                got: args[0].type_name(),
143            })? as u64;
144
145            let formatted = crate::formatting::format_duration(Duration::from_secs(secs));
146            Ok(Value::Str(formatted))
147        });
148
149        // tui_table_spec_new() -> record
150        // Returns an empty table specification that can be built up
151        engine.register_raw("tui_table_spec_new", |_vm, args| {
152            if !args.is_empty() {
153                return Err(VmError::Runtime(
154                    "tui_table_spec_new expects no arguments".into(),
155                ));
156            }
157
158            let mut fields = std::collections::HashMap::new();
159            fields.insert(
160                "columns".to_string(),
161                Value::Array(Rc::new(RefCell::new(vec![]))),
162            );
163            fields.insert(
164                "rows".to_string(),
165                Value::Array(Rc::new(RefCell::new(vec![]))),
166            );
167            fields.insert("title".to_string(), Value::Unit);
168            fields.insert("borders".to_string(), Value::Bool(true));
169
170            Ok(Value::Record(Rc::new(RefCell::new(fields))))
171        });
172
173        // tui_table_add_column(spec: record, header: string, width: int) -> record
174        // Add a column definition to a table spec
175        engine.register_raw("tui_table_add_column", |_vm, args| {
176            if args.len() != 3 {
177                return Err(VmError::Runtime(
178                    "tui_table_add_column expects 3 arguments: spec (record), header (string), width (int)".into(),
179                ));
180            }
181
182            let spec = args[0].as_record().ok_or(VmError::TypeMismatch {
183                expected: "record",
184                got: args[0].type_name(),
185            })?;
186
187            let header = args[1].as_str().ok_or(VmError::TypeMismatch {
188                expected: "string",
189                got: args[1].type_name(),
190            })?;
191
192            let width = args[2].as_int().ok_or(VmError::TypeMismatch {
193                expected: "int",
194                got: args[2].type_name(),
195            })?;
196
197            // Create column definition
198            let mut col_fields = std::collections::HashMap::new();
199            col_fields.insert("header".to_string(), Value::Str(header.to_string()));
200            col_fields.insert("width".to_string(), Value::Int(width));
201            let col = Value::Record(Rc::new(RefCell::new(col_fields)));
202
203            // Add to columns array
204            let spec_mut = spec.borrow_mut();
205            if let Some(columns_val) = spec_mut.get("columns") {
206                if let Some(columns_arr) = columns_val.as_array() {
207                    columns_arr.borrow_mut().push(col);
208                }
209            }
210
211            drop(spec_mut);
212            Ok(Value::Record(spec.clone()))
213        });
214
215        // tui_table_add_row(spec: record, cells: array<string>) -> record
216        // Add a data row to the table spec
217        engine.register_raw("tui_table_add_row", |_vm, args| {
218            if args.len() != 2 {
219                return Err(VmError::Runtime(
220                    "tui_table_add_row expects 2 arguments: spec (record), cells (array)".into(),
221                ));
222            }
223
224            let spec = args[0].as_record().ok_or(VmError::TypeMismatch {
225                expected: "record",
226                got: args[0].type_name(),
227            })?;
228
229            let cells = args[1].as_array().ok_or(VmError::TypeMismatch {
230                expected: "array",
231                got: args[1].type_name(),
232            })?;
233
234            // Add to rows array
235            let spec_mut = spec.borrow_mut();
236            if let Some(rows_val) = spec_mut.get("rows") {
237                if let Some(rows_arr) = rows_val.as_array() {
238                    rows_arr.borrow_mut().push(Value::Array(cells.clone()));
239                }
240            }
241
242            drop(spec_mut);
243            Ok(Value::Record(spec.clone()))
244        });
245
246        // tui_table_set_title(spec: record, title: string) -> record
247        engine.register_raw("tui_table_set_title", |_vm, args| {
248            if args.len() != 2 {
249                return Err(VmError::Runtime(
250                    "tui_table_set_title expects 2 arguments: spec (record), title (string)".into(),
251                ));
252            }
253
254            let spec = args[0].as_record().ok_or(VmError::TypeMismatch {
255                expected: "record",
256                got: args[0].type_name(),
257            })?;
258
259            let title = args[1].as_str().ok_or(VmError::TypeMismatch {
260                expected: "string",
261                got: args[1].type_name(),
262            })?;
263
264            spec.borrow_mut()
265                .insert("title".to_string(), Value::Str(title.to_string()));
266            Ok(Value::Record(spec.clone()))
267        });
268
269        Ok(())
270    }
271}
272
273impl Default for FusabiTuiModule {
274    fn default() -> Self {
275        Self::new()
276    }
277}