Skip to main content

osp_cli/config/
defaults.rs

1//! Builtin config defaults live in this file.
2//!
3//! Do not split builtin defaults across modules unless this file becomes
4//! impossible to understand.
5//!
6//! Keep different kinds of defaults in separate sections here instead:
7//!
8//! - semantic runtime fallbacks used directly by callers
9//! - literal builtin config defaults
10//! - computed defaults derived from runtime inputs
11
12use super::ConfigLayer;
13use super::runtime::RuntimeEnvironment;
14
15/// Default logical profile name used when no profile override is active.
16pub const DEFAULT_PROFILE_NAME: &str = "default";
17/// Default maximum number of REPL history entries to keep.
18pub const DEFAULT_REPL_HISTORY_MAX_ENTRIES: i64 = 1000;
19/// Default toggle for persistent REPL history.
20pub const DEFAULT_REPL_HISTORY_ENABLED: bool = true;
21/// Default toggle for deduplicating REPL history entries.
22pub const DEFAULT_REPL_HISTORY_DEDUPE: bool = true;
23/// Default toggle for profile-scoped REPL history storage.
24pub const DEFAULT_REPL_HISTORY_PROFILE_SCOPED: bool = true;
25/// Default maximum number of rows shown in the REPL history search menu.
26pub const DEFAULT_REPL_HISTORY_MENU_ROWS: i64 = 5;
27/// Default upper bound for cached session results.
28pub const DEFAULT_SESSION_CACHE_MAX_RESULTS: i64 = 64;
29/// Default debug verbosity level.
30pub const DEFAULT_DEBUG_LEVEL: i64 = 0;
31/// Default toggle for file logging.
32pub const DEFAULT_LOG_FILE_ENABLED: bool = false;
33/// Default log level used for file logging.
34pub const DEFAULT_LOG_FILE_LEVEL: &str = "warn";
35/// Default render width hint.
36pub const DEFAULT_UI_WIDTH: i64 = 72;
37/// Default left margin for rendered output.
38pub const DEFAULT_UI_MARGIN: i64 = 0;
39/// Default indentation width for nested output.
40pub const DEFAULT_UI_INDENT: i64 = 2;
41/// Default presentation preset name.
42pub const DEFAULT_UI_PRESENTATION: &str = "expressive";
43/// Default semantic guide-format preference.
44pub const DEFAULT_UI_GUIDE_DEFAULT_FORMAT: &str = "guide";
45/// Default grouped-message layout mode.
46pub const DEFAULT_UI_MESSAGES_LAYOUT: &str = "grouped";
47/// Default section chrome frame style.
48pub const DEFAULT_UI_CHROME_FRAME: &str = "top";
49/// Default rule-sharing policy for sibling section chrome.
50pub const DEFAULT_UI_CHROME_RULE_POLICY: &str = "shared";
51/// Default table border style.
52pub const DEFAULT_UI_TABLE_BORDER: &str = "square";
53/// Default REPL intro mode.
54pub const DEFAULT_REPL_INTRO: &str = "full";
55/// Default threshold for rendering short lists compactly.
56pub const DEFAULT_UI_SHORT_LIST_MAX: i64 = 1;
57/// Default threshold for rendering medium lists before expanding further.
58pub const DEFAULT_UI_MEDIUM_LIST_MAX: i64 = 5;
59/// Default grid column padding.
60pub const DEFAULT_UI_GRID_PADDING: i64 = 4;
61/// Default adaptive grid column weight.
62pub const DEFAULT_UI_COLUMN_WEIGHT: i64 = 3;
63/// Default minimum width before MREG output stacks columns.
64pub const DEFAULT_UI_MREG_STACK_MIN_COL_WIDTH: i64 = 10;
65/// Default threshold for stacked MREG overflow behavior.
66pub const DEFAULT_UI_MREG_STACK_OVERFLOW_RATIO: i64 = 200;
67/// Default table overflow strategy.
68pub 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}