use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use crate::display::Encoding;
use crate::error::{Error, ErrorKind};
use crate::system_profiler::{USBBus, USBDevice};
use crate::usb::{ClassCode, Direction};
fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
value: &T,
serializer: S,
) -> Result<S::Ok, S::Error> {
let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
value.serialize(serializer)
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, SerializeDisplay, DeserializeFromStr)]
pub enum Icon {
Vid(u16),
VidPid((u16, u16)),
VidPidMsb((u16, u8)),
Classifier(ClassCode),
ClassifierSubProtocol((ClassCode, u8, u8)),
UnknownVendor,
UndefinedClassifier,
TreeEdge,
TreeLine,
TreeCorner,
TreeBlank,
TreeBusStart,
TreeDeviceTerminator,
TreeConfigurationTerminator,
TreeInterfaceTerminator,
Endpoint(Direction),
}
impl FromStr for Icon {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value_split: Vec<&str> = s.split('#').collect();
let enum_name = value_split[0];
if value_split.len() == 1 {
match enum_name {
"unknown-vendor" => Ok(Icon::UnknownVendor),
"undefined-classifier" => Ok(Icon::UndefinedClassifier),
"tree-edge" => Ok(Icon::TreeEdge),
"tree-blank" => Ok(Icon::TreeBlank),
"tree-line" => Ok(Icon::TreeLine),
"tree-corner" => Ok(Icon::TreeCorner),
"tree-bus-start" => Ok(Icon::TreeBusStart),
"tree-device-terminator" => Ok(Icon::TreeDeviceTerminator),
"tree-configuration-terminator" => Ok(Icon::TreeConfigurationTerminator),
"tree-interface-terminator" => Ok(Icon::TreeInterfaceTerminator),
"endpoint_in" => Ok(Icon::Endpoint(Direction::In)),
"endpoint_out" => Ok(Icon::Endpoint(Direction::Out)),
_ => Err(Error::new(
ErrorKind::Parsing,
"Invalid Icon enum name or valued enum without value",
)),
}
} else {
let (parse_ints, errors): (Vec<Result<u32, _>>, Vec<_>) = value_split[1]
.split(':')
.map(|vs| u32::from_str_radix(vs.trim_start_matches("0x"), 16))
.partition(Result::is_ok);
let numbers: Vec<u16> = parse_ints.into_iter().map(|v| v.unwrap() as u16).collect();
if !errors.is_empty() {
return Err(Error::new(
ErrorKind::Parsing,
"Invalid value in enum string after #",
));
}
match value_split[0] {
"vid" => match numbers.first() {
Some(i) => Ok(Icon::Vid(*i)),
None => Err(Error::new(ErrorKind::Parsing, "No value for enum after $")),
},
"vid-pid" => match numbers.get(0..2) {
Some(slice) => Ok(Icon::VidPid((slice[0], slice[1]))),
None => Err(Error::new(ErrorKind::Parsing, "No value for enum after $")),
},
"vid-pid-msb" => match numbers.get(0..2) {
Some(slice) => Ok(Icon::VidPidMsb((slice[0], slice[1] as u8))),
None => Err(Error::new(ErrorKind::Parsing, "No value for enum after $")),
},
"classifier" => match numbers.first() {
Some(i) => Ok(Icon::Classifier(ClassCode::from(*i as u8))),
None => Err(Error::new(ErrorKind::Parsing, "No value for enum after $")),
},
"classifier-sub-protocol" => match numbers.get(0..3) {
Some(slice) => Ok(Icon::ClassifierSubProtocol((
ClassCode::from(slice[0] as u8),
slice[1] as u8,
slice[2] as u8,
))),
None => Err(Error::new(ErrorKind::Parsing, "No value for enum after $")),
},
_ => Err(Error::new(
ErrorKind::Parsing,
"Invalid Icon enum value holder",
)),
}
}
}
}
impl fmt::Display for Icon {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Icon::Vid(v) => write!(f, "vid#{:04x}", v),
Icon::VidPid((v, p)) => write!(f, "vid-pid#{:04x}:{:04x}", v, p),
Icon::VidPidMsb((v, p)) => write!(f, "vid-pid-msb#{:04x}:{:02x}", v, p),
Icon::Classifier(c) => write!(f, "classifier#{:02x}", u8::from(c.to_owned())),
Icon::ClassifierSubProtocol(c) => write!(
f,
"classifier-sub-protocol#{:02x}:{:02x}:{:02x}",
u8::from(c.0.to_owned()),
c.1,
c.2
),
Icon::Endpoint(Direction::In) => write!(f, "endpoint_in"),
Icon::Endpoint(Direction::Out) => write!(f, "endpoint_out"),
_ => {
let dbg_str = format!("{:?}", self);
write!(f, "{}", heck::AsKebabCase(dbg_str))
}
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
#[serde(default)]
pub struct IconTheme {
#[serde(serialize_with = "sort_alphabetically")]
pub user: Option<HashMap<Icon, String>>,
#[serde(serialize_with = "sort_alphabetically")]
pub tree: Option<HashMap<Icon, String>>,
}
impl Default for IconTheme {
fn default() -> Self {
IconTheme {
user: None,
tree: None,
}
}
}
lazy_static! {
static ref DEFAULT_UTF8_TREE: HashMap<Icon, &'static str> = {
HashMap::from([
(Icon::TreeEdge, "\u{251c}\u{2500}\u{2500}"), (Icon::TreeLine, "\u{2502} "), (Icon::TreeCorner, "\u{2514}\u{2500}\u{2500}"), (Icon::TreeBlank, " "), (Icon::TreeBusStart, "\u{25CF}"), (Icon::TreeDeviceTerminator, "\u{25CB}"), (Icon::TreeConfigurationTerminator, "\u{2022}"), (Icon::TreeInterfaceTerminator, "\u{25E6}"), (Icon::Endpoint(Direction::In), "\u{2192}"), (Icon::Endpoint(Direction::Out), "\u{2190}"), ])
};
static ref DEFAULT_ASCII_TREE: HashMap<Icon, &'static str> = {
HashMap::from([
(Icon::TreeEdge, "|__"), (Icon::TreeLine, "| "), (Icon::TreeCorner, "|__"),
(Icon::TreeBlank, " "), (Icon::TreeBusStart, "/: "),
(Icon::TreeDeviceTerminator, "O"), (Icon::TreeConfigurationTerminator, "o"), (Icon::TreeInterfaceTerminator, "."), (Icon::Endpoint(Direction::In), ">"), (Icon::Endpoint(Direction::Out), "<"), ])
};
pub static ref DEFAULT_ICONS: HashMap<Icon, &'static str> = {
HashMap::from([
(Icon::UnknownVendor, "\u{f287}"), (Icon::Vid(0x05ac), "\u{f179}"), (Icon::Vid(0x8086), "\u{f179}"), (Icon::Vid(0x045e), "\u{f0372}"), (Icon::Vid(0x18d1), "\u{f1a0}"), (Icon::Vid(0x1D6B), "\u{f17c}"), (Icon::Vid(0x1d50), "\u{e771}"), (Icon::VidPid((0x1915, 0x520c)), "\u{f00a3}"), (Icon::VidPid((0x1915, 0x520d)), "\u{f00a3}"), (Icon::VidPid((0x0483, 0x572B)), "\u{f00a3}"), (Icon::Vid(0x046d), "\u{f037d}"), (Icon::Vid(0x091e), "\u{e2a6}"), (Icon::VidPid((0x1d50, 0x6018)), "\u{f188}"), (Icon::Vid(0x1366), "\u{f188}"), (Icon::Vid(0xf1a0), "\u{f188}"), (Icon::VidPidMsb((0x0483, 0x37)), "\u{f188}"), (Icon::VidPid((0x0483, 0xdf11)), "\u{f019}"), (Icon::VidPid((0x1d50, 0x6017)), "\u{f188}"), (Icon::ClassifierSubProtocol((ClassCode::ApplicationSpecificInterface, 0x01, 0x01)), "\u{f188}"), (Icon::ClassifierSubProtocol((ClassCode::WirelessController, 0x01, 0x01)), "\u{f188}"), (Icon::Vid(0x2341), "\u{f2db}"), (Icon::Vid(0x239A), "\u{f2db}"), (Icon::Vid(0x2e8a), "\u{f315}"), (Icon::Vid(0x0483), "\u{f2db}"), (Icon::Vid(0x1915), "\u{f2db}"), (Icon::Vid(0x1fc9), "\u{f2db}"), (Icon::Vid(0x1050), "\u{f084}"), (Icon::Vid(0x0781), "\u{f129e}"), (Icon::VidPid((0x05ac, 0x8409)), "\u{ef61}"), (Icon::VidPid((0x05e3, 0x0749)), "\u{ef61}"), (Icon::VidPid((0x18D1, 0x2D05)), "\u{e70e}"), (Icon::VidPid((0x18D1, 0xd00d)), "\u{e70e}"), (Icon::VidPid((0x1d50, 0x606f)), "\u{f191d}"), (Icon::VidPidMsb((0x043e, 0x9a)), "\u{f0379}"), (Icon::Classifier(ClassCode::Audio), "\u{f001}"), (Icon::Classifier(ClassCode::Image), "\u{f03e}"), (Icon::Classifier(ClassCode::Video), "\u{f03d}"), (Icon::Classifier(ClassCode::Printer), "\u{f02f}"), (Icon::Classifier(ClassCode::MassStorage), "\u{f0a0}"), (Icon::Classifier(ClassCode::Hub), "\u{f126}"), (Icon::Classifier(ClassCode::ContentSecurity), "\u{f084}"), (Icon::Classifier(ClassCode::SmartCart), "\u{f084}"), (Icon::Classifier(ClassCode::PersonalHealthcare), "\u{f21e}"), (Icon::Classifier(ClassCode::AudioVideo), "\u{f0841}"), (Icon::Classifier(ClassCode::Billboard), "\u{f05a}"), (Icon::Classifier(ClassCode::I3CDevice), "\u{f493}"), (Icon::Classifier(ClassCode::Diagnostic), "\u{f489}"), (Icon::Classifier(ClassCode::WirelessController), "\u{f1eb}"), (Icon::Classifier(ClassCode::Miscellaneous), "\u{f074}"), (Icon::Classifier(ClassCode::CDCCommunications), "\u{e795}"), (Icon::Classifier(ClassCode::CDCData), "\u{e795}"), (Icon::Classifier(ClassCode::HID), "\u{f030c}"), (Icon::UndefinedClassifier, "\u{2636}"), ])
};
}
impl IconTheme {
pub fn new() -> Self {
Default::default()
}
pub fn get_tree_icon(&self, icon: &Icon, encoding: &Encoding) -> String {
if let Some(user_tree) = self.tree.as_ref() {
user_tree
.get(icon)
.map(|s| match encoding.str_is_valid(s) {
true => s.to_owned(),
false => get_default_tree_icon(icon, encoding),
})
.unwrap_or(get_default_tree_icon(icon, encoding))
} else {
get_default_tree_icon(icon, encoding)
}
}
pub fn get_default_vidpid_icon(vid: u16, pid: u16) -> String {
DEFAULT_ICONS
.get(&Icon::VidPid((vid, pid)))
.unwrap_or(
DEFAULT_ICONS
.get(&Icon::VidPidMsb((vid, (pid >> 8) as u8)))
.unwrap_or(
DEFAULT_ICONS
.get(&Icon::Vid(vid))
.unwrap_or(DEFAULT_ICONS.get(&Icon::UnknownVendor).unwrap_or(&"")),
),
)
.to_string()
}
pub fn get_vidpid_icon(&self, vid: u16, pid: u16) -> String {
if let Some(user_icons) = self.user.as_ref() {
user_icons
.get(&Icon::VidPid((vid, pid)))
.unwrap_or(
user_icons
.get(&Icon::VidPidMsb((vid, (pid >> 8) as u8)))
.unwrap_or(
user_icons.get(&Icon::Vid(vid)).unwrap_or(
user_icons
.get(&Icon::UnknownVendor)
.unwrap_or(&IconTheme::get_default_vidpid_icon(vid, pid)),
),
),
)
.to_owned()
} else {
IconTheme::get_default_vidpid_icon(vid, pid)
}
}
pub fn get_default_device_icon(d: &USBDevice) -> String {
if let (Some(vid), Some(pid)) = (d.vendor_id, d.product_id) {
IconTheme::get_default_vidpid_icon(vid, pid)
} else {
String::new()
}
}
pub fn get_device_icon(&self, d: &USBDevice) -> String {
if let (Some(vid), Some(pid)) = (d.vendor_id, d.product_id) {
self.get_vidpid_icon(vid, pid)
} else {
String::new()
}
}
pub fn get_bus_icon(&self, d: &USBBus) -> String {
if let (Some(vid), Some(pid)) = (d.pci_vendor, d.pci_device) {
self.get_vidpid_icon(vid, pid)
} else {
String::new()
}
}
pub fn get_default_classifier_icon(class: &ClassCode, sub: u8, protocol: u8) -> String {
DEFAULT_ICONS
.get(&Icon::ClassifierSubProtocol((
class.to_owned(),
sub,
protocol,
)))
.unwrap_or(
DEFAULT_ICONS
.get(&Icon::Classifier(class.to_owned()))
.unwrap_or(DEFAULT_ICONS.get(&Icon::UndefinedClassifier).unwrap_or(&"")),
)
.to_string()
}
pub fn get_classifier_icon(&self, class: &ClassCode, sub: u8, protocol: u8) -> String {
if let Some(user_icons) = self.user.as_ref() {
user_icons
.get(&Icon::ClassifierSubProtocol((
class.to_owned(),
sub,
protocol,
)))
.unwrap_or(
user_icons
.get(&Icon::Classifier(class.to_owned()))
.unwrap_or(&IconTheme::get_default_classifier_icon(
class, sub, protocol,
)),
)
.to_owned()
} else {
IconTheme::get_default_classifier_icon(class, sub, protocol)
}
}
}
pub fn get_default_tree_icon(i: &Icon, encoding: &Encoding) -> String {
match encoding {
Encoding::Utf8 | Encoding::Glyphs => DEFAULT_UTF8_TREE.get(i).unwrap().to_string(),
Encoding::Ascii => DEFAULT_ASCII_TREE.get(i).unwrap().to_string(),
}
}
pub fn get_ascii_tree_icon(i: &Icon) -> String {
DEFAULT_ASCII_TREE.get(i).unwrap().to_string()
}
pub fn defaults() -> HashMap<Icon, &'static str> {
DEFAULT_ICONS.clone()
}
pub fn example() -> HashMap<Icon, String> {
HashMap::from([
(Icon::UnknownVendor, "\u{f287}".into()), (Icon::Vid(0x05ac), "\u{f179}".into()), (Icon::VidPid((0x1d50, 0x6018)), "\u{f188}".into()), (Icon::VidPidMsb((0x0483, 0x37)), "\u{f188}".into()), (
Icon::ClassifierSubProtocol((ClassCode::ApplicationSpecificInterface, 0x01, 0x01)),
"\u{f188}".into(),
), (Icon::Vid(0x2e8a), "\u{f315}".into()), (
Icon::Classifier(ClassCode::CDCCommunications),
"\u{e795}".into(),
), (Icon::UndefinedClassifier, "\u{2636}".into()), ])
}
pub fn example_theme() -> IconTheme {
let tree_strings: HashMap<Icon, String> = DEFAULT_UTF8_TREE
.iter()
.map(|(k, v)| (k.to_owned(), v.to_string()))
.collect();
IconTheme {
user: Some(example()),
tree: Some(tree_strings),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_theme() {
let theme = IconTheme {
user: Some(HashMap::from([
(Icon::UnknownVendor, "\u{f287}".into()), ])),
..Default::default()
};
assert_eq!(
serde_json::to_string(&theme).unwrap(),
"{\"user\":{\"unknown-vendor\":\"\"},\"tree\":null}"
);
}
#[test]
fn test_deserialize_theme() {
let theme: IconTheme =
serde_json::from_str("{\"user\":{\"unknown-vendor\":\"\"},\"tree\":null}").unwrap();
let actual_theme = IconTheme {
user: Some(HashMap::from([
(Icon::UnknownVendor, "\u{f287}".into()), ])),
..Default::default()
};
assert_eq!(theme, actual_theme);
}
#[test]
fn test_serialize_defaults() {
serde_json::to_string(&defaults()).unwrap();
}
#[test]
fn test_serialize_example() {
println!("{}", serde_json::to_string_pretty(&example()).unwrap());
}
#[test]
fn test_deserialize_icon_tuples() {
let item: (Icon, &'static str) = (Icon::VidPid((0x1d50, 0x6018)), "\u{f188}");
let item_ser = serde_json::to_string(&item).unwrap();
assert_eq!(item_ser, r#"["vid-pid#1d50:6018",""]"#);
let item: (Icon, &'static str) = (Icon::Endpoint(Direction::In), ">");
let item_ser = serde_json::to_string(&item).unwrap();
assert_eq!(item_ser, r#"["endpoint_in",">"]"#);
let item: (Icon, &'static str) = (
Icon::ClassifierSubProtocol((ClassCode::HID, 0x01, 0x0a)),
"K",
);
let item_ser = serde_json::to_string(&item).unwrap();
assert_eq!(item_ser, r#"["classifier-sub-protocol#03:01:0a","K"]"#);
}
#[test]
fn icon_from_str() {
let str = "vid#1d50";
let icon = Icon::from_str(str);
assert_eq!(icon.unwrap(), Icon::Vid(7504));
let str = "vid-pid#1d50:6018";
let icon = Icon::from_str(str);
assert_eq!(icon.unwrap(), Icon::VidPid((7504, 24600)));
let str = "classifier#03";
let icon = Icon::from_str(str);
assert_eq!(icon.unwrap(), Icon::Classifier(ClassCode::HID));
let str = "classifier-sub-protocol#03:01:0a";
let icon = Icon::from_str(str);
assert_eq!(
icon.unwrap(),
Icon::ClassifierSubProtocol((ClassCode::HID, 1, 10))
);
let str = "endpoint_in";
let icon = Icon::from_str(str);
assert_eq!(icon.unwrap(), Icon::Endpoint(Direction::In));
let str = "unknown-vendor";
let icon = Icon::from_str(str);
assert_eq!(icon.unwrap(), Icon::UnknownVendor);
}
}