1use super::errors::{
2 Result, err_dependency_cycle, err_duplicated_task, err_task_not_found, err_unexpected_node,
3 err_zero_or_one_attribute_allowed,
4};
5use crate::command::execute;
6use antex::{ColorMode, StyledText, Text};
7use petgraph::Graph;
8use petgraph::algo::toposort;
9use petgraph::graph::NodeIndex;
10use std::collections::{BTreeMap, HashMap};
11use std::process::ExitStatus;
12
13#[derive(Debug, Clone)]
15pub enum Action {
16 Command(
17 String,
19 String,
21 ),
22 Task(
23 String,
25 ),
26}
27
28#[derive(Debug, Default, Clone)]
30pub struct Task {
31 name: String,
33 description: Option<String>,
35 actions: Vec<Action>,
37 commands: Vec<(String, String)>,
39}
40
41impl Task {
42 #[must_use]
44 pub fn new(name: String) -> Self {
45 Self {
46 name,
47 ..Default::default()
48 }
49 }
50
51 #[must_use]
53 pub fn get_name(&self) -> &str {
54 &self.name
55 }
56
57 #[must_use]
59 pub const fn get_description(&self) -> &Option<String> {
60 &self.description
61 }
62
63 pub fn set_description(&mut self, description: String) {
65 self.description = Some(description);
66 }
67
68 pub fn add_command(&mut self, command: String, dir: String) {
70 self.actions.push(Action::Command(command, dir));
71 }
72
73 pub fn add_task(&mut self, task: String) {
75 self.actions.push(Action::Task(task));
76 }
77}
78
79#[derive(Debug)]
81pub struct Tasks {
82 tasks: BTreeMap<String, Task>,
83 variables: HashMap<String, String>,
84}
85
86impl Tasks {
87 pub fn new(definitions: &str) -> Result<Self> {
97 let mut tasks = Self::parse_definitions(definitions)?;
98 let ordered_names = Self::sort_tasks_topologically(&tasks)?;
99 for name in &ordered_names {
100 let mut commands = vec![];
101 let task = tasks.tasks.get(name).unwrap(); for action in &task.actions {
103 match action {
104 Action::Command(command, dir) => {
105 commands.push((interpolate(command, &tasks.variables), dir.clone()));
106 }
107 Action::Task(name) => {
108 let subtask = tasks.tasks.get(name).unwrap(); commands.append(&mut subtask.commands.clone());
110 }
111 }
112 }
113 let task = tasks.tasks.get_mut(name).unwrap(); task.commands.clear();
115 task.commands.append(&mut commands);
116 }
117 Ok(tasks)
118 }
119
120 pub fn execute(&self, name: &str) -> Result<ExitStatus> {
126 let mut exit_status = ExitStatus::default();
127 if let Some(task) = self.tasks.get(name) {
128 for (command, dir) in &task.commands {
129 exit_status = execute(command, dir)?;
130 if !exit_status.success() {
131 break;
132 }
133 }
134 } else {
135 return Err(err_task_not_found(name.to_string()));
136 }
137 Ok(exit_status)
138 }
139
140 pub fn list(&self, cm: ColorMode) {
142 let width = self.tasks.keys().map(String::len).max().unwrap_or_default();
143 println!(
144 "{}",
145 Text::new(cm)
146 .bold()
147 .underline()
148 .s("Available tasks")
149 .reset()
150 );
151 for task in self.tasks.values() {
152 let name = task.get_name();
153 if let Some(description) = task.get_description() {
154 print!(
155 " {} {}",
156 Text::new(cm).bright_green().align_left(name, width).reset(),
157 description.lines().take(1).collect::<String>()
158 );
159 } else {
160 print!(" {}", Text::new(cm).bright_green().s(name).reset());
161 }
162 println!();
163 }
164 }
165
166 fn parse_definitions(definitions: &str) -> Result<Self> {
168 let mut tasks = Self {
169 tasks: BTreeMap::new(),
170 variables: HashMap::new(),
171 };
172 let root_node = diml::parse(definitions).expect("failed to parse the task file");
173 for top_level_node in root_node.children() {
174 match top_level_node.name() {
175 "note" => {} "vars" => {
177 for variable_node in top_level_node.children() {
179 for task_attribute_node in variable_node.children() {
180 match task_attribute_node.name() {
181 "note" => {} other => return Err(err_unexpected_node(other.to_string())),
183 }
184 }
185 tasks.variables.insert(
186 variable_node.name().to_string(),
187 variable_node.content().trim().to_string(),
188 );
189 }
190 }
191 "tasks" => {
192 for task_node in top_level_node.children() {
194 let task_name = task_node.name().to_string();
195 if tasks.tasks.contains_key(&task_name) {
196 return Err(err_duplicated_task(task_name));
197 }
198 let mut task = Task::new(task_name.clone());
199 for task_attribute_node in task_node.children() {
200 match task_attribute_node.name() {
201 "note" => {} "desc" => {
203 task.set_description(
204 task_attribute_node.content().trim().to_string(),
205 );
206 }
207 "task" => {
208 task.add_task(task_attribute_node.content().trim().to_string());
209 }
210 "cmd" => {
211 let command_text = task_attribute_node.content().trim();
212 let mut dir = None;
213 for command_attribute_node in task_attribute_node.children() {
214 match command_attribute_node.name() {
215 "note" => {} v @ "dir" if dir.is_some() => {
217 return Err(err_zero_or_one_attribute_allowed(
218 v.to_string(),
219 ));
220 }
221 "dir" => {
222 dir = Some(
223 command_attribute_node
224 .content()
225 .trim()
226 .to_string(),
227 );
228 }
229 other => {
230 return Err(err_unexpected_node(other.to_string()));
231 }
232 }
233 }
234 task.add_command(
235 command_text.to_string(),
236 dir.unwrap_or_else(|| ".".to_string()),
237 );
238 }
239 other => return Err(err_unexpected_node(other.to_string())),
240 }
241 }
242 tasks.tasks.insert(task_name.clone(), task);
243 }
244 }
245 other => return Err(err_unexpected_node(other.to_string())),
246 }
247 }
248 Ok(tasks)
249 }
250
251 fn sort_tasks_topologically(tasks: &Self) -> Result<Vec<String>> {
253 let mut graph = Graph::<String, ()>::new();
254 let mut name_by_index: HashMap<NodeIndex, String> = HashMap::new();
255 let mut index_by_name: HashMap<String, NodeIndex> = HashMap::new();
256 for (name, _) in tasks {
258 let node_index = graph.add_node(name.to_owned());
259 name_by_index.insert(node_index, name.to_owned());
260 index_by_name.insert(name.to_owned(), node_index);
261 }
262 for (name, task) in tasks {
264 let parent_index = index_by_name.get(name).copied().unwrap();
265 for action in &task.actions {
266 if let Action::Task(name) = action {
267 let Some(child_index) = index_by_name.get(name).copied() else {
268 return Err(err_task_not_found(name.to_owned()));
269 };
270 graph.add_edge(child_index, parent_index, ());
271 }
272 }
273 }
274 match toposort(&graph, None) {
276 Ok(indexes) => Ok(indexes
277 .iter()
278 .map(|index| name_by_index.get(index).cloned().unwrap())
279 .collect()),
280 Err(cycle) => Err(err_dependency_cycle(
281 name_by_index.get(&cycle.node_id()).cloned().unwrap(),
282 )),
283 }
284 }
285
286 fn iter(&self) -> std::collections::btree_map::Iter<'_, String, Task> {
287 self.tasks.iter()
288 }
289}
290
291fn interpolate(input: &str, global_variables: &HashMap<String, String>) -> String {
293 let mut interpolated = input.to_string();
294 for (name, value) in global_variables {
295 let pattern = format!("{{{{{name}}}}}");
296 interpolated = interpolated.replace(&pattern, value);
297 }
298 interpolated
299}
300
301impl<'a> IntoIterator for &'a Tasks {
302 type Item = (&'a String, &'a Task);
303 type IntoIter = std::collections::btree_map::Iter<'a, String, Task>;
304
305 fn into_iter(self) -> Self::IntoIter {
306 self.iter()
307 }
308}