fn js_error_to_string(value: JsValue) -> String { value .as_string() .or_else(|| { js_sys::Reflect::get(&value, &JsValue::from_str("message")) .ok() .and_then(|message| message.as_string()) }) .unwrap_or_else(|| "Tauri command failed".to_string()) } fn color_to_hex(color: &[u8; 3]) -> String { format!("#{:02x}{:02x}{:02x}", color[0], color[1], color[2]) } fn hex_to_rgb(hex: &str) -> [u8; 3] { if hex.len() != 7 || !hex.starts_with('#') { return [255, 255, 255]; } [ u8::from_str_radix(&hex[1..3], 16).unwrap_or(255), u8::from_str_radix(&hex[3..5], 16).unwrap_or(255), u8::from_str_radix(&hex[5..7], 16).unwrap_or(255), ] } fn bytes_to_hex(bytes: &[u8]) -> String { bytes .iter() .map(|byte| format!("{byte:02x}")) .collect::>() .join(" ") } fn parse_hex_bytes(input: &str) -> Vec { input .split(|ch: char| ch.is_whitespace() || ch == ',' || ch == ':') .filter(|segment| !segment.is_empty()) .filter_map(|segment| { let cleaned = segment.strip_prefix("0x").unwrap_or(segment); u8::from_str_radix(cleaned, 16).ok() }) .collect() } fn format_log_timestamp(timestamp_ms: u64) -> String { if timestamp_ms == 0 { return "--:--:--".to_string(); } let date = js_sys::Date::new(&JsValue::from_f64(timestamp_ms as f64)); format!( "{:02}:{:02}:{:02}", date.get_hours(), date.get_minutes(), date.get_seconds() ) } fn format_macro_operations_yaml(operations: &[MacroOperationState]) -> Result { let yaml_ops = operations .iter() .map(|operation| (operation.category.clone(), operation.payload.clone())) .collect::>(); serde_yaml::to_string(&yaml_ops).map_err(|error| error.to_string()) } fn parse_macro_operations_yaml(input: &str) -> Result, String> { let value = serde_yaml::from_str::(input).map_err(|error| error.to_string())?; let Some(items) = value.as_sequence() else { return Err("Macro YAML must be a list of operations".to_string()); }; items.iter() .map(|item| { if let Some(mapping) = item.as_mapping() { let category = mapping .get(serde_yaml::Value::String("category".to_string())) .and_then(serde_yaml::Value::as_str) .ok_or_else(|| "Macro operation object is missing category".to_string())?; let payload = mapping .get(serde_yaml::Value::String("payload".to_string())) .cloned() .unwrap_or(serde_yaml::Value::Null); return Ok(MacroOperationState { category: category.to_string(), payload: serde_json::to_value(payload).map_err(|error| error.to_string())?, }); } let Some(pair) = item.as_sequence() else { return Err("Macro operation must be a [category, payload] pair or object".to_string()); }; if pair.len() != 2 { return Err("Macro operation pair must contain exactly two items".to_string()); } let category = pair[0] .as_str() .ok_or_else(|| "Macro operation category must be a string".to_string())?; Ok(MacroOperationState { category: category.to_string(), payload: serde_json::to_value(pair[1].clone()).map_err(|error| error.to_string())?, }) }) .collect() } fn mock_led_state(profile: String) -> LedState { LedState { profile, regions: vec![ LedRegionState { region: "wheel".to_string(), effect: "wave".to_string(), mode: 1, speed: 180, colors: vec![], brightness: 200, }, LedRegionState { region: "logo".to_string(), effect: "static".to_string(), mode: 0, speed: 0, colors: vec![[0, 255, 0]], brightness: 180, }, LedRegionState { region: "strip".to_string(), effect: "spectrum".to_string(), mode: 1, speed: 180, colors: vec![], brightness: 255, }, ], } }