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}