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