1use rite_model::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, .. } => {
62 Some((Icon::Cross, format!("Ceremony failed: {}", error.message)))
63 }
64 StepFact::EntropySeeded { source, .. } => {
65 Some((Icon::Info, format!("Entropy source seeded ({source})")))
66 }
67 StepFact::EntropyContributed { epoch, .. } => Some((
68 Icon::Info,
69 format!("Entropy contribution folded (epoch {epoch})"),
70 )),
71 StepFact::EntropyDrawn { path, .. } => {
72 Some((Icon::Info, format!("Random value drawn: {path}")))
73 }
74 _ => Some((Icon::Info, "unknown fact variant".to_string())),
78 }
79}
80
81#[must_use]
88pub fn signal_summary(signal: &UiSignal) -> Option<(Icon, String)> {
89 match signal {
90 UiSignal::LogLine { icon, text, .. } => Some((*icon, text.clone())),
91 UiSignal::Progress {
92 phase, fraction, ..
93 } => {
94 let text = match fraction {
95 Some(f) => format!("{phase}: {:>5.1}%", f * 100.0),
96 None => format!("{phase}…"),
97 };
98 Some((Icon::Spinner, text))
99 }
100 UiSignal::CeremonyOverview { .. } | UiSignal::SystemInfo(_) | UiSignal::Environment(_) => {
101 None
102 }
103 }
104}
105
106#[must_use]
111pub fn truncate_for_display(s: &str, max_chars: usize) -> String {
112 match s.char_indices().nth(max_chars) {
113 Some((boundary, _)) => format!("{}...", &s[..boundary]),
114 None => s.to_string(),
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::truncate_for_display;
121
122 #[test]
123 fn truncate_passes_through_short_input() {
124 assert_eq!(truncate_for_display("short", 10), "short");
125 }
126
127 #[test]
128 fn truncate_adds_ellipsis_when_too_long() {
129 assert_eq!(
130 truncate_for_display("this is a long string", 10),
131 "this is a ..."
132 );
133 }
134
135 #[test]
136 fn truncate_respects_utf8_code_point_boundaries() {
137 let s = "🦀🦀🦀🦀🦀";
139 assert_eq!(truncate_for_display(s, 2), "🦀🦀...");
140 assert_eq!(truncate_for_display(s, 5), "🦀🦀🦀🦀🦀");
141 }
142}