1use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex};
7
8use indexmap::IndexMap;
9use vantage_core::{Result, error};
10
11use crate::rhai_engine::CompiledScript;
12
13#[derive(Clone, Debug)]
17pub struct CmdSpec {
18 pub script: Arc<str>,
19 pub detail: Option<Arc<str>>,
25 pub command: Option<String>,
26 pub env: IndexMap<String, String>,
27}
28
29impl CmdSpec {
30 pub fn new(script: impl Into<Arc<str>>) -> Self {
31 Self {
32 script: script.into(),
33 detail: None,
34 command: None,
35 env: IndexMap::new(),
36 }
37 }
38
39 pub fn with_detail(mut self, detail: impl Into<Arc<str>>) -> Self {
41 self.detail = Some(detail.into());
42 self
43 }
44
45 pub fn with_command(mut self, command: impl Into<String>) -> Self {
47 self.command = Some(command.into());
48 self
49 }
50
51 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
54 self.env.insert(key.into(), value.into());
55 self
56 }
57}
58
59#[derive(Clone)]
65pub struct Cmd {
66 command: Arc<str>,
67 env: Arc<IndexMap<String, String>>,
68 pass_path: bool,
69 base_dir: Option<Arc<Path>>,
70 scripts: Arc<IndexMap<String, CmdSpec>>,
71 compiled: Arc<Mutex<HashMap<String, Arc<CompiledScript>>>>,
75 compile_counts: Arc<Mutex<HashMap<String, usize>>>,
78}
79
80impl std::fmt::Debug for Cmd {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("Cmd")
83 .field("command", &self.command)
84 .field("env", &self.env)
85 .field("pass_path", &self.pass_path)
86 .field("base_dir", &self.base_dir)
87 .field("scripts", &self.scripts)
88 .finish()
89 }
90}
91
92impl Cmd {
93 pub fn new(command: impl Into<Arc<str>>) -> Self {
95 Self {
96 command: command.into(),
97 env: Arc::new(IndexMap::new()),
98 pass_path: true,
99 base_dir: None,
100 scripts: Arc::new(IndexMap::new()),
101 compiled: Arc::new(Mutex::new(HashMap::new())),
102 compile_counts: Arc::new(Mutex::new(HashMap::new())),
103 }
104 }
105
106 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
109 Arc::make_mut(&mut self.env).insert(key.into(), value.into());
110 self
111 }
112
113 pub fn with_pass_path(mut self, pass_path: bool) -> Self {
117 self.pass_path = pass_path;
118 self
119 }
120
121 pub fn with_base_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
131 self.base_dir = Some(Arc::from(base_dir.into()));
132 self
133 }
134
135 pub fn with_script(self, name: impl Into<String>, script: impl Into<Arc<str>>) -> Self {
137 self.with_table(name, CmdSpec::new(script))
138 }
139
140 pub fn with_table(mut self, name: impl Into<String>, spec: CmdSpec) -> Self {
142 Arc::make_mut(&mut self.scripts).insert(name.into(), spec);
143 self
144 }
145
146 pub fn command(&self) -> &str {
148 &self.command
149 }
150
151 pub(crate) fn pass_path(&self) -> bool {
152 self.pass_path
153 }
154
155 pub(crate) fn base_dir(&self) -> Option<Arc<Path>> {
156 self.base_dir.clone()
157 }
158
159 pub(crate) fn spec_for(&self, name: &str) -> Result<&CmdSpec> {
160 self.scripts.get(name).ok_or_else(|| {
161 error!(
162 "no command script registered for table",
163 table = name.to_string()
164 )
165 })
166 }
167
168 pub(crate) fn effective_command(&self, spec: &CmdSpec) -> String {
170 spec.command
171 .clone()
172 .unwrap_or_else(|| self.command.to_string())
173 }
174
175 pub(crate) fn effective_env(&self, spec: &CmdSpec) -> IndexMap<String, String> {
178 let mut env = (*self.env).clone();
179 for (k, v) in &spec.env {
180 env.insert(k.clone(), v.clone());
181 }
182 env
183 }
184
185 pub(crate) fn compiled_list_script(&self, name: &str) -> Result<Arc<CompiledScript>> {
188 let spec = self.spec_for(name)?.clone();
189 self.compiled_for(name.to_string(), &spec, &spec.script)
190 }
191
192 pub(crate) fn compiled_detail_script(&self, name: &str) -> Result<Option<Arc<CompiledScript>>> {
196 let spec = self.spec_for(name)?.clone();
197 let Some(detail) = spec.detail.clone() else {
198 return Ok(None);
199 };
200 Ok(Some(self.compiled_for(
201 format!("{name}::detail"),
202 &spec,
203 &detail,
204 )?))
205 }
206
207 fn compiled_for(
210 &self,
211 cache_key: String,
212 spec: &CmdSpec,
213 script: &str,
214 ) -> Result<Arc<CompiledScript>> {
215 let mut cache = self.compiled.lock().unwrap();
216 if let Some(existing) = cache.get(&cache_key) {
217 return Ok(existing.clone());
218 }
219 let command = self.effective_command(spec);
220 let env = self.effective_env(spec);
221 let compiled = Arc::new(CompiledScript::compile(
222 command,
223 env,
224 self.pass_path(),
225 self.base_dir(),
226 script,
227 )?);
228 *self
229 .compile_counts
230 .lock()
231 .unwrap()
232 .entry(cache_key.clone())
233 .or_insert(0) += 1;
234 cache.insert(cache_key, compiled.clone());
235 Ok(compiled)
236 }
237
238 pub(crate) fn has_detail_script(&self, name: &str) -> bool {
240 self.spec_for(name)
241 .map(|s| s.detail.is_some())
242 .unwrap_or(false)
243 }
244
245 pub fn compile_count(&self, name: &str) -> usize {
248 self.compile_counts
249 .lock()
250 .unwrap()
251 .get(name)
252 .copied()
253 .unwrap_or(0)
254 }
255
256 pub fn vista_factory(&self) -> crate::vista::factory::CmdVistaFactory {
258 crate::vista::factory::CmdVistaFactory::new(self.clone())
259 }
260}