1use anyhow::Result;
2use console::style;
3use std::collections::HashMap;
4use theater::chain::ChainEvent;
5use theater::id::TheaterId;
6
7pub struct EventDisplayOptions {
9 pub format: String,
10 pub detailed: bool,
11 pub json: bool,
12}
13
14impl Default for EventDisplayOptions {
15 fn default() -> Self {
16 Self {
17 format: "compact".to_string(),
18 detailed: false,
19 json: false,
20 }
21 }
22}
23
24pub fn display_events(
26 events: &[ChainEvent],
27 actor_id: Option<&TheaterId>,
28 options: &EventDisplayOptions,
29 start_count: usize,
30) -> Result<usize> {
31 let mut count = start_count;
32
33 let effective_format = if options.json {
35 "json"
36 } else {
37 &options.format
38 };
39
40 if effective_format == "compact" && start_count == 0 {
42 if let Some(id) = actor_id {
43 println!(
44 "{} Events for actor: {}",
45 style("ℹ").blue().bold(),
46 style(id.to_string()).cyan()
47 );
48 }
49
50 println!(
51 "{:<12} {:<12} {:<25} {}",
52 "HASH", "PARENT", "EVENT TYPE", "DESCRIPTION"
53 );
54 println!("{}", style("─".repeat(100)).dim());
55 }
56
57 if events.is_empty() && start_count == 0 {
59 println!(" No events found.");
60 return Ok(0);
61 }
62
63 for event in events {
65 count += 1;
66 display_single_event(event, effective_format)?;
67 }
68
69 Ok(count)
70}
71
72pub fn display_single_event(event: &ChainEvent, format: &str) -> Result<()> {
74 match format {
75 "json" => {
76 let output = serde_json::json!({
77 "event": event,
78 });
79
80 println!("{}", serde_json::to_string_pretty(&output)?);
81 }
82 "compact" => {
83 let hash_str = hex::encode(&event.hash);
85 let short_hash = if hash_str.len() > 8 {
86 format!("{}..{}", &hash_str[0..4], &hash_str[hash_str.len() - 4..])
87 } else {
88 hash_str
89 };
90
91 let parent_hash = match &event.parent_hash {
93 Some(hash) => {
94 let parent_str = hex::encode(hash);
95 if parent_str.len() > 8 {
96 format!(
97 "{}..{}",
98 &parent_str[0..4],
99 &parent_str[parent_str.len() - 4..]
100 )
101 } else {
102 parent_str
103 }
104 }
105 None => "-".to_string(),
106 };
107
108 let colored_type = match event.event_type.split('.').next().unwrap_or("") {
110 "http" => style(&event.event_type).cyan(),
111 "filesystem" => style(&event.event_type).green(),
112 "message" => style(&event.event_type).magenta(),
113 "runtime" => style(&event.event_type).blue(),
114 "error" => style(&event.event_type).red(),
115 _ => style(&event.event_type).yellow(),
116 };
117
118 let description = event.description.clone().unwrap_or_else(|| {
120 if let Ok(text) = std::str::from_utf8(&event.data) {
121 if text.len() > 40 {
122 format!("{}...", &text[0..37])
123 } else {
124 text.to_string()
125 }
126 } else {
127 format!("{} bytes", event.data.len())
128 }
129 });
130
131 println!(
132 "{:<12} {:<12} {:<25} {}",
133 style(&short_hash).dim(),
134 style(&parent_hash).dim(),
135 colored_type,
136 description
137 );
138 }
139 "csv" => {
140 let timestamp = event.timestamp.to_string();
142 let event_type = &event.event_type;
143 let hash = hex::encode(&event.hash);
144 let parent_hash = event
145 .parent_hash
146 .as_ref()
147 .map(|h| hex::encode(h))
148 .unwrap_or_else(|| String::from(""));
149 let description = event
150 .description
151 .as_deref()
152 .unwrap_or("")
153 .replace(',', "\\,"); let data_size = event.data.len();
155
156 println!(
157 "{},{},{},{},{},{}",
158 timestamp, event_type, hash, parent_hash, description, data_size
159 );
160 }
161 "detailed" => {
162 let timestamp = chrono::DateTime::from_timestamp(event.timestamp as i64, 0)
163 .unwrap_or_else(|| chrono::DateTime::UNIX_EPOCH)
164 .format("%Y-%m-%d %H:%M:%S%.3f")
165 .to_string();
166
167 let hash_str = hex::encode(&event.hash);
168 let parent_hash = match &event.parent_hash {
169 Some(hash) => hex::encode(hash),
170 None => "-".to_string(),
171 };
172
173 let colored_type = match event.event_type.split('.').next().unwrap_or("") {
174 "http" => style(&event.event_type).cyan(),
175 "filesystem" => style(&event.event_type).green(),
176 "message" => style(&event.event_type).magenta(),
177 "runtime" => style(&event.event_type).blue(),
178 "error" => style(&event.event_type).red(),
179 _ => style(&event.event_type).yellow(),
180 };
181
182 println!(
184 "{} [{}] {}",
185 style("EVENT").bold().blue(),
186 hash_str,
187 colored_type
188 );
189
190 println!(" Timestamp: {}", timestamp);
191 println!(" Parent Hash: {}", parent_hash);
192 if let Some(desc) = &event.description {
193 println!(" Description: {}", desc);
194 } else {
195 println!(" Description: None");
196 }
197
198 println!(" Data Size: {} bytes", event.data.len());
199 if let Ok(text) = std::str::from_utf8(&event.data) {
200 println!(" Data: {}", text);
201 } else {
202 println!("\nHex Dump:");
204 print_hex_dump(&event.data, 16);
205 }
206
207 println!("");
208 }
209 _ => {
210 let timestamp = chrono::DateTime::from_timestamp(event.timestamp as i64, 0)
213 .unwrap_or_else(|| chrono::DateTime::UNIX_EPOCH)
214 .format("%Y-%m-%d %H:%M:%S%.3f")
215 .to_string();
216
217 let colored_type = match event.event_type.split('.').next().unwrap_or("") {
219 "http" => style(&event.event_type).cyan(),
220 "filesystem" => style(&event.event_type).green(),
221 "message" => style(&event.event_type).magenta(),
222 "runtime" => style(&event.event_type).blue(),
223 "error" => style(&event.event_type).red(),
224 _ => style(&event.event_type).yellow(),
225 };
226
227 println!(
229 "{} [{}] {}",
230 style("EVENT").bold().blue(),
231 timestamp,
232 colored_type
233 );
234
235 if let Some(desc) = &event.description {
237 println!(" {}", desc);
238 }
239
240 println!("");
241 }
242 }
243
244 Ok(())
245}
246
247pub fn display_events_header(format: &str) {
248 match format {
249 "compact" => {
250 println!(
251 "{:<12} {:<12} {:<25} {}",
252 "HASH", "PARENT", "EVENT TYPE", "DESCRIPTION"
253 );
254 println!("{}", style("─".repeat(100)).dim());
255 }
256 _ => {
257 println!("{} Events:", style("ℹ").blue().bold());
258 }
259 }
260}
261
262pub fn display_events_timeline(events: &[ChainEvent], actor_id: &TheaterId) -> Result<()> {
264 println!(
265 "{} Timeline for actor: {} {}",
266 style("ℹ").blue().bold(),
267 style(actor_id.to_string()).cyan(),
268 style("")
269 );
270
271 if events.is_empty() {
272 println!(" No events found.");
273 return Ok(());
274 }
275
276 let start_time = events.iter().map(|e| e.timestamp).min().unwrap_or(0);
278 let end_time = events.iter().map(|e| e.timestamp).max().unwrap_or(0);
279 let range_ms = end_time.saturating_sub(start_time) as f64;
280
281 let term_width = match term_size::dimensions() {
283 Some((w, _)) => (w as f64 * 0.7) as usize, None => 80, };
286
287 println!(
288 "\nTime span: {} to {} ({} sec)",
289 format_timestamp(start_time),
290 format_timestamp(end_time),
291 (range_ms / 1000.0).round()
292 );
293
294 println!("{}", style("─".repeat(term_width)).dim());
295
296 let mut event_types: HashMap<&str, usize> = HashMap::new();
298 for event in events {
299 *event_types.entry(&event.event_type).or_insert(0) += 1;
300 }
301
302 println!("Event types:");
304 for (event_type, count) in event_types.iter() {
305 let type_color = match event_type.split('.').next().unwrap_or("") {
306 "http" => style(event_type).cyan(),
307 "filesystem" => style(event_type).green(),
308 "message" => style(event_type).magenta(),
309 "runtime" => style(event_type).blue(),
310 "error" => style(event_type).red(),
311 _ => style(event_type).yellow(),
312 };
313 println!(" {} - {} events", type_color, count);
314 }
315
316 println!("{}", style("─".repeat(term_width)).dim());
317 println!("Timeline:");
318
319 for event in events {
321 let position = if range_ms > 0.0 {
323 ((event.timestamp - start_time) as f64 / range_ms * (term_width as f64 - 10.0)) as usize
324 } else {
325 0
326 };
327
328 let time_str = chrono::DateTime::from_timestamp(event.timestamp as i64, 0)
330 .unwrap_or_else(|| chrono::DateTime::UNIX_EPOCH)
331 .format("%H:%M:%S")
332 .to_string();
333
334 let event_type = match event.event_type.split('.').next().unwrap_or("") {
336 "http" => style(&event.event_type).cyan(),
337 "filesystem" => style(&event.event_type).green(),
338 "message" => style(&event.event_type).magenta(),
339 "runtime" => style(&event.event_type).blue(),
340 "error" => style(&event.event_type).red(),
341 _ => style(&event.event_type).yellow(),
342 };
343
344 print!("[{}] {} ", time_str, event_type);
346
347 for i in 0..term_width - 10 {
349 if i == position {
350 print!("{}", style("●").bold());
351 } else if i % 5 == 0 {
352 print!("{}", style(".").dim());
353 } else {
354 print!(" ");
355 }
356 }
357 println!("");
358
359 if let Some(desc) = &event.description {
361 let trimmed_desc = if desc.len() > 60 {
362 format!("{}...", &desc[0..57])
363 } else {
364 desc.clone()
365 };
366 println!(" {}", trimmed_desc);
367 }
368 }
369
370 Ok(())
371}
372
373pub fn format_timestamp(timestamp: u64) -> String {
375 chrono::DateTime::from_timestamp(timestamp as i64, 0)
376 .unwrap_or_else(|| chrono::DateTime::UNIX_EPOCH)
377 .format("%Y-%m-%d %H:%M:%S")
378 .to_string()
379}
380
381pub fn display_csv_header() {
383 println!("timestamp,event_type,hash,parent_hash,description,data_size");
384}
385
386pub fn pretty_stringify_event(event: &ChainEvent, full: bool) -> String {
388 let timestamp = chrono::DateTime::from_timestamp(event.timestamp as i64, 0)
389 .unwrap_or_else(|| chrono::DateTime::UNIX_EPOCH)
390 .format("%Y-%m-%d %H:%M:%S%.3f")
391 .to_string();
392
393 let event_type = match event.event_type.split('.').next().unwrap_or("") {
394 "http" => style(&event.event_type).cyan(),
395 "filesystem" => style(&event.event_type).green(),
396 "message" => style(&event.event_type).magenta(),
397 "runtime" => style(&event.event_type).blue(),
398 "error" => style(&event.event_type).red(),
399 _ => style(&event.event_type).yellow(),
400 };
401
402 let mut output = format!(
403 "{} [{}] [{}]\n",
404 style("►").bold().blue(),
405 event_type,
406 timestamp,
407 );
408
409 let hash_str = hex::encode(&event.hash);
410 let short_hash = if hash_str.len() > 8 {
411 format!("{}..{}", &hash_str[0..4], &hash_str[hash_str.len() - 4..])
412 } else {
413 hash_str
414 };
415 output.push_str(&format!(" Hash: {}\n", short_hash));
416
417 if let Some(parent) = &event.parent_hash {
418 let parent_str = hex::encode(parent);
419 let short_parent = if parent_str.len() > 8 {
420 format!(
421 "{}..{}",
422 &parent_str[0..4],
423 &parent_str[parent_str.len() - 4..]
424 )
425 } else {
426 parent_str
427 };
428 output.push_str(&format!(" Parent: {}\n", short_parent));
429 }
430
431 if let Some(desc) = &event.description {
432 output.push_str(&format!(" Description: {}\n", desc));
433 }
434
435 if let Ok(text) = std::str::from_utf8(&event.data) {
436 if !full {
437 let max_len = std::cmp::min(text.len(), 57);
439 output.push_str(&format!(
440 " Data: {}... ({} bytes total)\n",
441 &text[0..max_len],
442 event.data.len()
443 ));
444 } else {
445 output.push_str(&format!(" Data: {}\n", text));
446 }
447 } else if !event.data.is_empty() {
448 output.push_str(&format!(
449 " Data: {} bytes of binary data\n",
450 event.data.len()
451 ));
452 }
453
454 output.push_str("\n");
455 output
456}
457
458#[allow(dead_code)]
460pub fn print_hex_dump(data: &[u8], bytes_per_line: usize) {
461 let mut offset = 0;
462 while offset < data.len() {
463 let bytes_to_print = std::cmp::min(bytes_per_line, data.len() - offset);
464 let line_data = &data[offset..offset + bytes_to_print];
465
466 print!(" {:08x} ", offset);
468
469 for (i, byte) in line_data.iter().enumerate() {
471 print!("{:02x} ", byte);
472 if i == 7 {
473 print!(" "); }
475 }
476
477 if bytes_to_print < bytes_per_line {
479 for _ in 0..(bytes_per_line - bytes_to_print) {
480 print!(" ");
481 }
482 if bytes_to_print <= 7 {
484 print!(" ");
485 }
486 }
487
488 print!(" |");
490 for byte in line_data {
491 if *byte >= 32 && *byte <= 126 {
492 print!("{}", *byte as char);
494 } else {
495 print!(".");
497 }
498 }
499 println!("|");
500
501 offset += bytes_per_line;
502 }
503}