use anyhow::{Context, Result};
use clap::ValueEnum;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RunMatConfig {
pub runtime: RuntimeConfig,
pub jit: JitConfig,
pub gc: GcConfig,
pub plotting: PlottingConfig,
pub kernel: KernelConfig,
pub logging: LoggingConfig,
#[serde(default)]
pub packages: PackagesConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeConfig {
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(default)]
pub verbose: bool,
pub snapshot_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JitConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_jit_threshold")]
pub threshold: u32,
#[serde(default)]
pub optimization_level: JitOptLevel,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GcConfig {
pub preset: Option<GcPreset>,
pub young_size_mb: Option<usize>,
pub threads: Option<usize>,
#[serde(default)]
pub collect_stats: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlottingConfig {
#[serde(default)]
pub mode: PlotMode,
#[serde(default)]
pub force_headless: bool,
#[serde(default)]
pub backend: PlotBackend,
pub gui: Option<GuiConfig>,
pub export: Option<ExportConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuiConfig {
#[serde(default = "default_window_width")]
pub width: u32,
#[serde(default = "default_window_height")]
pub height: u32,
#[serde(default = "default_true")]
pub vsync: bool,
#[serde(default)]
pub maximized: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportConfig {
#[serde(default)]
pub format: ExportFormat,
#[serde(default = "default_dpi")]
pub dpi: u32,
pub output_dir: Option<PathBuf>,
pub jupyter: Option<JupyterConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JupyterConfig {
#[serde(default)]
pub output_format: JupyterOutputFormat,
#[serde(default = "default_true")]
pub enable_widgets: bool,
#[serde(default = "default_true")]
pub enable_static_fallback: bool,
pub widget: Option<JupyterWidgetConfig>,
pub static_export: Option<JupyterStaticConfig>,
pub performance: Option<JupyterPerformanceConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JupyterWidgetConfig {
#[serde(default = "default_true")]
pub client_side_rendering: bool,
#[serde(default)]
pub server_side_streaming: bool,
#[serde(default = "default_widget_cache_size")]
pub cache_size_mb: u32,
#[serde(default = "default_widget_fps")]
pub update_fps: u32,
#[serde(default = "default_true")]
pub gpu_acceleration: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JupyterStaticConfig {
#[serde(default = "default_jupyter_width")]
pub width: u32,
#[serde(default = "default_jupyter_height")]
pub height: u32,
#[serde(default = "default_jupyter_quality")]
pub quality: f32,
#[serde(default = "default_true")]
pub include_metadata: bool,
#[serde(default)]
pub preferred_formats: Vec<JupyterOutputFormat>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JupyterPerformanceConfig {
#[serde(default = "default_max_render_time")]
pub max_render_time_ms: u32,
#[serde(default = "default_true")]
pub progressive_rendering: bool,
#[serde(default = "default_lod_threshold")]
pub lod_threshold: u32,
#[serde(default = "default_true")]
pub texture_compression: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelConfig {
#[serde(default = "default_kernel_ip")]
pub ip: String,
pub key: Option<String>,
pub ports: Option<KernelPorts>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelPorts {
pub shell: Option<u16>,
pub iopub: Option<u16>,
pub stdin: Option<u16>,
pub control: Option<u16>,
pub heartbeat: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PackagesConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_registries")]
pub registries: Vec<Registry>,
#[serde(default)]
pub dependencies: HashMap<String, PackageSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Registry {
pub name: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "source", rename_all = "kebab-case")]
pub enum PackageSpec {
Registry {
version: String,
#[serde(default)]
registry: Option<String>,
#[serde(default)]
features: Vec<String>,
#[serde(default)]
optional: bool,
},
Git {
url: String,
#[serde(default)]
rev: Option<String>,
#[serde(default)]
features: Vec<String>,
#[serde(default)]
optional: bool,
},
Path {
path: String,
#[serde(default)]
features: Vec<String>,
#[serde(default)]
optional: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
#[serde(default)]
pub level: LogLevel,
#[serde(default)]
pub debug: bool,
pub file: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PlotMode {
Auto,
Gui,
Headless,
Jupyter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PlotBackend {
Auto,
Wgpu,
Static,
Web,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExportFormat {
Png,
Svg,
Pdf,
Html,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JupyterOutputFormat {
Widget,
Png,
Svg,
Base64,
PlotlyJson,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JitOptLevel {
None,
Size,
Speed,
Aggressive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum GcPreset {
LowLatency,
HighThroughput,
LowMemory,
Debug,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
fn default_timeout() -> u64 {
300
}
fn default_true() -> bool {
true
}
fn default_jit_threshold() -> u32 {
10
}
fn default_window_width() -> u32 {
1200
}
fn default_window_height() -> u32 {
800
}
fn default_dpi() -> u32 {
300
}
fn default_kernel_ip() -> String {
"127.0.0.1".to_string()
}
fn default_widget_cache_size() -> u32 {
64 }
fn default_widget_fps() -> u32 {
30 }
fn default_jupyter_width() -> u32 {
800
}
fn default_jupyter_height() -> u32 {
600
}
fn default_jupyter_quality() -> f32 {
0.9 }
fn default_max_render_time() -> u32 {
16 }
fn default_lod_threshold() -> u32 {
10000 }
fn default_registries() -> Vec<Registry> {
vec![Registry {
name: "runmat".to_string(),
url: "https://packages.runmat.org".to_string(),
}]
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self {
timeout: default_timeout(),
verbose: false,
snapshot_path: None,
}
}
}
impl Default for JitConfig {
fn default() -> Self {
Self {
enabled: true,
threshold: default_jit_threshold(),
optimization_level: JitOptLevel::Speed,
}
}
}
impl Default for PlottingConfig {
fn default() -> Self {
Self {
mode: PlotMode::Auto,
force_headless: false,
backend: PlotBackend::Auto,
gui: Some(GuiConfig::default()),
export: Some(ExportConfig::default()),
}
}
}
impl Default for GuiConfig {
fn default() -> Self {
Self {
width: default_window_width(),
height: default_window_height(),
vsync: true,
maximized: false,
}
}
}
impl Default for ExportConfig {
fn default() -> Self {
Self {
format: ExportFormat::Png,
dpi: default_dpi(),
output_dir: None,
jupyter: Some(JupyterConfig::default()),
}
}
}
impl Default for KernelConfig {
fn default() -> Self {
Self {
ip: default_kernel_ip(),
key: None,
ports: None,
}
}
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: LogLevel::Info,
debug: false,
file: None,
}
}
}
impl Default for PlotMode {
fn default() -> Self {
Self::Auto
}
}
impl Default for PlotBackend {
fn default() -> Self {
Self::Auto
}
}
impl Default for ExportFormat {
fn default() -> Self {
Self::Png
}
}
impl Default for JitOptLevel {
fn default() -> Self {
Self::Speed
}
}
impl Default for LogLevel {
fn default() -> Self {
Self::Info
}
}
impl Default for JupyterOutputFormat {
fn default() -> Self {
Self::Auto
}
}
impl Default for JupyterConfig {
fn default() -> Self {
Self {
output_format: JupyterOutputFormat::default(),
enable_widgets: true,
enable_static_fallback: true,
widget: Some(JupyterWidgetConfig::default()),
static_export: Some(JupyterStaticConfig::default()),
performance: Some(JupyterPerformanceConfig::default()),
}
}
}
impl Default for JupyterWidgetConfig {
fn default() -> Self {
Self {
client_side_rendering: true,
server_side_streaming: false,
cache_size_mb: default_widget_cache_size(),
update_fps: default_widget_fps(),
gpu_acceleration: true,
}
}
}
impl Default for JupyterStaticConfig {
fn default() -> Self {
Self {
width: default_jupyter_width(),
height: default_jupyter_height(),
quality: default_jupyter_quality(),
include_metadata: true,
preferred_formats: vec![
JupyterOutputFormat::Widget,
JupyterOutputFormat::Png,
JupyterOutputFormat::Svg,
],
}
}
}
impl Default for JupyterPerformanceConfig {
fn default() -> Self {
Self {
max_render_time_ms: default_max_render_time(),
progressive_rendering: true,
lod_threshold: default_lod_threshold(),
texture_compression: true,
}
}
}
pub struct ConfigLoader;
impl ConfigLoader {
pub fn load() -> Result<RunMatConfig> {
let mut config = Self::load_from_files()?;
Self::apply_environment_variables(&mut config)?;
Ok(config)
}
fn load_from_files() -> Result<RunMatConfig> {
let config_paths = Self::find_config_files();
for path in config_paths {
if path.exists() {
info!("Loading configuration from: {}", path.display());
return Self::load_from_file(&path);
}
}
debug!("No configuration file found, using defaults");
Ok(RunMatConfig::default())
}
fn find_config_files() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(config_path) = env::var("RUSTMAT_CONFIG") {
paths.push(PathBuf::from(config_path));
}
let current_dir_configs = [
".runmat", ".runmat.yaml",
".runmat.yml",
".runmat.json",
".runmat.toml",
"runmat.config.yaml",
"runmat.config.yml",
"runmat.config.json",
"runmat.config.toml",
];
for name in ¤t_dir_configs {
if let Ok(current_dir) = env::current_dir() {
paths.push(current_dir.join(name));
}
}
if let Some(home_dir) = dirs::home_dir() {
paths.push(home_dir.join(".runmat"));
paths.push(home_dir.join(".runmat.yaml"));
paths.push(home_dir.join(".runmat.yml"));
paths.push(home_dir.join(".runmat.json"));
paths.push(home_dir.join(".config/runmat/config.yaml"));
paths.push(home_dir.join(".config/runmat/config.yml"));
paths.push(home_dir.join(".config/runmat/config.json"));
}
#[cfg(unix)]
{
paths.push(PathBuf::from("/etc/runmat/config.yaml"));
paths.push(PathBuf::from("/etc/runmat/config.yml"));
paths.push(PathBuf::from("/etc/runmat/config.json"));
}
paths
}
pub fn load_from_file(path: &Path) -> Result<RunMatConfig> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
let config = match path.extension().and_then(|ext| ext.to_str()) {
None if path.file_name().and_then(|n| n.to_str()) == Some(".runmat") => {
toml::from_str(&content).with_context(|| {
format!("Failed to parse .runmat (TOML) config: {}", path.display())
})?
}
Some("runmat") => toml::from_str(&content).with_context(|| {
format!("Failed to parse .runmat (TOML) config: {}", path.display())
})?,
Some("yaml") | Some("yml") => serde_yaml::from_str(&content)
.with_context(|| format!("Failed to parse YAML config: {}", path.display()))?,
Some("json") => serde_json::from_str(&content)
.with_context(|| format!("Failed to parse JSON config: {}", path.display()))?,
Some("toml") => toml::from_str(&content)
.with_context(|| format!("Failed to parse TOML config: {}", path.display()))?,
_ => {
if let Ok(config) = toml::from_str(&content) {
config
} else if let Ok(config) = serde_yaml::from_str(&content) {
config
} else if let Ok(config) = serde_json::from_str(&content) {
config
} else {
return Err(anyhow::anyhow!(
"Could not parse config file {} (tried TOML, YAML, JSON)",
path.display()
));
}
}
};
Ok(config)
}
fn apply_environment_variables(config: &mut RunMatConfig) -> Result<()> {
if let Ok(timeout) = env::var("RUSTMAT_TIMEOUT") {
if let Ok(timeout) = timeout.parse() {
config.runtime.timeout = timeout;
}
}
if let Ok(verbose) = env::var("RUSTMAT_VERBOSE") {
config.runtime.verbose = parse_bool(&verbose).unwrap_or(false);
}
if let Ok(snapshot) = env::var("RUSTMAT_SNAPSHOT_PATH") {
config.runtime.snapshot_path = Some(PathBuf::from(snapshot));
}
if let Ok(jit_enabled) = env::var("RUSTMAT_JIT_ENABLE") {
config.jit.enabled = parse_bool(&jit_enabled).unwrap_or(true);
}
if let Ok(jit_disabled) = env::var("RUSTMAT_JIT_DISABLE") {
if parse_bool(&jit_disabled).unwrap_or(false) {
config.jit.enabled = false;
}
}
if let Ok(threshold) = env::var("RUSTMAT_JIT_THRESHOLD") {
if let Ok(threshold) = threshold.parse() {
config.jit.threshold = threshold;
}
}
if let Ok(opt_level) = env::var("RUSTMAT_JIT_OPT_LEVEL") {
config.jit.optimization_level = match opt_level.to_lowercase().as_str() {
"none" => JitOptLevel::None,
"size" => JitOptLevel::Size,
"speed" => JitOptLevel::Speed,
"aggressive" => JitOptLevel::Aggressive,
_ => config.jit.optimization_level,
};
}
if let Ok(preset) = env::var("RUSTMAT_GC_PRESET") {
config.gc.preset = match preset.to_lowercase().as_str() {
"low-latency" => Some(GcPreset::LowLatency),
"high-throughput" => Some(GcPreset::HighThroughput),
"low-memory" => Some(GcPreset::LowMemory),
"debug" => Some(GcPreset::Debug),
_ => config.gc.preset,
};
}
if let Ok(young_size) = env::var("RUSTMAT_GC_YOUNG_SIZE") {
if let Ok(young_size) = young_size.parse() {
config.gc.young_size_mb = Some(young_size);
}
}
if let Ok(threads) = env::var("RUSTMAT_GC_THREADS") {
if let Ok(threads) = threads.parse() {
config.gc.threads = Some(threads);
}
}
if let Ok(stats) = env::var("RUSTMAT_GC_STATS") {
config.gc.collect_stats = parse_bool(&stats).unwrap_or(false);
}
if let Ok(plot_mode) = env::var("RUSTMAT_PLOT_MODE") {
config.plotting.mode = match plot_mode.to_lowercase().as_str() {
"auto" => PlotMode::Auto,
"gui" => PlotMode::Gui,
"headless" => PlotMode::Headless,
"jupyter" => PlotMode::Jupyter,
_ => config.plotting.mode,
};
}
if let Ok(headless) = env::var("RUSTMAT_PLOT_HEADLESS") {
config.plotting.force_headless = parse_bool(&headless).unwrap_or(false);
}
if let Ok(backend) = env::var("RUSTMAT_PLOT_BACKEND") {
config.plotting.backend = match backend.to_lowercase().as_str() {
"auto" => PlotBackend::Auto,
"wgpu" => PlotBackend::Wgpu,
"static" => PlotBackend::Static,
"web" => PlotBackend::Web,
_ => config.plotting.backend,
};
}
if let Ok(debug) = env::var("RUSTMAT_DEBUG") {
config.logging.debug = parse_bool(&debug).unwrap_or(false);
}
if let Ok(log_level) = env::var("RUSTMAT_LOG_LEVEL") {
config.logging.level = match log_level.to_lowercase().as_str() {
"error" => LogLevel::Error,
"warn" => LogLevel::Warn,
"info" => LogLevel::Info,
"debug" => LogLevel::Debug,
"trace" => LogLevel::Trace,
_ => config.logging.level,
};
}
if let Ok(ip) = env::var("RUSTMAT_KERNEL_IP") {
config.kernel.ip = ip;
}
if let Ok(key) = env::var("RUSTMAT_KERNEL_KEY") {
config.kernel.key = Some(key);
}
Ok(())
}
pub fn save_to_file(config: &RunMatConfig, path: &Path) -> Result<()> {
let content = match path.extension().and_then(|ext| ext.to_str()) {
Some("yaml") | Some("yml") => {
serde_yaml::to_string(config).context("Failed to serialize config to YAML")?
}
Some("json") => serde_json::to_string_pretty(config)
.context("Failed to serialize config to JSON")?,
Some("toml") => {
toml::to_string_pretty(config).context("Failed to serialize config to TOML")?
}
_ => {
serde_yaml::to_string(config).context("Failed to serialize config to YAML")?
}
};
fs::write(path, content)
.with_context(|| format!("Failed to write config file: {}", path.display()))?;
info!("Configuration saved to: {}", path.display());
Ok(())
}
pub fn generate_sample_config() -> String {
let config = RunMatConfig::default();
serde_yaml::to_string(&config).unwrap_or_else(|_| "# Failed to generate config".to_string())
}
}
fn parse_bool(s: &str) -> Option<bool> {
match s.to_lowercase().as_str() {
"1" | "true" | "yes" | "on" | "enable" | "enabled" => Some(true),
"0" | "false" | "no" | "off" | "disable" | "disabled" => Some(false),
"" => Some(false),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_config_defaults() {
let config = RunMatConfig::default();
assert_eq!(config.runtime.timeout, 300);
assert!(config.jit.enabled);
assert_eq!(config.jit.threshold, 10);
assert_eq!(config.plotting.mode, PlotMode::Auto);
}
#[test]
fn test_yaml_serialization() {
let config = RunMatConfig::default();
let yaml = serde_yaml::to_string(&config).unwrap();
let parsed: RunMatConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(parsed.runtime.timeout, config.runtime.timeout);
assert_eq!(parsed.jit.enabled, config.jit.enabled);
}
#[test]
fn test_json_serialization() {
let config = RunMatConfig::default();
let json = serde_json::to_string_pretty(&config).unwrap();
let parsed: RunMatConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.runtime.timeout, config.runtime.timeout);
assert_eq!(parsed.plotting.mode, config.plotting.mode);
}
#[test]
fn test_file_loading() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join(".runmat.yaml");
let mut config = RunMatConfig::default();
config.runtime.timeout = 600;
config.jit.threshold = 20;
ConfigLoader::save_to_file(&config, &config_path).unwrap();
let loaded = ConfigLoader::load_from_file(&config_path).unwrap();
assert_eq!(loaded.runtime.timeout, 600);
assert_eq!(loaded.jit.threshold, 20);
}
#[test]
fn test_bool_parsing() {
assert_eq!(parse_bool("true"), Some(true));
assert_eq!(parse_bool("1"), Some(true));
assert_eq!(parse_bool("yes"), Some(true));
assert_eq!(parse_bool("false"), Some(false));
assert_eq!(parse_bool("0"), Some(false));
assert_eq!(parse_bool("invalid"), None);
}
}