//! Выполнение Lua-сценариев приложения. //! //! При запуске приложение выполняет [`STARTUP_SCRIPT`] из каталога [`script_dir`] //! (см. [`ScriptEngine::run_startup`]). Скриптам доступны: //! //! * объект `app` — управление окном и редактором (методы [`AppApi`], вызываются //! через двоеточие: `app:next_tab()`); //! * функция `include(path)` — выполняет другой Lua-файл относительно [`script_dir`] //! (или по абсолютному пути) в общем глобальном окружении; //! * стандартный `require`, который также ищет модули в [`script_dir`]. //! //! Каталог по умолчанию — текущий рабочий каталог. Его можно переопределить //! аргументом `--config <каталог>` при запуске приложения (см. //! [`parse_command_line`](crate::constants::parse_command_line)). use std::path::{Path, PathBuf}; use mlua::{Lua, Table}; use crate::api::{AppApi, DirEntryInfo, TabInfo}; use crate::constants::{STARTUP_SCRIPT, script_dir}; /// Вкладка превращается в Lua-таблицу `{ index = 0, title = "main.rs", icon = "file" }`. impl mlua::IntoLua for TabInfo { fn into_lua(self, lua: &Lua) -> mlua::Result { let tab = lua.create_table()?; tab.set("index", self.index)?; tab.set("title", self.title)?; tab.set("icon", self.icon)?; Ok(mlua::Value::Table(tab)) } } /// Элемент каталога превращается в Lua-таблицу /// `{ id = "src/main.rs", name = "main.rs", is_dir = false }`. impl mlua::IntoLua for DirEntryInfo { fn into_lua(self, lua: &Lua) -> mlua::Result { let entry = lua.create_table()?; entry.set("id", self.id)?; entry.set("name", self.name)?; entry.set("is_dir", self.is_dir)?; Ok(mlua::Value::Table(entry)) } } /// Движок Lua и состояние его глобального окружения. pub struct ScriptEngine { lua: Lua, /// Каталог, относительно которого ищутся `init.lua`, `include` и `require`. script_dir: PathBuf, } impl ScriptEngine { /// Создаёт движок и публикует в нём API приложения. /// /// Каталог сценариев берётся из [`script_dir`] (то есть из `--config` /// или текущего рабочего каталога). pub fn new(api: AppApi) -> mlua::Result { Self::new_in(api, script_dir()) } /// Создаёт движок с явно заданным каталогом сценариев. /// /// Полезно тестам и будущим режимам, где каталог конфигурации приходит /// не из глобального `--config`. pub fn new_in(api: AppApi, directory: impl AsRef) -> mlua::Result { let engine = Self { lua: Lua::new(), script_dir: directory.as_ref().to_path_buf(), }; engine.expose_app_api(api)?; engine.configure_require()?; Ok(engine) } /// Выполняет стартовый сценарий из каталога движка. /// /// # Errors /// Возвращает ошибку, если файла нет, его не удалось прочитать или в нём /// есть синтаксическая/исполняемая ошибка. pub fn run_startup(&self) -> mlua::Result<()> { self.run_file(&script_path(&self.script_dir, STARTUP_SCRIPT)) } /// Выполняет Lua-файл в глобальном окружении движка. pub fn run_file(&self, path: &Path) -> mlua::Result<()> { run_file(&self.lua, path).map(|_| ()) } /// Публикует объект `app` с функциями управления приложением. /// /// Все методы [`AppApi`] из `#[mlua::userdata_impl]`-блока доступны как /// методы userdata: `app:next_tab()`, `app:set_active_text("...")`. /// Номера вкладок всюду нумеруются с нуля. fn expose_app_api(&self, api: AppApi) -> mlua::Result<()> { self.lua .globals() .set("app", self.lua.create_userdata(api)?)?; Ok(()) } /// Добавляет каталог сценариев в `package.path`, чтобы `require` находил /// соседние Lua-модули, и объявляет `include`. fn configure_require(&self) -> mlua::Result<()> { let globals = self.lua.globals(); if let Ok(package) = globals.get::("package") { let current: String = package.get("path").unwrap_or_default(); let dir = self.script_dir.display().to_string().replace('\\', "/"); let extra = format!("{dir}/?.lua;{dir}/?/init.lua"); let path = if current.is_empty() { extra } else { format!("{extra};{current}") }; package.set("path", path)?; } // include выполняет файл сразу, как dofile, но путь по умолчанию // отсчитывается от каталога сценариев, а не от cwd. let directory = self.script_dir.clone(); globals.set( "include", self.lua.create_function(move |lua, path: String| { run_file(lua, &script_path(&directory, &path)) })?, )?; Ok(()) } } /// Путь к файлу сценария: абсолютные пути используются как есть, остальные /// отсчитываются от каталога сценариев. fn script_path(directory: &Path, path: &str) -> PathBuf { let path = Path::new(path); if path.is_absolute() { path.to_path_buf() } else { directory.join(path) } } /// Читает и выполняет Lua-файл, возвращая значения, которые вернул чанк. fn run_file(lua: &Lua, path: &Path) -> mlua::Result { let source = std::fs::read_to_string(path).map_err(|err| { mlua::Error::RuntimeError(format!( "не удалось прочитать сценарий {}: {err}", path.display() )) })?; lua.load(&source) .set_name(path.to_string_lossy().into_owned()) .eval() }