Skip to main content

cc_switch_lib/cli/commands/
prompts.rs

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 all prompt presets
12    List,
13    /// Show current active prompt
14    Current,
15    /// Activate a prompt preset
16    Activate {
17        /// Prompt preset ID
18        id: String,
19    },
20    /// Deactivate the current active prompt
21    Deactivate,
22    /// Create a new prompt preset
23    Create {
24        /// Prompt preset name
25        name: Option<String>,
26    },
27    /// Edit a prompt preset
28    Edit {
29        /// Prompt preset ID
30        id: String,
31    },
32    /// Rename a prompt preset
33    Rename {
34        /// Prompt preset ID
35        id: String,
36        /// New prompt name
37        name: Option<String>,
38    },
39    /// Delete a prompt preset
40    Delete {
41        /// Prompt preset ID
42        id: String,
43    },
44    /// Show prompt content
45    Show {
46        /// Prompt preset ID
47        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    // 创建表格
82    let mut table = create_table();
83    table.set_header(vec!["", "ID", "Name", "Description", "Updated"]);
84
85    // 按更新时间排序
86    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    // 找到当前激活的 prompt
137    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            // 显示内容预览(前 10 行)
167            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    // 检查 prompt 是否存在
192    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    // 执行激活
201    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    // 检查 prompt 是否存在
221    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    // 检查是否是当前激活的 prompt
227    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    // 显示将要删除的 prompt 信息
235    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    // 确认删除
244    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    // 执行删除
258    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    // Find currently enabled prompt
352    let active = prompts
353        .iter()
354        .find(|(_, p)| p.enabled)
355        .map(|(id, _)| id.clone());
356
357    match active {
358        Some(id) => {
359            // Deactivate the current prompt
360            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}