use super::CommandInstalled;
use color_eyre::eyre::Result;
use dialoguer::console::style;
use indicatif::{ProgressBar, ProgressStyle};
use std::{
collections::VecDeque,
io::{BufRead, BufReader},
process::{Command, Stdio},
sync::mpsc,
thread,
};
pub struct Forge {
pub default_template: String,
pub default_commit: String,
}
impl Default for Forge {
fn default() -> Self {
Self::new()
}
}
#[expect(clippy::unused_self)]
impl Forge {
pub fn new() -> Self {
Self {
default_template: "foundry-rs/forge-template".to_string(),
default_commit: "main".to_string(),
}
}
pub fn version(&self) -> Result<String> {
let output = Command::new("forge").arg("--version").output()?;
Ok(String::from_utf8(output.stdout)?
.trim()
.replace("forge", ""))
}
pub fn is_installed(&self) -> bool {
Command::new("forge").is_installed()
}
pub fn install_dependencies(&self) -> Result<()> {
let status = Command::new("forge")
.args(["soldeer", "update", "-d"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
if !status.success() {
return Err(color_eyre::eyre::eyre!(
"Failed to install dependencies. Check the output above for details."
));
}
Ok(())
}
pub fn build(&self) -> Result<()> {
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈")
.template("{spinner:.green} {msg}")?,
);
spinner.set_message("Building contracts...");
let mut child = Command::new("forge")
.arg("build")
.arg("--via-ir")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout = child.stdout.take().expect("Failed to capture stdout");
let stderr = child.stderr.take().expect("Failed to capture stderr");
let stdout_reader = BufReader::new(stdout);
let stderr_reader = BufReader::new(stderr);
let (tx, rx) = mpsc::channel();
let tx_stderr = tx.clone();
let mut output_buffer = VecDeque::with_capacity(100);
thread::spawn(move || {
for line in stdout_reader.lines().map_while(Result::ok) {
let _ = tx.send(line);
}
});
thread::spawn(move || {
for line in stderr_reader.lines().map_while(Result::ok) {
let _ = tx_stderr.send(line);
}
});
while child.try_wait()?.is_none() {
spinner.tick();
for line in rx.try_iter() {
if let Some(new) = push_output_line(&mut output_buffer, line) {
spinner.suspend(|| {
println!("{}", style(&new).dim());
});
}
}
thread::sleep(std::time::Duration::from_millis(100));
}
let status = child.wait()?;
for line in rx.iter() {
push_output_line(&mut output_buffer, line);
}
if !status.success() {
spinner.finish_with_message("❌ Failed to build contracts");
let mut message = String::from("Failed to build contracts.");
if output_buffer.is_empty() {
message.push_str(" `forge build` produced no output.");
} else {
message.push_str("\n\n");
message.push_str(&output_buffer.iter().fold(String::new(), |mut acc, line| {
acc.push_str(line);
acc.push('\n');
acc
}));
}
return Err(color_eyre::eyre::eyre!(message));
}
spinner.finish_with_message("✨ Contracts built successfully!");
Ok(())
}
}
fn push_output_line(output_buffer: &mut VecDeque<String>, line: String) -> Option<String> {
if output_buffer.contains(&line) {
return None;
}
output_buffer.push_back(line.clone());
if output_buffer.len() > 100 {
output_buffer.pop_front();
}
Some(line)
}
#[cfg(test)]
mod tests {
use super::push_output_line;
fn drain(lines: &[&str]) -> Vec<String> {
let mut buffer = std::collections::VecDeque::new();
lines
.iter()
.filter_map(|line| push_output_line(&mut buffer, (*line).to_owned()))
.collect()
}
#[test]
fn records_each_line_once() {
assert_eq!(drain(&["a", "b"]), vec!["a", "b"]);
}
#[test]
fn suppresses_repeated_lines() {
assert_eq!(drain(&["a", "b", "a"]), vec!["a", "b"]);
}
#[test]
fn keeps_the_last_hundred_lines() {
let lines: Vec<String> = (0..150).map(|i| i.to_string()).collect();
let refs: Vec<&str> = lines.iter().map(String::as_str).collect();
let mut buffer = std::collections::VecDeque::new();
for line in &refs {
push_output_line(&mut buffer, (*line).to_owned());
}
assert_eq!(buffer.len(), 100);
assert_eq!(buffer.front().map(String::as_str), Some("50"));
assert_eq!(buffer.back().map(String::as_str), Some("149"));
}
}