Skip to main content

kasl/commands/
template.rs

1//! Task template management command.
2//!
3//! Provides comprehensive template management functionality for kasl, enabling users to create, edit, delete, and search reusable task templates.
4//!
5//! ## Usage
6//!
7//! ```bash
8//! # List all templates
9//! kasl template list
10//!
11//! # Create new template
12//! kasl template add --name "bug-fix"
13//!
14//! # Search templates
15//! kasl template search "development"
16//!
17//! # Delete template
18//! kasl template remove "old-template"
19//! ```
20
21use 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/// Command-line arguments for template management operations.
31#[derive(Debug, Args)]
32pub struct TemplateArgs {
33    #[command(subcommand)]
34    command: Option<TemplateCommand>,
35}
36
37/// Available template management operations.
38#[derive(Debug, Subcommand)]
39enum TemplateCommand {
40    /// Add a new task template
41    ///
42    /// Creates a new reusable template with specified or interactive values.
43    /// Templates provide default values for task creation, streamlining
44    /// workflows for frequently created task types.
45    Add {
46        /// Unique name identifier for the template
47        ///
48        /// Must be unique across all templates and should be descriptive
49        /// enough to easily identify the template's purpose. Used for
50        /// referencing the template in task creation commands.
51        #[arg(short, long)]
52        name: Option<String>,
53    },
54
55    /// List all available templates
56    ///
57    /// Displays a formatted table of all existing templates with their
58    /// names, task names, comments, and default completion values.
59    /// Useful for reviewing available templates and their configurations.
60    List,
61
62    /// Show a single template's contents
63    ///
64    /// Displays the task name, comment and default completeness stored in
65    /// the template, so its effect on task creation is visible before use.
66    Show {
67        /// Name of the template to show
68        name: Option<String>,
69    },
70
71    /// Edit an existing template
72    ///
73    /// Modifies an existing template's properties including task name,
74    /// comment, and completion status. Provides interactive interface
75    /// for template selection if name is not specified.
76    Edit {
77        /// Name of the template to edit
78        ///
79        /// If not provided, an interactive selection interface will be
80        /// presented with all available templates.
81        name: Option<String>,
82    },
83
84    /// Remove a template
85    ///
86    /// Permanently removes a template from the system. Includes confirmation
87    /// prompt to prevent accidental removal. Removing a template does not
88    /// affect tasks that were previously created from it.
89    Remove {
90        /// Name of the template to remove
91        ///
92        /// If not provided, an interactive selection interface will be
93        /// presented with all available templates.
94        name: Option<String>,
95
96        /// Remove without asking for confirmation
97        #[arg(long, short = 'y')]
98        yes: bool,
99    },
100
101    /// Search templates by name or content
102    ///
103    /// Performs a text search across template names and task names,
104    /// returning all matching templates. Useful for finding templates
105    /// in large template libraries.
106    Search {
107        /// Search query string
108        ///
109        /// Searches both template names and task names for matches.
110        /// Case-insensitive partial matching is supported.
111        query: String,
112    },
113}
114
115/// Executes template management operations based on the specified subcommand.
116///
117/// # Examples
118///
119/// ```bash
120/// # Create a new template interactively
121/// kasl template add
122///
123/// # Create template with specific name
124/// kasl template add --name daily-standup
125///
126/// # List all templates
127/// kasl template list
128///
129/// # Edit a template
130/// kasl template edit daily-standup
131///
132/// # Search for templates
133/// kasl template search meeting
134///
135/// # Interactive mode
136/// kasl template
137/// ```
138pub 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
153/// Handles template creation with validation and uniqueness checking.
154fn handle_create(name: Option<String>) -> Result<()> {
155    let mut templates_db = Templates::new()?;
156
157    // Get template name (from args or interactive prompt)
158    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    // Validate template name uniqueness
166    if templates_db.exists(&name)? {
167        msg_error!(Message::TemplateAlreadyExists(name));
168        return Ok(());
169    }
170
171    // Collect template properties interactively
172    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    // Create and save the template
191    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
198/// Displays all available templates in a formatted table.
199fn 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
213/// Displays a single template's stored values.
214///
215/// Reuses the same table rendering as `list` so a template reads identically
216/// whether shown alone or among others. Without a name, the template is picked
217/// interactively.
218fn 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
247/// Handles template editing with interactive or direct name specification.
248fn handle_edit(name: Option<String>) -> Result<()> {
249    let mut templates_db = Templates::new()?;
250
251    // Get template name (direct or interactive selection)
252    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    // Fetch the template to edit
266    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    // Interactive editing with current values as defaults
277    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    // Update the template
298    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
305/// Handles safe template deletion with confirmation.
306fn handle_delete(name: Option<String>, assume_yes: bool) -> Result<()> {
307    let mut templates_db = Templates::new()?;
308
309    // Get template name (direct or interactive selection)
310    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        // Never block on a prompt when there is no one to answer it.
327        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
346/// Handles template search functionality.
347fn 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
361/// Handles interactive template management when no subcommand is provided.
362fn 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}