1use foundry_compilers::{
3 artifacts::remappings::Remapping,
4 report::{self, Reporter},
5};
6use itertools::Itertools;
7use semver::Version;
8use std::{
9 io,
10 io::{IsTerminal, prelude::*},
11 path::{Path, PathBuf},
12 sync::{
13 LazyLock,
14 mpsc::{self, TryRecvError},
15 },
16 thread,
17 time::Duration,
18};
19use yansi::Paint;
20
21use crate::shell;
22
23pub static SPINNERS: &[&[&str]] = &[
26 &["⠃", "⠊", "⠒", "⠢", "⠆", "⠰", "⠔", "⠒", "⠑", "⠘"],
27 &[" ", "⠁", "⠉", "⠙", "⠚", "⠖", "⠦", "⠤", "⠠"],
28 &["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"],
29 &["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"],
30 &[" ", "▘", "▀", "▜", "█", "▟", "▄", "▖"],
31];
32
33static TERM_SETTINGS: LazyLock<TermSettings> = LazyLock::new(TermSettings::from_env);
34
35pub struct TermSettings {
37 indicate_progress: bool,
38}
39
40impl TermSettings {
41 pub fn from_env() -> Self {
46 Self { indicate_progress: std::io::stderr().is_terminal() }
47 }
48}
49
50#[expect(missing_docs)]
51pub struct Spinner {
52 indicator: &'static [&'static str],
53 no_progress: bool,
54 message: String,
55 idx: usize,
56}
57
58#[expect(missing_docs)]
59impl Spinner {
60 pub fn new(msg: impl Into<String>) -> Self {
61 Self::with_indicator(SPINNERS[0], msg)
62 }
63
64 pub fn with_indicator(indicator: &'static [&'static str], msg: impl Into<String>) -> Self {
65 Self {
66 indicator,
67 no_progress: !TERM_SETTINGS.indicate_progress,
68 message: msg.into(),
69 idx: 0,
70 }
71 }
72
73 pub fn tick(&mut self) {
74 if self.no_progress {
75 return;
76 }
77
78 let indicator = self.indicator[self.idx % self.indicator.len()].green();
79 let indicator = Paint::new(format!("[{indicator}]")).bold();
80 let _ = sh_eprint!("\r\x1B[2K\r{indicator} {}", self.message);
83 io::stderr().flush().unwrap();
84
85 self.idx = self.idx.wrapping_add(1);
86 }
87
88 pub fn message(&mut self, msg: impl Into<String>) {
89 self.message = msg.into();
90 }
91}
92
93#[derive(Debug)]
97#[must_use = "Terminates the spinner on drop"]
98pub struct SpinnerReporter {
99 sender: mpsc::Sender<SpinnerMsg>,
101 project_root: Option<PathBuf>,
103}
104
105impl SpinnerReporter {
106 pub fn spawn(project_root: Option<PathBuf>) -> Self {
112 let (sender, rx) = mpsc::channel::<SpinnerMsg>();
113
114 std::thread::Builder::new()
115 .name("spinner".into())
116 .spawn(move || {
117 let mut spinner = Spinner::new("Compiling...");
118 let emits_progress = !spinner.no_progress;
123 loop {
124 spinner.tick();
125 match rx.try_recv() {
126 Ok(SpinnerMsg::Msg(msg)) => {
127 spinner.message(msg);
128 if emits_progress {
129 let _ = sh_eprintln!();
132 }
133 }
134 Ok(SpinnerMsg::Shutdown(ack)) => {
135 if emits_progress {
136 let _ = sh_eprintln!();
138 }
139 let _ = ack.send(());
140 break;
141 }
142 Err(TryRecvError::Disconnected) => break,
143 Err(TryRecvError::Empty) => thread::sleep(Duration::from_millis(100)),
144 }
145 }
146 })
147 .expect("failed to spawn thread");
148
149 Self { sender, project_root }
150 }
151
152 fn send_msg(&self, msg: impl Into<String>) {
153 let _ = self.sender.send(SpinnerMsg::Msg(msg.into()));
154 }
155}
156
157enum SpinnerMsg {
158 Msg(String),
159 Shutdown(mpsc::Sender<()>),
160}
161
162impl Drop for SpinnerReporter {
163 fn drop(&mut self) {
164 let (tx, rx) = mpsc::channel();
165 if self.sender.send(SpinnerMsg::Shutdown(tx)).is_ok() {
166 let _ = rx.recv();
167 }
168 }
169}
170
171impl Reporter for SpinnerReporter {
172 fn on_compiler_spawn(&self, compiler_name: &str, version: &Version, dirty_files: &[PathBuf]) {
173 if shell::verbosity() >= 5 {
176 self.send_msg(format!(
177 "Files to compile:\n{}",
178 dirty_files
179 .iter()
180 .map(|path| {
181 let trimmed_path = if let Some(project_root) = &self.project_root {
182 path.strip_prefix(project_root).unwrap_or(path)
183 } else {
184 path
185 };
186 format!("- {}", trimmed_path.display())
187 })
188 .sorted()
189 .format("\n")
190 ));
191 }
192
193 self.send_msg(format!(
194 "Compiling {} files with {} {}.{}.{}",
195 dirty_files.len(),
196 compiler_name,
197 version.major,
198 version.minor,
199 version.patch
200 ));
201 }
202
203 fn on_compiler_success(&self, compiler_name: &str, version: &Version, duration: &Duration) {
204 self.send_msg(format!(
205 "{} {}.{}.{} finished in {duration:.2?}",
206 compiler_name, version.major, version.minor, version.patch
207 ));
208 }
209
210 fn on_solc_installation_start(&self, version: &Version) {
211 self.send_msg(format!("Installing Solc version {version}"));
212 }
213
214 fn on_solc_installation_success(&self, version: &Version) {
215 self.send_msg(format!("Successfully installed Solc {version}"));
216 }
217
218 fn on_solc_installation_error(&self, version: &Version, error: &str) {
219 self.send_msg(format!("Failed to install Solc {version}: {error}").red().to_string());
220 }
221
222 fn on_unresolved_imports(&self, imports: &[(&Path, &Path)], remappings: &[Remapping]) {
223 self.send_msg(report::format_unresolved_imports(imports, remappings));
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 #[ignore]
233 fn can_spin() {
234 let mut s = Spinner::new("Compiling".to_string());
235 let ticks = 50;
236 for _ in 0..ticks {
237 std::thread::sleep(std::time::Duration::from_millis(100));
238 s.tick();
239 }
240 }
241
242 #[test]
243 fn can_format_properly() {
244 let r = SpinnerReporter::spawn(None);
245 let remappings: Vec<Remapping> = vec![
246 "library/=library/src/".parse().unwrap(),
247 "weird-erc20/=lib/weird-erc20/src/".parse().unwrap(),
248 "ds-test/=lib/ds-test/src/".parse().unwrap(),
249 "openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/".parse().unwrap(),
250 ];
251 let unresolved = vec![(Path::new("./src/Import.sol"), Path::new("src/File.col"))];
252 r.on_unresolved_imports(&unresolved, &remappings);
253 }
262}