kasl/commands/
template.rs1use crate::{
22 db::templates::{TaskTemplate, Templates},
23 libs::{messages::Message, pick, prompt::ensure_interactive, view::View},
24 msg_error, msg_info, msg_print, msg_success,
25};
26use anyhow::Result;
27use clap::{Args, Subcommand};
28use dialoguer::{Confirm, Input, Select, theme::ColorfulTheme};
29
30#[derive(Debug, Args)]
32pub struct TemplateArgs {
33 #[command(subcommand)]
34 command: Option<TemplateCommand>,
35}
36
37#[derive(Debug, Subcommand)]
39enum TemplateCommand {
40 Add {
46 #[arg(short, long)]
52 name: Option<String>,
53 },
54
55 List,
61
62 Show {
67 name: Option<String>,
69 },
70
71 Edit {
77 name: Option<String>,
82 },
83
84 Remove {
90 name: Option<String>,
95
96 #[arg(long, short = 'y')]
98 yes: bool,
99 },
100
101 Search {
107 query: String,
112 },
113}
114
115pub fn cmd(args: TemplateArgs) -> Result<()> {
139 match args.command {
140 Some(TemplateCommand::Add { name }) => handle_create(name),
141 Some(TemplateCommand::List) => handle_list(),
142 Some(TemplateCommand::Show { name }) => handle_show(name),
143 Some(TemplateCommand::Edit { name }) => handle_edit(name),
144 Some(TemplateCommand::Remove { name, yes }) => handle_delete(name, yes),
145 Some(TemplateCommand::Search { query }) => handle_search(query),
146 None => {
147 ensure_interactive("no subcommand given; run `kasl template list` or see `kasl template --help`")?;
148 handle_interactive()
149 }
150 }
151}
152
153fn handle_create(name: Option<String>) -> Result<()> {
155 let mut templates_db = Templates::new()?;
156
157 let name = name.unwrap_or_else(|| {
159 Input::with_theme(&ColorfulTheme::default())
160 .with_prompt(Message::PromptTemplateName.to_string())
161 .interact_text()
162 .unwrap()
163 });
164
165 if templates_db.exists(&name)? {
167 msg_error!(Message::TemplateAlreadyExists(name));
168 return Ok(());
169 }
170
171 let task_name = Input::with_theme(&ColorfulTheme::default())
173 .with_prompt(Message::PromptTemplateTaskName.to_string())
174 .interact_text()?;
175
176 let comment = Input::with_theme(&ColorfulTheme::default())
177 .with_prompt(Message::PromptTemplateComment.to_string())
178 .allow_empty(true)
179 .interact_text()?;
180
181 let completeness_range_msg = Message::TaskCompletenessRange.to_string();
182 let completeness = Input::with_theme(&ColorfulTheme::default())
183 .with_prompt(Message::PromptTemplateCompleteness.to_string())
184 .default(100)
185 .validate_with(|input: &i32| -> Result<(), &str> {
186 if *input >= 0 && *input <= 100 { Ok(()) } else { Err(&completeness_range_msg) }
187 })
188 .interact_text()?;
189
190 let template = TaskTemplate::new(name.clone(), task_name, comment, completeness);
192 templates_db.create(&template)?;
193
194 msg_success!(Message::TemplateCreated(name));
195 Ok(())
196}
197
198fn handle_list() -> Result<()> {
200 let mut templates_db = Templates::new()?;
201 let templates = templates_db.get_all()?;
202
203 if templates.is_empty() {
204 msg_info!(Message::NoTemplatesFound);
205 return Ok(());
206 }
207
208 msg_print!(Message::TemplateListHeader, true);
209 View::templates(&templates)?;
210 Ok(())
211}
212
213fn handle_show(name: Option<String>) -> Result<()> {
219 let mut templates_db = Templates::new()?;
220
221 let name = match name {
222 Some(n) => n,
223 None => {
224 ensure_interactive("template name is required outside an interactive terminal")?;
225
226 let templates = templates_db.get_all()?;
227 if templates.is_empty() {
228 msg_info!(Message::NoTemplatesFound);
229 return Ok(());
230 }
231
232 pick::template(&templates, &Message::SelectTemplate.to_string())?
233 }
234 };
235
236 match templates_db.get(&name)? {
237 Some(template) => {
238 msg_print!(Message::TemplateListHeader, true);
239 View::templates(&[template])?;
240 }
241 None => msg_error!(Message::TemplateNotFound(name)),
242 }
243
244 Ok(())
245}
246
247fn handle_edit(name: Option<String>) -> Result<()> {
249 let mut templates_db = Templates::new()?;
250
251 let name = match name {
253 Some(n) => n,
254 None => {
255 let templates = templates_db.get_all()?;
256 if templates.is_empty() {
257 msg_info!(Message::NoTemplatesFound);
258 return Ok(());
259 }
260
261 pick::template(&templates, &Message::SelectTemplateToEdit.to_string())?
262 }
263 };
264
265 let template = match templates_db.get(&name)? {
267 Some(t) => t,
268 None => {
269 msg_error!(Message::TemplateNotFound(name));
270 return Ok(());
271 }
272 };
273
274 msg_print!(Message::EditingTemplate(template.name.clone()), true);
275
276 let task_name = Input::with_theme(&ColorfulTheme::default())
278 .with_prompt(Message::PromptTemplateTaskName.to_string())
279 .default(template.task_name.clone())
280 .interact_text()?;
281
282 let comment = Input::with_theme(&ColorfulTheme::default())
283 .with_prompt(Message::PromptTemplateComment.to_string())
284 .default(template.comment.clone())
285 .allow_empty(true)
286 .interact_text()?;
287
288 let completeness_range_msg = Message::TaskCompletenessRange.to_string();
289 let completeness = Input::with_theme(&ColorfulTheme::default())
290 .with_prompt(Message::PromptTemplateCompleteness.to_string())
291 .default(template.completeness)
292 .validate_with(|input: &i32| -> Result<(), &str> {
293 if *input >= 0 && *input <= 100 { Ok(()) } else { Err(&completeness_range_msg) }
294 })
295 .interact_text()?;
296
297 let updated_template = TaskTemplate::new(name.clone(), task_name, comment, completeness);
299 templates_db.update(&updated_template)?;
300
301 msg_success!(Message::TemplateUpdated(name));
302 Ok(())
303}
304
305fn handle_delete(name: Option<String>, assume_yes: bool) -> Result<()> {
307 let mut templates_db = Templates::new()?;
308
309 let name = match name {
311 Some(n) => n,
312 None => {
313 ensure_interactive("template name is required outside an interactive terminal")?;
314
315 let templates = templates_db.get_all()?;
316 if templates.is_empty() {
317 msg_info!(Message::NoTemplatesFound);
318 return Ok(());
319 }
320
321 pick::template(&templates, &Message::SelectTemplateToDelete.to_string())?
322 }
323 };
324
325 if !assume_yes {
326 ensure_interactive(&format!("refusing to remove template '{}' without --yes outside an interactive terminal", name))?;
328
329 let confirmed = Confirm::with_theme(&ColorfulTheme::default())
330 .with_prompt(Message::ConfirmDeleteTemplate(name.clone()).to_string())
331 .default(false)
332 .interact()?;
333
334 if !confirmed {
335 msg_info!(Message::OperationCancelled);
336 return Ok(());
337 }
338 }
339
340 templates_db.delete(&name)?;
341 msg_success!(Message::TemplateDeleted(name));
342
343 Ok(())
344}
345
346fn handle_search(query: String) -> Result<()> {
348 let mut templates_db = Templates::new()?;
349 let templates = templates_db.search(&query)?;
350
351 if templates.is_empty() {
352 msg_info!(Message::NoTemplatesMatchingQuery(query));
353 return Ok(());
354 }
355
356 msg_print!(Message::TemplateSearchResults(query), true);
357 View::templates(&templates)?;
358 Ok(())
359}
360
361fn handle_interactive() -> Result<()> {
363 let options = vec!["Add new template", "List templates", "Show template", "Edit template", "Remove template"];
364
365 let selection = Select::with_theme(&ColorfulTheme::default())
366 .with_prompt(Message::SelectTemplateAction.to_string())
367 .items(&options)
368 .interact()?;
369
370 match selection {
371 0 => handle_create(None),
372 1 => handle_list(),
373 2 => handle_show(None),
374 3 => handle_edit(None),
375 4 => handle_delete(None, false),
376 _ => Ok(()),
377 }
378}