1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
use super::{api, def};
use crate::{
engine::def::{CommandMap, ProfileDef},
error::WefterErr,
fs::{
dirs::DirCfg,
hist::{History, HistoryRef},
res::{ResourceDir, ResourceDirTable},
},
tui::TuiInterface,
};
use anyhow::Result;
use mlua::Table;
use mlua::{FromLua, Lua};
use std::{cell::RefCell, rc::Rc};
use std::{fs, path::PathBuf};
/// Wrapper for the Lua interpreter and the variables it need to load
pub struct LuaInterpreter {
interpreter: Lua,
/// Some APIs can only be used when a profile is loaded, those that do not
/// require it are called `early-loading` modules, and they are initialized
/// at `Self::new`. Other modules are initialized at `Self::init` once
/// a profile is selected. This flag checks if the entire API has been registered
api_registered: bool,
history: HistoryRef,
}
// Private
impl LuaInterpreter {
/// Execute a single file as it were a function
fn exec<T: FromLua>(&mut self, path: &PathBuf) -> Result<T> {
if !path.is_file() {
return Err(WefterErr::NoSuchLuaFile(path.clone()).into());
}
let file = fs::read_to_string(&path)?;
let chunk = self.interpreter.load(file);
let result = chunk
.call::<T>(())
.map_err(|e| WefterErr::BadLuaExec(path.clone(), e))?;
Ok(result)
}
/// Register loader for the init.lua source directory
fn register_loader(&mut self, profile: &ResourceDir) -> Result<()> {
let path = profile.path.clone();
let globals = self.interpreter.globals();
let package: mlua::Table = globals.get("package")?;
let searchers: mlua::Table = package.get("searchers")?;
let loader = self.interpreter.create_function(move |lua, name: String| {
// Module as a lua file (foo.lua)
let mut file = path.clone();
let filename = name.replace(".", "/") + ".lua";
file.push(filename);
// Module as directory init.lua (foo/init.lua)
let mut dir = path.clone();
let dirname = name.replace(".", "/") + "/init.lua";
dir.push(dirname);
if !file.is_file() || !dir.is_file() {
return Result::Err(mlua::Error::runtime(format!(
"Could not find module entrypoint {:?}, {:?}",
file, dir
)));
}
let source = fs::read_to_string(if file.is_file() { file } else { dir })?;
let module = lua.load(source).set_name(name).into_function()?;
Result::Ok(mlua::Value::Function(module))
})?;
searchers.raw_insert(1, loader)?;
Ok(())
}
}
// Public
impl LuaInterpreter {
/// Create an instance of the interpreter and register the Wefter API module
pub fn new(dirs: &DirCfg) -> Result<Self> {
let l = Lua::new();
let globals = l.globals();
// Set global variables
globals.set(api::LUA_WEFTER_VERSION.0, api::LUA_WEFTER_VERSION.1)?;
globals.set(api::LUA_WEFTER_PROJECT_ROOT, dirs.root.clone())?;
// Create history for keeping track of IO operations
let history = HistoryRef::new(RefCell::new(History::new()));
/* Wefter API `early-loading` module registration
*
* Other APIs must be registered at initialization `LuaInterpreter::init(self)`
*/
let fs = l.create_table_from(api::fs_module(&l, history.clone())?)?;
// Create global api table `wefter` and register it as global
let wefter = l.create_table_from(vec![("fs", fs)])?;
l.globals().set(api::LUA_WEFTER_TABLE_NAME, wefter)?;
Ok(Self {
interpreter: l,
api_registered: false,
history: history.clone(),
})
}
/// Initialize modules and API
pub fn init(&mut self, res: &ResourceDir, tui: Rc<TuiInterface>) -> Result<()> {
// Register the loader for init.lua parent directory
self.register_loader(res)?;
// Get `wefter` api table
let l = &self.interpreter;
let wefter: Table = l.globals().get("wefter")?;
/* Wefter API module registration
*
* Early module registration occurs at `LuaInterpreter::new`
*/
let io = l.create_table_from(api::io_module(&l, tui.clone())?)?;
let template =
l.create_table_from(api::template_module(l, res.clone(), self.history.clone())?)?;
let txt = l.create_table_from(api::txt_module(&l)?)?;
// Register in global api table
wefter.set("io", io)?;
wefter.set("template", template)?;
wefter.set("txt", txt)?;
self.api_registered = true;
Ok(())
}
/// Run all the registered auto functions to tell which profiles
/// can be activated, returns profiles keys
pub fn run_auto(&mut self, res: &ResourceDirTable) -> Result<Vec<String>> {
res.iter()
// Get only ones with 'auto', keep only the path
.filter_map(|(k, v)| v.auto.clone().map(|e| (k.clone(), e)))
// Execute each auto.lua file
.map(|(k, p)| (k, self.exec::<bool>(&p)))
// Iter<K, Result<R, Err>> -> Iter<Result<(K, R), Err>>
.filter_map(|(k, r)| match r {
Result::Ok(true) => Some(Ok(k)),
Result::Ok(false) => None,
Result::Err(e) => Some(Err(e)),
})
.collect()
}
/// Run a configuration file
pub fn run_init(&mut self, res: &ResourceDir) -> Result<def::ProfileDef> {
// Check if file exists
if !res.init.is_file() {
return Err(WefterErr::NoSuchLuaFile(res.init.clone()).into());
}
// Get definition from init.lua
Ok(self.exec::<def::ProfileDef>(&res.init)?)
}
/// Execute a command given a profile definition (consumes interpreter)
pub fn exec_command(
self,
params: Vec<String>,
def: &ProfileDef,
) -> Result<HistoryRef, WefterErr> {
if !self.api_registered {
return Err(
WefterErr::ApplicationError("Interpreter not initialized!".to_string()).into(),
);
}
if params.is_empty() {
return Err(WefterErr::EmptyParameters.into());
}
// Reference to the current command definition
let mut cm: &CommandMap = &def.0;
// For each command
for (i, cmd) in params.iter().enumerate() {
// Get command definition
let def = cm
.get(cmd)
.ok_or_else(|| WefterErr::CommandNotFound(cmd.clone()))?;
// Call only the last command, previous commands are subcommands
let is_last = (i + 1) == params.len();
if is_last {
let exec = def.exec.clone().ok_or_else(|| {
WefterErr::MissingSubcommand(
cmd.clone(),
// During command parsing we make sure either exec or subcommand
// exists, so subcommand must exist
def.get_subcommands().unwrap(),
)
})?;
// Call function
exec.call::<()>(())
.map_err(|e| WefterErr::InterpreterError(e))?;
log::debug!("init.lua success for profile");
} else {
// Get list of subcommands, if the command does not have
// subcommands and is not the last command in list, then
// next subcommand is not valid
let subcommands =
def.subcommand
.as_ref()
.ok_or_else(|| WefterErr::SubcommandNotFound {
command: cmd.clone(),
// Get next command
subcommand: params[i + 1].clone(),
})?;
// Set next command list reference
cm = subcommands;
}
}
// Reference count should be zero at the end of this function
Ok(self.history)
}
}