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