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