use miette::Result;
use crate::config::{ConfigSource, ResolvedConfig};
use crate::plugin::PluginManager;
use crate::plugin::state::PluginCommandPreferences;
use crate::ui::RenderSettings;
use crate::ui::theme::DEFAULT_THEME_NAME;
use crate::ui::theme_catalog::ThemeCatalog;
use super::{
AppSession, LaunchContext, RuntimeContext, TerminalKind, UiState, build_logging_config,
build_render_runtime, debug_verbosity_from_config, message_verbosity_from_config,
plugin_path_discovery_enabled, plugin_process_timeout, resolve_default_render_width,
resolve_known_theme_name,
};
pub(crate) enum RenderSettingsSeed {
DefaultAuto,
Existing(Box<RenderSettings>),
}
impl RenderSettingsSeed {
fn into_settings(self, context: &RuntimeContext) -> RenderSettings {
match self {
Self::DefaultAuto => {
let mut settings = crate::ui::RenderSettings::builder()
.with_format(crate::core::output::OutputFormat::Auto)
.build();
settings.runtime = build_render_runtime(context.terminal_env());
settings
}
Self::Existing(mut settings) => {
if settings.runtime.terminal.is_none() {
settings.runtime.terminal = context.terminal_env().map(str::to_owned);
}
*settings
}
}
}
}
pub(crate) struct ResolvedHostInputs {
pub(crate) themes: ThemeCatalog,
pub(crate) ui: UiState,
pub(crate) plugins: PluginManager,
pub(crate) default_session: AppSession,
}
pub(crate) struct ResolvedHostDefaults {
pub(crate) themes: ThemeCatalog,
pub(crate) plugins: PluginManager,
pub(crate) default_session: AppSession,
}
impl ResolvedHostInputs {
pub(crate) fn derive(
context: &RuntimeContext,
config: &ResolvedConfig,
launch: &LaunchContext,
render_seed: RenderSettingsSeed,
theme_name_override: Option<&str>,
plugin_preferences_override: Option<PluginCommandPreferences>,
session_overrides: Option<crate::config::ConfigLayer>,
) -> Result<Self> {
let defaults = derive_host_defaults(
config,
launch,
plugin_preferences_override,
session_overrides,
);
let ui = derive_ui_state(
context,
config,
&defaults.themes,
render_seed,
theme_name_override,
)?;
Ok(Self {
themes: defaults.themes,
ui,
plugins: defaults.plugins,
default_session: defaults.default_session,
})
}
}
pub(crate) fn derive_host_defaults(
config: &ResolvedConfig,
launch: &LaunchContext,
plugin_preferences_override: Option<PluginCommandPreferences>,
session_overrides: Option<crate::config::ConfigLayer>,
) -> ResolvedHostDefaults {
let themes = crate::ui::theme_catalog::load_theme_catalog(config);
let plugins = build_plugin_manager(config, launch, plugin_preferences_override.as_ref());
let default_session = match session_overrides {
Some(overrides) => AppSession::from_resolved_config_with_overrides(config, overrides),
None => AppSession::from_resolved_config(config),
};
ResolvedHostDefaults {
themes,
plugins,
default_session,
}
}
pub(crate) fn derive_render_settings(
context: &RuntimeContext,
config: &ResolvedConfig,
themes: &ThemeCatalog,
render_seed: RenderSettingsSeed,
theme_name_override: Option<&str>,
) -> Result<RenderSettings> {
let mut render_settings = derive_base_render_settings(context, config, render_seed);
let selected_theme = selected_theme_name(config, theme_name_override);
render_settings.theme_name = resolve_known_theme_name(selected_theme, themes)?;
render_settings.theme = themes
.resolve(&render_settings.theme_name)
.map(|entry| entry.theme.clone());
Ok(render_settings)
}
pub(crate) fn derive_render_settings_or_fallback(
context: &RuntimeContext,
config: &ResolvedConfig,
themes: &ThemeCatalog,
render_seed: RenderSettingsSeed,
theme_name_override: Option<&str>,
) -> RenderSettings {
let mut render_settings = derive_base_render_settings(context, config, render_seed);
let selected_theme = selected_theme_name(config, theme_name_override);
render_settings.theme_name = resolve_known_theme_name(selected_theme, themes)
.unwrap_or_else(|_| DEFAULT_THEME_NAME.to_string());
render_settings.theme = themes
.resolve(&render_settings.theme_name)
.map(|entry| entry.theme.clone());
render_settings
}
fn derive_base_render_settings(
context: &RuntimeContext,
config: &ResolvedConfig,
render_seed: RenderSettingsSeed,
) -> RenderSettings {
let mut render_settings = render_seed.into_settings(context);
crate::ui::settings::apply_render_config_overrides(&mut render_settings, config);
apply_repl_render_defaults(context, config, &mut render_settings);
render_settings.width = Some(resolve_default_render_width(config));
render_settings
}
fn selected_theme_name<'a>(
config: &'a ResolvedConfig,
theme_name_override: Option<&'a str>,
) -> &'a str {
theme_name_override
.or_else(|| config.get_string("theme.name"))
.unwrap_or(DEFAULT_THEME_NAME)
}
pub(crate) fn derive_ui_state(
context: &RuntimeContext,
config: &ResolvedConfig,
themes: &ThemeCatalog,
render_seed: RenderSettingsSeed,
theme_name_override: Option<&str>,
) -> Result<UiState> {
let render_settings =
derive_render_settings(context, config, themes, render_seed, theme_name_override)?;
Ok(UiState::new(
render_settings,
message_verbosity_from_config(config),
debug_verbosity_from_config(config),
))
}
fn apply_repl_render_defaults(
context: &RuntimeContext,
config: &ResolvedConfig,
render_settings: &mut RenderSettings,
) {
let margin_is_builtin_default = config
.get_value_entry("ui.margin")
.map(|entry| matches!(entry.source, ConfigSource::BuiltinDefaults))
.unwrap_or(true);
if matches!(context.terminal_kind(), TerminalKind::Repl) && margin_is_builtin_default {
render_settings.margin = 2;
}
}
pub(crate) fn build_plugin_manager(
config: &ResolvedConfig,
launch: &LaunchContext,
preferences_override: Option<&PluginCommandPreferences>,
) -> PluginManager {
let manager = PluginManager::new(launch.plugin_dirs.clone())
.with_roots(launch.config_root.clone(), launch.cache_root.clone())
.with_default_roots(!launch.runtime_load.is_defaults_only())
.with_process_timeout(plugin_process_timeout(config))
.with_path_discovery(plugin_path_discovery_enabled(config))
.with_command_preferences(
crate::plugin::state::PluginCommandPreferences::from_resolved(config),
);
if let Some(preferences) = preferences_override {
manager.replace_command_preferences(preferences.clone());
}
manager
}
pub(crate) fn apply_runtime_side_effects(
config: &ResolvedConfig,
debug_verbosity: u8,
themes: &ThemeCatalog,
) {
super::logging::init_developer_logging(build_logging_config(config, debug_verbosity));
crate::ui::theme_catalog::log_theme_issues(&themes.issues);
}
#[cfg(test)]
mod tests {
use super::{
RenderSettingsSeed, ResolvedHostInputs, build_plugin_manager,
derive_render_settings_or_fallback, derive_ui_state,
};
use crate::app::{LaunchContext, RuntimeContext, TerminalKind};
use crate::config::{ConfigLayer, ConfigResolver, ResolveOptions};
fn resolved(entries: &[(&str, &str)]) -> crate::config::ResolvedConfig {
let mut defaults = ConfigLayer::default();
defaults.set("profile.default", "default");
for (key, value) in entries {
defaults.set(*key, *value);
}
let mut resolver = ConfigResolver::default();
resolver.set_defaults(defaults);
resolver
.resolve(ResolveOptions::default().with_terminal("cli"))
.expect("test config should resolve")
}
#[test]
fn derive_ui_state_layers_config_runtime_and_theme_selection_unit() {
let config = resolved(&[("theme.name", "plain"), ("ui.margin", "3")]);
let context = RuntimeContext::new(None, TerminalKind::Cli, Some("xterm".to_string()));
let themes = crate::ui::theme_catalog::load_theme_catalog(&config);
let ui = derive_ui_state(
&context,
&config,
&themes,
RenderSettingsSeed::DefaultAuto,
None,
)
.expect("ui state should derive");
assert_eq!(ui.render_settings.margin, 3);
assert_eq!(ui.render_settings.theme_name, "plain");
assert_eq!(
ui.render_settings.runtime.terminal.as_deref(),
Some("xterm")
);
}
#[test]
fn derive_ui_state_applies_repl_margin_default_without_affecting_cli_unit() {
let config = resolved(&[]);
let themes = crate::ui::theme_catalog::load_theme_catalog(&config);
let repl = derive_ui_state(
&RuntimeContext::new(None, TerminalKind::Repl, Some("xterm".to_string())),
&config,
&themes,
RenderSettingsSeed::DefaultAuto,
None,
)
.expect("repl ui state should derive");
let cli = derive_ui_state(
&RuntimeContext::new(None, TerminalKind::Cli, Some("xterm".to_string())),
&config,
&themes,
RenderSettingsSeed::DefaultAuto,
None,
)
.expect("cli ui state should derive");
assert_eq!(repl.render_settings.margin, 2);
assert_eq!(cli.render_settings.margin, 0);
}
#[test]
fn host_inputs_derivation_reuses_one_config_path_for_ui_plugins_and_session_unit() {
let config = resolved(&[("extensions.plugins.timeout_ms", "42")]);
let context = RuntimeContext::new(None, TerminalKind::Cli, None);
let launch = LaunchContext::default();
let derived = ResolvedHostInputs::derive(
&context,
&config,
&launch,
RenderSettingsSeed::DefaultAuto,
None,
None,
None,
)
.expect("host inputs should derive");
assert_eq!(derived.ui.debug_verbosity, 0);
assert_eq!(derived.plugins.process_timeout().as_millis(), 42,);
assert!(derived.default_session.history_enabled);
}
#[test]
fn build_plugin_manager_applies_launch_roots_and_preference_override_unit() {
let config = resolved(&[]);
let launch = LaunchContext::default();
let mut preferences = crate::plugin::state::PluginCommandPreferences::default();
preferences.set_provider("ldap user", "demo");
let manager = build_plugin_manager(&config, &launch, Some(&preferences));
assert_eq!(
manager
.command_preferences_snapshot()
.preferred_provider_for("ldap user"),
Some("demo")
);
}
#[test]
fn derive_render_settings_or_fallback_preserves_existing_seed_runtime_facts_unit() {
let config = resolved(&[("theme.name", "missing-theme")]);
let context = RuntimeContext::new(None, TerminalKind::Cli, Some("xterm".to_string()));
let themes = crate::ui::theme_catalog::load_theme_catalog(&config);
let mut existing = crate::ui::RenderSettings::default();
existing.runtime.terminal = Some("preserved-terminal".to_string());
let derived = derive_render_settings_or_fallback(
&context,
&config,
&themes,
RenderSettingsSeed::Existing(Box::new(existing)),
None,
);
assert_eq!(
derived.runtime.terminal.as_deref(),
Some("preserved-terminal")
);
assert_eq!(derived.theme_name, crate::ui::DEFAULT_THEME_NAME);
}
}