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::{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 /// 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 {
327 id: tag.id,
328 name: new_name.clone(),
329 color,
330 created_at: None,
331 })?;
332 msg_success!(Message::TagUpdated(new_name));
333 Ok(())
334}
335
336/// Handles safe tag deletion with usage impact analysis.
337///
338/// This function manages the tag deletion process with comprehensive
339/// safety measures to prevent accidental data loss and inform users
340/// about the impact of deletion:
341/// 1. **Tag Resolution**: Finds tag by name or ID
342/// 2. **Usage Analysis**: Counts how many tasks currently use the tag
343/// 3. **Impact Communication**: Informs user about affected tasks
344/// 4. **Confirmation Prompt**: Requires explicit user confirmation
345/// 5. **Safe Deletion**: Removes tag and updates task associations
346///
347/// ## Safety Features
348///
349/// - Confirms tag exists before showing deletion prompt
350/// - Analyzes and reports impact on existing tasks
351/// - Uses different confirmation messages based on usage
352/// - Defaults to "No" for safety
353/// - Provides escape opportunity before actual deletion
354/// - Clear feedback about cancellation vs. completion
355///
356/// ## Usage Impact
357///
358/// The function provides different confirmation prompts based on tag usage:
359/// - **Unused Tags**: Simple confirmation for deletion
360/// - **Used Tags**: Enhanced warning showing number of affected tasks
361/// - **Heavily Used Tags**: Additional emphasis on impact scope
362///
363/// # Arguments
364///
365/// * `tag_identifier` - Tag name or ID string for flexible tag identification
366fn handle_delete(tag_identifier: String) -> Result<()> {
367 let mut tags_db = Tags::new()?;
368
369 // Resolve tag by ID or name
370 let tag = if let Ok(id) = tag_identifier.parse::<i32>() {
371 tags_db.get_by_id(id)?
372 } else {
373 tags_db.get_by_name(&tag_identifier)?
374 };
375
376 let tag = match tag {
377 Some(t) => t,
378 None => {
379 msg_error!(Message::TagNotFound(tag_identifier));
380 return Ok(());
381 }
382 };
383
384 // Analyze usage impact
385 let task_count = tags_db.get_tasks_by_tag(tag.id.unwrap())?.len();
386
387 // Provide appropriate confirmation prompt based on usage
388 let prompt = if task_count > 0 {
389 Message::ConfirmDeleteTagWithTasks(tag.name.clone(), task_count)
390 } else {
391 Message::ConfirmDeleteTag(tag.name.clone())
392 };
393
394 let confirmed = Confirm::with_theme(&ColorfulTheme::default())
395 .with_prompt(prompt.to_string())
396 .default(false)
397 .interact()?;
398
399 if confirmed {
400 tags_db.delete(tag.id.unwrap())?;
401 msg_success!(Message::TagDeleted(tag.name));
402 } else {
403 msg_info!(Message::OperationCancelled);
404 }
405
406 Ok(())
407}
408
409/// Displays all tasks associated with a specific tag.
410///
411/// This function provides filtered task viewing based on tag assignment,
412/// allowing users to see all work items within a particular category.
413/// It's useful for project management and understanding work distribution
414/// across different areas.
415///
416/// ## Display Features
417///
418/// - **Tag Validation**: Ensures the specified tag exists
419/// - **Task Filtering**: Shows only tasks with the specified tag
420/// - **Standard Format**: Uses the same task display format as other commands
421/// - **Empty State Handling**: Provides helpful message when no tasks found
422///
423/// ## Use Cases
424///
425/// This functionality supports various workflows:
426/// - **Project Review**: See all tasks for a specific project
427/// - **Priority Management**: View all urgent or high-priority tasks
428/// - **Sprint Planning**: Review tasks by type or area
429/// - **Progress Tracking**: Monitor completion within categories
430///
431/// # Arguments
432///
433/// * `tag_name` - Name of the tag to filter tasks by
434async fn handle_show_tasks(tag_name: String) -> Result<()> {
435 let mut tags_db = Tags::new()?;
436
437 // Validate tag exists
438 let tag = match tags_db.get_by_name(&tag_name)? {
439 Some(t) => t,
440 None => {
441 msg_error!(Message::TagNotFound(tag_name));
442 return Ok(());
443 }
444 };
445
446 // Get task IDs associated with this tag
447 let task_ids = tags_db.get_tasks_by_tag(tag.id.unwrap())?;
448
449 if task_ids.is_empty() {
450 msg_info!(Message::NoTasksWithTag(tag_name));
451 return Ok(());
452 }
453
454 // Fetch and display the tasks
455 use crate::db::tasks::Tasks;
456 let tasks = Tasks::new()?.fetch(crate::libs::task::TaskFilter::ByIds(task_ids))?;
457
458 msg_print!(Message::TasksWithTag(tag_name), true);
459 View::tasks(&tasks)?;
460
461 Ok(())
462}
463
464/// Handles interactive tag management when no subcommand is provided.
465///
466/// This function provides a menu-driven interface for users who prefer
467/// interactive operation over command-line arguments. It presents all
468/// available tag operations in an easy-to-navigate menu format.
469///
470/// ## Interactive Menu
471///
472/// The menu presents these options:
473/// 1. **Create tag**: Launches tag creation workflow with prompts for name and color
474/// 2. **List tags**: Shows all available tags in formatted table
475/// 3. **Edit tag**: Tag selection interface followed by editing prompts
476/// 4. **Delete tag**: Tag selection interface with safety confirmations
477///
478/// Each menu option delegates to the appropriate specialized handler
479/// function, ensuring consistent behavior between interactive and
480/// command-line usage.
481///
482/// ## Tag Selection Interface
483///
484/// For edit and delete operations, the interactive mode provides:
485/// - **Tag List Display**: Shows all available tags for selection
486/// - **Empty State Handling**: Graceful handling when no tags exist
487/// - **User-Friendly Selection**: Clear presentation of tag options
488/// - **Operation Cancellation**: Easy way to exit without changes
489///
490/// ## User Experience
491///
492/// The interactive mode is designed for:
493/// - Users new to the command-line interface
494/// - Occasional tag management tasks
495/// - Exploration of available tag operations
496/// - Situations where remembering exact command syntax is inconvenient
497fn handle_interactive() -> Result<()> {
498 let options = vec!["Create tag", "List tags", "Edit tag", "Delete tag"];
499
500 let selection = Select::with_theme(&ColorfulTheme::default())
501 .with_prompt(Message::SelectTagAction.to_string())
502 .items(&options)
503 .interact()?;
504
505 match selection {
506 0 => {
507 // Interactive tag creation
508 let name = Input::with_theme(&ColorfulTheme::default())
509 .with_prompt(Message::PromptTagName.to_string())
510 .interact_text()?;
511 let color: String = Input::with_theme(&ColorfulTheme::default())
512 .with_prompt(Message::PromptTagColor.to_string())
513 .allow_empty(true)
514 .interact_text()?;
515 handle_create(name, if color.is_empty() { None } else { Some(color) })
516 }
517 1 => handle_list(),
518 2 => {
519 // Interactive tag editing with selection
520 let mut tags_db = Tags::new()?;
521 let tags = tags_db.get_all()?;
522 if tags.is_empty() {
523 msg_info!(Message::NoTagsFound);
524 return Ok(());
525 }
526 drop(tags_db);
527
528 let tag_names: Vec<String> = tags.iter().map(|t| t.name.clone()).collect();
529 let selection = Select::with_theme(&ColorfulTheme::default())
530 .with_prompt(Message::SelectTagToEdit.to_string())
531 .items(&tag_names)
532 .interact()?;
533 handle_edit(tag_names[selection].clone())
534 }
535 3 => {
536 // Interactive tag deletion with selection
537 let mut tags_db = Tags::new()?;
538 let tags = tags_db.get_all()?;
539 if tags.is_empty() {
540 msg_info!(Message::NoTagsFound);
541 return Ok(());
542 }
543 drop(tags_db);
544
545 let tag_names: Vec<String> = tags.iter().map(|t| t.name.clone()).collect();
546 let selection = Select::with_theme(&ColorfulTheme::default())
547 .with_prompt(Message::SelectTagToDelete.to_string())
548 .items(&tag_names)
549 .interact()?;
550 handle_delete(tag_names[selection].clone())
551 }
552 _ => Ok(()),
553 }
554}