1use serde::{Deserialize, Serialize};
9
10use crate::palette::Role;
11
12#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct ShortHash(pub String);
15
16impl ShortHash {
17 pub fn from_blake3_hex(full: &str) -> Self {
18 Self(full.chars().take(7).collect())
19 }
20}
21
22impl std::fmt::Display for ShortHash {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 f.write_str(&self.0)
25 }
26}
27
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(tag = "kind", rename_all = "kebab-case")]
32pub enum UiEvent {
33 Banner {
35 title: String,
36 subtitle: Option<String>,
37 },
38 Section { title: String },
40 Log { level: LogLevel, message: String },
42 PhaseBegin { phase: String },
44 PhaseEnd { phase: String, elapsed_ms: u64 },
46 Artifact {
48 name: String,
49 hash: ShortHash,
50 state: ArtifactState,
51 },
52 Summary {
54 root_hash: ShortHash,
55 total: usize,
56 built: usize,
57 cached: usize,
58 failed: usize,
59 },
60 Row { cells: Vec<Cell> },
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "kebab-case")]
66pub enum LogLevel {
67 Info,
68 Success,
69 Warn,
70 Error,
71 Dim,
72}
73
74impl LogLevel {
75 pub fn role(self) -> Role {
76 match self {
77 Self::Info => Role::Info,
78 Self::Success => Role::Success,
79 Self::Warn => Role::Warn,
80 Self::Error => Role::Error,
81 Self::Dim => Role::Dim,
82 }
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(tag = "state", rename_all = "kebab-case")]
89pub enum ArtifactState {
90 Built { elapsed_ms: u64 },
92 Cached,
94 Pending,
96 Failed { reason: String },
98}
99
100impl ArtifactState {
101 pub fn label(&self) -> &'static str {
102 match self {
103 Self::Built { .. } => "built",
104 Self::Cached => "cached",
105 Self::Pending => "pending",
106 Self::Failed { .. } => "failed",
107 }
108 }
109}
110
111#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
112pub struct Cell {
113 pub text: String,
114 #[serde(default)]
115 pub role: Option<Role>,
116}
117
118impl Cell {
119 pub fn plain(text: impl Into<String>) -> Self {
120 Self {
121 text: text.into(),
122 role: None,
123 }
124 }
125
126 pub fn with_role(text: impl Into<String>, role: Role) -> Self {
127 Self {
128 text: text.into(),
129 role: Some(role),
130 }
131 }
132}
133
134#[derive(Clone, Debug, Default, Serialize, Deserialize)]
137pub struct EventStream {
138 pub events: Vec<UiEvent>,
139}
140
141impl EventStream {
142 pub fn new() -> Self {
143 Self::default()
144 }
145
146 pub fn push(&mut self, e: UiEvent) {
147 self.events.push(e);
148 }
149
150 pub fn run_hash(&self) -> String {
152 let bytes = serde_json::to_vec(self).unwrap_or_default();
153 hex::encode(blake3::hash(&bytes).as_bytes())
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn short_hash_is_seven_chars() {
163 let sh = ShortHash::from_blake3_hex("cxx3i50lvlprhlqclm1mxmnp77bawjbx-fake-ignored");
164 assert_eq!(sh.0.len(), 7);
165 assert_eq!(sh.to_string(), "cxx3i50");
166 }
167
168 #[test]
169 fn artifact_state_labels() {
170 assert_eq!(ArtifactState::Cached.label(), "cached");
171 assert_eq!(ArtifactState::Built { elapsed_ms: 100 }.label(), "built");
172 }
173
174 #[test]
175 fn stream_run_hash_is_deterministic() {
176 let mut s = EventStream::new();
177 s.push(UiEvent::Section {
178 title: "boot".into(),
179 });
180 s.push(UiEvent::Log {
181 level: LogLevel::Info,
182 message: "hello".into(),
183 });
184 let h1 = s.run_hash();
185 let h2 = s.run_hash();
186 assert_eq!(h1, h2);
187 assert_eq!(h1.len(), 64);
188 }
189}