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 print_compiler_settings: bool,
105}
106
107impl SpinnerReporter {
108 pub fn spawn(project_root: Option<PathBuf>) -> Self {
114 let (sender, rx) = mpsc::channel::<SpinnerMsg>();
115
116 std::thread::Builder::new()
117 .name("spinner".into())
118 .spawn(move || {
119 let mut spinner = Spinner::new("Compiling...");
120 let emits_progress = !spinner.no_progress;
125 loop {
126 spinner.tick();
127 match rx.try_recv() {
128 Ok(SpinnerMsg::Msg(msg)) => {
129 spinner.message(msg);
130 if emits_progress {
131 let _ = sh_eprintln!();
134 }
135 }
136 Ok(SpinnerMsg::Shutdown(ack)) => {
137 if emits_progress {
138 let _ = sh_eprintln!();
140 }
141 let _ = ack.send(());
142 break;
143 }
144 Err(TryRecvError::Disconnected) => break,
145 Err(TryRecvError::Empty) => thread::sleep(Duration::from_millis(100)),
146 }
147 }
148 })
149 .expect("failed to spawn thread");
150
151 Self { sender, project_root, print_compiler_settings: false }
152 }
153
154 pub const fn with_compiler_settings(mut self, yes: bool) -> Self {
156 self.print_compiler_settings = yes;
157 self
158 }
159
160 fn send_msg(&self, msg: impl Into<String>) {
161 let _ = self.sender.send(SpinnerMsg::Msg(msg.into()));
162 }
163}
164
165enum SpinnerMsg {
166 Msg(String),
167 Shutdown(mpsc::Sender<()>),
168}
169
170impl Drop for SpinnerReporter {
171 fn drop(&mut self) {
172 let (tx, rx) = mpsc::channel();
173 if self.sender.send(SpinnerMsg::Shutdown(tx)).is_ok() {
174 let _ = rx.recv();
175 }
176 }
177}
178
179impl Reporter for SpinnerReporter {
180 fn on_compiler_spawn(&self, compiler_name: &str, version: &Version, dirty_files: &[PathBuf]) {
181 if shell::verbosity() >= 5 {
184 self.send_msg(format!(
185 "Files to compile:\n{}",
186 dirty_files
187 .iter()
188 .map(|path| {
189 let trimmed_path = if let Some(project_root) = &self.project_root {
190 path.strip_prefix(project_root).unwrap_or(path)
191 } else {
192 path
193 };
194 format!("- {}", trimmed_path.display())
195 })
196 .sorted()
197 .format("\n")
198 ));
199 }
200
201 self.send_msg(format!(
202 "Compiling {} files with {} {}.{}.{}",
203 dirty_files.len(),
204 compiler_name,
205 version.major,
206 version.minor,
207 version.patch
208 ));
209 }
210
211 fn on_compiler_settings(
212 &self,
213 compiler_name: &str,
214 version: &Version,
215 profile: &str,
216 settings: &str,
217 ) {
218 if self.print_compiler_settings {
219 self.send_msg(format!(
220 "Compiler settings for {compiler_name} {}.{}.{} (profile: {profile}): {settings}",
221 version.major, version.minor, version.patch
222 ));
223 }
224 }
225
226 fn on_compiler_success(&self, compiler_name: &str, version: &Version, duration: &Duration) {
227 self.send_msg(format!(
228 "{} {}.{}.{} finished in {duration:.2?}",
229 compiler_name, version.major, version.minor, version.patch
230 ));
231 }
232
233 fn on_solc_installation_start(&self, version: &Version) {
234 self.send_msg(format!("Installing Solc version {version}"));
235 }
236
237 fn on_solc_installation_success(&self, version: &Version) {
238 self.send_msg(format!("Successfully installed Solc {version}"));
239 }
240
241 fn on_solc_installation_error(&self, version: &Version, error: &str) {
242 self.send_msg(format!("Failed to install Solc {version}: {error}").red().to_string());
243 }
244
245 fn on_unresolved_imports(&self, imports: &[(&Path, &Path)], remappings: &[Remapping]) {
246 self.send_msg(report::format_unresolved_imports(imports, remappings));
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 #[ignore]
256 fn can_spin() {
257 let mut s = Spinner::new("Compiling".to_string());
258 let ticks = 50;
259 for _ in 0..ticks {
260 std::thread::sleep(std::time::Duration::from_millis(100));
261 s.tick();
262 }
263 }
264
265 #[test]
266 fn can_format_properly() {
267 let r = SpinnerReporter::spawn(None);
268 let remappings: Vec<Remapping> = vec![
269 "library/=library/src/".parse().unwrap(),
270 "weird-erc20/=lib/weird-erc20/src/".parse().unwrap(),
271 "ds-test/=lib/ds-test/src/".parse().unwrap(),
272 "openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/".parse().unwrap(),
273 ];
274 let unresolved = vec![(Path::new("./src/Import.sol"), Path::new("src/File.col"))];
275 r.on_unresolved_imports(&unresolved, &remappings);
276 }
285}