use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Style {
#[default]
Minimal,
Powerline,
Capsule,
Plain,
}
impl std::str::FromStr for Style {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"minimal" => Ok(Style::Minimal),
"powerline" => Ok(Style::Powerline),
"capsule" => Ok(Style::Capsule),
"plain" => Ok(Style::Plain),
other => Err(format!("Unknown style: {other}")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum IconSet {
#[default]
Plain,
Nerd,
Emoji,
}
impl std::str::FromStr for IconSet {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"plain" => Ok(IconSet::Plain),
"nerd" => Ok(IconSet::Nerd),
"emoji" => Ok(IconSet::Emoji),
other => Err(format!("Unknown icon set: {other}")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum DisplayMode {
#[default]
Full,
Compact,
}
impl DisplayMode {
pub fn toggled(self) -> Self {
match self {
DisplayMode::Full => DisplayMode::Compact,
DisplayMode::Compact => DisplayMode::Full,
}
}
pub fn as_str(self) -> &'static str {
match self {
DisplayMode::Full => "full",
DisplayMode::Compact => "compact",
}
}
}
impl std::str::FromStr for DisplayMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"full" => Ok(DisplayMode::Full),
"compact" => Ok(DisplayMode::Compact),
other => Err(format!("Unknown display mode: {other}")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub style: Style,
#[serde(default)]
pub icon_set: IconSet,
#[serde(default = "default_theme")]
pub theme: String,
#[serde(default)]
pub mode: DisplayMode,
#[serde(default = "default_segments")]
pub segments: Vec<String>,
#[serde(default = "default_compact_segments")]
pub compact_segments: Vec<String>,
#[serde(default)]
pub tokens: TokensConfig,
#[serde(default)]
pub session_cost: CostConfig,
#[serde(default)]
pub month_cost: MonthCostConfig,
#[serde(default)]
pub cycle_cost: CycleCostConfig,
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub reasoning: ReasoningConfig,
#[serde(default)]
pub total_tokens: TotalTokensConfig,
#[serde(default)]
pub cache_expiry: CacheExpiryConfig,
#[serde(default)]
pub pr: PrConfig,
#[serde(default)]
pub model: ModelConfig,
#[serde(default)]
pub tool_overhead: ToolOverheadConfig,
}
fn default_theme() -> String {
"colorblind".to_string()
}
fn default_compact_segments() -> Vec<String> {
vec![
"tokens".to_string(),
"session_cost".to_string(),
"month_cost".to_string(),
"cache_expiry".to_string(),
]
}
fn default_segments() -> Vec<String> {
vec![
"tokens".to_string(),
"session_cost".to_string(),
"month_cost".to_string(),
"cache".to_string(),
"reasoning".to_string(),
"total_tokens".to_string(),
"cache_expiry".to_string(),
]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokensConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
#[serde(default = "default_true")]
pub show_percentage: bool,
#[serde(default = "default_alert_threshold")]
pub alert_threshold: u64,
#[serde(default = "default_tokens_alert_icon")]
pub alert_icon: String,
}
impl Default for TokensConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
show_percentage: true,
alert_threshold: 100_000,
alert_icon: default_tokens_alert_icon(),
}
}
}
fn default_alert_threshold() -> u64 {
100_000
}
fn default_tokens_alert_icon() -> String {
"🔥 ".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
#[serde(default = "default_currency")]
pub currency_symbol: String,
#[serde(default)]
pub show_aic: bool,
#[serde(default = "default_decimals")]
pub decimal_places: usize,
#[serde(default)]
pub spike_alert: bool,
#[serde(default = "default_spike_ratio")]
pub spike_ratio: f64,
#[serde(default = "default_spike_min_usd")]
pub spike_min_usd: f64,
#[serde(default = "default_spike_icon")]
pub spike_icon: String,
#[serde(default)]
pub alert_above_usd: Option<f64>,
#[serde(default = "default_spend_alert_icon")]
pub alert_icon: String,
}
impl Default for CostConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
currency_symbol: default_currency(),
show_aic: false,
decimal_places: 2,
spike_alert: false,
spike_ratio: default_spike_ratio(),
spike_min_usd: default_spike_min_usd(),
spike_icon: default_spike_icon(),
alert_above_usd: None,
alert_icon: default_spend_alert_icon(),
}
}
}
fn default_spike_icon() -> String {
"📈 ".to_string()
}
fn default_spend_alert_icon() -> String {
"💸 ".to_string()
}
fn default_spike_ratio() -> f64 {
2.0
}
fn default_spike_min_usd() -> f64 {
0.05
}
fn default_currency() -> String {
"$".to_string()
}
fn default_decimals() -> usize {
2
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonthCostConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
#[serde(default = "default_currency")]
pub currency_symbol: String,
#[serde(default)]
pub show_aic: bool,
#[serde(default = "default_decimals")]
pub decimal_places: usize,
pub db_path: Option<PathBuf>,
#[serde(default)]
pub alert_above_usd: Option<f64>,
#[serde(default = "default_spend_alert_icon")]
pub alert_icon: String,
}
impl Default for MonthCostConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
currency_symbol: default_currency(),
show_aic: false,
decimal_places: 2,
db_path: None,
alert_above_usd: None,
alert_icon: default_spend_alert_icon(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CycleCostConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
#[serde(default = "default_currency")]
pub currency_symbol: String,
#[serde(default)]
pub show_aic: bool,
#[serde(default = "default_decimals")]
pub decimal_places: usize,
#[serde(default)]
pub alert_above_usd: Option<f64>,
#[serde(default = "default_spend_alert_icon")]
pub alert_icon: String,
#[serde(default = "default_cycle_cache_ttl")]
pub cache_ttl_seconds: u64,
}
fn default_cycle_cache_ttl() -> u64 {
300
}
impl Default for CycleCostConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
currency_symbol: default_currency(),
show_aic: false,
decimal_places: 2,
alert_above_usd: None,
alert_icon: default_spend_alert_icon(),
cache_ttl_seconds: default_cycle_cache_ttl(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
#[serde(default = "default_true")]
pub show_as_percentage: bool,
#[serde(default = "default_true")]
pub auto_hide_zero: bool,
#[serde(default = "default_true")]
pub show_trend: bool,
#[serde(default = "default_true")]
pub show_last_miss: bool,
#[serde(default = "default_cache_miss_min_tokens")]
pub miss_min_tokens: u64,
#[serde(default = "default_cache_miss_visible_seconds")]
pub miss_visible_seconds: u64,
}
fn default_cache_miss_min_tokens() -> u64 {
50_000
}
fn default_cache_miss_visible_seconds() -> u64 {
300
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
show_as_percentage: true,
auto_hide_zero: true,
show_trend: true,
show_last_miss: true,
miss_min_tokens: default_cache_miss_min_tokens(),
miss_visible_seconds: default_cache_miss_visible_seconds(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
#[serde(default = "default_true")]
pub auto_hide_zero: bool,
}
impl Default for ReasoningConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
auto_hide_zero: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TotalTokensConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
}
impl Default for TotalTokensConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheExpiryConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
#[serde(default = "default_cache_expiry_idle_seconds")]
pub idle_seconds: u64,
#[serde(default = "default_cache_expiry_min_tokens")]
pub min_tokens: u64,
#[serde(default)]
pub show_tokens: bool,
}
fn default_cache_expiry_idle_seconds() -> u64 {
1_800
}
fn default_cache_expiry_min_tokens() -> u64 {
50_000
}
impl Default for CacheExpiryConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
idle_seconds: default_cache_expiry_idle_seconds(),
min_tokens: default_cache_expiry_min_tokens(),
show_tokens: false,
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
style: Style::Minimal,
icon_set: IconSet::Plain,
theme: default_theme(),
mode: DisplayMode::Full,
segments: default_segments(),
compact_segments: default_compact_segments(),
tokens: TokensConfig::default(),
session_cost: CostConfig::default(),
month_cost: MonthCostConfig::default(),
cycle_cost: CycleCostConfig::default(),
cache: CacheConfig::default(),
reasoning: ReasoningConfig::default(),
total_tokens: TotalTokensConfig::default(),
cache_expiry: CacheExpiryConfig::default(),
pr: PrConfig::default(),
model: ModelConfig::default(),
tool_overhead: ToolOverheadConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
}
impl Default for ModelConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolOverheadConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
#[serde(default)]
pub min_tokens: u64,
}
impl Default for ToolOverheadConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
min_tokens: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub prefix: Option<String>,
#[serde(default = "default_true")]
pub hyperlinks: bool,
#[serde(default = "default_pr_cache_ttl")]
pub cache_ttl_seconds: u64,
#[serde(default)]
pub show_review_state: bool,
#[serde(default)]
pub show_unresolved: bool,
}
fn default_pr_cache_ttl() -> u64 {
60
}
impl Default for PrConfig {
fn default() -> Self {
Self {
enabled: true,
prefix: None,
hyperlinks: true,
cache_ttl_seconds: default_pr_cache_ttl(),
show_review_state: false,
show_unresolved: false,
}
}
}
impl Config {
pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
toml::from_str(content)
}
pub fn to_toml_string(&self) -> Result<String, toml::ser::Error> {
toml::to_string_pretty(self)
}
pub fn segments_for(&self, mode: DisplayMode) -> &[String] {
match mode {
DisplayMode::Full => &self.segments,
DisplayMode::Compact => &self.compact_segments,
}
}
pub fn default_config_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".copilot").join("powerline.toml"))
}
pub fn resolve_path(path: Option<&Path>) -> Option<PathBuf> {
path.map(Path::to_path_buf)
.or_else(Self::default_config_path)
}
pub fn load_from_file_or_default(path: Option<&Path>) -> Self {
Self::resolve_path(path)
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|content| Self::from_toml(&content).ok())
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let cfg = Config::default();
assert_eq!(cfg.style, Style::Minimal);
assert_eq!(cfg.icon_set, IconSet::Plain);
assert_eq!(cfg.theme, "colorblind");
assert_eq!(
cfg.segments,
vec![
"tokens",
"session_cost",
"month_cost",
"cache",
"reasoning",
"total_tokens",
"cache_expiry"
]
);
assert!(!cfg.session_cost.show_aic);
assert!(!cfg.month_cost.show_aic);
assert!(!cfg.session_cost.spike_alert);
assert_eq!(cfg.session_cost.spike_ratio, 2.0);
assert_eq!(cfg.session_cost.spike_min_usd, 0.05);
assert_eq!(cfg.session_cost.alert_above_usd, None);
assert_eq!(cfg.session_cost.alert_icon, "💸 ");
assert_eq!(cfg.month_cost.alert_above_usd, None);
assert_eq!(cfg.month_cost.alert_icon, "💸 ");
assert_eq!(cfg.tokens.alert_icon, "🔥 ");
assert_eq!(cfg.session_cost.spike_icon, "📈 ");
assert!(cfg.pr.enabled);
assert!(cfg.pr.hyperlinks);
assert_eq!(cfg.pr.cache_ttl_seconds, 60);
assert!(!cfg.pr.show_review_state);
assert!(!cfg.pr.show_unresolved);
let omitted = Config::from_toml("[pr]\nhyperlinks = true\n").unwrap();
assert!(!omitted.pr.show_review_state);
assert!(!omitted.pr.show_unresolved);
assert!(!cfg.segments.iter().any(|s| s == "pr"));
assert!(!cfg.segments.iter().any(|s| s == "model"));
assert!(!cfg.segments.iter().any(|s| s == "cycle_cost"));
assert!(!cfg.compact_segments.iter().any(|s| s == "cycle_cost"));
assert!(cfg.cycle_cost.enabled);
assert_eq!(cfg.cycle_cost.cache_ttl_seconds, 300);
assert_eq!(cfg.cycle_cost.alert_above_usd, None);
assert!(cfg.cache_expiry.enabled);
assert_eq!(cfg.cache_expiry.idle_seconds, 1_800);
assert_eq!(cfg.cache_expiry.min_tokens, 50_000);
assert!(!cfg.cache_expiry.show_tokens);
assert!(cfg.cache.show_last_miss);
assert_eq!(cfg.cache.miss_min_tokens, 50_000);
assert_eq!(cfg.cache.miss_visible_seconds, 300);
}
#[test]
fn test_cache_section_without_miss_keys_gets_defaults() {
let cfg = Config::from_toml("[cache]\nshow_trend = false\n").unwrap();
assert!(!cfg.cache.show_trend);
assert!(cfg.cache.show_last_miss);
assert_eq!(cfg.cache.miss_min_tokens, 50_000);
assert_eq!(cfg.cache.miss_visible_seconds, 300);
}
#[test]
fn test_tokens_section_without_alert_threshold_gets_default() {
let cfg = Config::from_toml("[tokens]\nshow_percentage = false\n").unwrap();
assert!(!cfg.tokens.show_percentage);
assert_eq!(cfg.tokens.alert_threshold, 100_000);
}
#[test]
fn test_tool_overhead_is_opt_in_and_always_shown_by_default() {
let cfg = Config::default();
assert!(!cfg.segments.iter().any(|s| s == "tool_overhead"));
assert!(!cfg.compact_segments.iter().any(|s| s == "tool_overhead"));
assert!(cfg.tool_overhead.enabled);
assert_eq!(cfg.tool_overhead.min_tokens, 0);
assert_eq!(cfg.tool_overhead.prefix, None);
}
#[test]
fn test_tool_overhead_section_without_enabled_keeps_it_on() {
let cfg = Config::from_toml("[tool_overhead]\nmin_tokens = 30000\n").unwrap();
assert!(cfg.tool_overhead.enabled);
assert_eq!(cfg.tool_overhead.min_tokens, 30_000);
}
#[test]
fn test_config_without_a_tool_overhead_section_gets_defaults() {
let cfg = Config::from_toml("theme = \"nord\"\n").unwrap();
assert!(cfg.tool_overhead.enabled);
assert_eq!(cfg.tool_overhead.min_tokens, 0);
}
#[test]
fn test_style_and_icon_set_parse_cli_names_case_insensitively() {
assert_eq!("Capsule".parse::<Style>(), Ok(Style::Capsule));
assert_eq!("powerline".parse::<Style>(), Ok(Style::Powerline));
assert!("fancy".parse::<Style>().is_err());
assert_eq!("NERD".parse::<IconSet>(), Ok(IconSet::Nerd));
assert_eq!("emoji".parse::<IconSet>(), Ok(IconSet::Emoji));
assert!("fancy".parse::<IconSet>().is_err());
}
#[test]
fn test_serialize_and_deserialize_roundtrip() {
let original = Config {
style: Style::Powerline,
icon_set: IconSet::Nerd,
session_cost: CostConfig {
show_aic: true,
..Default::default()
},
month_cost: MonthCostConfig {
show_aic: true,
..Default::default()
},
..Default::default()
};
let toml_str = original.to_toml_string().expect("serialize should succeed");
let parsed: Config = Config::from_toml(&toml_str).expect("deserialize should succeed");
assert_eq!(parsed.style, Style::Powerline);
assert_eq!(parsed.icon_set, IconSet::Nerd);
assert!(parsed.session_cost.show_aic);
assert!(parsed.month_cost.show_aic);
}
#[test]
fn test_parse_partial_toml() {
let toml_str = r#"
style = "capsule"
theme = "nord"
icon_set = "emoji"
[session_cost]
show_aic = true
[tokens]
alert_threshold = 100000
"#;
let parsed = Config::from_toml(toml_str).expect("partial TOML should parse");
assert_eq!(parsed.style, Style::Capsule);
assert_eq!(parsed.theme, "nord");
assert_eq!(parsed.icon_set, IconSet::Emoji);
assert!(parsed.session_cost.show_aic);
assert!(!parsed.session_cost.spike_alert);
assert_eq!(parsed.session_cost.spike_icon, "📈 ");
assert!(!parsed.month_cost.show_aic);
assert_eq!(parsed.tokens.alert_threshold, 100_000);
assert_eq!(parsed.tokens.alert_icon, "🔥 ");
}
#[test]
fn test_parse_session_cost_alert_limit() {
let toml_str = r#"
[session_cost]
alert_above_usd = 5.0
"#;
let parsed = Config::from_toml(toml_str).expect("TOML should parse");
assert_eq!(parsed.session_cost.alert_above_usd, Some(5.0));
assert_eq!(parsed.session_cost.alert_icon, "💸 ");
}
#[test]
fn test_parse_month_cost_alert_limit() {
let toml_str = r#"
[month_cost]
alert_above_usd = 300.0
"#;
let parsed = Config::from_toml(toml_str).expect("TOML should parse");
assert_eq!(parsed.month_cost.alert_above_usd, Some(300.0));
assert_eq!(parsed.month_cost.alert_icon, "💸 ");
assert_eq!(parsed.session_cost.alert_above_usd, None);
}
#[test]
fn test_display_mode_defaults_to_full_with_essential_compact_list() {
let cfg = Config::default();
assert_eq!(cfg.mode, DisplayMode::Full);
assert_eq!(cfg.segments_for(DisplayMode::Full), cfg.segments.as_slice());
assert_eq!(
cfg.segments_for(DisplayMode::Compact),
["tokens", "session_cost", "month_cost", "cache_expiry"]
);
}
#[test]
fn test_parse_compact_mode_and_custom_list() {
let parsed = Config::from_toml(
r#"
mode = "compact"
compact_segments = ["tokens", "model"]
"#,
)
.expect("compact config should parse");
assert_eq!(parsed.mode, DisplayMode::Compact);
assert_eq!(parsed.segments_for(parsed.mode), ["tokens", "model"]);
}
#[test]
fn test_display_mode_toggle_and_parse() {
assert_eq!(DisplayMode::Full.toggled(), DisplayMode::Compact);
assert_eq!(DisplayMode::Compact.toggled(), DisplayMode::Full);
for mode in [DisplayMode::Full, DisplayMode::Compact] {
assert_eq!(mode.as_str().parse::<DisplayMode>(), Ok(mode));
}
assert_eq!(
" Compact\n".parse::<DisplayMode>(),
Ok(DisplayMode::Compact)
);
assert!("sideways".parse::<DisplayMode>().is_err());
}
fn unknown_keys(example: &toml::Table, known: &toml::Table, path: &str) -> Vec<String> {
let mut unknown = Vec::new();
for (key, value) in example {
let key_path = format!("{path}{key}");
match (value, known.get(key)) {
(_, None) => unknown.push(key_path),
(toml::Value::Table(sub), Some(toml::Value::Table(known_sub))) => {
unknown.extend(unknown_keys(sub, known_sub, &format!("{key_path}.")));
}
_ => {}
}
}
unknown
}
#[test]
fn test_example_config_uses_only_known_keys() {
let example = include_str!("../examples/powerline.toml");
Config::from_toml(example).expect("example config should parse");
let mut all = Config::default();
let prefix = Some("x".to_string());
all.tokens.prefix = prefix.clone();
all.session_cost.prefix = prefix.clone();
all.session_cost.alert_above_usd = Some(1.0);
all.month_cost.prefix = prefix.clone();
all.month_cost.alert_above_usd = Some(1.0);
all.month_cost.db_path = Some(PathBuf::from("x.db"));
all.cycle_cost.prefix = prefix.clone();
all.cycle_cost.alert_above_usd = Some(1.0);
all.cache.prefix = prefix.clone();
all.reasoning.prefix = prefix.clone();
all.total_tokens.prefix = prefix.clone();
all.cache_expiry.prefix = prefix.clone();
all.model.prefix = prefix.clone();
all.tool_overhead.prefix = prefix.clone();
all.pr.prefix = prefix;
let known: toml::Table = toml::from_str(&all.to_toml_string().unwrap()).unwrap();
let example: toml::Table = toml::from_str(example).unwrap();
assert_eq!(unknown_keys(&example, &known, ""), Vec::<String>::new());
}
struct DocumentedDefault {
section: Option<String>,
key: String,
documented: Option<toml::Value>,
}
fn documented_defaults(example: &str) -> Vec<DocumentedDefault> {
let mut section = None;
let mut found = Vec::new();
for line in example.lines() {
if let Some(name) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
section = Some(name.to_string());
continue;
}
let Some((setting, comment)) = line.split_once('#') else {
continue;
};
let Some((key, value)) = setting.split_once('=') else {
continue;
};
let Some(text) = comment
.split_once("(default: ")
.and_then(|(_, rest)| rest.split_once(')'))
.map(|(text, _)| text)
else {
continue;
};
let documented = if value.trim().starts_with('[') {
let items = text.split(',').map(|item| item.trim().into()).collect();
Some(toml::Value::Array(items))
} else {
let text = text.split([';', ',']).next().unwrap().trim();
(!text.starts_with("unset")).then(|| {
let parsed: toml::Table = toml::from_str(&format!("v = {text}"))
.unwrap_or_else(|e| panic!("`(default: {text})`: {e}"));
parsed["v"].clone()
})
};
found.push(DocumentedDefault {
section: section.clone(),
key: key.trim().to_string(),
documented,
});
}
found
}
#[test]
fn test_example_config_default_comments_match_the_defaults() {
let defaults: toml::Table =
toml::from_str(&Config::default().to_toml_string().unwrap()).unwrap();
let mismatches: Vec<String> =
documented_defaults(include_str!("../examples/powerline.toml"))
.into_iter()
.filter_map(|option| {
let table = match &option.section {
Some(name) => defaults.get(name).and_then(|t| t.as_table()),
None => Some(&defaults),
};
let actual = table.and_then(|t| t.get(&option.key));
(actual != option.documented.as_ref()).then(|| {
format!(
"{}.{}: comment says {:?}, default is {actual:?}",
option.section.as_deref().unwrap_or(""),
option.key,
option.documented
)
})
})
.collect();
assert_eq!(mismatches, Vec::<String>::new());
}
}