1#![allow(clippy::needless_return)]
2#![doc = include_str!("../README.md")]
3
4use std::{
5 io::Write,
6 path::{Path, PathBuf},
7 sync::Arc,
8};
9
10use async_recursion::async_recursion;
11use conf::{Config, Plan};
12use futures::future::{try_join_all, TryJoinAll};
13use tokio::{
14 fs,
15 sync::{
16 mpsc::{self, Receiver, Sender},
17 Mutex,
18 },
19 task::JoinHandle,
20};
21
22mod cmd;
23pub mod conf;
24mod error;
25pub use error::Error;
26
27pub(crate) type IOResult<T> = std::io::Result<T>;
28pub type Result<T> = anyhow::Result<T>;
29
30pub async fn clean<P>(entry: P) -> Result<bool>
31where
32 P: AsRef<Path>,
33{
34 clean_with_config(entry, Config::home().await?).await
35}
36
37pub async fn clean_with_config<P>(entry: P, config: Config) -> Result<bool>
38where
39 P: AsRef<Path>,
40{
41 assert_dir_exists(entry.as_ref())?;
42 let ncpus = num_cpus::get();
43 let (tx, rx) = mpsc::channel::<Execution>(ncpus);
44
45 let tasks = spawn((ncpus >> 1).max(1), Arc::new(Mutex::new(rx)));
46 collect(entry, Arc::new(config), tx).await?;
47
48 return tasks
49 .await?
50 .into_iter()
51 .try_fold(true, |status, result| result.map(|each| each || status));
52
53 type ExecutionRecv = Arc<Mutex<Receiver<Execution<'static>>>>;
54 fn spawn(n: usize, rx: ExecutionRecv) -> TryJoinAll<JoinHandle<Result<bool>>> {
55 try_join_all((0..n).map(move |_| {
56 let rx = rx.clone();
57 tokio::spawn(async move {
58 let mut clean = false;
59 while let Some(execution) = rx.lock().await.recv().await {
60 clean = execution.run().await? || clean;
61 }
62 Result::Ok(clean)
63 })
64 }))
65 }
66
67 fn assert_dir_exists(path: &Path) -> Result<()> {
68 if !path.exists() {
69 return Err(Error::other(format!(
70 "Directory not found: {}",
71 path.display()
72 )))?;
73 }
74 if !path.is_dir() {
75 return Err(Error::other(format!(
76 "{} is not a directory",
77 path.display()
78 )))?;
79 }
80 Ok(())
81 }
82}
83
84#[async_recursion(?Send)]
85async fn collect<P>(entry: P, config: Arc<Config>, tx: Sender<Execution<'static>>) -> IOResult<()>
86where
87 P: AsRef<Path>,
88{
89 macro_rules! try_unwrap {
90 ($exp: expr) => {
91 match $exp {
92 Ok(value) => value,
93 Err(err) => match err.kind() {
94 std::io::ErrorKind::NotFound => return Ok(()),
95 _ => return Err(err),
96 },
97 }
98 };
99 }
100 let entry = entry.as_ref();
101 let mut dir = try_unwrap!(fs::read_dir(entry).await);
102
103 while let Some(current) = try_unwrap!(dir.next_entry().await).map(|e| e.path()) {
104 if let Some(plan) = config.parse(¤t) {
105 let _ = tx.send(Execution(plan, entry.to_owned())).await;
106 }
107 if current.is_dir() {
108 collect(current, config.clone(), tx.clone()).await?;
109 }
110 }
111
112 return Ok(());
113}
114
115#[derive(Debug, Clone)]
116struct Execution<'a>(Plan<'a>, PathBuf);
117
118impl<'a> Execution<'a> {
119 async fn run(&self) -> Result<bool> {
120 let result = self.0.run(&self.1).await;
121 write(self, &result)?;
122
123 return result;
124
125 use termcolor::{
126 Buffer, BufferWriter, Color, ColorChoice, ColorSpec, HyperlinkSpec, WriteColor,
127 };
128 fn write(exe: &Execution, result: &Result<bool>) -> Result<()> {
129 use std::io::{stdout, IsTerminal};
130 let out = BufferWriter::stdout(match stdout().is_terminal() {
131 true => ColorChoice::Always,
132 _ => ColorChoice::Never,
133 });
134 return Ok(out.print(&try_concat(tag(exe, &out), colorized(result, &out))?)?);
135 }
136
137 fn try_concat(head: IOResult<Buffer>, tail: IOResult<Buffer>) -> IOResult<Buffer> {
138 let mut buf = Buffer::ansi();
139 buf.write_all(head?.as_slice())?;
140 buf.write_all(tail?.as_slice())?;
141 buf.write_all(b"\n")?;
142 return Ok(buf);
143 }
144
145 fn tag(exe: &Execution, out: &BufferWriter) -> IOResult<Buffer> {
146 let mut buf = colorized_text(exe.0.cmd().as_ref(), Color::Cyan, out)?;
147 let url = {
148 use path_absolutize::Absolutize;
149 let url = format!("file://{}", exe.1.absolutize()?.display());
150 #[cfg(target_os = "windows")]
151 let url = url.replace('\\', "/");
152 url
153 };
154 write!(buf, " clean: ")?;
155 buf.set_hyperlink(&HyperlinkSpec::open(url.as_bytes()))?;
156 write!(buf, "{}", exe.1.display())?;
157 buf.set_hyperlink(&HyperlinkSpec::close())?;
158 write!(buf, "? ")?;
159 return Ok(buf);
160 }
161
162 fn colorized(result: &Result<bool>, out: &BufferWriter) -> IOResult<Buffer> {
163 let (fg, text) = if let Ok(true) = result {
164 (Color::Green, "ok")
165 } else {
166 (Color::Red, "error")
167 };
168 return colorized_text(text, fg, out);
169 }
170
171 fn colorized_text(text: &str, fg: Color, out: &BufferWriter) -> IOResult<Buffer> {
172 let mut buf = out.buffer();
173 let mut spec = ColorSpec::new();
174 buf.set_color(spec.set_fg(Some(fg)))?;
175 write!(buf, "{}", text)?;
176 spec.clear();
177 buf.set_color(&spec)?;
178 return Ok(buf);
179 }
180 }
181}