1use crate::{
22 db::templates::{TaskTemplate, Templates},
23 libs::{
24 messages::Message,
25 pick,
26 prompt::{ensure_interactive, is_interactive},
27 view::View,
28 },
29 msg_error, msg_info, msg_print, msg_success,
30};
31use anyhow::Result;
32use clap::{Args, Subcommand};
33use dialoguer::{Confirm, Input, Select, theme::ColorfulTheme};
34
35#[derive(Debug, Args)]
37pub struct TemplateArgs {
38 #[command(subcommand)]
39 command: Option<TemplateCommand>,
40}
41
42#[derive(Debug, Subcommand)]
44enum TemplateCommand {
45 Add {
51 #[arg(short, long)]
57 name: Option<String>,
58
59 #[arg(short = 't', long)]
65 task_name: Option<String>,
66
67 #[arg(long)]
69 comment: Option<String>,
70
71 #[arg(short, long)]
73 completeness: Option<i32>,
74 },
75
76 List,
82
83 Show {
88 name: Option<String>,
90 },
91
92 Edit {
98 name: Option<String>,
103 },
104
105 Remove {
111 name: Option<String>,
116
117 #[arg(long, short = 'y')]
119 yes: bool,
120 },
121
122 Search {
128 query: String,
133 },
134}
135
136pub fn cmd(args: TemplateArgs) -> Result<()> {
160 match args.command {
161 Some(TemplateCommand::Add {
162 name,
163 task_name,
164 comment,
165 completeness,
166 }) => handle_create(name, task_name, comment, completeness),
167 Some(TemplateCommand::List) => handle_list(),
168 Some(TemplateCommand::Show { name }) => handle_show(name),
169 Some(TemplateCommand::Edit { name }) => handle_edit(name),
170 Some(TemplateCommand::Remove { name, yes }) => handle_delete(name, yes),
171 Some(TemplateCommand::Search { query }) => handle_search(query),
172 None => {
173 ensure_interactive("no subcommand given; run `kasl template list` or see `kasl template --help`")?;
174 handle_interactive()
175 }
176 }
177}
178
179fn handle_create(name: Option<String>, task_name: Option<String>, comment: Option<String>, completeness: Option<i32>) -> Result<()> {
186 let mut templates_db = Templates::new()?;
187
188 if name.is_none() {
189 ensure_interactive("template name is required; pass --name outside an interactive terminal")?;
190 }
191
192 let name = match name {
193 Some(n) => n,
194 None => Input::with_theme(&ColorfulTheme::default())
195 .with_prompt(Message::PromptTemplateName.to_string())
196 .interact_text()?,
197 };
198
199 if templates_db.exists(&name)? {
201 msg_error!(Message::TemplateAlreadyExists(name));
202 return Ok(());
203 }
204
205 let interactive = is_interactive();
206
207 let task_name = match task_name {
208 Some(t) => t,
209 None if !interactive => name.clone(),
212 None => Input::with_theme(&ColorfulTheme::default())
213 .with_prompt(Message::PromptTemplateTaskName.to_string())
214 .interact_text()?,
215 };
216
217 let comment = match comment {
218 Some(c) => c,
219 None if !interactive => String::new(),
220 None => Input::with_theme(&ColorfulTheme::default())
221 .with_prompt(Message::PromptTemplateComment.to_string())
222 .allow_empty(true)
223 .interact_text()?,
224 };
225
226 let completeness_range_msg = Message::TaskCompletenessRange.to_string();
227 let completeness = match completeness {
228 Some(c) => {
229 if !(0..=100).contains(&c) {
230 msg_error!(Message::TaskCompletenessRange);
231 return Ok(());
232 }
233 c
234 }
235 None if !interactive => 100,
236 None => Input::with_theme(&ColorfulTheme::default())
237 .with_prompt(Message::PromptTemplateCompleteness.to_string())
238 .default(100)
239 .validate_with(|input: &i32| -> Result<(), &str> {
240 if *input >= 0 && *input <= 100 { Ok(()) } else { Err(&completeness_range_msg) }
241 })
242 .interact_text()?,
243 };
244
245 let template = TaskTemplate::new(name.clone(), task_name, comment, completeness);
247 templates_db.create(&template)?;
248
249 msg_success!(Message::TemplateCreated(name));
250 Ok(())
251}
252
253fn handle_list() -> Result<()> {
255 let mut templates_db = Templates::new()?;
256 let templates = templates_db.get_all()?;
257
258 if templates.is_empty() {
259 msg_info!(Message::NoTemplatesFound);
260 return Ok(());
261 }
262
263 msg_print!(Message::TemplateListHeader, true);
264 View::templates(&templates)?;
265 Ok(())
266}
267
268fn handle_show(name: Option<String>) -> Result<()> {
274 let mut templates_db = Templates::new()?;
275
276 let name = match name {
277 Some(n) => n,
278 None => {
279 ensure_interactive("template name is required outside an interactive terminal")?;
280
281 let templates = templates_db.get_all()?;
282 if templates.is_empty() {
283 msg_info!(Message::NoTemplatesFound);
284 return Ok(());
285 }
286
287 pick::template(&templates, &Message::SelectTemplate.to_string())?
288 }
289 };
290
291 match templates_db.get(&name)? {
292 Some(template) => {
293 msg_print!(Message::TemplateListHeader, true);
294 View::templates(&[template])?;
295 }
296 None => msg_error!(Message::TemplateNotFound(name)),
297 }
298
299 Ok(())
300}
301
302fn handle_edit(name: Option<String>) -> Result<()> {
304 let mut templates_db = Templates::new()?;
305
306 let name = match name {
308 Some(n) => n,
309 None => {
310 let templates = templates_db.get_all()?;
311 if templates.is_empty() {
312 msg_info!(Message::NoTemplatesFound);
313 return Ok(());
314 }
315
316 pick::template(&templates, &Message::SelectTemplateToEdit.to_string())?
317 }
318 };
319
320 let template = match templates_db.get(&name)? {
322 Some(t) => t,
323 None => {
324 msg_error!(Message::TemplateNotFound(name));
325 return Ok(());
326 }
327 };
328
329 msg_print!(Message::EditingTemplate(template.name.clone()), true);
330
331 let task_name = Input::with_theme(&ColorfulTheme::default())
333 .with_prompt(Message::PromptTemplateTaskName.to_string())
334 .default(template.task_name.clone())
335 .interact_text()?;
336
337 let comment = Input::with_theme(&ColorfulTheme::default())
338 .with_prompt(Message::PromptTemplateComment.to_string())
339 .default(template.comment.clone())
340 .allow_empty(true)
341 .interact_text()?;
342
343 let completeness_range_msg = Message::TaskCompletenessRange.to_string();
344 let completeness = Input::with_theme(&ColorfulTheme::default())
345 .with_prompt(Message::PromptTemplateCompleteness.to_string())
346 .default(template.completeness)
347 .validate_with(|input: &i32| -> Result<(), &str> {
348 if *input >= 0 && *input <= 100 { Ok(()) } else { Err(&completeness_range_msg) }
349 })
350 .interact_text()?;
351
352 let updated_template = TaskTemplate::new(name.clone(), task_name, comment, completeness);
354 templates_db.update(&updated_template)?;
355
356 msg_success!(Message::TemplateUpdated(name));
357 Ok(())
358}
359
360fn handle_delete(name: Option<String>, assume_yes: bool) -> Result<()> {
362 let mut templates_db = Templates::new()?;
363
364 let name = match name {
366 Some(n) => n,
367 None => {
368 ensure_interactive("template name is required outside an interactive terminal")?;
369
370 let templates = templates_db.get_all()?;
371 if templates.is_empty() {
372 msg_info!(Message::NoTemplatesFound);
373 return Ok(());
374 }
375
376 pick::template(&templates, &Message::SelectTemplateToDelete.to_string())?
377 }
378 };
379
380 if !assume_yes {
381 ensure_interactive(&format!("refusing to remove template '{}' without --yes outside an interactive terminal", name))?;
383
384 let confirmed = Confirm::with_theme(&ColorfulTheme::default())
385 .with_prompt(Message::ConfirmDeleteTemplate(name.clone()).to_string())
386 .default(false)
387 .interact()?;
388
389 if !confirmed {
390 msg_info!(Message::OperationCancelled);
391 return Ok(());
392 }
393 }
394
395 templates_db.delete(&name)?;
396 msg_success!(Message::TemplateDeleted(name));
397
398 Ok(())
399}
400
401fn handle_search(query: String) -> Result<()> {
403 let mut templates_db = Templates::new()?;
404 let templates = templates_db.search(&query)?;
405
406 if templates.is_empty() {
407 msg_info!(Message::NoTemplatesMatchingQuery(query));
408 return Ok(());
409 }
410
411 msg_print!(Message::TemplateSearchResults(query), true);
412 View::templates(&templates)?;
413 Ok(())
414}
415
416fn handle_interactive() -> Result<()> {
418 let options = vec!["Add new template", "List templates", "Show template", "Edit template", "Remove template"];
419
420 let selection = Select::with_theme(&ColorfulTheme::default())
421 .with_prompt(Message::SelectTemplateAction.to_string())
422 .items(&options)
423 .interact()?;
424
425 match selection {
426 0 => handle_create(None, None, None, None),
427 1 => handle_list(),
428 2 => handle_show(None),
429 3 => handle_edit(None),
430 4 => handle_delete(None, false),
431 _ => Ok(()),
432 }
433}