theater_cli/utils/
event_display.rs

1use anyhow::Result;
2use console::style;
3use std::collections::HashMap;
4use theater::chain::ChainEvent;
5use theater::id::TheaterId;
6
7/// Structure for configuring event display options
8pub 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
24/// Display a batch of events with formatting options
25pub 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    // Use the effective format (JSON overrides format option)
34    let effective_format = if options.json {
35        "json"
36    } else {
37        &options.format
38    };
39
40    // Print the header for compact format if this is the first batch
41    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 no events, show a message and return
58    if events.is_empty() && start_count == 0 {
59        println!("  No events found.");
60        return Ok(0);
61    }
62
63    // Display all events according to format
64    for event in events {
65        count += 1;
66        display_single_event(event, effective_format)?;
67    }
68
69    Ok(count)
70}
71
72/// Display a single event with formatting options
73pub 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            // Format event hash
84            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            // Format parent hash
92            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            // Get event type with color based on category
109            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            // Get a concise description
119            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            // Format timestamp
141            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(',', "\\,"); // Escape commas in the description
154            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            // Print detailed event info
183            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                // Print hex dump if binary data
203                println!("\nHex Dump:");
204                print_hex_dump(&event.data, 16);
205            }
206
207            println!("");
208        }
209        _ => {
210            // pretty format (default)
211            // Format timestamp
212            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            // Format event type with color based on category
218            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            // Print basic event info
228            println!(
229                "{} [{}] {}",
230                style("EVENT").bold().blue(),
231                timestamp,
232                colored_type
233            );
234
235            // Show event description if available
236            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
262/// Display a timeline view of events
263pub 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    // Get the time range
277    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    // Get terminal width for timeline display
282    let term_width = match term_size::dimensions() {
283        Some((w, _)) => (w as f64 * 0.7) as usize, // Use 70% of terminal width for timeline
284        None => 80, // Default to 80 if terminal size can't be determined
285    };
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    // Group events by type for the summary
297    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    // Display summary of event types
303    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    // Display timeline for each event
320    for event in events {
321        // Calculate position on the timeline
322        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        // Format timestamp
329        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        // Get event type with color
335        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 timeline with marker
345        print!("[{}] {} ", time_str, event_type);
346
347        // Display the timeline
348        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        // Print event description if available
360        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
373// Helper function to format timestamps in a human-readable way
374pub 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
381/// Create a CSV header row
382pub fn display_csv_header() {
383    println!("timestamp,event_type,hash,parent_hash,description,data_size");
384}
385
386/// Helper function to pretty-stringify an event for the pretty format
387pub 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            // Do either the 57 chars or the max length of the string
438            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/// Helper function to print a hex dump of binary data
459#[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 the offset
467        print!("    {:08x}  ", offset);
468
469        // Print hex values
470        for (i, byte) in line_data.iter().enumerate() {
471            print!("{:02x} ", byte);
472            if i == 7 {
473                print!(" "); // Extra space at the middle
474            }
475        }
476
477        // Pad with spaces if we don't have enough bytes
478        if bytes_to_print < bytes_per_line {
479            for _ in 0..(bytes_per_line - bytes_to_print) {
480                print!("   ");
481            }
482            // Extra space if we're missing the middle marker
483            if bytes_to_print <= 7 {
484                print!(" ");
485            }
486        }
487
488        // Print ASCII representation
489        print!(" |");
490        for byte in line_data {
491            if *byte >= 32 && *byte <= 126 {
492                // Printable ASCII
493                print!("{}", *byte as char);
494            } else {
495                // Non-printable
496                print!(".");
497            }
498        }
499        println!("|");
500
501        offset += bytes_per_line;
502    }
503}