ralph/cli/queue/
search.rs1use anyhow::{Result, bail};
19use clap::Args;
20
21use crate::cli::{load_and_validate_queues_read_only, resolve_list_limit};
22use crate::config::Resolved;
23use crate::contracts::{Task, TaskStatus};
24use crate::{outpututil, queue};
25
26use super::{QueueListFormat, StatusArg};
27
28#[derive(Args)]
30#[command(
32 after_long_help = "Examples:\n ralph queue search \"authentication\"\n ralph queue search \"RQ-\\d{4}\" --regex\n ralph queue search \"TODO\" --match-case\n ralph queue search \"fix\" --status todo --tag rust\n ralph queue search \"refactor\" --scope crates/ralph --tag rust\n ralph queue search \"auth bug\" --fuzzy\n ralph queue search \"fuzzy search\" --fuzzy --match-case"
33)]
34pub struct QueueSearchArgs {
35 #[arg(value_name = "QUERY")]
37 pub query: String,
38
39 #[arg(long)]
41 pub regex: bool,
42
43 #[arg(long)]
45 pub match_case: bool,
46
47 #[arg(long)]
49 pub fuzzy: bool,
50
51 #[arg(long, value_enum)]
53 pub status: Vec<StatusArg>,
54
55 #[arg(long)]
57 pub tag: Vec<String>,
58
59 #[arg(long)]
61 pub scope: Vec<String>,
62
63 #[arg(long)]
65 pub include_done: bool,
66
67 #[arg(long)]
69 pub only_done: bool,
70
71 #[arg(long, value_enum, default_value_t = QueueListFormat::Compact)]
73 pub format: QueueListFormat,
74
75 #[arg(long, default_value_t = 50)]
77 pub limit: u32,
78
79 #[arg(long)]
81 pub all: bool,
82
83 #[arg(long)]
85 pub scheduled: bool,
86}
87
88pub(crate) fn handle(resolved: &Resolved, args: QueueSearchArgs) -> Result<()> {
89 if args.include_done && args.only_done {
90 bail!(
91 "Conflicting flags: --include-done and --only-done are mutually exclusive. Choose either to include done tasks or to only search done tasks."
92 );
93 }
94
95 if args.fuzzy && args.regex {
96 bail!(
97 "Conflicting flags: --fuzzy and --regex are mutually exclusive. Choose either fuzzy matching or regex matching."
98 );
99 }
100
101 let (queue_file, done_file) =
102 load_and_validate_queues_read_only(resolved, args.include_done || args.only_done)?;
103 let done_ref = done_file
104 .as_ref()
105 .filter(|d| !d.tasks.is_empty() || resolved.done_path.exists());
106
107 let statuses: Vec<TaskStatus> = args.status.into_iter().map(|s| s.into()).collect();
108
109 let mut prefiltered: Vec<&Task> = Vec::new();
111 if !args.only_done {
112 prefiltered.extend(queue::filter_tasks(
113 &queue_file,
114 &statuses,
115 &args.tag,
116 &args.scope,
117 None,
118 ));
119 }
120 if (args.include_done || args.only_done)
121 && let Some(done_ref) = done_ref
122 {
123 prefiltered.extend(queue::filter_tasks(
124 done_ref,
125 &statuses,
126 &args.tag,
127 &args.scope,
128 None,
129 ));
130 }
131
132 if args.scheduled {
134 prefiltered.retain(|task| task.scheduled_start.is_some());
135 }
136
137 let search_options = queue::SearchOptions {
139 use_regex: args.regex,
140 case_sensitive: args.match_case,
141 use_fuzzy: args.fuzzy,
142 scopes: args.scope.clone(),
143 };
144
145 let results =
147 queue::search_tasks_with_options(prefiltered.into_iter(), &args.query, &search_options)?;
148
149 let limit = resolve_list_limit(args.limit, args.all);
150 let max = limit.unwrap_or(usize::MAX);
151 let results: Vec<&Task> = results.into_iter().take(max).collect();
152
153 match args.format {
154 QueueListFormat::Compact => {
155 for task in results {
156 println!("{}", outpututil::format_task_compact(task));
157 }
158 }
159 QueueListFormat::Long => {
160 for task in results {
161 println!("{}", outpututil::format_task_detailed(task));
162 }
163 }
164 QueueListFormat::Json => {
165 let owned_tasks: Vec<Task> = results.into_iter().cloned().collect();
166 let json = serde_json::to_string_pretty(&owned_tasks)?;
167 println!("{json}");
168 }
169 }
170
171 Ok(())
172}