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//! ## Features
6//!
7//! - **Template CRUD**: Create, read, update, and delete operations
8//! - **Search Functionality**: Find templates by name or content
9//! - **Interactive Management**: User-friendly interfaces for all operations
10//! - **Validation**: Ensures template data integrity and uniqueness
11//! - **Integration**: Seamless integration with task creation workflows
12//!
13//! ## Usage
14//!
15//! ```bash
16//! # List all templates
17//! kasl template list
18//!
19//! # Create new template
20//! kasl template add --name "bug-fix"
21//!
22//! # Search templates
23//! kasl template search "development"
24//!
25//! # Delete template
26//! kasl template remove "old-template"
27//! ```
28
29use crate::{
30    db::templates::{TaskTemplate, Templates},
31    libs::{messages::Message, prompt::ensure_interactive, view::View},
32    msg_error, msg_info, msg_print, msg_success,
33};
34use anyhow::Result;
35use clap::{Args, Subcommand};
36use dialoguer::{Confirm, Input, Select, theme::ColorfulTheme};
37
38/// Command-line arguments for template management operations.
39///
40/// The template command uses subcommands to organize different template
41/// management operations, providing a clean and intuitive interface for
42/// users to manage their template library.
43#[derive(Debug, Args)]
44pub struct TemplateArgs {
45    #[command(subcommand)]
46    command: Option<TemplateCommand>,
47}
48
49/// Available template management operations.
50///
51/// Each subcommand provides specific functionality for template lifecycle
52/// management, from creation through deletion, with support for both
53/// direct command-line usage and interactive operation.
54#[derive(Debug, Subcommand)]
55enum TemplateCommand {
56    /// Add a new task template
57    ///
58    /// Creates a new reusable template with specified or interactive values.
59    /// Templates provide default values for task creation, streamlining
60    /// workflows for frequently created task types.
61    Add {
62        /// Unique name identifier for the template
63        ///
64        /// Must be unique across all templates and should be descriptive
65        /// enough to easily identify the template's purpose. Used for
66        /// referencing the template in task creation commands.
67        #[arg(short, long)]
68        name: Option<String>,
69    },
70
71    /// List all available templates
72    ///
73    /// Displays a formatted table of all existing templates with their
74    /// names, task names, comments, and default completion values.
75    /// Useful for reviewing available templates and their configurations.
76    List,
77
78    /// Show a single template's contents
79    ///
80    /// Displays the task name, comment and default completeness stored in
81    /// the template, so its effect on task creation is visible before use.
82    Show {
83        /// Name of the template to show
84        name: Option<String>,
85    },
86
87    /// Edit an existing template
88    ///
89    /// Modifies an existing template's properties including task name,
90    /// comment, and completion status. Provides interactive interface
91    /// for template selection if name is not specified.
92    Edit {
93        /// Name of the template to edit
94        ///
95        /// If not provided, an interactive selection interface will be
96        /// presented with all available templates.
97        name: Option<String>,
98    },
99
100    /// Remove a template
101    ///
102    /// Permanently removes a template from the system. Includes confirmation
103    /// prompt to prevent accidental removal. Removing a template does not
104    /// affect tasks that were previously created from it.
105    Remove {
106        /// Name of the template to remove
107        ///
108        /// If not provided, an interactive selection interface will be
109        /// presented with all available templates.
110        name: Option<String>,
111
112        /// Remove without asking for confirmation
113        #[arg(long, short = 'y')]
114        yes: bool,
115    },
116
117    /// Search templates by name or content
118    ///
119    /// Performs a text search across template names and task names,
120    /// returning all matching templates. Useful for finding templates
121    /// in large template libraries.
122    Search {
123        /// Search query string
124        ///
125        /// Searches both template names and task names for matches.
126        /// Case-insensitive partial matching is supported.
127        query: String,
128    },
129}
130
131/// Executes template management operations based on the specified subcommand.
132///
133/// This function serves as the main dispatcher for template operations,
134/// routing to appropriate handlers based on user input. When no subcommand
135/// is provided, it enters interactive mode for operation selection.
136///
137/// ## Operation Routing
138///
139/// - **Create**: Template creation with validation and uniqueness checking
140/// - **List**: Formatted display of all available templates
141/// - **Edit**: Interactive or direct template modification
142/// - **Delete**: Safe template removal with confirmation
143/// - **Search**: Text-based template discovery
144/// - **Interactive**: Menu-driven operation selection when no subcommand given
145///
146/// # Arguments
147///
148/// * `args` - Parsed command-line arguments containing operation specification
149///
150/// # Returns
151///
152/// Returns `Ok(())` on successful operation completion, or an error if
153/// the requested operation fails due to validation, database, or user input issues.
154///
155/// # Examples
156///
157/// ```bash
158/// # Create a new template interactively
159/// kasl template add
160///
161/// # Create template with specific name
162/// kasl template add --name daily-standup
163///
164/// # List all templates
165/// kasl template list
166///
167/// # Edit a template
168/// kasl template edit daily-standup
169///
170/// # Search for templates
171/// kasl template search meeting
172///
173/// # Interactive mode
174/// kasl template
175/// ```
176pub fn cmd(args: TemplateArgs) -> Result<()> {
177    match args.command {
178        Some(TemplateCommand::Add { name }) => handle_create(name),
179        Some(TemplateCommand::List) => handle_list(),
180        Some(TemplateCommand::Show { name }) => handle_show(name),
181        Some(TemplateCommand::Edit { name }) => handle_edit(name),
182        Some(TemplateCommand::Remove { name, yes }) => handle_delete(name, yes),
183        Some(TemplateCommand::Search { query }) => handle_search(query),
184        None => {
185            ensure_interactive("no subcommand given; run `kasl template list` or see `kasl template --help`")?;
186            handle_interactive()
187        }
188    }
189}
190
191/// Handles template creation with validation and uniqueness checking.
192///
193/// This function manages the complete template creation workflow:
194/// 1. **Name Collection**: Gets template name from args or interactive prompt
195/// 2. **Uniqueness Validation**: Ensures template name doesn't already exist
196/// 3. **Property Collection**: Gathers task name, comment, and completion values
197/// 4. **Validation**: Ensures all required fields are properly formatted
198/// 5. **Database Storage**: Saves the new template with proper error handling
199///
200/// ## Template Properties
201///
202/// Templates store these key properties:
203/// - **Name**: Unique identifier for referencing the template
204/// - **Task Name**: Default name for tasks created from this template
205/// - **Comment**: Default comment/description for tasks
206/// - **Completeness**: Default completion percentage (0-100)
207///
208/// ## Validation Rules
209///
210/// - Template names must be unique across the entire template library
211/// - Task names are required and cannot be empty
212/// - Completion values must be between 0 and 100 inclusive
213/// - Comments are optional and can be empty
214///
215/// # Arguments
216///
217/// * `name` - Optional template name from command line, or None for interactive prompt
218fn handle_create(name: Option<String>) -> Result<()> {
219    let mut templates_db = Templates::new()?;
220
221    // Get template name (from args or interactive prompt)
222    let name = name.unwrap_or_else(|| {
223        Input::with_theme(&ColorfulTheme::default())
224            .with_prompt(Message::PromptTemplateName.to_string())
225            .interact_text()
226            .unwrap()
227    });
228
229    // Validate template name uniqueness
230    if templates_db.exists(&name)? {
231        msg_error!(Message::TemplateAlreadyExists(name));
232        return Ok(());
233    }
234
235    // Collect template properties interactively
236    let task_name = Input::with_theme(&ColorfulTheme::default())
237        .with_prompt(Message::PromptTemplateTaskName.to_string())
238        .interact_text()?;
239
240    let comment = Input::with_theme(&ColorfulTheme::default())
241        .with_prompt(Message::PromptTemplateComment.to_string())
242        .allow_empty(true)
243        .interact_text()?;
244
245    let completeness_range_msg = Message::TaskCompletenessRange.to_string();
246    let completeness = Input::with_theme(&ColorfulTheme::default())
247        .with_prompt(Message::PromptTemplateCompleteness.to_string())
248        .default(100)
249        .validate_with(|input: &i32| -> Result<(), &str> {
250            if *input >= 0 && *input <= 100 { Ok(()) } else { Err(&completeness_range_msg) }
251        })
252        .interact_text()?;
253
254    // Create and save the template
255    let template = TaskTemplate::new(name.clone(), task_name, comment, completeness);
256    templates_db.create(&template)?;
257
258    msg_success!(Message::TemplateCreated(name));
259    Ok(())
260}
261
262/// Displays all available templates in a formatted table.
263///
264/// This function retrieves all templates from the database and presents
265/// them in a user-friendly table format showing all relevant properties.
266/// The display helps users understand their available templates and
267/// their configurations.
268///
269/// ## Display Format
270///
271/// The table includes these columns:
272/// - **Template Name**: Unique identifier for the template
273/// - **Task Name**: Default task name that will be used
274/// - **Comment**: Default comment or description
275/// - **Completeness**: Default completion percentage
276///
277/// ## Empty State Handling
278///
279/// When no templates exist, the function provides helpful guidance
280/// about creating the first template rather than displaying an empty table.
281fn handle_list() -> Result<()> {
282    let mut templates_db = Templates::new()?;
283    let templates = templates_db.get_all()?;
284
285    if templates.is_empty() {
286        msg_info!(Message::NoTemplatesFound);
287        return Ok(());
288    }
289
290    msg_print!(Message::TemplateListHeader, true);
291    View::templates(&templates)?;
292    Ok(())
293}
294
295/// Displays a single template's stored values.
296///
297/// Reuses the same table rendering as `list` so a template reads identically
298/// whether shown alone or among others. Without a name, the template is picked
299/// interactively.
300///
301/// # Arguments
302///
303/// * `name` - Template name, or None to select one interactively
304fn handle_show(name: Option<String>) -> Result<()> {
305    let mut templates_db = Templates::new()?;
306
307    let name = match name {
308        Some(n) => n,
309        None => {
310            ensure_interactive("template name is required outside an interactive terminal")?;
311
312            let templates = templates_db.get_all()?;
313            if templates.is_empty() {
314                msg_info!(Message::NoTemplatesFound);
315                return Ok(());
316            }
317
318            let template_names: Vec<String> = templates.iter().map(|t| t.name.clone()).collect();
319            let selection = Select::with_theme(&ColorfulTheme::default())
320                .with_prompt(Message::SelectTemplate.to_string())
321                .items(&template_names)
322                .interact()?;
323
324            template_names[selection].clone()
325        }
326    };
327
328    match templates_db.get(&name)? {
329        Some(template) => {
330            msg_print!(Message::TemplateListHeader, true);
331            View::templates(&[template])?;
332        }
333        None => msg_error!(Message::TemplateNotFound(name)),
334    }
335
336    Ok(())
337}
338
339/// Handles template editing with interactive or direct name specification.
340///
341/// This function provides comprehensive template editing capabilities:
342/// 1. **Template Selection**: Uses provided name or interactive selection
343/// 2. **Current State Display**: Shows existing template values
344/// 3. **Interactive Editing**: Prompts for new values with current values as defaults
345/// 4. **Validation**: Ensures edited values meet requirements
346/// 5. **Database Update**: Saves changes with proper error handling
347///
348/// ## Selection Methods
349///
350/// - **Direct**: When template name is provided via command line
351/// - **Interactive**: When no name is provided, presents selection interface
352///
353/// ## Editing Interface
354///
355/// For each editable property, the interface:
356/// - Shows the current value as the default
357/// - Allows the user to accept current value or enter new one
358/// - Validates new values according to template rules
359/// - Provides clear feedback about validation errors
360///
361/// # Arguments
362///
363/// * `name` - Optional template name to edit, or None for interactive selection
364fn handle_edit(name: Option<String>) -> Result<()> {
365    let mut templates_db = Templates::new()?;
366
367    // Get template name (direct or interactive selection)
368    let name = match name {
369        Some(n) => n,
370        None => {
371            let templates = templates_db.get_all()?;
372            if templates.is_empty() {
373                msg_info!(Message::NoTemplatesFound);
374                return Ok(());
375            }
376
377            let template_names: Vec<String> = templates.iter().map(|t| t.name.clone()).collect();
378            let selection = Select::with_theme(&ColorfulTheme::default())
379                .with_prompt(Message::SelectTemplateToEdit.to_string())
380                .items(&template_names)
381                .interact()?;
382
383            template_names[selection].clone()
384        }
385    };
386
387    // Fetch the template to edit
388    let template = match templates_db.get(&name)? {
389        Some(t) => t,
390        None => {
391            msg_error!(Message::TemplateNotFound(name));
392            return Ok(());
393        }
394    };
395
396    msg_print!(Message::EditingTemplate(template.name.clone()), true);
397
398    // Interactive editing with current values as defaults
399    let task_name = Input::with_theme(&ColorfulTheme::default())
400        .with_prompt(Message::PromptTemplateTaskName.to_string())
401        .default(template.task_name.clone())
402        .interact_text()?;
403
404    let comment = Input::with_theme(&ColorfulTheme::default())
405        .with_prompt(Message::PromptTemplateComment.to_string())
406        .default(template.comment.clone())
407        .allow_empty(true)
408        .interact_text()?;
409
410    let completeness_range_msg = Message::TaskCompletenessRange.to_string();
411    let completeness = Input::with_theme(&ColorfulTheme::default())
412        .with_prompt(Message::PromptTemplateCompleteness.to_string())
413        .default(template.completeness)
414        .validate_with(|input: &i32| -> Result<(), &str> {
415            if *input >= 0 && *input <= 100 { Ok(()) } else { Err(&completeness_range_msg) }
416        })
417        .interact_text()?;
418
419    // Update the template
420    let updated_template = TaskTemplate::new(name.clone(), task_name, comment, completeness);
421    templates_db.update(&updated_template)?;
422
423    msg_success!(Message::TemplateUpdated(name));
424    Ok(())
425}
426
427/// Handles safe template deletion with confirmation.
428///
429/// This function manages the template deletion process with appropriate
430/// safety measures to prevent accidental data loss:
431/// 1. **Template Selection**: Direct name or interactive selection
432/// 2. **Existence Validation**: Ensures template exists before attempting deletion
433/// 3. **Confirmation Prompt**: Requires explicit user confirmation
434/// 4. **Safe Deletion**: Removes template only after confirmation
435/// 5. **User Feedback**: Provides clear feedback about operation result
436///
437/// ## Safety Features
438///
439/// - Confirms template exists before showing deletion prompt
440/// - Uses clear, unambiguous confirmation language
441/// - Defaults to "No" for safety
442/// - Provides escape opportunity before actual deletion
443/// - Clear feedback about cancellation vs. completion
444///
445/// # Arguments
446///
447/// * `name` - Optional template name to delete, or None for interactive selection
448fn handle_delete(name: Option<String>, assume_yes: bool) -> Result<()> {
449    let mut templates_db = Templates::new()?;
450
451    // Get template name (direct or interactive selection)
452    let name = match name {
453        Some(n) => n,
454        None => {
455            ensure_interactive("template name is required outside an interactive terminal")?;
456
457            let templates = templates_db.get_all()?;
458            if templates.is_empty() {
459                msg_info!(Message::NoTemplatesFound);
460                return Ok(());
461            }
462
463            let template_names: Vec<String> = templates.iter().map(|t| t.name.clone()).collect();
464            let selection = Select::with_theme(&ColorfulTheme::default())
465                .with_prompt(Message::SelectTemplateToDelete.to_string())
466                .items(&template_names)
467                .interact()?;
468
469            template_names[selection].clone()
470        }
471    };
472
473    if !assume_yes {
474        // Never block on a prompt when there is no one to answer it.
475        ensure_interactive(&format!("refusing to remove template '{}' without --yes outside an interactive terminal", name))?;
476
477        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
478            .with_prompt(Message::ConfirmDeleteTemplate(name.clone()).to_string())
479            .default(false)
480            .interact()?;
481
482        if !confirmed {
483            msg_info!(Message::OperationCancelled);
484            return Ok(());
485        }
486    }
487
488    templates_db.delete(&name)?;
489    msg_success!(Message::TemplateDeleted(name));
490
491    Ok(())
492}
493
494/// Handles template search functionality.
495///
496/// This function performs text-based searching across template names and
497/// task names, returning all matches in a formatted display. The search
498/// is case-insensitive and supports partial matching for user convenience.
499///
500/// ## Search Algorithm
501///
502/// The search functionality:
503/// - Performs case-insensitive matching
504/// - Searches both template names and task names
505/// - Supports partial string matching
506/// - Returns all matching templates
507/// - Displays results in standard template table format
508///
509/// ## Search Scope
510///
511/// The search covers these template fields:
512/// - **Template Name**: The unique identifier
513/// - **Task Name**: The default task name
514///
515/// Comments and completion values are not included in search to focus
516/// on the most relevant identifying information.
517///
518/// # Arguments
519///
520/// * `query` - Search string to match against template and task names
521fn handle_search(query: String) -> Result<()> {
522    let mut templates_db = Templates::new()?;
523    let templates = templates_db.search(&query)?;
524
525    if templates.is_empty() {
526        msg_info!(Message::NoTemplatesMatchingQuery(query));
527        return Ok(());
528    }
529
530    msg_print!(Message::TemplateSearchResults(query), true);
531    View::templates(&templates)?;
532    Ok(())
533}
534
535/// Handles interactive template management when no subcommand is provided.
536///
537/// This function provides a menu-driven interface for users who prefer
538/// interactive operation over command-line arguments. It presents all
539/// available template operations in an easy-to-navigate menu format.
540///
541/// ## Interactive Menu
542///
543/// The menu presents these options:
544/// 1. **Create new template**: Launches template creation workflow
545/// 2. **List templates**: Shows all available templates
546/// 3. **Edit template**: Template selection and editing interface
547/// 4. **Delete template**: Template selection and safe deletion
548///
549/// Each menu option delegates to the appropriate specialized handler
550/// function, ensuring consistent behavior between interactive and
551/// command-line usage.
552///
553/// ## User Experience
554///
555/// The interactive mode is designed for:
556/// - Users new to the command-line interface
557/// - Occasional template management tasks
558/// - Exploration of available template operations
559/// - Situations where remembering exact command syntax is inconvenient
560fn handle_interactive() -> Result<()> {
561    let options = vec!["Add new template", "List templates", "Show template", "Edit template", "Remove template"];
562
563    let selection = Select::with_theme(&ColorfulTheme::default())
564        .with_prompt(Message::SelectTemplateAction.to_string())
565        .items(&options)
566        .interact()?;
567
568    match selection {
569        0 => handle_create(None),
570        1 => handle_list(),
571        2 => handle_show(None),
572        3 => handle_edit(None),
573        4 => handle_delete(None, false),
574        _ => Ok(()),
575    }
576}