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