tl_cli/cli/commands/
styles.rs1use anyhow::{Result, bail};
4use inquire::{Confirm, Editor, Text};
5
6use super::load_config;
7use crate::config::CustomStyle;
8use crate::style::{PRESETS, get_preset, is_preset, sorted_custom_keys, validate_custom_key};
9use crate::ui::{Style, handle_prompt_cancellation};
10
11pub fn list_styles() -> Result<()> {
13 let (_manager, config) = load_config()?;
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 show_style(name: &str) -> Result<()> {
47 if let Some(preset) = get_preset(name) {
49 println!("{}", Style::header("Preset style"));
50 println!();
51 println!(" {} {}", Style::label("Name:"), Style::value(preset.key));
52 println!(
53 " {} {}",
54 Style::label("Desc:"),
55 Style::secondary(preset.description)
56 );
57 println!();
58 println!("{}", Style::label("Prompt:"));
59 println!("{}", preset.prompt);
60 return Ok(());
61 }
62
63 let (_manager, config) = load_config()?;
65
66 let custom = config
67 .styles
68 .get(name)
69 .ok_or_else(|| anyhow::anyhow!("Style '{name}' not found"))?;
70
71 println!("{}", Style::header("Custom style"));
72 println!();
73 println!("{} {}", Style::label("Name:"), Style::value(name));
74 println!(
75 "{} {}",
76 Style::label("Desc:"),
77 Style::secondary(&custom.description)
78 );
79 println!();
80 println!("{}", Style::label("Prompt:"));
81 println!("{}", custom.prompt);
82
83 Ok(())
84}
85
86pub fn add_style() -> Result<()> {
88 handle_prompt_cancellation(add_style_inner)
89}
90
91fn add_style_inner() -> Result<()> {
92 let (manager, mut config) = load_config()?;
93
94 let name = Text::new("Style name:")
96 .with_help_message("Alphanumeric and underscores only (e.g., my_style)")
97 .prompt()?;
98
99 let name = name.trim().to_string();
100
101 validate_custom_key(&name).map_err(|e| anyhow::anyhow!("{e}"))?;
103
104 if config.styles.contains_key(&name) {
106 bail!("Style '{name}' already exists. Use 'tl styles edit {name}' to modify it.");
107 }
108
109 let description = Text::new("Description:")
111 .with_help_message("Short description for display (e.g., \"Ojisan-style texting\")")
112 .prompt()?;
113
114 let description = description.trim().to_string();
115
116 if description.is_empty() {
117 bail!("Description cannot be empty");
118 }
119
120 let prompt = Editor::new("Prompt (opens editor):")
122 .with_help_message("Instructions for the LLM. Save and close editor when done.")
123 .with_predefined_text(
124 "# Enter the prompt for the LLM below.\n# Lines starting with # are ignored.\n\n",
125 )
126 .prompt()?;
127
128 let prompt = filter_comment_lines(&prompt);
129
130 if prompt.is_empty() {
131 bail!("Prompt cannot be empty");
132 }
133
134 config.styles.insert(
136 name.clone(),
137 CustomStyle {
138 description,
139 prompt,
140 },
141 );
142 manager.save(&config)?;
143
144 println!();
145 println!(
146 "{} Style '{}' added",
147 Style::success("✓"),
148 Style::value(&name)
149 );
150
151 Ok(())
152}
153
154fn filter_comment_lines(text: &str) -> String {
156 text.lines()
157 .filter(|line| !line.trim_start().starts_with('#'))
158 .collect::<Vec<_>>()
159 .join("\n")
160 .trim()
161 .to_string()
162}
163
164pub fn edit_style(name: &str) -> Result<()> {
166 handle_prompt_cancellation(|| edit_style_inner(name))
167}
168
169fn edit_style_inner(name: &str) -> Result<()> {
170 if is_preset(name) {
172 bail!("Cannot edit preset style '{name}'. Preset styles are immutable.");
173 }
174
175 let (manager, mut config) = load_config()?;
176
177 let current = config.styles.get(name).cloned().ok_or_else(|| {
179 anyhow::anyhow!("Style '{name}' not found. Use 'tl styles add' to create it.")
180 })?;
181
182 println!(
183 "{} '{}':",
184 Style::header("Editing style"),
185 Style::value(name)
186 );
187 println!();
188
189 let description = Text::new("Description:")
191 .with_default(¤t.description)
192 .prompt()?;
193
194 let description = description.trim().to_string();
195
196 if description.is_empty() {
197 bail!("Description cannot be empty");
198 }
199
200 let prompt = Editor::new("Prompt (opens editor):")
202 .with_help_message("Edit the prompt for the LLM. Save and close editor when done.")
203 .with_predefined_text(¤t.prompt)
204 .prompt()?;
205
206 let prompt = prompt.trim().to_string();
207
208 if prompt.is_empty() {
209 bail!("Prompt cannot be empty");
210 }
211
212 config.styles.insert(
214 name.to_string(),
215 CustomStyle {
216 description,
217 prompt,
218 },
219 );
220 manager.save(&config)?;
221
222 println!();
223 println!(
224 "{} Style '{}' updated",
225 Style::success("✓"),
226 Style::value(name)
227 );
228
229 Ok(())
230}
231
232pub fn remove_style(name: &str) -> Result<()> {
234 handle_prompt_cancellation(|| remove_style_inner(name))
235}
236
237fn remove_style_inner(name: &str) -> Result<()> {
238 if is_preset(name) {
240 bail!("Cannot remove preset style '{name}'. Preset styles are immutable.");
241 }
242
243 let (manager, mut config) = load_config()?;
244
245 if !config.styles.contains_key(name) {
247 bail!("Style '{name}' not found");
248 }
249
250 let confirm = Confirm::new(&format!("Remove style '{name}'?"))
252 .with_default(false)
253 .prompt()?;
254
255 if !confirm {
256 println!("Cancelled");
257 return Ok(());
258 }
259
260 if config.tl.style.as_deref() == Some(name) {
262 println!(
263 "{} This is your default style. You may want to run 'tl configure' to set a new default.",
264 Style::warning("Warning:")
265 );
266 }
267
268 config.styles.remove(name);
270 manager.save(&config)?;
271
272 println!();
273 println!(
274 "{} Style '{}' removed",
275 Style::success("✓"),
276 Style::value(name)
277 );
278
279 Ok(())
280}