1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-07-27 17:07:44 +00:00

config: nuke old config and implement a new system

This commit is contained in:
RGBCube 2025-05-19 21:25:26 +03:00
parent 2995909544
commit 0d3a88be03
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
7 changed files with 158 additions and 474 deletions

View file

@ -10,7 +10,7 @@ rust-version = "1.85"
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
dirs = "6.0"
clap = { version = "4.0", features = ["derive"] }
clap = { version = "4.0", features = ["derive", "env"] }
num_cpus = "1.16"
ctrlc = "3.4"
log = "0.4"

138
src/config.rs Normal file
View file

@ -0,0 +1,138 @@
use std::{fs, path::Path};
use anyhow::{Context, bail};
use serde::{Deserialize, Serialize};
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default()
}
#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
pub struct CpuDelta {
/// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs.
#[arg(short = 'c', long = "for")]
#[serde(rename = "for", skip_serializing_if = "is_default")]
pub for_: Option<Vec<u32>>,
/// Set the CPU governor.
#[arg(short = 'g', long)]
#[serde(skip_serializing_if = "is_default")]
pub governor: Option<String>, // TODO: Validate with clap for available governors.
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
#[arg(short = 'p', long, alias = "epp")]
#[serde(skip_serializing_if = "is_default")]
pub energy_performance_preference: Option<String>, // TODO: Validate with clap for available governors.
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
#[arg(short = 'b', long, alias = "epb")]
#[serde(skip_serializing_if = "is_default")]
pub energy_performance_bias: Option<String>, // TODO: Validate with clap for available governors.
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
#[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
#[serde(skip_serializing_if = "is_default")]
pub frequency_mhz_minimum: Option<u64>,
/// Set maximum CPU frequency in MHz. Short form: --freq-max.
#[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))]
#[serde(skip_serializing_if = "is_default")]
pub frequency_mhz_maximum: Option<u64>,
/// Set turbo boost behaviour. Has to be for all CPUs.
#[arg(short = 't', long, conflicts_with = "for_")]
#[serde(skip_serializing_if = "is_default")]
pub turbo: Option<bool>,
}
#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
pub struct PowerDelta {
/// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies.
#[arg(short = 'p', long = "for")]
#[serde(rename = "for", skip_serializing_if = "is_default")]
pub for_: Option<Vec<String>>,
/// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start.
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
#[serde(skip_serializing_if = "is_default")]
pub charge_threshold_start: Option<u8>,
/// Set the percentage where charging will stop. Short form: --charge-end.
#[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))]
#[serde(skip_serializing_if = "is_default")]
pub charge_threshold_end: Option<u8>,
/// Set ACPI platform profile. Has to be for all power supplies.
#[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")]
#[serde(skip_serializing_if = "is_default")]
pub platform_profile: Option<String>,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum Condition {
ChargeLessThan(u8),
ChargeMoreThan(u8),
TemperatureLessThan(u8),
TemperatureMoreThan(u8),
UtilizationLessThan(u8),
UtilizationMoreThan(u8),
Charging,
OnBattery,
False,
#[default]
True,
All(Vec<Condition>),
Any(Vec<Condition>),
Not(Box<Condition>),
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct DaemonConfigLayer {
priority: u8,
#[serde(default, skip_serializing_if = "is_default")]
if_: Condition,
#[serde(default, skip_serializing_if = "is_default")]
cpu: CpuDelta,
#[serde(default, skip_serializing_if = "is_default")]
power: PowerDelta,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(transparent, default, rename_all = "kebab-case")]
pub struct DaemonConfig(pub Vec<DaemonConfigLayer>);
impl DaemonConfig {
pub fn load_from(path: &Path) -> anyhow::Result<Self> {
let contents = fs::read_to_string(path).with_context(|| {
format!("failed to read config from '{path}'", path = path.display())
})?;
let config: Self = toml::from_str(&contents).context("failed to parse config file")?;
{
let mut priorities = Vec::with_capacity(config.0.len());
for layer in &config.0 {
if priorities.contains(&layer.priority) {
bail!("each config layer must have a different priority")
}
priorities.push(layer.priority);
}
}
Ok(config)
}
}

View file

@ -1,128 +0,0 @@
// Configuration loading functionality
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Context as _;
use crate::config::types::{AppConfig, AppConfigToml, DaemonConfig, ProfileConfig};
/// The primary function to load application configuration from a specific path or from default locations.
///
/// # Arguments
///
/// * `specific_path` - If provided, only attempts to load from this path and errors if not found
///
/// # Returns
///
/// * `Ok(AppConfig)` - Successfully loaded configuration
/// * `Err(ConfigError)` - Error loading or parsing configuration
pub fn load_config() -> anyhow::Result<AppConfig> {
load_config_from_path(None)
}
/// Load configuration from a specific path or try default paths
pub fn load_config_from_path(specific_path: Option<&str>) -> anyhow::Result<AppConfig> {
// If a specific path is provided, only try that one
if let Some(path_str) = specific_path {
let path = Path::new(path_str);
if path.exists() {
return load_and_parse_config(path);
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Specified config file not found: {}", path.display()),
))?;
}
// Check for SUPERFREQ_CONFIG environment variable
if let Ok(env_path) = std::env::var("SUPERFREQ_CONFIG") {
let env_path = Path::new(&env_path);
if env_path.exists() {
println!(
"Loading config from SUPERFREQ_CONFIG: {}",
env_path.display()
);
return load_and_parse_config(env_path);
}
eprintln!(
"Warning: Config file specified by SUPERFREQ_CONFIG not found: {}",
env_path.display()
);
}
// System-wide paths
let config_paths = vec![
PathBuf::from("/etc/xdg/superfreq/config.toml"),
PathBuf::from("/etc/superfreq.toml"),
];
for path in config_paths {
if path.exists() {
println!("Loading config from: {}", path.display());
match load_and_parse_config(&path) {
Ok(config) => return Ok(config),
Err(e) => {
eprintln!("Error with config file {}: {}", path.display(), e);
// Continue trying other files
}
}
}
}
println!("No configuration file found or all failed to parse. Using default configuration.");
// Construct default AppConfig by converting default AppConfigToml
let default_toml_config = AppConfigToml::default();
Ok(AppConfig {
charger: ProfileConfig::from(default_toml_config.charger),
battery: ProfileConfig::from(default_toml_config.battery),
ignored_power_supplies: default_toml_config.ignored_power_supplies,
daemon: DaemonConfig::default(),
})
}
/// Load and parse a configuration file
fn load_and_parse_config(path: &Path) -> anyhow::Result<AppConfig> {
let contents = fs::read_to_string(path).with_context(|| {
format!(
"failed to read config file from '{path}'",
path = path.display(),
)
})?;
let toml_app_config =
toml::from_str::<AppConfigToml>(&contents).context("failed to parse config toml")?;
// Handle inheritance of values from global to profile configs
let mut charger_profile = toml_app_config.charger.clone();
let mut battery_profile = toml_app_config.battery.clone();
// Clone global battery_charge_thresholds once if it exists
if let Some(global_thresholds) = toml_app_config.battery_charge_thresholds {
// Apply to charger profile if not already set
if charger_profile.battery_charge_thresholds.is_none() {
charger_profile.battery_charge_thresholds = Some(global_thresholds.clone());
}
// Apply to battery profile if not already set
if battery_profile.battery_charge_thresholds.is_none() {
battery_profile.battery_charge_thresholds = Some(global_thresholds);
}
}
// Convert AppConfigToml to AppConfig
Ok(AppConfig {
charger: ProfileConfig::from(charger_profile),
battery: ProfileConfig::from(battery_profile),
ignored_power_supplies: toml_app_config.ignored_power_supplies,
daemon: DaemonConfig {
poll_interval_sec: toml_app_config.daemon.poll_interval_sec,
adaptive_interval: toml_app_config.daemon.adaptive_interval,
min_poll_interval_sec: toml_app_config.daemon.min_poll_interval_sec,
max_poll_interval_sec: toml_app_config.daemon.max_poll_interval_sec,
throttle_on_battery: toml_app_config.daemon.throttle_on_battery,
log_level: toml_app_config.daemon.log_level,
stats_file_path: toml_app_config.daemon.stats_file_path,
},
})
}

View file

@ -1,5 +0,0 @@
pub mod load;
pub mod types;
pub use load::*;
pub use types::*;

View file

@ -1,282 +0,0 @@
use anyhow::bail;
// Configuration types and structures for superfreq
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
/// Defines constant-returning functions used for default values.
/// This hopefully reduces repetition since we have way too many
/// default functions that just return constants.
macro_rules! default_const {
($($name:ident -> $type:ty = $value:expr;)*) => {
$(
const fn $name() -> $type {
$value
}
)*
};
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct PowerSupplyChargeThresholds {
pub start: u8,
pub stop: u8,
}
impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds {
type Error = anyhow::Error;
fn try_from((start, stop): (u8, u8)) -> anyhow::Result<Self> {
if stop == 0 {
bail!("stop threshold must be greater than 0%");
}
if start >= stop {
bail!("start threshold ({start}) must be less than stop threshold ({stop})");
}
if stop > 100 {
bail!("stop threshold ({stop}) cannot exceed 100%");
}
Ok(PowerSupplyChargeThresholds { start, stop })
}
}
// Structs for configuration using serde::Deserialize
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProfileConfig {
pub governor: Option<String>,
pub turbo: Option<bool>,
pub epp: Option<String>, // Energy Performance Preference (EPP)
pub epb: Option<String>, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs
pub min_freq_mhz: Option<u32>,
pub max_freq_mhz: Option<u32>,
pub platform_profile: Option<String>,
#[serde(default)]
pub turbo_auto_settings: TurboAutoSettings,
#[serde(default)]
pub enable_auto_turbo: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
}
impl Default for ProfileConfig {
fn default() -> Self {
Self {
governor: Some("schedutil".to_string()), // common sensible default (?)
turbo: None,
epp: None, // defaults depend on governor and system
epb: None, // defaults depend on governor and system
min_freq_mhz: None, // no override
max_freq_mhz: None, // no override
platform_profile: None, // no override
turbo_auto_settings: TurboAutoSettings::default(),
enable_auto_turbo: default_enable_auto_turbo(),
battery_charge_thresholds: None,
}
}
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct AppConfig {
#[serde(default)]
pub charger: ProfileConfig,
#[serde(default)]
pub battery: ProfileConfig,
pub ignored_power_supplies: Option<Vec<String>>,
#[serde(default)]
pub daemon: DaemonConfig,
}
// Intermediate structs for TOML parsing
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProfileConfigToml {
pub governor: Option<String>,
pub turbo: Option<String>, // "always", "auto", "never"
pub epp: Option<String>,
pub epb: Option<String>,
pub min_freq_mhz: Option<u32>,
pub max_freq_mhz: Option<u32>,
pub platform_profile: Option<String>,
pub turbo_auto_settings: Option<TurboAutoSettings>,
#[serde(default = "default_enable_auto_turbo")]
pub enable_auto_turbo: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct AppConfigToml {
#[serde(default)]
pub charger: ProfileConfigToml,
#[serde(default)]
pub battery: ProfileConfigToml,
#[serde(skip_serializing_if = "Option::is_none")]
pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
pub ignored_power_supplies: Option<Vec<String>>,
#[serde(default)]
pub daemon: DaemonConfigToml,
}
impl Default for ProfileConfigToml {
fn default() -> Self {
Self {
governor: Some("schedutil".to_string()),
turbo: Some("auto".to_string()),
epp: None,
epb: None,
min_freq_mhz: None,
max_freq_mhz: None,
platform_profile: None,
turbo_auto_settings: None,
enable_auto_turbo: default_enable_auto_turbo(),
battery_charge_thresholds: None,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TurboAutoSettings {
#[serde(default = "default_load_threshold_high")]
pub load_threshold_high: f32,
#[serde(default = "default_load_threshold_low")]
pub load_threshold_low: f32,
#[serde(default = "default_temp_threshold_high")]
pub temp_threshold_high: f32,
/// Initial turbo boost state when no previous state exists.
/// Set to `true` to start with turbo enabled, `false` to start with turbo disabled.
/// This is only used at first launch or after a reset.
#[serde(default = "default_initial_turbo_state")]
pub initial_turbo_state: bool,
}
// Default thresholds for Auto turbo mode
pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // enable turbo if load is above this
pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this
pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this
pub const DEFAULT_INITIAL_TURBO_STATE: bool = false; // by default, start with turbo disabled
default_const! {
default_load_threshold_high -> f32 = DEFAULT_LOAD_THRESHOLD_HIGH;
default_load_threshold_low -> f32 = DEFAULT_LOAD_THRESHOLD_LOW;
default_temp_threshold_high -> f32 = DEFAULT_TEMP_THRESHOLD_HIGH;
default_initial_turbo_state -> bool = DEFAULT_INITIAL_TURBO_STATE;
}
impl Default for TurboAutoSettings {
fn default() -> Self {
Self {
load_threshold_high: DEFAULT_LOAD_THRESHOLD_HIGH,
load_threshold_low: DEFAULT_LOAD_THRESHOLD_LOW,
temp_threshold_high: DEFAULT_TEMP_THRESHOLD_HIGH,
initial_turbo_state: DEFAULT_INITIAL_TURBO_STATE,
}
}
}
impl From<ProfileConfigToml> for ProfileConfig {
fn from(toml_config: ProfileConfigToml) -> Self {
Self {
governor: toml_config.governor,
turbo: toml_config
.turbo
.and_then(|s| match s.to_lowercase().as_str() {
"always" => Some(true),
"auto" => None,
"never" => Some(false),
_ => panic!("invalid turbo value: {s}, must be one of: always, auto, never"),
}),
epp: toml_config.epp,
epb: toml_config.epb,
min_freq_mhz: toml_config.min_freq_mhz,
max_freq_mhz: toml_config.max_freq_mhz,
platform_profile: toml_config.platform_profile,
turbo_auto_settings: toml_config.turbo_auto_settings.unwrap_or_default(),
enable_auto_turbo: toml_config.enable_auto_turbo,
battery_charge_thresholds: toml_config.battery_charge_thresholds,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DaemonConfig {
#[serde(default = "default_poll_interval_sec")]
pub poll_interval_sec: u64,
#[serde(default = "default_adaptive_interval")]
pub adaptive_interval: bool,
#[serde(default = "default_min_poll_interval_sec")]
pub min_poll_interval_sec: u64,
#[serde(default = "default_max_poll_interval_sec")]
pub max_poll_interval_sec: u64,
#[serde(default = "default_throttle_on_battery")]
pub throttle_on_battery: bool,
#[serde(default = "default_log_level")]
pub log_level: LogLevel,
#[serde(default = "default_stats_file_path")]
pub stats_file_path: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Error,
Warning,
Info,
Debug,
}
impl Default for DaemonConfig {
fn default() -> Self {
Self {
poll_interval_sec: default_poll_interval_sec(),
adaptive_interval: default_adaptive_interval(),
min_poll_interval_sec: default_min_poll_interval_sec(),
max_poll_interval_sec: default_max_poll_interval_sec(),
throttle_on_battery: default_throttle_on_battery(),
log_level: default_log_level(),
stats_file_path: default_stats_file_path(),
}
}
}
default_const! {
default_poll_interval_sec -> u64 = 5;
default_adaptive_interval -> bool = false;
default_min_poll_interval_sec -> u64 = 1;
default_max_poll_interval_sec -> u64 = 30;
default_throttle_on_battery -> bool = true;
default_log_level -> LogLevel = LogLevel::Info;
default_stats_file_path -> Option<String> = None;
default_enable_auto_turbo -> bool = true;
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DaemonConfigToml {
#[serde(default = "default_poll_interval_sec")]
pub poll_interval_sec: u64,
#[serde(default = "default_adaptive_interval")]
pub adaptive_interval: bool,
#[serde(default = "default_min_poll_interval_sec")]
pub min_poll_interval_sec: u64,
#[serde(default = "default_max_poll_interval_sec")]
pub max_poll_interval_sec: u64,
#[serde(default = "default_throttle_on_battery")]
pub throttle_on_battery: bool,
#[serde(default = "default_log_level")]
pub log_level: LogLevel,
#[serde(default = "default_stats_file_path")]
pub stats_file_path: Option<String>,
}
impl Default for DaemonConfigToml {
fn default() -> Self {
Self {
poll_interval_sec: default_poll_interval_sec(),
adaptive_interval: default_adaptive_interval(),
min_poll_interval_sec: default_min_poll_interval_sec(),
max_poll_interval_sec: default_max_poll_interval_sec(),
throttle_on_battery: default_throttle_on_battery(),
log_level: default_log_level(),
stats_file_path: default_stats_file_path(),
}
}
}

View file

@ -14,7 +14,7 @@ fn read_u64(path: impl AsRef<Path>) -> anyhow::Result<u64> {
let content = fs::read_to_string(path)?;
Ok(content.trim().parse::<u64>()?)
Ok(content.trim().parse()?)
}
fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
@ -73,7 +73,7 @@ impl Cpu {
};
// Has to match "cpu{N}".
let Ok(number) = cpu_prefix_removed.parse::<u32>() else {
let Ok(number) = cpu_prefix_removed.parse() else {
continue;
};

View file

@ -10,6 +10,7 @@ use anyhow::Context;
use clap::Parser as _;
use std::fmt::Write as _;
use std::io::Write as _;
use std::path::PathBuf;
use std::{io, process};
use yansi::Paint as _;
@ -29,57 +30,17 @@ enum Command {
Info,
/// Start the daemon.
Start,
Start {
/// The daemon config path.
#[arg(long, env = "SUPERFREQ_CONFIG")]
config: PathBuf,
},
/// Modify CPU attributes.
CpuSet {
/// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs.
#[arg(short = 'c', long = "for")]
for_: Option<Vec<u32>>,
/// Set the CPU governor.
#[arg(short = 'g', long)]
governor: Option<String>, // TODO: Validate with clap for available governors.
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
#[arg(short = 'p', long, alias = "epp")]
energy_performance_preference: Option<String>,
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
#[arg(short = 'b', long, alias = "epb")]
energy_performance_bias: Option<String>,
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
#[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
frequency_mhz_minimum: Option<u64>,
/// Set maximum CPU frequency in MHz. Short form: --freq-max.
#[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))]
frequency_mhz_maximum: Option<u64>,
/// Set turbo boost behaviour. Has to be for all CPUs.
#[arg(short = 't', long, conflicts_with = "for_")]
turbo: Option<bool>,
},
CpuSet(config::CpuDelta),
/// Modify power supply attributes.
PowerSet {
/// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies.
#[arg(short = 'p', long = "for")]
for_: Option<Vec<String>>,
/// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start.
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
charge_threshold_start: Option<u8>,
/// Set the percentage where charging will stop. Short form: --charge-end.
#[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))]
charge_threshold_end: Option<u8>,
/// Set ACPI platform profile. Has to be for all power supplies.
#[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")]
platform_profile: Option<String>,
},
PowerSet(config::PowerDelta),
}
fn real_main() -> anyhow::Result<()> {
@ -91,17 +52,17 @@ fn real_main() -> anyhow::Result<()> {
.format_module_path(false)
.init();
let config = config::load_config().context("failed to load config")?;
match cli.command {
Command::Info => todo!(),
Command::Start => {
daemon::run_daemon(config)?;
Ok(())
Command::Start { config } => {
let config = config::DaemonConfig::load_from(&config)
.context("failed to load daemon config file")?;
daemon::run(config)
}
Command::CpuSet {
Command::CpuSet(config::CpuDelta {
for_,
governor,
energy_performance_preference,
@ -109,7 +70,7 @@ fn real_main() -> anyhow::Result<()> {
frequency_mhz_minimum,
frequency_mhz_maximum,
turbo,
} => {
}) => {
let cpus = match for_ {
Some(numbers) => {
let mut cpus = Vec::with_capacity(numbers.len());
@ -152,12 +113,12 @@ fn real_main() -> anyhow::Result<()> {
Ok(())
}
Command::PowerSet {
Command::PowerSet(config::PowerDelta {
for_,
charge_threshold_start,
charge_threshold_end,
platform_profile,
} => {
}) => {
let power_supplies = match for_ {
Some(names) => {
let mut power_supplies = Vec::with_capacity(names.len());