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//! # Add a new tag with color
20//! kasl tag add urgent --color red
21//!
22//! # Show a tag and the tasks that carry it
23//! kasl tag show urgent
24//!
25//! # Remove a tag
26//! kasl tag remove old-tag
27//! ```
28
29use crate::{
30    db::tags::{Tag, Tags},
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 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    /// Add 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    Add {
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    /// Show a tag and the tasks that carry it
86    ///
87    /// Displays the tag's properties along with every task currently
88    /// assigned it, regardless of completion status or creation date.
89    Show {
90        /// Tag name or ID to show
91        tag: String,
92    },
93
94    /// Edit an existing tag's properties
95    ///
96    /// Modifies an existing tag's name and color properties. Tag editing
97    /// affects all tasks that currently use the tag, so changes should be
98    /// made carefully to maintain consistent categorization.
99    Edit {
100        /// Tag name or ID to edit
101        ///
102        /// Can specify either the tag name (string) or database ID (number)
103        /// for the tag to be edited. If the input can be parsed as a number,
104        /// it will be treated as an ID; otherwise, it's treated as a name.
105        tag: String,
106    },
107
108    /// Remove a tag and unassign it from all tasks
109    ///
110    /// Permanently removes a tag from the system and unassigns it from
111    /// all tasks that currently use it. Includes safety confirmation
112    /// prompts, especially when the tag is actively used by tasks.
113    Remove {
114        /// Tag name or ID to remove
115        ///
116        /// Can specify either the tag name (string) or database ID (number)
117        /// for the tag to be removed. The system will confirm the operation
118        /// and show how many tasks will be affected.
119        tag: String,
120
121        /// Remove without asking for confirmation
122        #[arg(long, short = 'y')]
123        yes: bool,
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/// - **Add**: Tag creation with validation and color assignment
136/// - **List**: Formatted display of all available tags
137/// - **Show**: Display the tag and the tasks assigned it
138/// - **Edit**: Interactive or direct tag modification
139/// - **Remove**: Safe tag removal with usage impact analysis
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 add 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 show backend
174///
175/// # Interactive mode
176/// kasl tag
177/// ```
178pub async fn cmd(args: TagArgs) -> Result<()> {
179    match args.command {
180        Some(TagCommand::Add { name, color }) => handle_create(name, color),
181        Some(TagCommand::List) => handle_list(),
182        Some(TagCommand::Show { tag }) => handle_show_tasks(tag).await,
183        Some(TagCommand::Edit { tag }) => handle_edit(tag),
184        Some(TagCommand::Remove { tag, yes }) => handle_delete(tag, yes),
185        None => {
186            ensure_interactive("no subcommand given; run `kasl tag list` or see `kasl tag --help`")?;
187            handle_interactive()
188        }
189    }
190}
191
192/// Handles tag creation with validation and uniqueness checking.
193///
194/// This function manages the complete tag creation workflow:
195/// 1. **Uniqueness Validation**: Ensures tag name doesn't already exist
196/// 2. **Tag Creation**: Creates new tag with specified name and optional color
197/// 3. **Database Storage**: Saves the new tag with proper error handling
198/// 4. **User Feedback**: Provides confirmation of successful creation
199///
200/// ## Tag Properties
201///
202/// New tags are created with these properties:
203/// - **Name**: Unique identifier provided by user
204/// - **Color**: Optional visual indicator (defaults to system-assigned if not provided)
205/// - **Creation Date**: Automatically set to current timestamp
206///
207/// ## Validation Rules
208///
209/// - Tag names must be unique across the entire tag library
210/// - Tag names cannot be empty or contain only whitespace
211/// - Color values are optional and accept standard color names or hex codes
212///
213/// # Arguments
214///
215/// * `name` - Unique name for the new tag
216/// * `color` - Optional color specification for visual organization
217fn handle_create(name: String, color: Option<String>) -> Result<()> {
218    let mut tags_db = Tags::new()?;
219
220    // Validate tag name uniqueness
221    if tags_db.get_by_name(&name)?.is_some() {
222        msg_error!(Message::TagAlreadyExists(name));
223        return Ok(());
224    }
225
226    // Create and save the new tag
227    let tag = Tag::new(name.clone(), color);
228    tags_db.create(&tag)?;
229
230    msg_success!(Message::TagCreated(name));
231    Ok(())
232}
233
234/// Displays all available tags in a formatted table.
235///
236/// This function retrieves all tags from the database and presents them
237/// in a user-friendly table format showing all relevant properties.
238/// The display helps users understand their available tags and their
239/// configurations for effective task categorization.
240///
241/// ## Display Format
242///
243/// The table includes these columns:
244/// - **ID**: Database identifier for the tag
245/// - **Name**: The tag's unique name
246/// - **Color**: Visual color indicator (if assigned)
247///
248/// ## Empty State Handling
249///
250/// When no tags exist, the function provides helpful guidance about
251/// creating the first tag rather than displaying an empty table.
252fn handle_list() -> Result<()> {
253    let mut tags_db = Tags::new()?;
254    let tags = tags_db.get_all()?;
255
256    if tags.is_empty() {
257        msg_info!(Message::NoTagsFound);
258        return Ok(());
259    }
260
261    msg_print!(Message::TagListHeader, true);
262    View::tags(&tags)?;
263    Ok(())
264}
265
266/// Handles tag editing with flexible identifier support.
267///
268/// This function provides comprehensive tag editing capabilities:
269/// 1. **Tag Resolution**: Finds tag by name or ID
270/// 2. **Current State Display**: Shows existing tag properties
271/// 3. **Interactive Editing**: Prompts for new values with current values as defaults
272/// 4. **Validation**: Ensures edited values meet tag requirements
273/// 5. **Database Update**: Saves changes with proper error handling
274///
275/// ## Identifier Resolution
276///
277/// The function accepts flexible tag identification:
278/// - **Numeric Input**: Treated as database ID
279/// - **String Input**: Treated as tag name
280/// - **Automatic Detection**: Parses input to determine type
281///
282/// ## Editing Interface
283///
284/// For each editable property, the interface:
285/// - Shows the current value as the default
286/// - Allows the user to accept current value or enter new one
287/// - Validates new values according to tag rules
288/// - Provides clear feedback about validation errors
289///
290/// # Arguments
291///
292/// * `tag_identifier` - Tag name or ID string for flexible tag identification
293fn handle_edit(tag_identifier: String) -> Result<()> {
294    // Editing is prompt-driven; there is nothing to fall back on without a terminal.
295    ensure_interactive("`kasl tag edit` is interactive and needs a terminal")?;
296
297    let mut tags_db = Tags::new()?;
298
299    // Resolve tag by ID or name
300    let tag = if let Ok(id) = tag_identifier.parse::<i32>() {
301        tags_db.get_by_id(id)?
302    } else {
303        tags_db.get_by_name(&tag_identifier)?
304    };
305
306    let tag = match tag {
307        Some(t) => t,
308        None => {
309            msg_error!(Message::TagNotFound(tag_identifier));
310            return Ok(());
311        }
312    };
313
314    msg_print!(Message::EditingTag(tag.name.clone()), true);
315
316    // Interactive editing with current values as defaults
317    let new_name = Input::with_theme(&ColorfulTheme::default())
318        .with_prompt(Message::PromptTagName.to_string())
319        .default(tag.name.clone())
320        .interact_text()?;
321
322    let new_color = Input::with_theme(&ColorfulTheme::default())
323        .with_prompt(Message::PromptTagColor.to_string())
324        .default(tag.color.unwrap_or_default())
325        .allow_empty(true)
326        .interact_text()?;
327
328    // Handle empty color input
329    let color = if new_color.is_empty() { None } else { Some(new_color) };
330
331    // Update the tag
332    tags_db.update(&Tag {
333        id: tag.id,
334        name: new_name.clone(),
335        color,
336        created_at: None,
337    })?;
338    msg_success!(Message::TagUpdated(new_name));
339    Ok(())
340}
341
342/// Handles safe tag deletion with usage impact analysis.
343///
344/// This function manages the tag deletion process with comprehensive
345/// safety measures to prevent accidental data loss and inform users
346/// about the impact of deletion:
347/// 1. **Tag Resolution**: Finds tag by name or ID
348/// 2. **Usage Analysis**: Counts how many tasks currently use the tag
349/// 3. **Impact Communication**: Informs user about affected tasks
350/// 4. **Confirmation Prompt**: Requires explicit user confirmation
351/// 5. **Safe Deletion**: Removes tag and updates task associations
352///
353/// ## Safety Features
354///
355/// - Confirms tag exists before showing deletion prompt
356/// - Analyzes and reports impact on existing tasks
357/// - Uses different confirmation messages based on usage
358/// - Defaults to "No" for safety
359/// - Provides escape opportunity before actual deletion
360/// - Clear feedback about cancellation vs. completion
361///
362/// ## Usage Impact
363///
364/// The function provides different confirmation prompts based on tag usage:
365/// - **Unused Tags**: Simple confirmation for deletion
366/// - **Used Tags**: Enhanced warning showing number of affected tasks
367/// - **Heavily Used Tags**: Additional emphasis on impact scope
368///
369/// # Arguments
370///
371/// * `tag_identifier` - Tag name or ID string for flexible tag identification
372fn handle_delete(tag_identifier: String, assume_yes: bool) -> Result<()> {
373    let mut tags_db = Tags::new()?;
374
375    // Resolve tag by ID or name
376    let tag = if let Ok(id) = tag_identifier.parse::<i32>() {
377        tags_db.get_by_id(id)?
378    } else {
379        tags_db.get_by_name(&tag_identifier)?
380    };
381
382    let tag = match tag {
383        Some(t) => t,
384        None => {
385            msg_error!(Message::TagNotFound(tag_identifier));
386            return Ok(());
387        }
388    };
389
390    // Analyze usage impact
391    let task_count = tags_db.get_tasks_by_tag(tag.id.unwrap())?.len();
392
393    if !assume_yes {
394        // Never block on a prompt when there is no one to answer it.
395        ensure_interactive(&format!("refusing to remove tag '{}' without --yes outside an interactive terminal", tag.name))?;
396
397        // Provide appropriate confirmation prompt based on usage
398        let prompt = if task_count > 0 {
399            Message::ConfirmDeleteTagWithTasks(tag.name.clone(), task_count)
400        } else {
401            Message::ConfirmDeleteTag(tag.name.clone())
402        };
403
404        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
405            .with_prompt(prompt.to_string())
406            .default(false)
407            .interact()?;
408
409        if !confirmed {
410            msg_info!(Message::OperationCancelled);
411            return Ok(());
412        }
413    }
414
415    tags_db.delete(tag.id.unwrap())?;
416    msg_success!(Message::TagDeleted(tag.name));
417
418    Ok(())
419}
420
421/// Displays all tasks associated with a specific tag.
422///
423/// This function provides filtered task viewing based on tag assignment,
424/// allowing users to see all work items within a particular category.
425/// It's useful for project management and understanding work distribution
426/// across different areas.
427///
428/// ## Display Features
429///
430/// - **Tag Validation**: Ensures the specified tag exists
431/// - **Task Filtering**: Shows only tasks with the specified tag
432/// - **Standard Format**: Uses the same task display format as other commands
433/// - **Empty State Handling**: Provides helpful message when no tasks found
434///
435/// ## Use Cases
436///
437/// This functionality supports various workflows:
438/// - **Project Review**: See all tasks for a specific project
439/// - **Priority Management**: View all urgent or high-priority tasks
440/// - **Sprint Planning**: Review tasks by type or area
441/// - **Progress Tracking**: Monitor completion within categories
442///
443/// # Arguments
444///
445/// * `tag_name` - Name of the tag to filter tasks by
446async fn handle_show_tasks(tag_name: String) -> Result<()> {
447    let mut tags_db = Tags::new()?;
448
449    // Validate tag exists
450    let tag = match tags_db.get_by_name(&tag_name)? {
451        Some(t) => t,
452        None => {
453            msg_error!(Message::TagNotFound(tag_name));
454            return Ok(());
455        }
456    };
457
458    // Get task IDs associated with this tag
459    let task_ids = tags_db.get_tasks_by_tag(tag.id.unwrap())?;
460
461    if task_ids.is_empty() {
462        msg_info!(Message::NoTasksWithTag(tag_name));
463        return Ok(());
464    }
465
466    // Fetch and display the tasks
467    use crate::db::tasks::Tasks;
468    let tasks = Tasks::new()?.fetch(crate::libs::task::TaskFilter::ByIds(task_ids))?;
469
470    msg_print!(Message::TasksWithTag(tag_name), true);
471    View::tasks(&tasks)?;
472
473    Ok(())
474}
475
476/// Handles interactive tag management when no subcommand is provided.
477///
478/// This function provides a menu-driven interface for users who prefer
479/// interactive operation over command-line arguments. It presents all
480/// available tag operations in an easy-to-navigate menu format.
481///
482/// ## Interactive Menu
483///
484/// The menu presents these options:
485/// 1. **Create tag**: Launches tag creation workflow with prompts for name and color
486/// 2. **List tags**: Shows all available tags in formatted table
487/// 3. **Edit tag**: Tag selection interface followed by editing prompts
488/// 4. **Delete tag**: Tag selection interface with safety confirmations
489///
490/// Each menu option delegates to the appropriate specialized handler
491/// function, ensuring consistent behavior between interactive and
492/// command-line usage.
493///
494/// ## Tag Selection Interface
495///
496/// For edit and delete operations, the interactive mode provides:
497/// - **Tag List Display**: Shows all available tags for selection
498/// - **Empty State Handling**: Graceful handling when no tags exist
499/// - **User-Friendly Selection**: Clear presentation of tag options
500/// - **Operation Cancellation**: Easy way to exit without changes
501///
502/// ## User Experience
503///
504/// The interactive mode is designed for:
505/// - Users new to the command-line interface
506/// - Occasional tag management tasks
507/// - Exploration of available tag operations
508/// - Situations where remembering exact command syntax is inconvenient
509fn handle_interactive() -> Result<()> {
510    let options = vec!["Add tag", "List tags", "Edit tag", "Remove tag"];
511
512    let selection = Select::with_theme(&ColorfulTheme::default())
513        .with_prompt(Message::SelectTagAction.to_string())
514        .items(&options)
515        .interact()?;
516
517    match selection {
518        0 => {
519            // Interactive tag creation
520            let name = Input::with_theme(&ColorfulTheme::default())
521                .with_prompt(Message::PromptTagName.to_string())
522                .interact_text()?;
523            let color: String = Input::with_theme(&ColorfulTheme::default())
524                .with_prompt(Message::PromptTagColor.to_string())
525                .allow_empty(true)
526                .interact_text()?;
527            handle_create(name, if color.is_empty() { None } else { Some(color) })
528        }
529        1 => handle_list(),
530        2 => {
531            // Interactive tag editing 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::SelectTagToEdit.to_string())
543                .items(&tag_names)
544                .interact()?;
545            handle_edit(tag_names[selection].clone())
546        }
547        3 => {
548            // Interactive tag deletion with selection
549            let mut tags_db = Tags::new()?;
550            let tags = tags_db.get_all()?;
551            if tags.is_empty() {
552                msg_info!(Message::NoTagsFound);
553                return Ok(());
554            }
555            drop(tags_db);
556
557            let tag_names: Vec<String> = tags.iter().map(|t| t.name.clone()).collect();
558            let selection = Select::with_theme(&ColorfulTheme::default())
559                .with_prompt(Message::SelectTagToDelete.to_string())
560                .items(&tag_names)
561                .interact()?;
562            handle_delete(tag_names[selection].clone(), false)
563        }
564        _ => Ok(()),
565    }
566}