relux_runtime/observe/
progress.rs1use std::io::Write;
2use std::time::Instant;
3
4use colored::Colorize;
5use tokio::sync::mpsc;
6
7#[derive(Debug, Clone)]
8pub enum ProgressEvent {
9 Send,
10 MatchStart,
11 MatchDone,
12 SleepStart,
13 SleepDone,
14 ShellSwitch(String),
15 FnEnter(String),
16 FnExit,
17 ShellSpawn,
18 EffectSetup(String),
19 EffectTeardown,
20 Cleanup,
21 FailPattern,
22 Timeout,
23 Failure,
24 Error(String),
25 Warning(String),
26 Annotation(String),
27}
28
29pub type ProgressTx = mpsc::UnboundedSender<ProgressEvent>;
30
31pub fn channel() -> (ProgressTx, mpsc::UnboundedReceiver<ProgressEvent>) {
32 mpsc::unbounded_channel()
33}
34
35enum TimedWait {
36 Match,
37 Sleep,
38}
39
40pub fn spawn_printer(
43 mut rx: mpsc::UnboundedReceiver<ProgressEvent>,
44) -> tokio::task::JoinHandle<String> {
45 tokio::spawn(async move {
46 let mut collected = String::new();
47 let mut timed: Option<(TimedWait, Instant)> = None;
48 let mut timed_tick_count: usize = 0;
49
50 loop {
51 let event = if timed.is_some() {
52 match tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv()).await {
53 Ok(Some(ev)) => Some(ev),
54 Ok(None) => None,
55 Err(_) => {
56 if let Some((kind, started)) = &timed {
57 let ch = match kind {
58 TimedWait::Match => '~',
59 TimedWait::Sleep => 'z',
60 };
61 let elapsed_secs = started.elapsed().as_secs() as usize;
62 while timed_tick_count < elapsed_secs {
63 emit(&mut collected, ch);
64 timed_tick_count += 1;
65 }
66 }
67 continue;
68 }
69 }
70 } else {
71 rx.recv().await
72 };
73
74 let Some(event) = event else {
75 break;
76 };
77
78 match event {
79 ProgressEvent::Send => {
80 emit(&mut collected, '.');
81 }
82 ProgressEvent::MatchStart => {
83 timed = Some((TimedWait::Match, Instant::now()));
84 timed_tick_count = 0;
85 }
86 ProgressEvent::MatchDone => {
87 timed = None;
88 emit(&mut collected, '.');
89 }
90 ProgressEvent::SleepStart => {
91 timed = Some((TimedWait::Sleep, Instant::now()));
92 timed_tick_count = 0;
93 }
94 ProgressEvent::SleepDone => {
95 timed = None;
96 }
97 ProgressEvent::ShellSwitch(_) => {
98 emit(&mut collected, '|');
99 }
100 ProgressEvent::FnEnter(_) => {
101 emit(&mut collected, '{');
102 }
103 ProgressEvent::FnExit => {
104 emit(&mut collected, '}');
105 }
106 ProgressEvent::ShellSpawn => {
107 emit(&mut collected, 's');
108 }
109 ProgressEvent::EffectSetup(_) => {
110 emit(&mut collected, '+');
111 }
112 ProgressEvent::EffectTeardown => {
113 emit(&mut collected, '-');
114 }
115 ProgressEvent::Cleanup => {
116 emit(&mut collected, 'c');
117 }
118 ProgressEvent::FailPattern => {
119 timed = None;
120 emit(&mut collected, '!');
121 }
122 ProgressEvent::Timeout => {
123 timed = None;
124 emit(&mut collected, 'T');
125 }
126 ProgressEvent::Failure => {
127 timed = None;
128 emit(&mut collected, 'F');
129 }
130 ProgressEvent::Error(_) => {
131 timed = None;
132 emit(&mut collected, 'E');
133 }
134 ProgressEvent::Warning(_) => {
135 emit(&mut collected, 'W');
136 }
137 ProgressEvent::Annotation(text) => {
138 let s = format!("({text})");
139 collected.push_str(&s);
140 eprint!("{}", s.dimmed());
141 let _ = std::io::stderr().flush();
142 }
143 }
144 }
145
146 collected
147 })
148}
149
150fn emit(collected: &mut String, ch: char) {
151 collected.push(ch);
152 let s = ch.to_string().dimmed();
153 eprint!("{s}");
154 let _ = std::io::stderr().flush();
155}