Skip to main content

kasl/commands/
tag.rs

1//! Tag management command for task organization and categorization.
2//!
3//! Provides comprehensive tag management functionality, enabling users to create, organize, and utilize tags for better task categorization.
4//!
5//! ## Features
6//!
7//! - **Tag CRUD Operations**: Create, read, update, and delete tag definitions
8//! - **Color Coding**: Visual organization with customizable tag colors
9//! - **Task Association**: Link tags to tasks for categorization
10//! - **Filtering**: Find tasks by tag assignments
11//! - **Auto-Creation**: Automatically create tags when assigned to tasks
12//!
13//! ## Usage
14//!
15//! ```bash
16//! # List all tags
17//! kasl tag list
18//!
19//! # Create new tag with color
20//! kasl tag create --name "urgent" --color "red"
21//!
22//! # Delete tag
23//! kasl tag delete "old-tag"
24//!
25//! # Show tag usage statistics
26//! kasl tag stats
27//! ```
28
29use crate::{
30    db::tags::{Tag, Tags},
31    libs::{messages::Message, view::View},
32    msg_error, msg_info, msg_print, msg_success,
33};
34use anyhow::Result;
35use clap::{Args, Subcommand};
36use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
37
38/// Command-line arguments for tag management operations.
39///
40/// The tag command uses subcommands to organize different tag management
41/// operations, providing a clean and intuitive interface for users to
42/// manage their tag library and task associations.
43#[derive(Debug, Args)]
44pub struct TagArgs {
45    #[command(subcommand)]
46    command: Option<TagCommand>,
47}
48
49/// Available tag management operations.
50///
51/// Each subcommand provides specific functionality for tag lifecycle
52/// management and task association operations, supporting both direct
53/// command-line usage and interactive workflows.
54#[derive(Debug, Subcommand)]
55enum TagCommand {
56    /// Create a new tag with optional color
57    ///
58    /// Creates a new tag that can be assigned to tasks for categorization.
59    /// Tags can optionally include color information for visual organization
60    /// in user interfaces and reports.
61    Create {
62        /// Unique name for the tag
63        ///
64        /// Must be unique across all tags and should be descriptive
65        /// enough to clearly indicate the tag's purpose. Common examples
66        /// include project names, priorities, or task types.
67        name: String,
68
69        /// Optional color for visual organization
70        ///
71        /// Specifies a color name or code for visual representation of the tag.
72        /// Common color names like "red", "blue", "green" are supported,
73        /// as well as hex color codes for precise color specification.
74        #[arg(short, long)]
75        color: Option<String>,
76    },
77
78    /// List all available tags
79    ///
80    /// Displays a formatted table of all existing tags with their names,
81    /// colors, and creation dates. Useful for reviewing the current tag
82    /// library and understanding available categorization options.
83    List,
84
85    /// Edit an existing tag's properties
86    ///
87    /// Modifies an existing tag's name and color properties. Tag editing
88    /// affects all tasks that currently use the tag, so changes should be
89    /// made carefully to maintain consistent categorization.
90    Edit {
91        /// Tag name or ID to edit
92        ///
93        /// Can specify either the tag name (string) or database ID (number)
94        /// for the tag to be edited. If the input can be parsed as a number,
95        /// it will be treated as an ID; otherwise, it's treated as a name.
96        tag: String,
97    },
98
99    /// Delete a tag and remove it from all tasks
100    ///
101    /// Permanently removes a tag from the system and unassigns it from
102    /// all tasks that currently use it. Includes safety confirmation
103    /// prompts, especially when the tag is actively used by tasks.
104    Delete {
105        /// Tag name or ID to delete
106        ///
107        /// Can specify either the tag name (string) or database ID (number)
108        /// for the tag to be deleted. The system will confirm the operation
109        /// and show how many tasks will be affected.
110        tag: String,
111    },
112
113    /// Show all tasks that have a specific tag
114    ///
115    /// Displays a filtered list of tasks that are currently assigned the
116    /// specified tag. This provides a quick way to see all work items
117    /// within a particular category or project.
118    Tasks {
119        /// Tag name to filter tasks by
120        ///
121        /// Shows all tasks that have been assigned this tag, regardless
122        /// of their completion status or creation date.
123        tag: String,
124    },
125}
126
127/// Executes tag management operations based on the specified subcommand.
128///
129/// This function serves as the main dispatcher for tag operations, routing
130/// to appropriate handlers based on user input. When no subcommand is provided,
131/// it enters interactive mode for operation selection.
132///
133/// ## Operation Routing
134///
135/// - **Create**: Tag creation with validation and color assignment
136/// - **List**: Formatted display of all available tags
137/// - **Edit**: Interactive or direct tag modification
138/// - **Delete**: Safe tag removal with usage impact analysis
139/// - **Tasks**: Display tasks filtered by tag assignment
140/// - **Interactive**: Menu-driven operation selection when no subcommand given
141///
142/// ## Error Handling
143///
144/// Each operation includes appropriate error handling for:
145/// - Tag not found scenarios
146/// - Database connectivity issues
147/// - Validation failures
148/// - User input errors
149/// - Concurrent modification conflicts
150///
151/// # Arguments
152///
153/// * `args` - Parsed command-line arguments containing operation specification
154///
155/// # Returns
156///
157/// Returns `Ok(())` on successful operation completion, or an error if
158/// the requested operation fails due to validation, database, or user input issues.
159///
160/// # Examples
161///
162/// ```bash
163/// # Create a new urgent tag with red color
164/// kasl tag create urgent --color red
165///
166/// # List all available tags
167/// kasl tag list
168///
169/// # Edit a tag's properties
170/// kasl tag edit urgent
171///
172/// # Show all tasks tagged as "backend"
173/// kasl tag tasks backend
174///
175/// # Interactive mode
176/// kasl tag
177/// ```
178pub async fn cmd(args: TagArgs) -> Result<()> {
179    match args.command {
180        Some(TagCommand::Create { name, color }) => handle_create(name, color),
181        Some(TagCommand::List) => handle_list(),
182        Some(TagCommand::Edit { tag }) => handle_edit(tag),
183        Some(TagCommand::Delete { tag }) => handle_delete(tag),
184        Some(TagCommand::Tasks { tag }) => handle_show_tasks(tag).await,
185        None => handle_interactive(),
186    }
187}
188
189/// Handles tag creation with validation and uniqueness checking.
190///
191/// This function manages the complete tag creation workflow:
192/// 1. **Uniqueness Validation**: Ensures tag name doesn't already exist
193/// 2. **Tag Creation**: Creates new tag with specified name and optional color
194/// 3. **Database Storage**: Saves the new tag with proper error handling
195/// 4. **User Feedback**: Provides confirmation of successful creation
196///
197/// ## Tag Properties
198///
199/// New tags are created with these properties:
200/// - **Name**: Unique identifier provided by user
201/// - **Color**: Optional visual indicator (defaults to system-assigned if not provided)
202/// - **Creation Date**: Automatically set to current timestamp
203///
204/// ## Validation Rules
205///
206/// - Tag names must be unique across the entire tag library
207/// - Tag names cannot be empty or contain only whitespace
208/// - Color values are optional and accept standard color names or hex codes
209///
210/// # Arguments
211///
212/// * `name` - Unique name for the new tag
213/// * `color` - Optional color specification for visual organization
214fn handle_create(name: String, color: Option<String>) -> Result<()> {
215    let mut tags_db = Tags::new()?;
216
217    // Validate tag name uniqueness
218    if tags_db.get_by_name(&name)?.is_some() {
219        msg_error!(Message::TagAlreadyExists(name));
220        return Ok(());
221    }
222
223    // Create and save the new tag
224    let tag = Tag::new(name.clone(), color);
225    tags_db.create(&tag)?;
226
227    msg_success!(Message::TagCreated(name));
228    Ok(())
229}
230
231/// Displays all available tags in a formatted table.
232///
233/// This function retrieves all tags from the database and presents them
234/// in a user-friendly table format showing all relevant properties.
235/// The display helps users understand their available tags and their
236/// configurations for effective task categorization.
237///
238/// ## Display Format
239///
240/// The table includes these columns:
241/// - **ID**: Database identifier for the tag
242/// - **Name**: The tag's unique name
243/// - **Color**: Visual color indicator (if assigned)
244///
245/// ## Empty State Handling
246///
247/// When no tags exist, the function provides helpful guidance about
248/// creating the first tag rather than displaying an empty table.
249fn handle_list() -> Result<()> {
250    let mut tags_db = Tags::new()?;
251    let tags = tags_db.get_all()?;
252
253    if tags.is_empty() {
254        msg_info!(Message::NoTagsFound);
255        return Ok(());
256    }
257
258    msg_print!(Message::TagListHeader, true);
259    View::tags(&tags)?;
260    Ok(())
261}
262
263/// Handles tag editing with flexible identifier support.
264///
265/// This function provides comprehensive tag editing capabilities:
266/// 1. **Tag Resolution**: Finds tag by name or ID
267/// 2. **Current State Display**: Shows existing tag properties
268/// 3. **Interactive Editing**: Prompts for new values with current values as defaults
269/// 4. **Validation**: Ensures edited values meet tag requirements
270/// 5. **Database Update**: Saves changes with proper error handling
271///
272/// ## Identifier Resolution
273///
274/// The function accepts flexible tag identification:
275/// - **Numeric Input**: Treated as database ID
276/// - **String Input**: Treated as tag name
277/// - **Automatic Detection**: Parses input to determine type
278///
279/// ## Editing Interface
280///
281/// For each editable property, the interface:
282/// - Shows the current value as the default
283/// - Allows the user to accept current value or enter new one
284/// - Validates new values according to tag rules
285/// - Provides clear feedback about validation errors
286///
287/// # Arguments
288///
289/// * `tag_identifier` - Tag name or ID string for flexible tag identification
290fn handle_edit(tag_identifier: String) -> Result<()> {
291    let mut tags_db = Tags::new()?;
292
293    // Resolve tag by ID or name
294    let tag = if let Ok(id) = tag_identifier.parse::<i32>() {
295        tags_db.get_by_id(id)?
296    } else {
297        tags_db.get_by_name(&tag_identifier)?
298    };
299
300    let tag = match tag {
301        Some(t) => t,
302        None => {
303            msg_error!(Message::TagNotFound(tag_identifier));
304            return Ok(());
305        }
306    };
307
308    msg_print!(Message::EditingTag(tag.name.clone()), true);
309
310    // Interactive editing with current values as defaults
311    let new_name = Input::with_theme(&ColorfulTheme::default())
312        .with_prompt(Message::PromptTagName.to_string())
313        .default(tag.name.clone())
314        .interact_text()?;
315
316    let new_color = Input::with_theme(&ColorfulTheme::default())
317        .with_prompt(Message::PromptTagColor.to_string())
318        .default(tag.color.unwrap_or_default())
319        .allow_empty(true)
320        .interact_text()?;
321
322    // Handle empty color input
323    let color = if new_color.is_empty() { None } else { Some(new_color) };
324
325    // Update the tag
326    tags_db.update(&Tag{id: tag.id, name: new_name.clone(), color, created_at: None})?;
327    msg_success!(Message::TagUpdated(new_name));
328    Ok(())
329}
330
331/// Handles safe tag deletion with usage impact analysis.
332///
333/// This function manages the tag deletion process with comprehensive
334/// safety measures to prevent accidental data loss and inform users
335/// about the impact of deletion:
336/// 1. **Tag Resolution**: Finds tag by name or ID
337/// 2. **Usage Analysis**: Counts how many tasks currently use the tag
338/// 3. **Impact Communication**: Informs user about affected tasks
339/// 4. **Confirmation Prompt**: Requires explicit user confirmation
340/// 5. **Safe Deletion**: Removes tag and updates task associations
341///
342/// ## Safety Features
343///
344/// - Confirms tag exists before showing deletion prompt
345/// - Analyzes and reports impact on existing tasks
346/// - Uses different confirmation messages based on usage
347/// - Defaults to "No" for safety
348/// - Provides escape opportunity before actual deletion
349/// - Clear feedback about cancellation vs. completion
350///
351/// ## Usage Impact
352///
353/// The function provides different confirmation prompts based on tag usage:
354/// - **Unused Tags**: Simple confirmation for deletion
355/// - **Used Tags**: Enhanced warning showing number of affected tasks
356/// - **Heavily Used Tags**: Additional emphasis on impact scope
357///
358/// # Arguments
359///
360/// * `tag_identifier` - Tag name or ID string for flexible tag identification
361fn handle_delete(tag_identifier: String) -> Result<()> {
362    let mut tags_db = Tags::new()?;
363
364    // Resolve tag by ID or name
365    let tag = if let Ok(id) = tag_identifier.parse::<i32>() {
366        tags_db.get_by_id(id)?
367    } else {
368        tags_db.get_by_name(&tag_identifier)?
369    };
370
371    let tag = match tag {
372        Some(t) => t,
373        None => {
374            msg_error!(Message::TagNotFound(tag_identifier));
375            return Ok(());
376        }
377    };
378
379    // Analyze usage impact
380    let task_count = tags_db.get_tasks_by_tag(tag.id.unwrap())?.len();
381
382    // Provide appropriate confirmation prompt based on usage
383    let prompt = if task_count > 0 {
384        Message::ConfirmDeleteTagWithTasks(tag.name.clone(), task_count)
385    } else {
386        Message::ConfirmDeleteTag(tag.name.clone())
387    };
388
389    let confirmed = Confirm::with_theme(&ColorfulTheme::default())
390        .with_prompt(prompt.to_string())
391        .default(false)
392        .interact()?;
393
394    if confirmed {
395        tags_db.delete(tag.id.unwrap())?;
396        msg_success!(Message::TagDeleted(tag.name));
397    } else {
398        msg_info!(Message::OperationCancelled);
399    }
400
401    Ok(())
402}
403
404/// Displays all tasks associated with a specific tag.
405///
406/// This function provides filtered task viewing based on tag assignment,
407/// allowing users to see all work items within a particular category.
408/// It's useful for project management and understanding work distribution
409/// across different areas.
410///
411/// ## Display Features
412///
413/// - **Tag Validation**: Ensures the specified tag exists
414/// - **Task Filtering**: Shows only tasks with the specified tag
415/// - **Standard Format**: Uses the same task display format as other commands
416/// - **Empty State Handling**: Provides helpful message when no tasks found
417///
418/// ## Use Cases
419///
420/// This functionality supports various workflows:
421/// - **Project Review**: See all tasks for a specific project
422/// - **Priority Management**: View all urgent or high-priority tasks
423/// - **Sprint Planning**: Review tasks by type or area
424/// - **Progress Tracking**: Monitor completion within categories
425///
426/// # Arguments
427///
428/// * `tag_name` - Name of the tag to filter tasks by
429async fn handle_show_tasks(tag_name: String) -> Result<()> {
430    let mut tags_db = Tags::new()?;
431
432    // Validate tag exists
433    let tag = match tags_db.get_by_name(&tag_name)? {
434        Some(t) => t,
435        None => {
436            msg_error!(Message::TagNotFound(tag_name));
437            return Ok(());
438        }
439    };
440
441    // Get task IDs associated with this tag
442    let task_ids = tags_db.get_tasks_by_tag(tag.id.unwrap())?;
443
444    if task_ids.is_empty() {
445        msg_info!(Message::NoTasksWithTag(tag_name));
446        return Ok(());
447    }
448
449    // Fetch and display the tasks
450    use crate::db::tasks::Tasks;
451    let tasks = Tasks::new()?.fetch(crate::libs::task::TaskFilter::ByIds(task_ids))?;
452
453    msg_print!(Message::TasksWithTag(tag_name), true);
454    View::tasks(&tasks)?;
455
456    Ok(())
457}
458
459/// Handles interactive tag management when no subcommand is provided.
460///
461/// This function provides a menu-driven interface for users who prefer
462/// interactive operation over command-line arguments. It presents all
463/// available tag operations in an easy-to-navigate menu format.
464///
465/// ## Interactive Menu
466///
467/// The menu presents these options:
468/// 1. **Create tag**: Launches tag creation workflow with prompts for name and color
469/// 2. **List tags**: Shows all available tags in formatted table
470/// 3. **Edit tag**: Tag selection interface followed by editing prompts
471/// 4. **Delete tag**: Tag selection interface with safety confirmations
472///
473/// Each menu option delegates to the appropriate specialized handler
474/// function, ensuring consistent behavior between interactive and
475/// command-line usage.
476///
477/// ## Tag Selection Interface
478///
479/// For edit and delete operations, the interactive mode provides:
480/// - **Tag List Display**: Shows all available tags for selection
481/// - **Empty State Handling**: Graceful handling when no tags exist
482/// - **User-Friendly Selection**: Clear presentation of tag options
483/// - **Operation Cancellation**: Easy way to exit without changes
484///
485/// ## User Experience
486///
487/// The interactive mode is designed for:
488/// - Users new to the command-line interface
489/// - Occasional tag management tasks
490/// - Exploration of available tag operations
491/// - Situations where remembering exact command syntax is inconvenient
492fn handle_interactive() -> Result<()> {
493    let options = vec!["Create tag", "List tags", "Edit tag", "Delete tag"];
494
495    let selection = Select::with_theme(&ColorfulTheme::default())
496        .with_prompt(Message::SelectTagAction.to_string())
497        .items(&options)
498        .interact()?;
499
500    match selection {
501        0 => {
502            // Interactive tag creation
503            let name = Input::with_theme(&ColorfulTheme::default())
504                .with_prompt(Message::PromptTagName.to_string())
505                .interact_text()?;
506            let color: String = Input::with_theme(&ColorfulTheme::default())
507                .with_prompt(Message::PromptTagColor.to_string())
508                .allow_empty(true)
509                .interact_text()?;
510            handle_create(name, if color.is_empty() { None } else { Some(color) })
511        }
512        1 => handle_list(),
513        2 => {
514            // Interactive tag editing with selection
515            let mut tags_db = Tags::new()?;
516            let tags = tags_db.get_all()?;
517            if tags.is_empty() {
518                msg_info!(Message::NoTagsFound);
519                return Ok(());
520            }
521            drop(tags_db);
522
523            let tag_names: Vec<String> = tags.iter().map(|t| t.name.clone()).collect();
524            let selection = Select::with_theme(&ColorfulTheme::default())
525                .with_prompt(Message::SelectTagToEdit.to_string())
526                .items(&tag_names)
527                .interact()?;
528            handle_edit(tag_names[selection].clone())
529        }
530        3 => {
531            // Interactive tag deletion with selection
532            let mut tags_db = Tags::new()?;
533            let tags = tags_db.get_all()?;
534            if tags.is_empty() {
535                msg_info!(Message::NoTagsFound);
536                return Ok(());
537            }
538            drop(tags_db);
539
540            let tag_names: Vec<String> = tags.iter().map(|t| t.name.clone()).collect();
541            let selection = Select::with_theme(&ColorfulTheme::default())
542                .with_prompt(Message::SelectTagToDelete.to_string())
543                .items(&tag_names)
544                .interact()?;
545            handle_delete(tag_names[selection].clone())
546        }
547        _ => Ok(()),
548    }
549}