This commit is contained in:
2026-10-29 22:40:40 +03:00
commit dce595fbc0
18 changed files with 7983 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
//! Выполнение Lua-сценариев приложения.
//!
//! При запуске приложение выполняет [`STARTUP_SCRIPT`] из каталога [`SCRIPT_DIR`]
//! (см. [`ScriptEngine::run_startup`]). Скриптам доступны:
//!
//! * объект `app` — управление окном и редактором (методы [`AppApi`], вызываются
//! через двоеточие: `app:next_tab()`);
//! * функция `include(path)` — выполняет другой Lua-файл относительно [`SCRIPT_DIR`]
//! (или по абсолютному пути) в общем глобальном окружении;
//! * стандартный `require`, который также ищет модули в [`SCRIPT_DIR`].
use std::path::{Path, PathBuf};
use mlua::{Lua, Table};
use crate::api::{AppApi, TabInfo};
use crate::constants::{SCRIPT_DIR, STARTUP_SCRIPT};
/// Вкладка превращается в Lua-таблицу `{ index = 0, title = "main.rs" }`.
impl mlua::IntoLua for TabInfo {
fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
let tab = lua.create_table()?;
tab.set("index", self.index)?;
tab.set("title", self.title)?;
Ok(mlua::Value::Table(tab))
}
}
/// Движок Lua и состояние его глобального окружения.
pub struct ScriptEngine {
lua: Lua,
}
impl ScriptEngine {
/// Создаёт движок и публикует в нём API приложения.
pub fn new(api: AppApi) -> mlua::Result<Self> {
let engine = Self { lua: Lua::new() };
engine.expose_app_api(api)?;
engine.configure_require()?;
Ok(engine)
}
/// Выполняет стартовый сценарий из [`SCRIPT_DIR`].
///
/// # Errors
/// Возвращает ошибку, если файла нет, его не удалось прочитать или в нём
/// есть синтаксическая/исполняемая ошибка.
pub fn run_startup(&self) -> mlua::Result<()> {
self.run_file(&script_path(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::<Table>("package") {
let current: String = package.get("path").unwrap_or_default();
let dir = SCRIPT_DIR;
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.
globals.set(
"include",
self.lua
.create_function(|lua, path: String| run_file(lua, &script_path(&path)))?,
)?;
Ok(())
}
}
/// Путь к файлу сценария: абсолютные пути используются как есть, остальные
/// отсчитываются от [`SCRIPT_DIR`].
fn script_path(path: &str) -> PathBuf {
let path = Path::new(path);
if path.is_absolute() {
path.to_path_buf()
} else {
Path::new(SCRIPT_DIR).join(path)
}
}
/// Читает и выполняет Lua-файл, возвращая значения, которые вернул чанк.
fn run_file(lua: &Lua, path: &Path) -> mlua::Result<mlua::MultiValue> {
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()
}