1use rite_model::{ErrorClass, StepFact};
15
16use crate::protocol::{Icon, UiSignal};
17
18#[must_use]
27pub fn fact_summary(fact: &StepFact) -> Option<(Icon, String)> {
28 match fact {
29 StepFact::CeremonyStarted { name, .. } => Some((Icon::Info, format!("Ceremony: {name}"))),
30 StepFact::ActStarted { label, .. } => Some((Icon::Info, format!("Act: {label}"))),
31 StepFact::StepStarted {
32 id,
33 label,
34 role_name,
35 ..
36 } => Some((
37 Icon::Info,
38 format!("Step {label} ({id}), role: {role_name}"),
39 )),
40 StepFact::PromptAnswered { .. }
41 | StepFact::BackendOperation { .. }
42 | StepFact::AttestationRecorded { .. }
43 | StepFact::CeremonyCompleted { .. } => None,
44 StepFact::ArtifactWritten { path, .. } => Some((
45 Icon::Checkmark,
46 format!("Artifact written: {}", path.display()),
47 )),
48 StepFact::DeviationRecorded { text, .. } => {
49 Some((Icon::Warning, format!("Deviation: {text}")))
50 }
51 StepFact::StepCompleted { outcome, .. } => match outcome {
52 rite_model::StepOutcome::Completed { message } => {
53 Some((Icon::Checkmark, message.clone()))
54 }
55 _ => Some((Icon::Checkmark, "Step completed".to_string())),
56 },
57 StepFact::StepAttemptFailed { attempt, error, .. } => Some((
58 Icon::Cross,
59 format!("Attempt {attempt} failed: {}", error.message),
60 )),
61 StepFact::CeremonyFailed { error, .. } => match error.class {
62 ErrorClass::Abort => Some((Icon::Info, error.message.clone())),
65 _ => Some((Icon::Cross, format!("Ceremony failed: {}", error.message))),
66 },
67 StepFact::EntropySeeded { source, .. } => {
68 Some((Icon::Info, format!("Entropy source seeded ({source})")))
69 }
70 StepFact::EntropyContributed { epoch, .. } => Some((
71 Icon::Info,
72 format!("Entropy contribution folded (epoch {epoch})"),
73 )),
74 StepFact::EntropyDrawn { path, .. } => {
75 Some((Icon::Info, format!("Random value drawn: {path}")))
76 }
77 _ => Some((Icon::Info, "unknown fact variant".to_string())),
81 }
82}
83
84#[must_use]
91pub fn signal_summary(signal: &UiSignal) -> Option<(Icon, String)> {
92 match signal {
93 UiSignal::LogLine { icon, text, .. } => Some((*icon, text.clone())),
94 UiSignal::Progress {
95 phase, fraction, ..
96 } => {
97 let text = match fraction {
98 Some(f) => format!("{phase}: {:>5.1}%", f * 100.0),
99 None => format!("{phase}…"),
100 };
101 Some((Icon::Spinner, text))
102 }
103 UiSignal::CeremonyOverview { .. } | UiSignal::SystemInfo(_) | UiSignal::Environment(_) => {
104 None
105 }
106 }
107}
108
109#[must_use]
114pub fn truncate_for_display(s: &str, max_chars: usize) -> String {
115 match s.char_indices().nth(max_chars) {
116 Some((boundary, _)) => format!("{}...", &s[..boundary]),
117 None => s.to_string(),
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::truncate_for_display;
124
125 #[test]
126 fn truncate_passes_through_short_input() {
127 assert_eq!(truncate_for_display("short", 10), "short");
128 }
129
130 #[test]
131 fn truncate_adds_ellipsis_when_too_long() {
132 assert_eq!(
133 truncate_for_display("this is a long string", 10),
134 "this is a ..."
135 );
136 }
137
138 #[test]
139 fn truncate_respects_utf8_code_point_boundaries() {
140 let s = "🦀🦀🦀🦀🦀";
142 assert_eq!(truncate_for_display(s, 2), "🦀🦀...");
143 assert_eq!(truncate_for_display(s, 5), "🦀🦀🦀🦀🦀");
144 }
145}