gravityfile_plugin/lua/
runtime.rs

1//! Lua runtime implementation.
2
3use std::collections::HashMap;
4use std::path::Path;
5
6use mlua::{Function, Lua, MultiValue, Table, Value as LuaValue};
7
8use crate::config::{PluginConfig, PluginMetadata};
9use crate::hooks::{Hook, HookContext, HookResult};
10use crate::runtime::{BoxFuture, IsolatedContext, PluginHandle, PluginRuntime};
11use crate::sandbox::SandboxConfig;
12use crate::types::{PluginError, PluginResult, Value};
13
14use super::bindings;
15use super::isolate::LuaIsolatedContext;
16
17/// A loaded Lua plugin.
18struct LoadedLuaPlugin {
19    /// Plugin name/id.
20    name: String,
21
22    /// The plugin's module table.
23    module: mlua::RegistryKey,
24
25    /// Plugin metadata.
26    metadata: PluginMetadata,
27
28    /// Hooks implemented by this plugin.
29    hooks: Vec<String>,
30}
31
32/// Lua plugin runtime.
33pub struct LuaRuntime {
34    /// The main Lua state.
35    lua: Lua,
36
37    /// Loaded plugins by handle.
38    plugins: HashMap<PluginHandle, LoadedLuaPlugin>,
39
40    /// Next plugin handle ID.
41    next_handle: usize,
42
43    /// Runtime configuration.
44    config: Option<PluginConfig>,
45
46    /// Whether the runtime has been initialized.
47    initialized: bool,
48}
49
50impl LuaRuntime {
51    /// Create a new Lua runtime.
52    pub fn new() -> PluginResult<Self> {
53        let lua = Lua::new();
54
55        // Disable potentially dangerous standard library functions
56        lua.globals()
57            .set("loadfile", LuaValue::Nil)
58            .map_err(|e| PluginError::LoadError {
59                name: "lua".into(),
60                message: e.to_string(),
61            })?;
62
63        lua.globals()
64            .set("dofile", LuaValue::Nil)
65            .map_err(|e| PluginError::LoadError {
66                name: "lua".into(),
67                message: e.to_string(),
68            })?;
69
70        Ok(Self {
71            lua,
72            plugins: HashMap::new(),
73            next_handle: 0,
74            config: None,
75            initialized: false,
76        })
77    }
78
79    /// Initialize the Lua runtime with the gravityfile API.
80    fn init_api(&self) -> PluginResult<()> {
81        let globals = self.lua.globals();
82
83        // Create the 'gf' namespace (gravityfile API)
84        let gf = self
85            .lua
86            .create_table()
87            .map_err(|e| PluginError::LoadError {
88                name: "lua".into(),
89                message: format!("Failed to create gf table: {}", e),
90            })?;
91
92        // Add version info
93        gf.set("version", env!("CARGO_PKG_VERSION"))
94            .map_err(|e| PluginError::LoadError {
95                name: "lua".into(),
96                message: e.to_string(),
97            })?;
98
99        // Add logging functions
100        let log_info = self
101            .lua
102            .create_function(|_, msg: String| {
103                tracing::info!(target: "plugin", "{}", msg);
104                Ok(())
105            })
106            .map_err(|e| PluginError::LoadError {
107                name: "lua".into(),
108                message: e.to_string(),
109            })?;
110        gf.set("log_info", log_info).ok();
111
112        let log_warn = self
113            .lua
114            .create_function(|_, msg: String| {
115                tracing::warn!(target: "plugin", "{}", msg);
116                Ok(())
117            })
118            .map_err(|e| PluginError::LoadError {
119                name: "lua".into(),
120                message: e.to_string(),
121            })?;
122        gf.set("log_warn", log_warn).ok();
123
124        let log_error = self
125            .lua
126            .create_function(|_, msg: String| {
127                tracing::error!(target: "plugin", "{}", msg);
128                Ok(())
129            })
130            .map_err(|e| PluginError::LoadError {
131                name: "lua".into(),
132                message: e.to_string(),
133            })?;
134        gf.set("log_error", log_error).ok();
135
136        // Add notify function
137        let notify = self
138            .lua
139            .create_function(|_, (msg, level): (String, Option<String>)| {
140                let level = level.unwrap_or_else(|| "info".to_string());
141                tracing::info!(target: "plugin_notify", level = level, "{}", msg);
142                Ok(())
143            })
144            .map_err(|e| PluginError::LoadError {
145                name: "lua".into(),
146                message: e.to_string(),
147            })?;
148        gf.set("notify", notify).ok();
149
150        globals
151            .set("gf", gf)
152            .map_err(|e| PluginError::LoadError {
153                name: "lua".into(),
154                message: e.to_string(),
155            })?;
156
157        // Create the 'fs' namespace (filesystem API)
158        let fs = bindings::create_fs_api(&self.lua)?;
159        globals.set("fs", fs).map_err(|e| PluginError::LoadError {
160            name: "lua".into(),
161            message: e.to_string(),
162        })?;
163
164        // Create the 'ui' namespace (UI elements)
165        let ui = bindings::create_ui_api(&self.lua)?;
166        globals.set("ui", ui).map_err(|e| PluginError::LoadError {
167            name: "lua".into(),
168            message: e.to_string(),
169        })?;
170
171        Ok(())
172    }
173
174    /// Convert a Lua value to our Value type.
175    fn lua_to_value(lua_val: LuaValue) -> Value {
176        match lua_val {
177            LuaValue::Nil => Value::Null,
178            LuaValue::Boolean(b) => Value::Bool(b),
179            LuaValue::Integer(i) => Value::Integer(i),
180            LuaValue::Number(n) => Value::Float(n),
181            LuaValue::String(s) => Value::String(s.to_string_lossy()),
182            LuaValue::Table(t) => {
183                // Check if it's an array or object
184                let mut is_array = true;
185                let mut max_index = 0i64;
186
187                for pair in t.clone().pairs::<i64, LuaValue>() {
188                    if let Ok((k, _)) = pair {
189                        if k > 0 {
190                            max_index = max_index.max(k);
191                        } else {
192                            is_array = false;
193                            break;
194                        }
195                    } else {
196                        is_array = false;
197                        break;
198                    }
199                }
200
201                if is_array && max_index > 0 {
202                    let mut arr = Vec::new();
203                    for i in 1..=max_index {
204                        if let Ok(v) = t.get::<LuaValue>(i) {
205                            arr.push(Self::lua_to_value(v));
206                        }
207                    }
208                    Value::Array(arr)
209                } else {
210                    let mut obj = std::collections::HashMap::new();
211                    for pair in t.pairs::<String, LuaValue>() {
212                        if let Ok((k, v)) = pair {
213                            obj.insert(k, Self::lua_to_value(v));
214                        }
215                    }
216                    Value::Object(obj)
217                }
218            }
219            _ => Value::Null,
220        }
221    }
222
223    /// Convert our Value type to a Lua value.
224    fn value_to_lua(&self, lua: &Lua, val: &Value) -> mlua::Result<LuaValue> {
225        match val {
226            Value::Null => Ok(LuaValue::Nil),
227            Value::Bool(b) => Ok(LuaValue::Boolean(*b)),
228            Value::Integer(i) => Ok(LuaValue::Integer(*i)),
229            Value::Float(f) => Ok(LuaValue::Number(*f)),
230            Value::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
231            Value::Array(arr) => {
232                let table = lua.create_table()?;
233                for (i, v) in arr.iter().enumerate() {
234                    table.set(i + 1, self.value_to_lua(lua, v)?)?;
235                }
236                Ok(LuaValue::Table(table))
237            }
238            Value::Object(obj) => {
239                let table = lua.create_table()?;
240                for (k, v) in obj {
241                    table.set(k.as_str(), self.value_to_lua(lua, v)?)?;
242                }
243                Ok(LuaValue::Table(table))
244            }
245            Value::Bytes(b) => Ok(LuaValue::String(lua.create_string(b)?)),
246        }
247    }
248
249    /// Convert a Hook to a Lua table.
250    fn hook_to_lua(&self, lua: &Lua, hook: &Hook) -> mlua::Result<Table> {
251        let table = lua.create_table()?;
252
253        // Serialize hook to JSON then to Lua table
254        let json = serde_json::to_string(hook).map_err(|e| mlua::Error::external(e))?;
255        let json_val: serde_json::Value =
256            serde_json::from_str(&json).map_err(|e| mlua::Error::external(e))?;
257
258        fn json_to_lua(lua: &Lua, val: &serde_json::Value) -> mlua::Result<LuaValue> {
259            match val {
260                serde_json::Value::Null => Ok(LuaValue::Nil),
261                serde_json::Value::Bool(b) => Ok(LuaValue::Boolean(*b)),
262                serde_json::Value::Number(n) => {
263                    if let Some(i) = n.as_i64() {
264                        Ok(LuaValue::Integer(i))
265                    } else {
266                        Ok(LuaValue::Number(n.as_f64().unwrap_or(0.0)))
267                    }
268                }
269                serde_json::Value::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
270                serde_json::Value::Array(arr) => {
271                    let t = lua.create_table()?;
272                    for (i, v) in arr.iter().enumerate() {
273                        t.set(i + 1, json_to_lua(lua, v)?)?;
274                    }
275                    Ok(LuaValue::Table(t))
276                }
277                serde_json::Value::Object(obj) => {
278                    let t = lua.create_table()?;
279                    for (k, v) in obj {
280                        t.set(k.as_str(), json_to_lua(lua, v)?)?;
281                    }
282                    Ok(LuaValue::Table(t))
283                }
284            }
285        }
286
287        if let serde_json::Value::Object(obj) = json_val {
288            for (k, v) in obj {
289                table.set(k.as_str(), json_to_lua(lua, &v)?)?;
290            }
291        }
292
293        Ok(table)
294    }
295}
296
297impl Default for LuaRuntime {
298    fn default() -> Self {
299        Self::new().expect("Failed to create Lua runtime")
300    }
301}
302
303impl PluginRuntime for LuaRuntime {
304    fn name(&self) -> &'static str {
305        "lua"
306    }
307
308    fn file_extensions(&self) -> &'static [&'static str] {
309        &[".lua"]
310    }
311
312    fn init(&mut self, config: &PluginConfig) -> PluginResult<()> {
313        if self.initialized {
314            return Ok(());
315        }
316
317        self.config = Some(config.clone());
318        self.init_api()?;
319        self.initialized = true;
320
321        Ok(())
322    }
323
324    fn load_plugin(&mut self, id: &str, source: &Path) -> PluginResult<PluginHandle> {
325        // Read the plugin source
326        let code = std::fs::read_to_string(source)?;
327
328        // Load and execute the plugin
329        let chunk = self.lua.load(&code).set_name(id);
330
331        let module: Table = chunk.eval().map_err(|e| PluginError::LoadError {
332            name: id.to_string(),
333            message: e.to_string(),
334        })?;
335
336        // Detect which hooks are implemented
337        let mut hooks = vec![];
338        for hook_name in [
339            "on_navigate",
340            "on_drill_down",
341            "on_back",
342            "on_scan_start",
343            "on_scan_progress",
344            "on_scan_complete",
345            "on_delete_start",
346            "on_delete_complete",
347            "on_copy_start",
348            "on_copy_complete",
349            "on_move_start",
350            "on_move_complete",
351            "on_render",
352            "on_action",
353            "on_mode_change",
354            "on_startup",
355            "on_shutdown",
356        ] {
357            if module.contains_key(hook_name).unwrap_or(false) {
358                hooks.push(hook_name.to_string());
359            }
360        }
361
362        // Store in registry
363        let key = self.lua.create_registry_value(module).map_err(|e| {
364            PluginError::LoadError {
365                name: id.to_string(),
366                message: e.to_string(),
367            }
368        })?;
369
370        let handle = PluginHandle::new(self.next_handle);
371        self.next_handle += 1;
372
373        // Create default metadata (would normally come from plugin.toml)
374        let metadata = PluginMetadata {
375            name: id.to_string(),
376            runtime: "lua".to_string(),
377            ..Default::default()
378        };
379
380        self.plugins.insert(
381            handle,
382            LoadedLuaPlugin {
383                name: id.to_string(),
384                module: key,
385                metadata,
386                hooks,
387            },
388        );
389
390        Ok(handle)
391    }
392
393    fn unload_plugin(&mut self, handle: PluginHandle) -> PluginResult<()> {
394        if let Some(plugin) = self.plugins.remove(&handle) {
395            self.lua.remove_registry_value(plugin.module).ok();
396        }
397        Ok(())
398    }
399
400    fn get_metadata(&self, handle: PluginHandle) -> Option<&PluginMetadata> {
401        self.plugins.get(&handle).map(|p| &p.metadata)
402    }
403
404    fn has_hook(&self, handle: PluginHandle, hook_name: &str) -> bool {
405        self.plugins
406            .get(&handle)
407            .map(|p| p.hooks.contains(&hook_name.to_string()))
408            .unwrap_or(false)
409    }
410
411    fn call_hook_sync(
412        &self,
413        handle: PluginHandle,
414        hook: &Hook,
415        _ctx: &HookContext,
416    ) -> PluginResult<HookResult> {
417        let plugin = self.plugins.get(&handle).ok_or_else(|| PluginError::NotFound {
418            path: std::path::PathBuf::new(),
419        })?;
420
421        let module: Table = self.lua.registry_value(&plugin.module).map_err(|e| {
422            PluginError::ExecutionError {
423                name: plugin.name.clone(),
424                message: e.to_string(),
425            }
426        })?;
427
428        let hook_name = hook.name();
429        let func: Function = match module.get(hook_name) {
430            Ok(f) => f,
431            Err(_) => return Ok(HookResult::default()),
432        };
433
434        // Convert hook and context to Lua
435        let hook_table = self.hook_to_lua(&self.lua, hook).map_err(|e| {
436            PluginError::ExecutionError {
437                name: plugin.name.clone(),
438                message: e.to_string(),
439            }
440        })?;
441
442        // Call the function
443        let result: LuaValue = func.call((module.clone(), hook_table)).map_err(|e| {
444            PluginError::ExecutionError {
445                name: plugin.name.clone(),
446                message: e.to_string(),
447            }
448        })?;
449
450        // Convert result
451        let mut hook_result = HookResult::ok();
452        if let LuaValue::Table(t) = result {
453            if let Ok(prevent) = t.get::<bool>("prevent_default") {
454                if prevent {
455                    hook_result = hook_result.prevent_default();
456                }
457            }
458            if let Ok(stop) = t.get::<bool>("stop_propagation") {
459                if stop {
460                    hook_result = hook_result.stop_propagation();
461                }
462            }
463            if let Ok(val) = t.get::<LuaValue>("value") {
464                hook_result.value = Some(Self::lua_to_value(val));
465            }
466        }
467
468        Ok(hook_result)
469    }
470
471    fn call_hook_async<'a>(
472        &'a self,
473        handle: PluginHandle,
474        hook: &'a Hook,
475        ctx: &'a HookContext,
476    ) -> BoxFuture<'a, PluginResult<HookResult>> {
477        // For now, just call sync version
478        // TODO: Implement true async with spawn_blocking
479        Box::pin(async move { self.call_hook_sync(handle, hook, ctx) })
480    }
481
482    fn call_method<'a>(
483        &'a self,
484        handle: PluginHandle,
485        method: &'a str,
486        args: Vec<Value>,
487    ) -> BoxFuture<'a, PluginResult<Value>> {
488        Box::pin(async move {
489            let plugin = self.plugins.get(&handle).ok_or_else(|| PluginError::NotFound {
490                path: std::path::PathBuf::new(),
491            })?;
492
493            let module: Table = self.lua.registry_value(&plugin.module).map_err(|e| {
494                PluginError::ExecutionError {
495                    name: plugin.name.clone(),
496                    message: e.to_string(),
497                }
498            })?;
499
500            let func: Function = module.get(method).map_err(|e| PluginError::ExecutionError {
501                name: plugin.name.clone(),
502                message: format!("Method '{}' not found: {}", method, e),
503            })?;
504
505            // Convert args to Lua
506            let lua_args: Vec<LuaValue> = args
507                .iter()
508                .map(|v| self.value_to_lua(&self.lua, v))
509                .collect::<Result<_, _>>()
510                .map_err(|e| PluginError::ExecutionError {
511                    name: plugin.name.clone(),
512                    message: e.to_string(),
513                })?;
514
515            let result: LuaValue = func
516                .call(MultiValue::from_vec(
517                    std::iter::once(LuaValue::Table(module))
518                        .chain(lua_args)
519                        .collect(),
520                ))
521                .map_err(|e| PluginError::ExecutionError {
522                    name: plugin.name.clone(),
523                    message: e.to_string(),
524                })?;
525
526            Ok(Self::lua_to_value(result))
527        })
528    }
529
530    fn create_isolated_context(
531        &self,
532        sandbox: &SandboxConfig,
533    ) -> PluginResult<Box<dyn IsolatedContext>> {
534        Ok(Box::new(LuaIsolatedContext::new(sandbox.clone())?))
535    }
536
537    fn loaded_plugins(&self) -> Vec<PluginHandle> {
538        self.plugins.keys().copied().collect()
539    }
540
541    fn shutdown(&mut self) -> PluginResult<()> {
542        self.plugins.clear();
543        Ok(())
544    }
545}