1use std::collections::BTreeSet;
2
3use crate::config::explain::selected_value;
4use crate::config::{
5 ActiveProfileSource, BootstrapConfigExplain, ConfigError, ConfigExplain, ConfigSource,
6 ConfigValue, ResolveOptions, ResolvedValue, Scope, normalize_identifier,
7 validate_bootstrap_value,
8};
9
10use crate::config::selector::{LayerRef, ScopeSelector};
11
12#[derive(Debug, Clone)]
16pub(crate) struct ResolutionFrame {
17 pub(crate) active_profile: String,
18 pub(crate) active_profile_source: ActiveProfileSource,
19 pub(crate) terminal: Option<String>,
20 pub(crate) known_profiles: BTreeSet<String>,
21}
22
23pub(crate) fn prepare_resolution(
24 layers: [LayerRef<'_>; 7],
25 options: ResolveOptions,
26) -> Result<ResolutionFrame, ConfigError> {
27 validate_layers(layers)?;
28 let terminal = options.terminal.map(|value| normalize_identifier(&value));
29 let profile_override = options
30 .profile_override
31 .map(|value| normalize_identifier(&value));
32 let known_profiles = collect_known_profiles(layers);
33 let profile_selection = resolve_active_profile(
34 layers,
35 profile_override.as_deref(),
36 terminal.as_deref(),
37 &known_profiles,
38 )?;
39
40 tracing::debug!(
41 active_profile = %profile_selection.profile,
42 active_profile_source = %profile_selection.source.as_str(),
43 terminal = ?terminal,
44 known_profiles = known_profiles.len(),
45 "prepared config resolution frame"
46 );
47
48 Ok(ResolutionFrame {
49 active_profile: profile_selection.profile,
50 active_profile_source: profile_selection.source,
51 terminal,
52 known_profiles,
53 })
54}
55
56pub(crate) fn explain_default_profile_key(
57 layers: [LayerRef<'_>; 7],
58 options: ResolveOptions,
59) -> Result<ConfigExplain, ConfigError> {
60 Ok(explain_default_profile_bootstrap(layers, options)?.into())
61}
62
63pub(crate) fn explain_default_profile_bootstrap(
64 layers: [LayerRef<'_>; 7],
65 options: ResolveOptions,
66) -> Result<BootstrapConfigExplain, ConfigError> {
67 let frame = prepare_resolution(layers, options)?;
68 let selector = ScopeSelector::global(frame.terminal.as_deref());
71 let explain_layers = layers
72 .into_iter()
73 .filter_map(|layer| selector.explain_layer(layer, "profile.default"))
74 .collect::<Vec<_>>();
75
76 let final_entry = select_default_profile_across_layers(layers, selector)
77 .map(|selected| selected_value(&selected))
78 .or_else(|| {
79 Some(ResolvedValue {
80 raw_value: ConfigValue::String("default".to_string()),
81 value: ConfigValue::String("default".to_string()),
82 source: ConfigSource::Derived,
83 scope: Scope::global(),
84 origin: None,
85 })
86 });
87
88 Ok(BootstrapConfigExplain {
89 key: "profile.default".to_string(),
90 active_profile: frame.active_profile,
91 active_profile_source: frame.active_profile_source,
92 terminal: frame.terminal,
93 known_profiles: frame.known_profiles,
94 layers: explain_layers,
95 final_entry,
96 })
97}
98
99impl From<BootstrapConfigExplain> for ConfigExplain {
100 fn from(value: BootstrapConfigExplain) -> Self {
101 Self {
102 key: value.key,
103 active_profile: value.active_profile,
104 active_profile_source: value.active_profile_source,
105 terminal: value.terminal,
106 known_profiles: value.known_profiles,
107 layers: value.layers,
108 final_entry: value.final_entry,
109 interpolation: None,
110 }
111 }
112}
113
114fn validate_layers(layers: [LayerRef<'_>; 7]) -> Result<(), ConfigError> {
115 for layer in layers {
116 layer.layer.validate_entries()?;
117 }
118
119 Ok(())
120}
121
122fn collect_known_profiles(layers: [LayerRef<'_>; 7]) -> BTreeSet<String> {
123 let mut known = BTreeSet::new();
124
125 for layer in layers {
126 for entry in &layer.layer.entries {
127 if let Some(profile) = entry.scope.profile.as_deref() {
128 known.insert(profile.to_string());
129 }
130 }
131 }
132
133 known
134}
135
136fn resolve_active_profile(
137 layers: [LayerRef<'_>; 7],
138 explicit: Option<&str>,
139 terminal: Option<&str>,
140 known_profiles: &BTreeSet<String>,
141) -> Result<ActiveProfileSelection, ConfigError> {
142 tracing::debug!(
143 explicit_profile = ?explicit,
144 terminal = ?terminal,
145 known_profiles = known_profiles.len(),
146 "resolving active profile"
147 );
148 let selection = if let Some(profile) = explicit {
149 ActiveProfileSelection {
150 profile: normalize_identifier(profile),
151 source: ActiveProfileSource::Override,
152 }
153 } else {
154 ActiveProfileSelection {
155 profile: resolve_default_profile(layers, terminal)?,
156 source: ActiveProfileSource::DefaultProfile,
157 }
158 };
159
160 if selection.profile.trim().is_empty() {
161 return Err(ConfigError::MissingDefaultProfile);
162 }
163
164 if !known_profiles.is_empty() && !known_profiles.contains(&selection.profile) {
165 tracing::warn!(
166 active_profile = %selection.profile,
167 known_profiles = known_profiles.len(),
168 "resolved unknown active profile"
169 );
170 return Err(ConfigError::UnknownProfile {
171 profile: selection.profile,
172 known: known_profiles.iter().cloned().collect::<Vec<String>>(),
173 });
174 }
175
176 tracing::debug!(
177 active_profile = %selection.profile,
178 active_profile_source = %selection.source.as_str(),
179 "resolved active profile"
180 );
181
182 Ok(selection)
183}
184
185fn resolve_default_profile(
186 layers: [LayerRef<'_>; 7],
187 terminal: Option<&str>,
188) -> Result<String, ConfigError> {
189 let mut picked: Option<ConfigValue> = None;
190 let selector = ScopeSelector::global(terminal);
193
194 for layer in layers {
195 if let Some(selected) = selector.select(layer, "profile.default") {
196 picked = Some(selected.entry.value.clone());
197 }
198 }
199
200 match picked {
201 None => {
202 tracing::debug!(terminal = ?terminal, "using implicit default profile");
203 Ok("default".to_string())
204 }
205 Some(value) => {
206 validate_bootstrap_value("profile.default", &value)?;
207 match value.reveal() {
208 ConfigValue::String(profile) => {
209 let normalized = normalize_identifier(profile);
210 tracing::debug!(
211 terminal = ?terminal,
212 selected_profile = %normalized,
213 "resolved profile.default from loaded layers"
214 );
215 Ok(normalized)
216 }
217 other => Err(ConfigError::InvalidBootstrapValue {
218 key: "profile.default".to_string(),
219 reason: format!("expected string, got {other:?}"),
220 }),
221 }
222 }
223 }
224}
225
226fn select_default_profile_across_layers<'a>(
227 layers: [LayerRef<'a>; 7],
228 selector: ScopeSelector<'a>,
229) -> Option<crate::config::selector::SelectedLayerEntry<'a>> {
230 let mut selected = None;
231
232 for layer in layers {
233 if let Some(entry) = selector.select(layer, "profile.default") {
234 selected = Some(entry);
235 }
236 }
237
238 selected
239}
240
241#[derive(Debug, Clone)]
242struct ActiveProfileSelection {
243 profile: String,
244 source: ActiveProfileSource,
245}