1use super::ConfigLayer;
13use super::runtime::RuntimeEnvironment;
14
15pub const DEFAULT_PROFILE_NAME: &str = "default";
17pub const DEFAULT_REPL_HISTORY_MAX_ENTRIES: i64 = 1000;
19pub const DEFAULT_REPL_HISTORY_ENABLED: bool = true;
21pub const DEFAULT_REPL_HISTORY_DEDUPE: bool = true;
23pub const DEFAULT_REPL_HISTORY_PROFILE_SCOPED: bool = true;
25pub const DEFAULT_REPL_HISTORY_MENU_ROWS: i64 = 5;
27pub const DEFAULT_SESSION_CACHE_MAX_RESULTS: i64 = 64;
29pub const DEFAULT_DEBUG_LEVEL: i64 = 0;
31pub const DEFAULT_LOG_FILE_ENABLED: bool = false;
33pub const DEFAULT_LOG_FILE_LEVEL: &str = "warn";
35pub const DEFAULT_UI_WIDTH: i64 = 72;
37pub const DEFAULT_UI_MARGIN: i64 = 0;
39pub const DEFAULT_UI_INDENT: i64 = 2;
41pub const DEFAULT_UI_PRESENTATION: &str = "expressive";
43pub const DEFAULT_UI_GUIDE_DEFAULT_FORMAT: &str = "guide";
45pub const DEFAULT_UI_MESSAGES_LAYOUT: &str = "grouped";
47pub const DEFAULT_UI_CHROME_FRAME: &str = "top";
49pub const DEFAULT_UI_CHROME_RULE_POLICY: &str = "shared";
51pub const DEFAULT_UI_TABLE_BORDER: &str = "square";
53pub const DEFAULT_REPL_INTRO: &str = "full";
55pub const DEFAULT_UI_SHORT_LIST_MAX: i64 = 1;
57pub const DEFAULT_UI_MEDIUM_LIST_MAX: i64 = 5;
59pub const DEFAULT_UI_GRID_PADDING: i64 = 4;
61pub const DEFAULT_UI_COLUMN_WEIGHT: i64 = 3;
63pub const DEFAULT_UI_MREG_STACK_MIN_COL_WIDTH: i64 = 10;
65pub const DEFAULT_UI_MREG_STACK_OVERFLOW_RATIO: i64 = 200;
67pub const DEFAULT_UI_TABLE_OVERFLOW: &str = "clip";
69
70const DEFAULT_EXTENSIONS_PLUGINS_TIMEOUT_MS: i64 =
71 crate::plugin::DEFAULT_PLUGIN_PROCESS_TIMEOUT_MS as i64;
72
73const EMPTY_STYLE_OVERRIDE_KEYS: &[&str] = &[
74 "color.text",
75 "color.text.muted",
76 "color.key",
77 "color.border",
78 "color.prompt.text",
79 "color.prompt.command",
80 "color.table.header",
81 "color.mreg.key",
82 "color.value",
83 "color.value.number",
84 "color.value.bool_true",
85 "color.value.bool_false",
86 "color.value.null",
87 "color.value.ipv4",
88 "color.value.ipv6",
89 "color.panel.border",
90 "color.panel.title",
91 "color.code",
92 "color.json.key",
93];
94
95const LITERAL_DEFAULTS: &[LiteralDefault] = &[
96 LiteralDefault::string("profile.default", DEFAULT_PROFILE_NAME),
97 LiteralDefault::string("repl.input_mode", "auto"),
98 LiteralDefault::bool("repl.simple_prompt", false),
99 LiteralDefault::string("repl.shell_indicator", "[{shell}]"),
100 LiteralDefault::string("repl.intro", DEFAULT_REPL_INTRO),
101 LiteralDefault::int("repl.history.max_entries", DEFAULT_REPL_HISTORY_MAX_ENTRIES),
102 LiteralDefault::bool("repl.history.enabled", DEFAULT_REPL_HISTORY_ENABLED),
103 LiteralDefault::bool("repl.history.dedupe", DEFAULT_REPL_HISTORY_DEDUPE),
104 LiteralDefault::bool(
105 "repl.history.profile_scoped",
106 DEFAULT_REPL_HISTORY_PROFILE_SCOPED,
107 ),
108 LiteralDefault::int("repl.history.menu_rows", DEFAULT_REPL_HISTORY_MENU_ROWS),
109 LiteralDefault::int(
110 "session.cache.max_results",
111 DEFAULT_SESSION_CACHE_MAX_RESULTS,
112 ),
113 LiteralDefault::int("debug.level", DEFAULT_DEBUG_LEVEL),
114 LiteralDefault::bool("log.file.enabled", DEFAULT_LOG_FILE_ENABLED),
115 LiteralDefault::string("log.file.level", DEFAULT_LOG_FILE_LEVEL),
116 LiteralDefault::int("ui.width", DEFAULT_UI_WIDTH),
117 LiteralDefault::int("ui.margin", DEFAULT_UI_MARGIN),
118 LiteralDefault::int("ui.indent", DEFAULT_UI_INDENT),
119 LiteralDefault::string("ui.presentation", DEFAULT_UI_PRESENTATION),
120 LiteralDefault::string("ui.help.level", "inherit"),
121 LiteralDefault::string("ui.guide.default_format", DEFAULT_UI_GUIDE_DEFAULT_FORMAT),
122 LiteralDefault::string("ui.messages.layout", DEFAULT_UI_MESSAGES_LAYOUT),
123 LiteralDefault::string("ui.message.verbosity", "success"),
124 LiteralDefault::string("ui.chrome.frame", DEFAULT_UI_CHROME_FRAME),
125 LiteralDefault::string("ui.chrome.rule_policy", DEFAULT_UI_CHROME_RULE_POLICY),
126 LiteralDefault::string("ui.table.overflow", DEFAULT_UI_TABLE_OVERFLOW),
127 LiteralDefault::string("ui.table.border", DEFAULT_UI_TABLE_BORDER),
128 LiteralDefault::string("ui.help.table_chrome", "none"),
129 LiteralDefault::string("ui.help.entry_indent", "inherit"),
130 LiteralDefault::string("ui.help.entry_gap", "inherit"),
131 LiteralDefault::string("ui.help.section_spacing", "inherit"),
132 LiteralDefault::int("ui.short_list_max", DEFAULT_UI_SHORT_LIST_MAX),
133 LiteralDefault::int("ui.medium_list_max", DEFAULT_UI_MEDIUM_LIST_MAX),
134 LiteralDefault::int("ui.grid_padding", DEFAULT_UI_GRID_PADDING),
135 LiteralDefault::int("ui.column_weight", DEFAULT_UI_COLUMN_WEIGHT),
136 LiteralDefault::int(
137 "ui.mreg.stack_min_col_width",
138 DEFAULT_UI_MREG_STACK_MIN_COL_WIDTH,
139 ),
140 LiteralDefault::int(
141 "ui.mreg.stack_overflow_ratio",
142 DEFAULT_UI_MREG_STACK_OVERFLOW_RATIO,
143 ),
144 LiteralDefault::int(
145 "extensions.plugins.timeout_ms",
146 DEFAULT_EXTENSIONS_PLUGINS_TIMEOUT_MS,
147 ),
148 LiteralDefault::bool("extensions.plugins.discovery.path", false),
149];
150
151#[derive(Clone, Copy)]
152enum LiteralDefaultValue {
153 String(&'static str),
154 Bool(bool),
155 Integer(i64),
156}
157
158#[derive(Clone, Copy)]
159struct LiteralDefault {
160 key: &'static str,
161 value: LiteralDefaultValue,
162}
163
164impl LiteralDefault {
165 const fn string(key: &'static str, value: &'static str) -> Self {
166 Self {
167 key,
168 value: LiteralDefaultValue::String(value),
169 }
170 }
171
172 const fn bool(key: &'static str, value: bool) -> Self {
173 Self {
174 key,
175 value: LiteralDefaultValue::Bool(value),
176 }
177 }
178
179 const fn int(key: &'static str, value: i64) -> Self {
180 Self {
181 key,
182 value: LiteralDefaultValue::Integer(value),
183 }
184 }
185
186 fn seed(self, layer: &mut ConfigLayer) {
187 match self.value {
188 LiteralDefaultValue::String(value) => layer.set(self.key, value),
189 LiteralDefaultValue::Bool(value) => layer.set(self.key, value),
190 LiteralDefaultValue::Integer(value) => layer.set(self.key, value),
191 }
192 }
193}
194
195pub(super) fn build_builtin_defaults(
196 env: &RuntimeEnvironment,
197 default_theme_name: &str,
198 default_repl_prompt: &str,
199) -> ConfigLayer {
200 let mut layer = ConfigLayer::default();
201 seed_literal_defaults(&mut layer);
202 seed_computed_defaults(&mut layer, env, default_theme_name, default_repl_prompt);
203 layer
204}
205
206fn seed_literal_defaults(layer: &mut ConfigLayer) {
207 for default in LITERAL_DEFAULTS {
208 default.seed(layer);
209 }
210
211 for key in EMPTY_STYLE_OVERRIDE_KEYS {
212 layer.set(*key, String::new());
213 }
214}
215
216fn seed_computed_defaults(
217 layer: &mut ConfigLayer,
218 env: &RuntimeEnvironment,
219 default_theme_name: &str,
220 default_repl_prompt: &str,
221) {
222 layer.set("theme.name", default_theme_name);
223 layer.set("user.name", env.user_name());
224 layer.set("domain", env.domain_name());
225 layer.set("repl.prompt", default_repl_prompt);
226 layer.set("repl.history.path", env.repl_history_path());
227 layer.set("log.file.path", env.log_file_path());
228
229 let theme_path = env.theme_paths();
230 if !theme_path.is_empty() {
231 layer.set("theme.path", theme_path);
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::config::{ConfigResolver, ResolveOptions};
239
240 fn resolve_defaults(
241 env: RuntimeEnvironment,
242 default_theme_name: &str,
243 default_repl_prompt: &str,
244 ) -> crate::config::ResolvedConfig {
245 let mut resolver = ConfigResolver::default();
246 resolver.set_defaults(build_builtin_defaults(
247 &env,
248 default_theme_name,
249 default_repl_prompt,
250 ));
251 resolver
252 .resolve(ResolveOptions::default().with_terminal("cli"))
253 .expect("builtin defaults should resolve")
254 }
255
256 #[test]
257 fn literal_default_helpers_seed_string_bool_and_integer_entries_unit() {
258 let mut layer = ConfigLayer::default();
259 LiteralDefault::string("test.string", "alpha").seed(&mut layer);
260 LiteralDefault::bool("test.bool", true).seed(&mut layer);
261 LiteralDefault::int("test.int", 7).seed(&mut layer);
262
263 assert_eq!(layer.entries().len(), 3);
264 assert_eq!(layer.entries()[0].key, "test.string");
265 assert_eq!(layer.entries()[1].key, "test.bool");
266 assert_eq!(layer.entries()[2].key, "test.int");
267 }
268
269 #[test]
270 fn builtin_defaults_seed_literal_and_computed_environment_values_unit() {
271 let resolved = resolve_defaults(
272 RuntimeEnvironment::from_pairs([
273 ("XDG_CONFIG_HOME", "/tmp/osp-config"),
274 ("XDG_STATE_HOME", "/tmp/osp-state"),
275 ("USER", "alice"),
276 ("HOSTNAME", "shell.example.com"),
277 ]),
278 "nord",
279 "osp> ",
280 );
281
282 assert_eq!(resolved.active_profile(), DEFAULT_PROFILE_NAME);
283 assert_eq!(
284 resolved.get_bool("repl.history.enabled"),
285 Some(DEFAULT_REPL_HISTORY_ENABLED)
286 );
287 assert_eq!(resolved.get_string("theme.name"), Some("nord"));
288 assert_eq!(resolved.get_string("user.name"), Some("alice"));
289 assert_eq!(resolved.get_string("domain"), Some("example.com"));
290 assert_eq!(resolved.get_string("repl.prompt"), Some("osp> "));
291 assert_eq!(resolved.get_string("color.text"), Some(""));
292 assert_eq!(
293 resolved.get_string_list("theme.path"),
294 Some(vec!["/tmp/osp-config/osp/themes".to_string()])
295 );
296 assert_eq!(
297 resolved.get_string("repl.history.path"),
298 Some("/tmp/osp-state/osp/history/alice@default.history")
299 );
300 assert_eq!(
301 resolved.get_string("log.file.path"),
302 Some("/tmp/osp-state/osp/osp.log")
303 );
304 }
305
306 #[test]
307 fn builtin_defaults_fall_back_without_theme_path_when_config_root_is_missing_unit() {
308 let resolved = resolve_defaults(RuntimeEnvironment::defaults_only(), "dracula", "osp> ");
309
310 assert_eq!(resolved.get_string("theme.name"), Some("dracula"));
311 assert_eq!(resolved.get_string("user.name"), Some("anonymous"));
312 assert_eq!(resolved.get_string("domain"), Some("local"));
313 assert_eq!(resolved.get_string_list("theme.path"), None);
314 assert!(resolved.get_string("repl.history.path").is_some());
315 assert!(resolved.get_string("log.file.path").is_some());
316 }
317}