tl_cli/cli/commands/
styles.rs

1//! Styles command handler for managing translation styles.
2
3use anyhow::{Result, bail};
4use inquire::{Confirm, Text};
5
6use crate::config::{ConfigManager, CustomStyle};
7use crate::style::{PRESETS, is_preset, sorted_custom_keys, validate_custom_key};
8use crate::ui::{Style, handle_prompt_cancellation};
9
10/// Lists all available styles (presets and custom).
11pub fn list_styles() -> Result<()> {
12    let manager = ConfigManager::new()?;
13    let config = manager.load_or_default();
14
15    // Print preset styles
16    println!("{}", Style::header("Preset styles"));
17    for preset in PRESETS {
18        println!(
19            "  {}  {}",
20            Style::value(format!("{:10}", preset.key)),
21            Style::secondary(preset.description)
22        );
23    }
24
25    // Print custom styles if any
26    if !config.styles.is_empty() {
27        println!();
28        println!("{}", Style::header("Custom styles"));
29        for key in sorted_custom_keys(&config.styles) {
30            let description = config
31                .styles
32                .get(key)
33                .map_or("", |s| s.description.as_str());
34            println!(
35                "  {}  {}",
36                Style::value(format!("{key:10}")),
37                Style::secondary(description)
38            );
39        }
40    }
41
42    Ok(())
43}
44
45/// Adds a new custom style interactively.
46pub fn add_style() -> Result<()> {
47    handle_prompt_cancellation(add_style_inner)
48}
49
50fn add_style_inner() -> Result<()> {
51    let manager = ConfigManager::new()?;
52    let mut config = manager.load_or_default();
53
54    // Get style name
55    let name = Text::new("Style name:")
56        .with_help_message("Alphanumeric and underscores only (e.g., my_style)")
57        .prompt()?;
58
59    let name = name.trim().to_string();
60
61    // Validate name
62    validate_custom_key(&name).map_err(|e| anyhow::anyhow!("{e}"))?;
63
64    // Check if already exists
65    if config.styles.contains_key(&name) {
66        bail!("Style '{name}' already exists. Use 'tl styles edit {name}' to modify it.");
67    }
68
69    // Get style description (short, for display)
70    let description = Text::new("Description:")
71        .with_help_message("Short description for display (e.g., \"Ojisan-style texting\")")
72        .prompt()?;
73
74    let description = description.trim().to_string();
75
76    if description.is_empty() {
77        bail!("Description cannot be empty");
78    }
79
80    // Get style prompt (instructions for LLM)
81    let prompt = Text::new("Prompt:")
82        .with_help_message(
83            "Instructions for the LLM (e.g., \"Use excessive emojis and overly familiar tone\")",
84        )
85        .prompt()?;
86
87    let prompt = prompt.trim().to_string();
88
89    if prompt.is_empty() {
90        bail!("Prompt cannot be empty");
91    }
92
93    // Save
94    config.styles.insert(
95        name.clone(),
96        CustomStyle {
97            description,
98            prompt,
99        },
100    );
101    manager.save(&config)?;
102
103    println!();
104    println!(
105        "{} Style '{}' added",
106        Style::success("✓"),
107        Style::value(&name)
108    );
109
110    Ok(())
111}
112
113/// Edits an existing custom style.
114pub fn edit_style(name: &str) -> Result<()> {
115    handle_prompt_cancellation(|| edit_style_inner(name))
116}
117
118fn edit_style_inner(name: &str) -> Result<()> {
119    // Check if it's a preset
120    if is_preset(name) {
121        bail!("Cannot edit preset style '{name}'. Preset styles are immutable.");
122    }
123
124    let manager = ConfigManager::new()?;
125    let mut config = manager.load_or_default();
126
127    // Check if exists
128    let current = config.styles.get(name).cloned().ok_or_else(|| {
129        anyhow::anyhow!("Style '{name}' not found. Use 'tl styles add' to create it.")
130    })?;
131
132    println!(
133        "{} '{}':",
134        Style::header("Editing style"),
135        Style::value(name)
136    );
137    println!();
138
139    // Get new description
140    let description = Text::new("Description:")
141        .with_default(&current.description)
142        .prompt()?;
143
144    let description = description.trim().to_string();
145
146    if description.is_empty() {
147        bail!("Description cannot be empty");
148    }
149
150    // Get new prompt
151    let prompt = Text::new("Prompt:")
152        .with_default(&current.prompt)
153        .prompt()?;
154
155    let prompt = prompt.trim().to_string();
156
157    if prompt.is_empty() {
158        bail!("Prompt cannot be empty");
159    }
160
161    // Save
162    config.styles.insert(
163        name.to_string(),
164        CustomStyle {
165            description,
166            prompt,
167        },
168    );
169    manager.save(&config)?;
170
171    println!();
172    println!(
173        "{} Style '{}' updated",
174        Style::success("✓"),
175        Style::value(name)
176    );
177
178    Ok(())
179}
180
181/// Removes a custom style.
182pub fn remove_style(name: &str) -> Result<()> {
183    handle_prompt_cancellation(|| remove_style_inner(name))
184}
185
186fn remove_style_inner(name: &str) -> Result<()> {
187    // Check if it's a preset
188    if is_preset(name) {
189        bail!("Cannot remove preset style '{name}'. Preset styles are immutable.");
190    }
191
192    let manager = ConfigManager::new()?;
193    let mut config = manager.load_or_default();
194
195    // Check if exists
196    if !config.styles.contains_key(name) {
197        bail!("Style '{name}' not found");
198    }
199
200    // Confirm removal
201    let confirm = Confirm::new(&format!("Remove style '{name}'?"))
202        .with_default(false)
203        .prompt()?;
204
205    if !confirm {
206        println!("Cancelled");
207        return Ok(());
208    }
209
210    // Warn if it's the default style
211    if config.tl.style.as_deref() == Some(name) {
212        println!(
213            "{} This is your default style. You may want to run 'tl configure' to set a new default.",
214            Style::warning("Warning:")
215        );
216    }
217
218    // Remove
219    config.styles.remove(name);
220    manager.save(&config)?;
221
222    println!();
223    println!(
224        "{} Style '{}' removed",
225        Style::success("✓"),
226        Style::value(name)
227    );
228
229    Ok(())
230}