1use rite_model::{ErrorClass, StepFact};
15
16use crate::protocol::{Icon, UiSignal};
17
18const UNSUMMARISED: &str = "unknown fact variant";
20
21#[must_use]
31pub fn fact_summary(fact: &StepFact) -> Option<(Icon, String)> {
32 match fact {
33 StepFact::CeremonyStarted { name, .. } => Some((Icon::Info, format!("Ceremony: {name}"))),
34 StepFact::ActStarted { label, .. } => Some((Icon::Info, format!("Act: {label}"))),
35 StepFact::StepStarted { id, label, role } => {
36 Some((Icon::Info, format!("Step {label} ({id}), role: {role}")))
37 }
38 StepFact::PromptAnswered { .. }
39 | StepFact::BackendOperation { .. }
40 | StepFact::AttestationRecorded { .. }
41 | StepFact::MachineInfoRecorded { .. }
42 | StepFact::RoleDeclared { .. }
45 | StepFact::CeremonyCompleted { .. } => None,
46 StepFact::RoleAssigned { role, person } => {
47 Some((Icon::Info, format!("Role {role}: {person}")))
48 }
49 StepFact::ParameterBound { name, value } => {
50 Some((Icon::Info, format!("Parameter {name} = {value}")))
51 }
52 StepFact::MaterialLoaded { name, identifier } => Some((
53 Icon::Checkmark,
54 match identifier {
55 Some(identifier) => format!("Material loaded: {name} ({identifier})"),
56 None => format!("Material loaded: {name}"),
57 },
58 )),
59 StepFact::MaterialDigest { name, digest } => {
60 Some((Icon::Info, format!("Material {name}: {digest}")))
61 }
62 StepFact::BackendBound { name, identity, .. } => {
63 Some((Icon::Info, format!("Backend {name}: {identity}")))
64 }
65 StepFact::ArtifactWritten { file, .. } => Some((
66 Icon::Checkmark,
67 format!("Artifact written: artifacts/{file}"),
68 )),
69 StepFact::DeviationRecorded { text, .. } => {
70 Some((Icon::Warning, format!("Deviation: {text}")))
71 }
72 StepFact::StepCompleted { outcome, .. } => match outcome {
73 rite_model::StepOutcome::Completed { message } => {
74 Some((Icon::Checkmark, message.clone()))
75 }
76 _ => Some((Icon::Checkmark, "Step completed".to_string())),
77 },
78 StepFact::StepAttemptFailed { attempt, error, .. } => Some((
79 Icon::Cross,
80 format!("Attempt {attempt} failed: {}", error.message),
81 )),
82 StepFact::CeremonyFailed { error, .. } => match error.class {
83 ErrorClass::Abort => Some((Icon::Info, error.message.clone())),
86 _ => Some((Icon::Cross, format!("Ceremony failed: {}", error.message))),
87 },
88 StepFact::EntropySeeded { source, .. } => {
89 Some((Icon::Info, format!("Entropy source seeded ({source})")))
90 }
91 StepFact::EntropyContributed { epoch, .. } => Some((
92 Icon::Info,
93 format!("Entropy contribution folded (epoch {epoch})"),
94 )),
95 StepFact::EntropyDrawn { path, .. } => {
96 Some((Icon::Info, format!("Random value drawn: {path}")))
97 }
98 StepFact::WrapRecipientRecorded {
102 source,
103 fingerprint,
104 declared,
105 ..
106 } => Some((
107 Icon::Info,
108 if *declared {
109 format!("Recipient {source} matches the declared {fingerprint}")
110 } else {
111 format!("Recipient {source} is {fingerprint}, undeclared")
112 },
113 )),
114 _ => Some((Icon::Info, UNSUMMARISED.to_string())),
119 }
120}
121
122#[must_use]
129pub fn signal_summary(signal: &UiSignal) -> Option<(Icon, String)> {
130 match signal {
131 UiSignal::LogLine { icon, text, .. } => Some((*icon, text.clone())),
132 UiSignal::Progress {
133 phase, fraction, ..
134 } => {
135 let text = match fraction {
136 Some(f) => format!("{phase}: {:>5.1}%", f * 100.0),
137 None => format!("{phase}…"),
138 };
139 Some((Icon::Spinner, text))
140 }
141 UiSignal::CeremonyOverview { .. } | UiSignal::SystemInfo(_) | UiSignal::Environment(_) => {
142 None
143 }
144 }
145}
146
147#[must_use]
152pub fn truncate_for_display(s: &str, max_chars: usize) -> String {
153 match s.char_indices().nth(max_chars) {
154 Some((boundary, _)) => format!("{}...", &s[..boundary]),
155 None => s.to_string(),
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::{UNSUMMARISED, fact_summary, truncate_for_display};
162 use rite_model::{StepFact, StepId};
163
164 #[test]
167 fn the_wrap_recipient_carries_its_fingerprint() {
168 let (_, line) = fact_summary(&StepFact::WrapRecipientRecorded {
169 step: StepId::new("wrap"),
170 source: "escrow_pubkey".to_string(),
171 fingerprint: "sha256:abcd".to_string(),
172 declared: false,
173 })
174 .expect("the live feed surfaces the recipient");
175 assert_ne!(line, UNSUMMARISED);
176 assert!(line.contains("escrow_pubkey"), "{line}");
177 assert!(line.contains("sha256:abcd"), "{line}");
178 }
179
180 #[test]
181 fn truncate_passes_through_short_input() {
182 assert_eq!(truncate_for_display("short", 10), "short");
183 }
184
185 #[test]
186 fn truncate_adds_ellipsis_when_too_long() {
187 assert_eq!(
188 truncate_for_display("this is a long string", 10),
189 "this is a ..."
190 );
191 }
192
193 #[test]
194 fn truncate_respects_utf8_code_point_boundaries() {
195 let s = "🦀🦀🦀🦀🦀";
197 assert_eq!(truncate_for_display(s, 2), "🦀🦀...");
198 assert_eq!(truncate_for_display(s, 5), "🦀🦀🦀🦀🦀");
199 }
200}