tl_cli/cli/commands/
styles.rs1use 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
10pub fn list_styles() -> Result<()> {
12 let manager = ConfigManager::new()?;
13 let config = manager.load_or_default();
14
15 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 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
45pub 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 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_custom_key(&name).map_err(|e| anyhow::anyhow!("{e}"))?;
63
64 if config.styles.contains_key(&name) {
66 bail!("Style '{name}' already exists. Use 'tl styles edit {name}' to modify it.");
67 }
68
69 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 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 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
113pub fn edit_style(name: &str) -> Result<()> {
115 handle_prompt_cancellation(|| edit_style_inner(name))
116}
117
118fn edit_style_inner(name: &str) -> Result<()> {
119 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 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 let description = Text::new("Description:")
141 .with_default(¤t.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 let prompt = Text::new("Prompt:")
152 .with_default(¤t.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 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
181pub fn remove_style(name: &str) -> Result<()> {
183 handle_prompt_cancellation(|| remove_style_inner(name))
184}
185
186fn remove_style_inner(name: &str) -> Result<()> {
187 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 if !config.styles.contains_key(name) {
197 bail!("Style '{name}' not found");
198 }
199
200 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 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 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}