#[cfg(feature = "regex_icon")]
use regex;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;
use crate::display::Encoding;
use crate::error::{Error, ErrorKind};
use crate::profiler::{Bus, Device};
use crate::usb::{BaseClass, 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(BaseClass),
ClassifierSubProtocol((BaseClass, u8, u8)),
Name(String),
UnknownVendor,
UndefinedClassifier,
TreeEdge,
TreeLine,
TreeCorner,
TreeBlank,
TreeBusStart,
TreeDeviceTerminator,
TreeConfigurationTerminator,
TreeInterfaceTerminator,
TreeDisconnectedTerminator,
TreeHubTerminator,
Endpoint(Direction),
Profiled,
Connected,
Disconnected,
}
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),
"tree-disconnected-terminator" => Ok(Icon::TreeDisconnectedTerminator),
"endpoint_in" => Ok(Icon::Endpoint(Direction::In)),
"endpoint_out" => Ok(Icon::Endpoint(Direction::Out)),
"profiled" => Ok(Icon::Profiled),
"connected" => Ok(Icon::Connected),
"disconnected" => Ok(Icon::Disconnected),
"tree-hub-terminator" => Ok(Icon::TreeHubTerminator),
_ => Err(Error::new(
ErrorKind::Parsing,
"Invalid Icon enum name or valued enum without value",
)),
}
} else if matches!(enum_name, "name") {
#[cfg(feature = "regex_icon")]
match regex::Regex::new(value_split[1]) {
Ok(_) => Ok(Icon::Name(value_split[1].to_string())),
Err(_) => Err(Error::new(
ErrorKind::Parsing,
&format!(
"Invalid regex pattern in Icon::Name enum string: {}",
value_split[1]
),
)),
}
#[cfg(not(feature = "regex_icon"))]
Err(Error::new(
ErrorKind::Parsing,
"regex_icon feature not enabled for Icon::Name matching",
))
} 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(BaseClass::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((
BaseClass::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#{v:04x}"),
Icon::VidPid((v, p)) => write!(f, "vid-pid#{v:04x}:{p:04x}"),
Icon::VidPidMsb((v, p)) => write!(f, "vid-pid-msb#{v:04x}:{p:02x}"),
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::Name(s) => write!(f, "name#{s}"),
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, Clone)]
#[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,
}
}
}
pub static DEFAULT_UTF8_TREE: LazyLock<HashMap<Icon, &'static str>> = LazyLock::new(|| {
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::TreeDisconnectedTerminator, "\u{2715}"), (Icon::TreeHubTerminator, "\u{229b}"), (Icon::Endpoint(Direction::In), "\u{2192}"), (Icon::Endpoint(Direction::Out), "\u{2190}"), ])
});
pub static DEFAULT_ASCII_TREE: LazyLock<HashMap<Icon, &'static str>> = LazyLock::new(|| {
HashMap::from([
(Icon::TreeEdge, "|__"), (Icon::TreeLine, "| "), (Icon::TreeCorner, "|__"),
(Icon::TreeBlank, " "), (Icon::TreeBusStart, "/: "),
(Icon::TreeDeviceTerminator, "O"),
(Icon::TreeConfigurationTerminator, "o"),
(Icon::TreeInterfaceTerminator, "."),
(Icon::TreeDisconnectedTerminator, "X"),
(Icon::TreeHubTerminator, "*"),
(Icon::Endpoint(Direction::In), ">"),
(Icon::Endpoint(Direction::Out), "<"),
])
});
pub static DEFAULT_ICONS: LazyLock<HashMap<Icon, &'static str>> = LazyLock::new(|| {
HashMap::from([
(Icon::UnknownVendor, "\u{f287}"), (Icon::Vid(0x05ac), "\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((0x1915, 0x520e)), "\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((BaseClass::ApplicationSpecificInterface, 0x01, 0x01)),
"\u{f188}",
), (
Icon::ClassifierSubProtocol((BaseClass::WirelessController, 0x01, 0x01)),
"\u{f188}",
), (Icon::Vid(0x2341), "\u{e733}"), (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}"), #[cfg(feature = "regex_icon")]
(
Icon::Name(r".*^[sS][dD]\s[cC]ard\s[rR]eader.*".to_string()),
"\u{ef61}",
), (Icon::VidPid((0x18D1, 0x2D05)), "\u{e70e}"), (Icon::VidPid((0x18D1, 0xd00d)), "\u{e70e}"), (Icon::VidPid((0x1d50, 0x606f)), "\u{f191d}"), (Icon::VidPid((0x1209, 0xca01)), "\u{f191d}"), (Icon::VidPid((0x1209, 0xca02)), "\u{f191d}"), (Icon::VidPid((0x05c6, 0x9008)), "\u{f019}"), (Icon::VidPidMsb((0x043e, 0x9a)), "\u{f0379}"), (Icon::Classifier(BaseClass::Audio), "\u{f001}"), (Icon::Classifier(BaseClass::Image), "\u{f03e}"), (Icon::Classifier(BaseClass::Video), "\u{f03d}"), (Icon::Classifier(BaseClass::Printer), "\u{f02f}"), (Icon::Classifier(BaseClass::MassStorage), "\u{f0a0}"), (Icon::Classifier(BaseClass::Hub), "\u{f126}"), (Icon::Classifier(BaseClass::ContentSecurity), "\u{f084}"), (Icon::Classifier(BaseClass::SmartCard), "\u{f084}"), (Icon::Classifier(BaseClass::PersonalHealthcare), "\u{f21e}"), (Icon::Classifier(BaseClass::AudioVideo), "\u{f0841}"), (Icon::Classifier(BaseClass::Billboard), "\u{f05a}"), (Icon::Classifier(BaseClass::I3cDevice), "\u{f493}"), (Icon::Classifier(BaseClass::Diagnostic), "\u{f489}"), (Icon::Classifier(BaseClass::WirelessController), "\u{f1eb}"), (Icon::Classifier(BaseClass::Miscellaneous), "\u{f074}"), (Icon::Classifier(BaseClass::CdcCommunications), "\u{e795}"), (Icon::Classifier(BaseClass::CdcData), "\u{e795}"), (Icon::Classifier(BaseClass::Hid), "\u{f030c}"), (Icon::UndefinedClassifier, "\u{2636}"), (Icon::Profiled, "\u{f041a}"), (Icon::Connected, "\u{f0c53}"), (Icon::Disconnected, "\u{f015b}"), ])
});
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: &Device) -> String {
if let (Some(vid), Some(pid)) = (d.vendor_id, d.product_id) {
IconTheme::get_default_vidpid_icon(vid, pid)
} else {
String::new()
}
}
#[cfg(feature = "regex_icon")]
pub fn get_device_icon(&self, d: &Device) -> String {
match self.get_name_icon(&d.name) {
s if !s.is_empty() => s,
_ => {
if let (Some(vid), Some(pid)) = (d.vendor_id, d.product_id) {
self.get_vidpid_icon(vid, pid)
} else {
String::new()
}
}
}
}
#[cfg(not(feature = "regex_icon"))]
pub fn get_device_icon(&self, d: &Device) -> String {
if let (Some(vid), Some(pid)) = (d.vendor_id, d.product_id) {
self.get_vidpid_icon(vid, pid)
} else {
DEFAULT_ICONS
.get(&Icon::UnknownVendor)
.unwrap_or(&"")
.to_string()
}
}
pub fn get_bus_icon(&self, d: &Bus) -> String {
if let (Some(vid), Some(pid)) = (d.pci_vendor, d.pci_device) {
self.get_vidpid_icon(vid, pid)
} else {
DEFAULT_ICONS
.get(&Icon::UnknownVendor)
.unwrap_or(&"")
.to_string()
}
}
pub fn get_default_classifier_icon(class: &BaseClass, 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: &BaseClass, 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)
}
}
#[cfg(feature = "regex_icon")]
pub fn get_default_name_icon(name: &str) -> String {
DEFAULT_ICONS
.iter()
.find(|(k, _)| {
if let Icon::Name(s) = k {
regex::Regex::new(s).is_ok_and(|r| r.is_match(name))
} else {
false
}
})
.map(|(_, v)| v.to_owned())
.unwrap_or("")
.to_string()
}
#[cfg(feature = "regex_icon")]
pub fn get_name_icon(&self, name: &str) -> String {
if let Some(user_icons) = self.user.as_ref() {
user_icons
.iter()
.find(|(k, _)| {
if let Icon::Name(s) = k {
regex::Regex::new(s).is_ok_and(|r| r.is_match(name))
} else {
false
}
})
.map(|(_, v)| v.to_owned())
.unwrap_or(String::new())
} else {
IconTheme::get_default_name_icon(name)
}
}
pub fn get_event_icon(&self, event: &crate::profiler::DeviceEvent) -> String {
use crate::profiler::DeviceEvent;
match event {
DeviceEvent::Profiled(_) => self
.user
.as_ref()
.and_then(|u| u.get(&Icon::Profiled))
.unwrap_or(
&DEFAULT_ICONS
.get(&Icon::Profiled)
.unwrap_or(&"")
.to_string(),
)
.to_string(),
DeviceEvent::Connected(_) => self
.user
.as_ref()
.and_then(|u| u.get(&Icon::Connected))
.unwrap_or(
&DEFAULT_ICONS
.get(&Icon::Connected)
.unwrap_or(&"")
.to_string(),
)
.to_string(),
DeviceEvent::Disconnected(_) => self
.user
.as_ref()
.and_then(|u| u.get(&Icon::Disconnected))
.unwrap_or(
&DEFAULT_ICONS
.get(&Icon::Disconnected)
.unwrap_or(&"")
.to_string(),
)
.to_string(),
}
}
}
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((BaseClass::ApplicationSpecificInterface, 0x01, 0x01)),
"\u{f188}".into(),
), (Icon::Vid(0x2e8a), "\u{f315}".into()), (
Icon::Classifier(BaseClass::CdcCommunications),
"\u{e795}".into(),
), (Icon::UndefinedClassifier, "\u{2636}".into()), #[cfg(feature = "regex_icon")]
(
Icon::Name(r".*^[sS][dD]\s[cC]ard\s[rR]eader.*".to_string()),
"\u{ef61}".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((BaseClass::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(BaseClass::Hid));
let str = "classifier-sub-protocol#03:01:0a";
let icon = Icon::from_str(str);
assert_eq!(
icon.unwrap(),
Icon::ClassifierSubProtocol((BaseClass::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);
if cfg!(feature = "regex_icon") {
let str = "name#test";
let icon = Icon::from_str(str);
assert_eq!(icon.unwrap(), Icon::Name("test".to_string()));
let str = r"name#.*^[sS][dD]\s[cC]ard\s[rR]eader.*";
let icon = Icon::from_str(str);
assert_eq!(
icon.unwrap(),
Icon::Name(r".*^[sS][dD]\s[cC]ard\s[rR]eader.*".to_string())
);
}
}
#[test]
#[cfg(feature = "regex_icon")]
fn icon_match_name() {
let mut device = Device {
name: "SD Card Reader".to_string(),
..Default::default()
};
let theme = IconTheme {
user: Some(HashMap::from([(
Icon::Name(r".*^[sS][dD]\s[cC]ard\s[rR]eader.*".to_string()),
"\u{ef61}".into(),
)])),
..Default::default()
};
let icon = theme.get_device_icon(&device);
assert_eq!(icon, "\u{ef61}");
device.name = "sD Card reader 2".to_string();
let icon = theme.get_device_icon(&device);
assert_eq!(icon, "\u{ef61}");
}
}