1use clap::Subcommand;
2
3use crate::app_config::AppType;
4use crate::cli::ui::{create_table, highlight, info, success};
5use crate::error::AppError;
6use crate::services::PromptService;
7use crate::store::AppState;
8
9#[derive(Subcommand)]
10pub enum PromptsCommand {
11 List,
13 Current,
15 Activate {
17 id: String,
19 },
20 Deactivate,
22 Create {
24 name: Option<String>,
26 },
27 Edit {
29 id: String,
31 },
32 Rename {
34 id: String,
36 name: Option<String>,
38 },
39 Delete {
41 id: String,
43 },
44 Show {
46 id: String,
48 },
49}
50
51pub fn execute(cmd: PromptsCommand, app: Option<AppType>) -> Result<(), AppError> {
52 let app_type = app.unwrap_or(AppType::Claude);
53
54 match cmd {
55 PromptsCommand::List => list_prompts(app_type),
56 PromptsCommand::Current => show_current(app_type),
57 PromptsCommand::Activate { id } => activate_prompt(app_type, &id),
58 PromptsCommand::Deactivate => deactivate_prompt(app_type),
59 PromptsCommand::Create { name } => create_prompt(app_type, name),
60 PromptsCommand::Edit { id } => edit_prompt(app_type, &id),
61 PromptsCommand::Rename { id, name } => rename_prompt(app_type, &id, name),
62 PromptsCommand::Delete { id } => delete_prompt(app_type, &id),
63 PromptsCommand::Show { id } => show_prompt(app_type, &id),
64 }
65}
66
67fn get_state() -> Result<AppState, AppError> {
68 AppState::try_new()
69}
70
71fn list_prompts(app_type: AppType) -> Result<(), AppError> {
72 let state = get_state()?;
73 let prompts = PromptService::get_prompts(&state, app_type.clone())?;
74
75 if prompts.is_empty() {
76 println!("{}", info("No prompt presets found."));
77 println!("Use 'cc-switch prompts create' to create a new prompt preset.");
78 return Ok(());
79 }
80
81 let mut table = create_table();
83 table.set_header(vec!["", "ID", "Name", "Description", "Updated"]);
84
85 let mut prompt_list: Vec<_> = prompts.into_iter().collect();
87 prompt_list.sort_by(|(_, a), (_, b)| b.updated_at.unwrap_or(0).cmp(&a.updated_at.unwrap_or(0)));
88
89 for (id, prompt) in prompt_list {
90 let enabled_marker = if prompt.enabled { "✓" } else { " " };
91 let updated = prompt
92 .updated_at
93 .map(|ts| {
94 use chrono::{DateTime, Utc};
95 DateTime::<Utc>::from_timestamp(ts, 0)
96 .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
97 .unwrap_or_else(|| "Unknown".to_string())
98 })
99 .unwrap_or_else(|| "Unknown".to_string());
100
101 let description = prompt
102 .description
103 .as_deref()
104 .unwrap_or("")
105 .chars()
106 .take(40)
107 .collect::<String>();
108 let description = if prompt.description.as_ref().map(|d| d.len()).unwrap_or(0) > 40 {
109 format!("{}...", description)
110 } else {
111 description
112 };
113
114 let row = vec![
115 enabled_marker.to_string(),
116 id.clone(),
117 prompt.name.clone(),
118 description,
119 updated,
120 ];
121
122 table.add_row(row);
123 }
124
125 println!("{}", table);
126 println!("\n{} Application: {}", info("ℹ"), app_type.as_str());
127 println!("{} ✓ = Currently active", info("→"));
128
129 Ok(())
130}
131
132fn show_current(app_type: AppType) -> Result<(), AppError> {
133 let state = get_state()?;
134 let prompts = PromptService::get_prompts(&state, app_type.clone())?;
135
136 let active = prompts
138 .iter()
139 .find(|(_, p)| p.enabled)
140 .map(|(id, p)| (id.clone(), p.clone()));
141
142 match active {
143 Some((id, prompt)) => {
144 let updated = prompt
145 .updated_at
146 .and_then(|ts| {
147 use chrono::{DateTime, Utc};
148 DateTime::<Utc>::from_timestamp(ts, 0)
149 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
150 })
151 .unwrap_or_else(|| "Unknown".to_string());
152
153 println!("{}", highlight("Current Active Prompt"));
154 println!("{}", "=".repeat(50));
155 println!("ID: {}", id);
156 println!("Name: {}", prompt.name);
157 if let Some(desc) = &prompt.description {
158 println!("Description: {}", desc);
159 }
160 println!("Updated: {}", updated);
161 println!("App: {}", app_type.as_str());
162 println!();
163 println!("{}", highlight("Content Preview:"));
164 println!("{}", "-".repeat(50));
165
166 let lines: Vec<&str> = prompt.content.lines().collect();
168 let preview_lines = lines.iter().take(10);
169 for line in preview_lines {
170 println!("{}", line);
171 }
172
173 if lines.len() > 10 {
174 println!("...");
175 println!("{}", info(&format!("({} more lines)", lines.len() - 10)));
176 }
177 }
178 None => {
179 println!("{}", info("No active prompt preset."));
180 println!("Use 'cc-switch prompts activate <id>' to activate a prompt.");
181 }
182 }
183
184 Ok(())
185}
186
187fn activate_prompt(app_type: AppType, id: &str) -> Result<(), AppError> {
188 let state = get_state()?;
189 let app_str = app_type.as_str().to_string();
190
191 let prompts = PromptService::get_prompts(&state, app_type.clone())?;
193 if !prompts.contains_key(id) {
194 return Err(AppError::Message(format!(
195 "Prompt preset '{}' not found",
196 id
197 )));
198 }
199
200 PromptService::enable_prompt(&state, app_type, id)?;
202
203 println!(
204 "{}",
205 success(&format!("✓ Activated prompt preset '{}'", id))
206 );
207 println!("{}", info(&format!(" Application: {}", app_str)));
208 println!();
209 println!(
210 "{}",
211 info("Note: The prompt has been synced to the live configuration file.")
212 );
213
214 Ok(())
215}
216
217fn delete_prompt(app_type: AppType, id: &str) -> Result<(), AppError> {
218 let state = get_state()?;
219
220 let prompts = PromptService::get_prompts(&state, app_type.clone())?;
222 let prompt = prompts
223 .get(id)
224 .ok_or_else(|| AppError::Message(format!("Prompt preset '{}' not found", id)))?;
225
226 if prompt.enabled {
228 return Err(AppError::Message(
229 "Cannot delete the currently active prompt. Please activate another prompt first."
230 .to_string(),
231 ));
232 }
233
234 println!("{}", highlight("Prompt to be deleted:"));
236 println!("ID: {}", id);
237 println!("Name: {}", prompt.name);
238 if let Some(desc) = &prompt.description {
239 println!("Desc: {}", desc);
240 }
241 println!();
242
243 let confirm = inquire::Confirm::new(&format!(
245 "Are you sure you want to delete prompt preset '{}'?",
246 id
247 ))
248 .with_default(false)
249 .prompt()
250 .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
251
252 if !confirm {
253 println!("{}", info("Cancelled."));
254 return Ok(());
255 }
256
257 PromptService::delete_prompt(&state, app_type, id)?;
259
260 println!("{}", success(&format!("✓ Deleted prompt preset '{}'", id)));
261
262 Ok(())
263}
264
265fn show_prompt(app_type: AppType, id: &str) -> Result<(), AppError> {
266 let state = get_state()?;
267 let prompts = PromptService::get_prompts(&state, app_type)?;
268
269 let prompt = prompts
270 .get(id)
271 .ok_or_else(|| AppError::Message(format!("Prompt preset '{}' not found", id)))?;
272
273 let updated = prompt
274 .updated_at
275 .and_then(|ts| {
276 use chrono::{DateTime, Utc};
277 DateTime::<Utc>::from_timestamp(ts, 0)
278 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
279 })
280 .unwrap_or_else(|| "Unknown".to_string());
281
282 println!("{}", highlight(&format!("Prompt Preset: {}", prompt.name)));
283 println!("{}", "=".repeat(50));
284 println!("ID: {}", id);
285 println!("Name: {}", prompt.name);
286 if let Some(desc) = &prompt.description {
287 println!("Description: {}", desc);
288 }
289 println!(
290 "Status: {}",
291 if prompt.enabled {
292 highlight("Active")
293 } else {
294 "Inactive".to_string()
295 }
296 );
297 println!("Updated: {}", updated);
298 println!();
299 println!("{}", highlight("Content:"));
300 println!("{}", "-".repeat(50));
301 println!("{}", prompt.content);
302 println!("{}", "-".repeat(50));
303 println!("Lines: {}", prompt.content.lines().count());
304 println!("Size: {} bytes", prompt.content.len());
305
306 Ok(())
307}
308
309fn create_prompt(app_type: AppType, name: Option<String>) -> Result<(), AppError> {
310 let state = get_state()?;
311 let default_name = format!("Prompt {}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
312 let name = match name {
313 Some(name) => name,
314 None => inquire::Text::new("Prompt name:")
315 .with_initial_value(&default_name)
316 .prompt()
317 .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?,
318 };
319 let trimmed_name = name.trim();
320 if trimmed_name.is_empty() {
321 return Err(AppError::InvalidInput(
322 "Prompt preset name cannot be empty".to_string(),
323 ));
324 }
325
326 let initial = "# Write your prompt here\n";
327
328 println!("{}", highlight("Create New Prompt Preset"));
329 println!("{}", info("Opening external editor..."));
330
331 let edited = crate::cli::editor::open_external_editor(initial)?;
332 let prompt = PromptService::create_prompt(&state, app_type.clone(), trimmed_name, &edited)?;
333
334 println!(
335 "{}",
336 success(&format!("✓ Created prompt preset '{}'", prompt.id))
337 );
338 println!("{}", info(&format!(" Name: {}", prompt.name)));
339 println!("{}", info(&format!(" Application: {}", app_type.as_str())));
340 println!(
341 "{}",
342 info("Tip: Use 'cc-switch prompts list' to view all presets.")
343 );
344 Ok(())
345}
346
347fn deactivate_prompt(app_type: AppType) -> Result<(), AppError> {
348 let state = get_state()?;
349 let prompts = PromptService::get_prompts(&state, app_type.clone())?;
350
351 let active = prompts
353 .iter()
354 .find(|(_, p)| p.enabled)
355 .map(|(id, _)| id.clone());
356
357 match active {
358 Some(id) => {
359 PromptService::disable_prompt(&state, app_type.clone(), &id)?;
361
362 println!(
363 "{}",
364 success(&format!("✓ Deactivated prompt preset '{}'", id))
365 );
366 println!("{}", info(&format!(" Application: {}", app_type.as_str())));
367 println!();
368 println!(
369 "{}",
370 info("Note: The live configuration file has been cleared.")
371 );
372 }
373 None => {
374 println!("{}", info("No active prompt to deactivate."));
375 println!("Use 'cc-switch prompts activate <id>' to activate a prompt preset.");
376 }
377 }
378
379 Ok(())
380}
381
382fn edit_prompt(_app_type: AppType, id: &str) -> Result<(), AppError> {
383 let state = get_state()?;
384 let prompts = PromptService::get_prompts(&state, _app_type.clone())?;
385 let Some(mut prompt) = prompts.get(id).cloned() else {
386 return Err(AppError::InvalidInput(format!(
387 "Prompt preset '{id}' not found"
388 )));
389 };
390
391 println!("{}", info(&format!("Editing prompt preset '{}'...", id)));
392 println!("{}", info("Opening external editor..."));
393
394 let edited = crate::cli::editor::open_external_editor(&prompt.content)?;
395
396 if edited.trim_end() == prompt.content.trim_end() {
397 println!("{}", info("No changes detected."));
398 return Ok(());
399 }
400
401 let timestamp = std::time::SystemTime::now()
402 .duration_since(std::time::UNIX_EPOCH)
403 .unwrap_or_default()
404 .as_secs() as i64;
405 prompt.content = edited.trim_end().to_string();
406 prompt.updated_at = Some(timestamp);
407
408 PromptService::upsert_prompt(&state, _app_type.clone(), id, prompt)?;
409
410 println!("{}", success(&format!("✓ Updated prompt preset '{id}'")));
411 Ok(())
412}
413
414fn rename_prompt(app_type: AppType, id: &str, name: Option<String>) -> Result<(), AppError> {
415 let state = get_state()?;
416 let prompts = PromptService::get_prompts(&state, app_type.clone())?;
417 let Some(prompt) = prompts.get(id) else {
418 return Err(AppError::InvalidInput(format!(
419 "Prompt preset '{id}' not found"
420 )));
421 };
422
423 let new_name = match name {
424 Some(name) => name,
425 None => inquire::Text::new("New prompt name:")
426 .with_initial_value(&prompt.name)
427 .prompt()
428 .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?,
429 };
430
431 let trimmed = new_name.trim();
432 if trimmed.is_empty() {
433 return Err(AppError::InvalidInput(
434 "Prompt preset name cannot be empty".to_string(),
435 ));
436 }
437
438 if trimmed == prompt.name {
439 println!("{}", info("No changes detected."));
440 return Ok(());
441 }
442
443 PromptService::rename_prompt(&state, app_type.clone(), id, trimmed)?;
444
445 println!("{}", success(&format!("✓ Renamed prompt preset '{id}'")));
446 println!("{}", info(&format!(" Name: {}", trimmed)));
447 println!("{}", info(&format!(" Application: {}", app_type.as_str())));
448 Ok(())
449}