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}