1use foundry_compilers::{
3 artifacts::remappings::Remapping,
4 report::{self, BasicStdoutReporter, 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 {
43 Self { indicate_progress: std::io::stdout().is_terminal() }
44 }
45}
46
47#[expect(missing_docs)]
48pub struct Spinner {
49 indicator: &'static [&'static str],
50 no_progress: bool,
51 message: String,
52 idx: usize,
53}
54
55#[expect(missing_docs)]
56impl Spinner {
57 pub fn new(msg: impl Into<String>) -> Self {
58 Self::with_indicator(SPINNERS[0], msg)
59 }
60
61 pub fn with_indicator(indicator: &'static [&'static str], msg: impl Into<String>) -> Self {
62 Self {
63 indicator,
64 no_progress: !TERM_SETTINGS.indicate_progress,
65 message: msg.into(),
66 idx: 0,
67 }
68 }
69
70 pub fn tick(&mut self) {
71 if self.no_progress {
72 return;
73 }
74
75 let indicator = self.indicator[self.idx % self.indicator.len()].green();
76 let indicator = Paint::new(format!("[{indicator}]")).bold();
77 let _ = sh_print!("\r\x1B[2K\r{indicator} {}", self.message);
78 io::stdout().flush().unwrap();
79
80 self.idx = self.idx.wrapping_add(1);
81 }
82
83 pub fn message(&mut self, msg: impl Into<String>) {
84 self.message = msg.into();
85 }
86}
87
88#[derive(Debug)]
92#[must_use = "Terminates the spinner on drop"]
93pub struct SpinnerReporter {
94 sender: mpsc::Sender<SpinnerMsg>,
96 project_root: Option<PathBuf>,
98}
99
100impl SpinnerReporter {
101 pub fn spawn(project_root: Option<PathBuf>) -> Self {
107 let (sender, rx) = mpsc::channel::<SpinnerMsg>();
108
109 std::thread::Builder::new()
110 .name("spinner".into())
111 .spawn(move || {
112 let mut spinner = Spinner::new("Compiling...");
113 loop {
114 spinner.tick();
115 match rx.try_recv() {
116 Ok(SpinnerMsg::Msg(msg)) => {
117 spinner.message(msg);
118 let _ = sh_println!();
120 }
121 Ok(SpinnerMsg::Shutdown(ack)) => {
122 let _ = sh_println!();
124 let _ = ack.send(());
125 break;
126 }
127 Err(TryRecvError::Disconnected) => break,
128 Err(TryRecvError::Empty) => thread::sleep(Duration::from_millis(100)),
129 }
130 }
131 })
132 .expect("failed to spawn thread");
133
134 Self { sender, project_root }
135 }
136
137 fn send_msg(&self, msg: impl Into<String>) {
138 let _ = self.sender.send(SpinnerMsg::Msg(msg.into()));
139 }
140}
141
142enum SpinnerMsg {
143 Msg(String),
144 Shutdown(mpsc::Sender<()>),
145}
146
147impl Drop for SpinnerReporter {
148 fn drop(&mut self) {
149 let (tx, rx) = mpsc::channel();
150 if self.sender.send(SpinnerMsg::Shutdown(tx)).is_ok() {
151 let _ = rx.recv();
152 }
153 }
154}
155
156impl Reporter for SpinnerReporter {
157 fn on_compiler_spawn(&self, compiler_name: &str, version: &Version, dirty_files: &[PathBuf]) {
158 if shell::verbosity() >= 5 {
161 self.send_msg(format!(
162 "Files to compile:\n{}",
163 dirty_files
164 .iter()
165 .map(|path| {
166 let trimmed_path = if let Some(project_root) = &self.project_root {
167 path.strip_prefix(project_root).unwrap_or(path)
168 } else {
169 path
170 };
171 format!("- {}", trimmed_path.display())
172 })
173 .sorted()
174 .format("\n")
175 ));
176 }
177
178 self.send_msg(format!(
179 "Compiling {} files with {} {}.{}.{}",
180 dirty_files.len(),
181 compiler_name,
182 version.major,
183 version.minor,
184 version.patch
185 ));
186 }
187
188 fn on_compiler_success(&self, compiler_name: &str, version: &Version, duration: &Duration) {
189 self.send_msg(format!(
190 "{} {}.{}.{} finished in {duration:.2?}",
191 compiler_name, version.major, version.minor, version.patch
192 ));
193 }
194
195 fn on_solc_installation_start(&self, version: &Version) {
196 self.send_msg(format!("Installing Solc version {version}"));
197 }
198
199 fn on_solc_installation_success(&self, version: &Version) {
200 self.send_msg(format!("Successfully installed Solc {version}"));
201 }
202
203 fn on_solc_installation_error(&self, version: &Version, error: &str) {
204 self.send_msg(format!("Failed to install Solc {version}: {error}").red().to_string());
205 }
206
207 fn on_unresolved_imports(&self, imports: &[(&Path, &Path)], remappings: &[Remapping]) {
208 self.send_msg(report::format_unresolved_imports(imports, remappings));
209 }
210}
211
212pub fn with_spinner_reporter<T>(project_root: Option<PathBuf>, f: impl FnOnce() -> T) -> T {
217 let reporter = if TERM_SETTINGS.indicate_progress {
218 report::Report::new(SpinnerReporter::spawn(project_root))
219 } else {
220 report::Report::new(BasicStdoutReporter::default())
221 };
222 report::with_scoped(&reporter, f)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 #[ignore]
231 fn can_spin() {
232 let mut s = Spinner::new("Compiling".to_string());
233 let ticks = 50;
234 for _ in 0..ticks {
235 std::thread::sleep(std::time::Duration::from_millis(100));
236 s.tick();
237 }
238 }
239
240 #[test]
241 fn can_format_properly() {
242 let r = SpinnerReporter::spawn(None);
243 let remappings: Vec<Remapping> = vec![
244 "library/=library/src/".parse().unwrap(),
245 "weird-erc20/=lib/weird-erc20/src/".parse().unwrap(),
246 "ds-test/=lib/ds-test/src/".parse().unwrap(),
247 "openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/".parse().unwrap(),
248 ];
249 let unresolved = vec![(Path::new("./src/Import.sol"), Path::new("src/File.col"))];
250 r.on_unresolved_imports(&unresolved, &remappings);
251 }
260}