use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use crate::colour;
use crate::display;
use crate::display::Block;
use crate::error::{Error, ErrorKind, Result};
use crate::icon;
use crate::profiler::FilterEntry;
use crate::types::VidPid;
use crate::usb::BaseClass;
const CONF_DIR: &str = "cyme";
const CONF_NAME: &str = "cyme.json";
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
pub struct Config {
#[serde(skip)]
filepath: Option<PathBuf>,
pub icons: icon::IconTheme,
#[serde(alias = "colors")]
pub colours: colour::ColourTheme,
pub blocks: Option<Vec<display::DeviceBlocks>>,
pub bus_blocks: Option<Vec<display::BusBlocks>>,
pub config_blocks: Option<Vec<display::ConfigurationBlocks>>,
pub interface_blocks: Option<Vec<display::InterfaceBlocks>>,
pub endpoint_blocks: Option<Vec<display::EndpointBlocks>>,
pub block_operation: Option<display::BlockOperation>,
pub mask_serials: Option<display::MaskSerial>,
pub group_devices: Option<display::Group>,
pub encoding: Option<display::Encoding>,
pub icon_when: Option<display::IconWhen>,
pub color_when: Option<display::ColorWhen>,
pub sort_devices: Option<display::Sort>,
pub sort_buses: bool,
pub max_variable_string_len: Option<usize>,
pub no_auto_width: bool,
pub lsusb: bool,
pub tree: bool,
pub verbose: u8,
pub more: bool,
pub hide_buses: bool,
pub hide_hubs: bool,
pub list_root_hubs: bool,
pub decimal: bool,
pub no_padding: bool,
#[serde(skip_serializing)]
pub no_color: bool,
#[serde(skip_serializing)]
pub ascii: bool,
#[serde(skip_serializing)]
pub no_icons: bool,
pub headings: bool,
pub force_libusb: bool,
pub json: bool,
pub print_non_critical_profiler_stderr: bool,
pub filter_include: Vec<crate::profiler::FilterEntry>,
pub filter_exclude: Vec<crate::profiler::FilterEntry>,
pub mute_hubs: bool,
}
impl Config {
pub fn new() -> Self {
Default::default()
}
#[cfg(not(debug_assertions))]
pub fn sys() -> Result<Self> {
if let Some(p) = Self::config_file_path() {
let path = p.join(CONF_NAME);
log::info!("Looking for system config {:?}", &path);
return match Self::from_file(&path) {
Ok(c) => {
log::info!("Loaded system config {:?}", c);
Ok(c)
}
Err(e) => {
if e.kind() == ErrorKind::Parsing {
log::warn!("{}", e);
Err(e)
} else {
Ok(Self::new())
}
}
};
} else {
Ok(Self::new())
}
}
#[cfg(debug_assertions)]
pub fn sys() -> Result<Self> {
log::warn!("Running in debug, not checking for system config");
Ok(Self::new())
}
pub fn example() -> Self {
Config {
icons: icon::example_theme(),
blocks: Some(display::DeviceBlocks::example_blocks()),
bus_blocks: Some(display::BusBlocks::example_blocks()),
config_blocks: Some(display::ConfigurationBlocks::example_blocks()),
interface_blocks: Some(display::InterfaceBlocks::example_blocks()),
endpoint_blocks: Some(display::EndpointBlocks::example_blocks()),
mask_serials: None,
group_devices: Some(display::Group::default()),
encoding: Some(display::Encoding::default()),
icon_when: Some(display::IconWhen::default()),
color_when: Some(display::ColorWhen::default()),
sort_devices: Some(display::Sort::default()),
..Default::default()
}
}
pub fn example_with_filter() -> Self {
Config {
filter_include: vec![
FilterEntry {
vidpid: Some(VidPid(Some(0x1d50), Some(0x6018))),
..Default::default()
},
FilterEntry {
vidpid: Some(VidPid(Some(0x05ac), None)),
..Default::default()
},
FilterEntry {
name: Some("black magic".into()),
..Default::default()
},
FilterEntry {
class: Some(BaseClass::Hid),
..Default::default()
},
FilterEntry {
vidpid: Some(VidPid(Some(0x1366), None)),
name: Some("J-Link".into()),
..Default::default()
},
FilterEntry {
bus: Some(1),
number: Some(3),
..Default::default()
},
],
filter_exclude: vec![
FilterEntry {
name: Some("Hub".into()),
..Default::default()
},
FilterEntry {
serial: Some("zf3ds2".into()),
case_sensitive: true,
..Default::default()
},
FilterEntry {
vidpid: Some(VidPid(Some(0x1d6b), Some(0x0002))),
..Default::default()
},
],
..Default::default()
}
}
pub fn from_file<P: AsRef<Path>>(file_path: P) -> Result<Self> {
let f = File::open(&file_path)?;
let mut config: Self = serde_json::from_reader(BufReader::new(f)).map_err(|e| {
Error::new(
ErrorKind::Parsing,
&format!(
"Failed to parse config at {:?}; Error({})",
file_path.as_ref(),
e
),
)
})?;
config.filepath = Some(file_path.as_ref().to_path_buf());
Ok(config)
}
pub fn config_file_path() -> Option<PathBuf> {
dirs::config_dir().map(|x| x.join(CONF_DIR))
}
pub fn filepath(&self) -> Option<&Path> {
self.filepath.as_deref()
}
pub fn save_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
log::info!("Saving config to {:?}", path.as_ref().display());
if let Some(parent) = path.as_ref().parent() {
log::debug!("Creating parent folders for {:?}", parent.display());
std::fs::create_dir_all(parent)?;
}
let f = File::create(&path)?;
serde_json::to_writer_pretty(f, self)
.map_err(|e| Error::new(ErrorKind::Io, &format!("Failed to save config: Error({e})")))
}
pub fn save(&self) -> Result<()> {
if let Some(p) = self.filepath() {
self.save_file(p)
} else if let Some(p) = Self::config_file_path() {
self.save_file(p.join(CONF_NAME))
} else {
Err(Error::new(
ErrorKind::Io,
"Unable to determine config file path",
))
}
}
pub fn merge_print_settings(&mut self, settings: &display::PrintSettings) {
self.blocks = settings.device_blocks.clone();
self.bus_blocks = settings.bus_blocks.clone();
self.config_blocks = settings.config_blocks.clone();
self.interface_blocks = settings.interface_blocks.clone();
self.endpoint_blocks = settings.endpoint_blocks.clone();
self.more = settings.more;
self.decimal = settings.decimal;
self.mask_serials = settings.mask_serials;
self.group_devices = Some(settings.group_devices);
self.encoding = Some(settings.encoding);
self.icon_when = Some(settings.icon_when);
self.color_when = Some(settings.color_when);
self.sort_devices = Some(settings.sort_devices);
self.sort_buses = settings.sort_buses;
self.no_color = settings.colours.is_none();
self.no_padding = settings.no_padding;
self.headings = settings.headings;
self.tree = settings.tree;
self.max_variable_string_len = settings.max_variable_string_len;
self.no_auto_width = !settings.auto_width;
self.no_icons = matches!(settings.icon_when, display::IconWhen::Never)
|| !matches!(settings.encoding, display::Encoding::Glyphs);
self.ascii = matches!(settings.encoding, display::Encoding::Ascii);
self.verbose = settings.verbosity;
self.json = settings.json;
self.mute_hubs = settings.mute_hubs;
}
pub fn print_settings(&self) -> display::PrintSettings {
let colours = if self.no_color {
None
} else {
Some(self.colours.clone())
};
let icons = if self.no_icons {
None
} else {
Some(self.icons.clone())
};
let encoding = self.encoding.unwrap_or({
if self.ascii {
display::Encoding::Ascii
} else if self.no_icons {
display::Encoding::Utf8
} else {
display::Encoding::Glyphs
}
});
let group_devices = if self.group_devices == Some(display::Group::Bus) && self.tree {
log::warn!("--group-devices with --tree is ignored; will print as tree");
display::Group::NoGroup
} else {
self.group_devices.unwrap_or(display::Group::NoGroup)
};
display::PrintSettings {
device_blocks: self.blocks.clone(),
bus_blocks: self.bus_blocks.clone(),
config_blocks: self.config_blocks.clone(),
interface_blocks: self.interface_blocks.clone(),
endpoint_blocks: self.endpoint_blocks.clone(),
more: self.more,
decimal: self.decimal,
mask_serials: self.mask_serials,
group_devices,
sort_devices: self.sort_devices.unwrap_or_else(|| {
if self.tree {
display::Sort::BranchPosition
} else {
display::Sort::default()
}
}),
sort_buses: self.sort_buses,
no_padding: self.no_padding,
headings: self.headings,
tree: self.tree,
max_variable_string_len: self.max_variable_string_len,
auto_width: !self.no_auto_width,
icon_when: self.icon_when.unwrap_or_default(),
color_when: self.color_when.unwrap_or_default(),
encoding,
icons,
colours,
verbosity: self.verbose,
json: self.json,
mute_hubs: self.mute_hubs,
..Default::default()
}
}
}
impl From<&display::PrintSettings> for Config {
fn from(settings: &display::PrintSettings) -> Self {
let mut c = Config::new();
c.merge_print_settings(settings);
c
}
}
impl From<&Config> for display::PrintSettings {
fn from(c: &Config) -> Self {
c.print_settings()
}
}
#[cfg(test)]
mod tests {
use super::*;
use colored::*;
#[test]
#[cfg(feature = "regex_icon")]
fn test_deserialize_example_file() {
let path = PathBuf::from("./doc").join("cyme_example_config.json");
assert!(Config::from_file(path).is_ok());
}
#[test]
#[cfg(feature = "regex_icon")]
fn test_deserialize_example_filter_file() {
let path = PathBuf::from("./doc").join("cyme_example_filter_config.json");
let c = Config::from_file(path);
assert!(
c.is_ok(),
"Failed to deserialize example filter config: {:?}",
c.err()
);
}
#[test]
fn test_deserialize_config_no_theme() {
let path = PathBuf::from("./tests/data").join("config_no_theme.json");
assert!(Config::from_file(path).is_ok());
}
#[test]
fn test_deserialize_config_missing_args() {
let path = PathBuf::from("./tests/data").join("config_missing_args.json");
assert!(Config::from_file(path).is_ok());
}
#[test]
fn test_save_config() {
let path = PathBuf::from("./tests/data").join("config_save.json");
let c = Config::new();
assert!(c.save_file(&path).is_ok());
assert!(Config::from_file(path).is_ok());
}
#[test]
fn test_filter_serialize_deserialize() {
let config = Config {
filter_include: vec![
FilterEntry {
vidpid: Some(VidPid(Some(0x1d50), Some(0x6018))),
..Default::default()
},
FilterEntry {
name: Some("black magic".into()),
..Default::default()
},
],
filter_exclude: vec![FilterEntry {
name: Some("Keyboard".into()),
..Default::default()
}],
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let restored: Config = serde_json::from_str(&json).unwrap();
assert_eq!(config.filter_include, restored.filter_include);
assert_eq!(config.filter_exclude, restored.filter_exclude);
let raw = r#"{"filter-include": [{"vidpid": "1d50:6018"}, {"name": "black magic"}], "filter-exclude": [{"name": "Keyboard"}]}"#;
let from_raw: Config = serde_json::from_str(raw).unwrap();
assert_eq!(
from_raw.filter_include[0].vidpid,
Some(VidPid(Some(0x1d50), Some(0x6018)))
);
assert_eq!(
from_raw.filter_include[1].name.as_deref(),
Some("black magic")
);
assert_eq!(from_raw.filter_exclude[0].name.as_deref(), Some("Keyboard"));
let raw = r#"{"filter-include": [{"vidpid": "05ac", "name": "Keyboard"}]}"#;
let from_raw: Config = serde_json::from_str(raw).unwrap();
assert_eq!(
from_raw.filter_include[0].vidpid,
Some(VidPid(Some(0x05ac), None))
);
assert_eq!(from_raw.filter_include[0].name.as_deref(), Some("Keyboard"));
assert!(
serde_json::from_str::<Config>(r#"{"filter-include": [{"vidpid": "gggg"}]}"#).is_err()
);
}
#[test]
fn test_deserialize_colors_alias() {
let raw = r#"{"colors": {"name": "red", "muted": "blue"}}"#;
let c: Config = serde_json::from_str(raw).unwrap();
assert_eq!(c.colours.name, Some(Color::Red));
assert_eq!(c.colours.muted, Some(Color::Blue));
}
}