1use crate::common::CommonParams;
2use crate::config::Config;
3use crate::instruction_presets::{
4 PresetType, get_instruction_preset_library, list_presets_formatted_by_type,
5};
6use crate::log_debug;
7use crate::providers::{Provider, ProviderConfig};
8use crate::ui;
9use anyhow::Context;
10use anyhow::{Result, anyhow};
11use colored::Colorize;
12use std::collections::HashMap;
13
14mod colors {
16 use crate::theme;
17 use crate::theme::names::tokens;
18
19 pub fn accent_primary() -> (u8, u8, u8) {
20 let c = theme::current().color(tokens::ACCENT_PRIMARY);
21 (c.r, c.g, c.b)
22 }
23
24 pub fn accent_secondary() -> (u8, u8, u8) {
25 let c = theme::current().color(tokens::ACCENT_SECONDARY);
26 (c.r, c.g, c.b)
27 }
28
29 pub fn accent_tertiary() -> (u8, u8, u8) {
30 let c = theme::current().color(tokens::ACCENT_TERTIARY);
31 (c.r, c.g, c.b)
32 }
33
34 pub fn warning() -> (u8, u8, u8) {
35 let c = theme::current().color(tokens::WARNING);
36 (c.r, c.g, c.b)
37 }
38
39 pub fn success() -> (u8, u8, u8) {
40 let c = theme::current().color(tokens::SUCCESS);
41 (c.r, c.g, c.b)
42 }
43
44 pub fn text_secondary() -> (u8, u8, u8) {
45 let c = theme::current().color(tokens::TEXT_SECONDARY);
46 (c.r, c.g, c.b)
47 }
48
49 pub fn text_dim() -> (u8, u8, u8) {
50 let c = theme::current().color(tokens::TEXT_DIM);
51 (c.r, c.g, c.b)
52 }
53}
54
55fn apply_config_changes(
74 config: &mut Config,
75 common: &CommonParams,
76 model: Option<String>,
77 fast_model: Option<String>,
78 token_limit: Option<usize>,
79 param: Option<Vec<String>>,
80 api_key: Option<String>,
81 subagent_timeout: Option<u64>,
82) -> anyhow::Result<bool> {
83 let mut changes_made = false;
84
85 let common_changes = common.apply_to_config(config)?;
87 changes_made |= common_changes;
88
89 if let Some(provider_str) = &common.provider {
91 let provider: Provider = provider_str.parse().map_err(|_| {
92 anyhow!(
93 "Invalid provider: {}. Available: {}",
94 provider_str,
95 Provider::all_names().join(", ")
96 )
97 })?;
98
99 if !config.providers.contains_key(provider.name()) {
101 config.providers.insert(
102 provider.name().to_string(),
103 ProviderConfig::with_defaults(provider),
104 );
105 changes_made = true;
106 }
107 }
108
109 let provider_config = config
110 .providers
111 .get_mut(&config.default_provider)
112 .context("Could not get default provider")?;
113
114 if let Some(key) = api_key
116 && provider_config.api_key != key
117 {
118 provider_config.api_key = key;
119 changes_made = true;
120 }
121
122 if let Some(model) = model
124 && provider_config.model != model
125 {
126 provider_config.model = model;
127 changes_made = true;
128 }
129
130 if let Some(fast_model) = fast_model
132 && provider_config.fast_model != Some(fast_model.clone())
133 {
134 provider_config.fast_model = Some(fast_model);
135 changes_made = true;
136 }
137
138 if let Some(params) = param {
140 let additional_params = parse_additional_params(¶ms);
141 if provider_config.additional_params != additional_params {
142 provider_config.additional_params = additional_params;
143 changes_made = true;
144 }
145 }
146
147 if let Some(use_gitmoji) = common.resolved_gitmoji()
149 && config.use_gitmoji != use_gitmoji
150 {
151 config.use_gitmoji = use_gitmoji;
152 changes_made = true;
153 }
154
155 if let Some(instr) = &common.instructions
157 && config.instructions != *instr
158 {
159 config.instructions.clone_from(instr);
160 changes_made = true;
161 }
162
163 if let Some(limit) = token_limit
165 && provider_config.token_limit != Some(limit)
166 {
167 provider_config.token_limit = Some(limit);
168 changes_made = true;
169 }
170
171 if let Some(preset) = &common.preset {
173 let preset_library = get_instruction_preset_library();
174 if preset_library.get_preset(preset).is_some() {
175 if config.instruction_preset != *preset {
176 config.instruction_preset.clone_from(preset);
177 changes_made = true;
178 }
179 } else {
180 return Err(anyhow!("Invalid preset: {}", preset));
181 }
182 }
183
184 if let Some(timeout) = subagent_timeout
186 && config.subagent_timeout_secs != timeout
187 {
188 config.subagent_timeout_secs = timeout;
189 changes_made = true;
190 }
191
192 Ok(changes_made)
193}
194
195#[allow(clippy::too_many_lines)]
197pub fn handle_config_command(
198 common: &CommonParams,
199 api_key: Option<String>,
200 model: Option<String>,
201 fast_model: Option<String>,
202 token_limit: Option<usize>,
203 param: Option<Vec<String>>,
204 subagent_timeout: Option<u64>,
205) -> anyhow::Result<()> {
206 log_debug!(
207 "Starting 'config' command with common: {:?}, api_key: {:?}, model: {:?}, token_limit: {:?}, param: {:?}, subagent_timeout: {:?}",
208 common,
209 api_key,
210 model,
211 token_limit,
212 param,
213 subagent_timeout
214 );
215
216 let mut config = Config::load()?;
217
218 let changes_made = apply_config_changes(
220 &mut config,
221 common,
222 model,
223 fast_model,
224 token_limit,
225 param,
226 api_key,
227 subagent_timeout,
228 )?;
229
230 if changes_made {
231 config.save()?;
232 ui::print_success("Configuration updated successfully.");
233 ui::print_newline();
234 }
235
236 print_configuration(&config);
238
239 Ok(())
240}
241
242fn print_project_config() {
247 if let Ok(project_config) = Config::load_project_config() {
248 ui::print_message(&format!(
249 "\n{}",
250 "Current project configuration:".bright_cyan().bold()
251 ));
252 print_configuration(&project_config);
253 } else {
254 ui::print_message(&format!(
255 "\n{}",
256 "No project configuration file found.".yellow()
257 ));
258 ui::print_message("You can create one with the project-config command.");
259 }
260}
261
262pub fn handle_project_config_command(
285 common: &CommonParams,
286 model: Option<String>,
287 fast_model: Option<String>,
288 token_limit: Option<usize>,
289 param: Option<Vec<String>>,
290 subagent_timeout: Option<u64>,
291 print: bool,
292) -> anyhow::Result<()> {
293 log_debug!(
294 "Starting 'project-config' command with common: {:?}, model: {:?}, token_limit: {:?}, param: {:?}, subagent_timeout: {:?}, print: {}",
295 common,
296 model,
297 token_limit,
298 param,
299 subagent_timeout,
300 print
301 );
302
303 println!("\n{}", "✨ Project Configuration".bright_magenta().bold());
304
305 if print {
306 print_project_config();
307 return Ok(());
308 }
309
310 let mut config = Config::load_project_config().unwrap_or_else(|_| Config {
311 default_provider: String::new(),
312 providers: HashMap::new(),
313 use_gitmoji: true,
314 instructions: String::new(),
315 instruction_preset: String::new(),
316 theme: String::new(),
317 subagent_timeout_secs: 120,
318 temp_instructions: None,
319 temp_preset: None,
320 is_project_config: true,
321 gitmoji_override: None,
322 });
323
324 let mut changes_made = false;
325
326 let provider_name = apply_provider_settings(
328 &mut config,
329 common,
330 model,
331 fast_model,
332 token_limit,
333 param,
334 &mut changes_made,
335 )?;
336
337 apply_common_settings(&mut config, common, subagent_timeout, &mut changes_made)?;
339
340 display_project_config_result(&config, changes_made, &provider_name)?;
342
343 Ok(())
344}
345
346fn apply_provider_settings(
348 config: &mut Config,
349 common: &CommonParams,
350 model: Option<String>,
351 fast_model: Option<String>,
352 token_limit: Option<usize>,
353 param: Option<Vec<String>>,
354 changes_made: &mut bool,
355) -> anyhow::Result<String> {
356 if let Some(provider_str) = &common.provider {
358 let provider: Provider = provider_str.parse().map_err(|_| {
359 anyhow!(
360 "Invalid provider: {}. Available: {}",
361 provider_str,
362 Provider::all_names().join(", ")
363 )
364 })?;
365
366 if config.default_provider != provider.name() {
367 config.default_provider = provider.name().to_string();
368 config
369 .providers
370 .entry(provider.name().to_string())
371 .or_default();
372 *changes_made = true;
373 }
374 }
375
376 let provider_name = common
378 .provider
379 .clone()
380 .or_else(|| {
381 if config.default_provider.is_empty() {
382 None
383 } else {
384 Some(config.default_provider.clone())
385 }
386 })
387 .unwrap_or_else(|| Provider::default().name().to_string());
388
389 if model.is_some() || fast_model.is_some() || token_limit.is_some() || param.is_some() {
391 config.providers.entry(provider_name.clone()).or_default();
392 }
393
394 if let Some(m) = model
396 && let Some(pc) = config.providers.get_mut(&provider_name)
397 && pc.model != m
398 {
399 pc.model = m;
400 *changes_made = true;
401 }
402
403 if let Some(fm) = fast_model
404 && let Some(pc) = config.providers.get_mut(&provider_name)
405 && pc.fast_model != Some(fm.clone())
406 {
407 pc.fast_model = Some(fm);
408 *changes_made = true;
409 }
410
411 if let Some(limit) = token_limit
412 && let Some(pc) = config.providers.get_mut(&provider_name)
413 && pc.token_limit != Some(limit)
414 {
415 pc.token_limit = Some(limit);
416 *changes_made = true;
417 }
418
419 if let Some(params) = param
420 && let Some(pc) = config.providers.get_mut(&provider_name)
421 {
422 let additional_params = parse_additional_params(¶ms);
423 if pc.additional_params != additional_params {
424 pc.additional_params = additional_params;
425 *changes_made = true;
426 }
427 }
428
429 Ok(provider_name)
430}
431
432fn apply_common_settings(
434 config: &mut Config,
435 common: &CommonParams,
436 subagent_timeout: Option<u64>,
437 changes_made: &mut bool,
438) -> anyhow::Result<()> {
439 if let Some(use_gitmoji) = common.resolved_gitmoji()
440 && config.use_gitmoji != use_gitmoji
441 {
442 config.use_gitmoji = use_gitmoji;
443 *changes_made = true;
444 }
445
446 if let Some(instr) = &common.instructions
447 && config.instructions != *instr
448 {
449 config.instructions.clone_from(instr);
450 *changes_made = true;
451 }
452
453 if let Some(preset) = &common.preset {
454 let preset_library = get_instruction_preset_library();
455 if preset_library.get_preset(preset).is_some() {
456 if config.instruction_preset != *preset {
457 config.instruction_preset.clone_from(preset);
458 *changes_made = true;
459 }
460 } else {
461 return Err(anyhow!("Invalid preset: {}", preset));
462 }
463 }
464
465 if let Some(timeout) = subagent_timeout
466 && config.subagent_timeout_secs != timeout
467 {
468 config.subagent_timeout_secs = timeout;
469 *changes_made = true;
470 }
471
472 Ok(())
473}
474
475fn display_project_config_result(
477 config: &Config,
478 changes_made: bool,
479 _provider_name: &str,
480) -> anyhow::Result<()> {
481 if changes_made {
482 config.save_as_project_config()?;
483 ui::print_success("Project configuration created/updated successfully.");
484 println!();
485 println!(
486 "{}",
487 "Note: API keys are never stored in project configuration files."
488 .yellow()
489 .italic()
490 );
491 println!();
492 println!("{}", "Current project configuration:".bright_cyan().bold());
493 print_configuration(config);
494 } else {
495 println!("{}", "No changes made to project configuration.".yellow());
496 println!();
497
498 if let Ok(project_config) = Config::load_project_config() {
499 println!("{}", "Current project configuration:".bright_cyan().bold());
500 print_configuration(&project_config);
501 } else {
502 println!("{}", "No project configuration exists yet.".bright_yellow());
503 println!(
504 "{}",
505 "Use this command with options like --model or --provider to create one."
506 .bright_white()
507 );
508 }
509 }
510 Ok(())
511}
512
513fn print_configuration(config: &Config) {
515 let purple = colors::accent_primary();
516 let cyan = colors::accent_secondary();
517 let coral = colors::accent_tertiary();
518 let yellow = colors::warning();
519 let green = colors::success();
520 let dim = colors::text_secondary();
521 let dim_sep = colors::text_dim();
522
523 println!();
524 println!(
525 "{} {} {}",
526 "━━━".truecolor(purple.0, purple.1, purple.2),
527 "IRIS CONFIGURATION"
528 .truecolor(cyan.0, cyan.1, cyan.2)
529 .bold(),
530 "━━━".truecolor(purple.0, purple.1, purple.2)
531 );
532 println!();
533
534 print_section_header("GLOBAL");
536
537 print_config_row("Provider", &config.default_provider, cyan, true);
538 print_config_row(
539 "Gitmoji",
540 if config.use_gitmoji {
541 "enabled"
542 } else {
543 "disabled"
544 },
545 if config.use_gitmoji { green } else { dim },
546 false,
547 );
548 print_config_row("Preset", &config.instruction_preset, yellow, false);
549 print_config_row(
550 "Subagent Timeout",
551 &format!("{}s", config.subagent_timeout_secs),
552 coral,
553 false,
554 );
555
556 if !config.instructions.is_empty() {
558 println!();
559 print_section_header("INSTRUCTIONS");
560 for line in config.instructions.lines() {
561 println!(" {}", line.truecolor(dim.0, dim.1, dim.2).italic());
562 }
563 }
564
565 let mut providers: Vec<_> = config
569 .providers
570 .iter()
571 .filter(|(_, cfg)| config.is_project_config || !cfg.api_key.is_empty())
572 .collect();
573 providers.sort_by_key(|(name, _)| name.as_str());
574
575 for (provider_name, provider_config) in providers {
576 println!();
577 let is_active = provider_name == &config.default_provider;
578 let header = if is_active {
579 format!("{} ✦", provider_name.to_uppercase())
580 } else {
581 provider_name.to_uppercase()
582 };
583 print_section_header(&header);
584
585 print_config_row("Model", &provider_config.model, cyan, true);
587
588 let fast_model = provider_config.fast_model.as_deref().unwrap_or("(default)");
590 print_config_row("Fast Model", fast_model, cyan, false);
591
592 if let Some(limit) = provider_config.token_limit {
594 print_config_row("Token Limit", &limit.to_string(), coral, false);
595 }
596
597 if !provider_config.additional_params.is_empty() {
599 println!(
600 " {} {}",
601 "Params".truecolor(dim.0, dim.1, dim.2),
602 "─".truecolor(dim_sep.0, dim_sep.1, dim_sep.2)
603 );
604 for (key, value) in &provider_config.additional_params {
605 println!(
606 " {} {} {}",
607 key.truecolor(cyan.0, cyan.1, cyan.2),
608 "→".truecolor(dim_sep.0, dim_sep.1, dim_sep.2),
609 value.truecolor(dim.0, dim.1, dim.2)
610 );
611 }
612 }
613 }
614
615 println!();
616 println!(
617 "{}",
618 "─".repeat(40).truecolor(dim_sep.0, dim_sep.1, dim_sep.2)
619 );
620 println!();
621}
622
623fn print_section_header(name: &str) {
625 let purple = colors::accent_primary();
626 let dim_sep = colors::text_dim();
627 println!(
628 "{} {} {}",
629 "─".truecolor(purple.0, purple.1, purple.2),
630 name.truecolor(purple.0, purple.1, purple.2).bold(),
631 "─"
632 .repeat(30 - name.len().min(28))
633 .truecolor(dim_sep.0, dim_sep.1, dim_sep.2)
634 );
635}
636
637fn print_config_row(label: &str, value: &str, value_color: (u8, u8, u8), highlight: bool) {
639 let dim = colors::text_secondary();
640 let label_styled = format!("{label:>12}").truecolor(dim.0, dim.1, dim.2);
641
642 let value_styled = if highlight {
643 value
644 .truecolor(value_color.0, value_color.1, value_color.2)
645 .bold()
646 } else {
647 value.truecolor(value_color.0, value_color.1, value_color.2)
648 };
649
650 println!("{label_styled} {value_styled}");
651}
652
653fn parse_additional_params(params: &[String]) -> HashMap<String, String> {
655 params
656 .iter()
657 .filter_map(|param| {
658 let parts: Vec<&str> = param.splitn(2, '=').collect();
659 if parts.len() == 2 {
660 Some((parts[0].to_string(), parts[1].to_string()))
661 } else {
662 None
663 }
664 })
665 .collect()
666}
667
668pub fn handle_list_presets_command() -> Result<()> {
670 let library = get_instruction_preset_library();
671
672 let both_presets = list_presets_formatted_by_type(&library, Some(PresetType::Both));
674 let commit_only_presets = list_presets_formatted_by_type(&library, Some(PresetType::Commit));
675 let review_only_presets = list_presets_formatted_by_type(&library, Some(PresetType::Review));
676
677 println!(
678 "{}",
679 "\nGit-Iris Instruction Presets\n".bright_magenta().bold()
680 );
681
682 println!(
683 "{}",
684 "General Presets (usable for both commit and review):"
685 .bright_cyan()
686 .bold()
687 );
688 println!("{both_presets}\n");
689
690 if !commit_only_presets.is_empty() {
691 println!("{}", "Commit-specific Presets:".bright_green().bold());
692 println!("{commit_only_presets}\n");
693 }
694
695 if !review_only_presets.is_empty() {
696 println!("{}", "Review-specific Presets:".bright_blue().bold());
697 println!("{review_only_presets}\n");
698 }
699
700 println!("{}", "Usage:".bright_yellow().bold());
701 println!(" git-iris gen --preset <preset-key>");
702 println!(" git-iris review --preset <preset-key>");
703 println!("\nPreset types: [B] = Both commands, [C] = Commit only, [R] = Review only");
704
705 Ok(())
706}