1use crate::hooks::HookEvent;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[serde(default)]
11pub struct PluginConfig {
12 pub enabled: bool,
14
15 pub hook_timeout_ms: u64,
17
18 pub paths: Vec<String>,
20
21 pub continue_on_error: bool,
23
24 #[serde(default)]
26 pub hooks: HookConfig,
27
28 #[serde(skip)]
34 pub trust_local: bool,
35}
36
37impl PluginConfig {
38 pub fn new() -> Self {
40 PluginConfig {
41 enabled: true,
42 hook_timeout_ms: 5000,
43 paths: vec![],
44 continue_on_error: false,
45 hooks: HookConfig::default(),
46 trust_local: false,
47 }
48 }
49
50 pub fn local_plugins_dir(project_root: &std::path::Path) -> PathBuf {
52 project_root.join(".hx").join("plugins")
53 }
54
55 pub fn hook_timeout(&self) -> std::time::Duration {
57 std::time::Duration::from_millis(self.hook_timeout_ms)
58 }
59
60 pub fn all_paths(&self, project_root: &std::path::Path) -> Vec<PathBuf> {
65 let mut paths = Vec::new();
66
67 if self.trust_local {
69 paths.push(Self::local_plugins_dir(project_root));
70 }
71
72 for path in &self.paths {
74 let expanded = shellexpand::tilde(path);
75 paths.push(PathBuf::from(expanded.as_ref()));
76 }
77
78 if let Some(config_dir) = dirs::config_dir() {
80 paths.push(config_dir.join("hx").join("plugins"));
81 }
82
83 paths
84 }
85
86 pub fn scripts_for_hook(&self, event: HookEvent) -> &[String] {
88 match event {
89 HookEvent::PreBuild => &self.hooks.pre_build,
90 HookEvent::PostBuild => &self.hooks.post_build,
91 HookEvent::PreTest => &self.hooks.pre_test,
92 HookEvent::PostTest => &self.hooks.post_test,
93 HookEvent::PreRun => &self.hooks.pre_run,
94 HookEvent::PostRun => &self.hooks.post_run,
95 HookEvent::PreClean => &self.hooks.pre_clean,
96 HookEvent::PostClean => &self.hooks.post_clean,
97 HookEvent::PreLock => &self.hooks.pre_lock,
98 HookEvent::PostLock => &self.hooks.post_lock,
99 HookEvent::Init => &self.hooks.init,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106#[serde(default)]
107pub struct HookConfig {
108 pub pre_build: Vec<String>,
110
111 pub post_build: Vec<String>,
113
114 pub pre_test: Vec<String>,
116
117 pub post_test: Vec<String>,
119
120 pub pre_run: Vec<String>,
122
123 pub post_run: Vec<String>,
125
126 pub pre_clean: Vec<String>,
128
129 pub post_clean: Vec<String>,
131
132 pub pre_lock: Vec<String>,
134
135 pub post_lock: Vec<String>,
137
138 pub init: Vec<String>,
140}
141
142impl HookConfig {
143 pub fn has_any_hooks(&self) -> bool {
145 !self.pre_build.is_empty()
146 || !self.post_build.is_empty()
147 || !self.pre_test.is_empty()
148 || !self.post_test.is_empty()
149 || !self.pre_run.is_empty()
150 || !self.post_run.is_empty()
151 || !self.pre_clean.is_empty()
152 || !self.post_clean.is_empty()
153 || !self.pre_lock.is_empty()
154 || !self.post_lock.is_empty()
155 || !self.init.is_empty()
156 }
157
158 pub fn as_map(&self) -> HashMap<HookEvent, &[String]> {
160 let mut map = HashMap::new();
161 map.insert(HookEvent::PreBuild, self.pre_build.as_slice());
162 map.insert(HookEvent::PostBuild, self.post_build.as_slice());
163 map.insert(HookEvent::PreTest, self.pre_test.as_slice());
164 map.insert(HookEvent::PostTest, self.post_test.as_slice());
165 map.insert(HookEvent::PreRun, self.pre_run.as_slice());
166 map.insert(HookEvent::PostRun, self.post_run.as_slice());
167 map.insert(HookEvent::PreClean, self.pre_clean.as_slice());
168 map.insert(HookEvent::PostClean, self.post_clean.as_slice());
169 map.insert(HookEvent::PreLock, self.pre_lock.as_slice());
170 map.insert(HookEvent::PostLock, self.post_lock.as_slice());
171 map.insert(HookEvent::Init, self.init.as_slice());
172 map
173 }
174}
175
176mod dirs {
178 use std::path::PathBuf;
179
180 pub fn config_dir() -> Option<PathBuf> {
181 directories::BaseDirs::new().map(|dirs| dirs.config_dir().to_path_buf())
182 }
183}
184
185mod shellexpand {
187 use std::borrow::Cow;
188
189 pub fn tilde(path: &str) -> Cow<'_, str> {
190 if path.starts_with("~/")
191 && let Some(home) = directories::BaseDirs::new()
192 {
193 let home_str = home.home_dir().to_string_lossy();
194 return Cow::Owned(format!("{}{}", home_str, &path[1..]));
195 }
196 Cow::Borrowed(path)
197 }
198}
199
200impl From<hx_config::PluginConfig> for PluginConfig {
202 fn from(config: hx_config::PluginConfig) -> Self {
203 PluginConfig {
204 enabled: config.enabled,
205 hook_timeout_ms: config.hook_timeout_ms,
206 paths: config.paths,
207 continue_on_error: config.continue_on_error,
208 hooks: HookConfig::from(config.hooks),
209 trust_local: false,
210 }
211 }
212}
213
214impl From<hx_config::PluginHookConfig> for HookConfig {
215 fn from(hooks: hx_config::PluginHookConfig) -> Self {
216 HookConfig {
217 pre_build: hooks.pre_build,
218 post_build: hooks.post_build,
219 pre_test: hooks.pre_test,
220 post_test: hooks.post_test,
221 pre_run: hooks.pre_run,
222 post_run: hooks.post_run,
223 pre_clean: hooks.pre_clean,
224 post_clean: hooks.post_clean,
225 pre_lock: hooks.pre_lock,
226 post_lock: hooks.post_lock,
227 init: hooks.init,
228 }
229 }
230}