1use std::env::current_dir;
2use std::fmt;
3use std::fs::{self, create_dir_all};
4use std::path::{Path, PathBuf};
5
6use dialoguer::Confirm;
7
8use anyhow::Result;
9use async_trait::async_trait;
10use globset::GlobBuilder;
11use indicatif::{ProgressBar, ProgressStyle};
12use once_cell::sync::Lazy;
13use symlink::{remove_symlink_file, symlink_file};
14
15use tokio::fs::rename;
16use walkdir::WalkDir;
17
18use crate::exec::{Action, Ctx};
19use crate::overlays::{self, Overlay};
20use crate::ui::style::DialogTheme;
21use crate::ui::{self, emojis, style};
22use crate::utils::short_path;
23
24static SPINNER_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
25 ProgressStyle::with_template("{spinner:.cyan} {wide_msg}")
26 .unwrap()
27 .tick_chars(style::TICK_CHARS_BRAILLE_4_6_DOWN.as_str())
28});
29
30pub async fn link(ctx: Ctx, overlay: &Overlay, to: &Path) -> Result<()> {
31 ui::info(format!(
32 "{} {}",
33 emojis::LINK,
34 style::white("Linking files"),
35 ))?;
36
37 let progress = ProgressBar::new_spinner()
38 .with_style(SPINNER_STYLE.clone())
39 .with_message("");
40
41 let exclude = GlobBuilder::new(&overlays::GLOB_PATTERN)
42 .literal_separator(true)
43 .build()?
44 .compile_matcher();
45 let files = WalkDir::new(&overlay.root)
46 .min_depth(1)
47 .into_iter()
48 .filter_map(Result::ok)
49 .filter(|e| !exclude.is_match(e.path()));
50
51 for file in files {
52 let rel_path = file.path().strip_prefix(&overlay.root)?;
54 let target = to.join(rel_path);
55 let path = file.path();
56 let action: Box<dyn Action> = match () {
57 _ if path.is_dir() => Box::new(EnsureDir::new(target)),
58 _ if path.is_file() => Box::new(EnsureLink::new(
59 ctx.clone(),
60 file.clone().into_path(),
61 target,
62 )),
63 _ => Box::new(EnsureLink::new(
64 ctx.clone(),
65 file.clone().into_path(),
66 target,
67 )),
68 };
69 if ctx.verbose || ctx.dry_run {
70 progress.println(format!("{}", action));
71 }
72 progress.set_message(format!("{}", action));
73 action.execute(ctx.clone()).await?;
74 }
75 progress.finish_and_clear();
77 Ok(())
78}
79
80pub async fn add_file(ctx: Ctx, overlay: &Overlay, file: &PathBuf) -> Result<()> {
81 let src = if file.is_relative() {
82 ¤t_dir()?.join(file)
83 } else {
84 file
85 };
86 if ctx.debug {
87 println!("{:#?}", src);
88 }
89 let root = overlay.resolve_target(&ctx)?;
90 if ctx.debug {
91 println!("{:#?}", root);
92 }
93 let rel_path = match src.strip_prefix(&root) {
94 Ok(tail) => tail,
95 Err(_) => {
96 return Err(anyhow::anyhow!(
97 "{} is not included in {}",
98 src.display(),
99 root.display(),
100 ))
101 }
102 };
103 let target = overlay.root.join(rel_path);
104
105 let move_action = MoveFile::new(ctx.clone(), src.clone(), target.clone());
106 let link_action = EnsureLink::new(ctx.clone(), target, src.to_path_buf());
107
108 if ctx.verbose || ctx.dry_run {
109 println!("{}", move_action);
110 }
111 move_action.execute(ctx.clone()).await?;
112
113 if ctx.verbose || ctx.dry_run {
114 println!("{}", link_action);
115 }
116 link_action.execute(ctx.clone()).await?;
117
118 Ok(())
119}
120
121pub struct EnsureLink {
122 pub ctx: Ctx,
123 pub source: PathBuf,
124 pub target: PathBuf,
125}
126
127impl EnsureLink {
128 pub fn new(ctx: Ctx, source: PathBuf, target: PathBuf) -> Self {
129 Self {
130 ctx,
131 source,
132 target,
133 }
134 }
135}
136
137impl fmt::Display for EnsureLink {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 let overlay = self.ctx.overlay.as_ref().unwrap();
145 let rel_path = self
146 .source
147 .to_str()
148 .unwrap()
149 .strip_prefix(overlay.root.to_str().unwrap())
150 .unwrap();
151 let target_root = self
152 .target
153 .to_str()
154 .unwrap()
155 .strip_suffix(rel_path)
156 .unwrap();
157 write!(
158 f,
159 "{} {} {}{} {} {}{}{}",
160 emojis::LINK,
161 style::white("link:"),
162 style::white("{"),
163 short_path(overlay.root.to_str().unwrap()),
164 style::white("->"),
165 short_path(target_root),
166 style::white("}"),
167 rel_path,
168 )
169 }
170}
171
172#[async_trait]
173impl Action for EnsureLink {
174 async fn execute(&self, ctx: Ctx) -> Result<()> {
175 if self.target.exists() {
176 if self.target.is_symlink() {
177 let src = fs::read_link(self.target.as_path())?;
178 if src != self.source {
179 if ctx.force
180 || Confirm::with_theme(&DialogTheme::default())
181 .with_prompt(format!(
182 " Do you want to overwrite {} currently linked to {}?",
183 style::yellow(short_path(self.target.to_str().unwrap())),
184 style::yellow(short_path(src.to_str().unwrap())),
185 ))
186 .interact()
187 .unwrap()
188 {
189 remove_symlink_file(self.target.as_path())?;
190 } else {
191 return Err(anyhow::anyhow!("Link {} exists", self.target.display()));
192 }
193 } else {
194 return Ok(());
195 }
196 } else if self.target.is_file() {
197 return Err(anyhow::anyhow!("File {} exists", self.target.display()));
199 } else {
200 return Err(anyhow::anyhow!("{} is a directory", self.target.display()));
201 }
202 }
203 if !ctx.dry_run {
204 symlink_file(self.source.as_path(), self.target.as_path())?;
205 }
206
207 Ok(())
208 }
209}
210
211pub struct EnsureDir {
212 pub path: PathBuf,
213 }
215
216impl EnsureDir {
217 pub fn new(path: PathBuf) -> Self {
218 Self { path }
219 }
220}
221
222impl fmt::Display for EnsureDir {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 write!(
225 f,
226 "{} {} {}",
227 emojis::DIRECTORY,
228 style::white("create directory:"),
229 self.path.display(),
230 )
231 }
232}
233
234#[async_trait]
235impl Action for EnsureDir {
236 async fn execute(&self, ctx: Ctx) -> Result<()> {
237 if !ctx.dry_run {
238 create_dir_all(self.path.as_path())?;
239 }
240 Ok(())
241 }
242}
243
244pub struct MoveFile {
245 pub ctx: Ctx,
246 pub src: PathBuf,
247 pub dst: PathBuf,
248}
249
250impl MoveFile {
251 pub fn new(ctx: Ctx, src: PathBuf, dst: PathBuf) -> Self {
252 Self { ctx, src, dst }
253 }
254}
255
256impl fmt::Display for MoveFile {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 let overlay = self.ctx.overlay.as_ref().unwrap();
259 let src_root = overlay.resolve_target(&self.ctx).unwrap();
260
261 let rel_path = self
262 .src
263 .to_str()
264 .unwrap()
265 .strip_prefix(src_root.to_str().unwrap())
266 .unwrap();
267 let target_root = self.dst.to_str().unwrap().strip_suffix(rel_path).unwrap();
268 write!(
269 f,
270 "{} {} {}{} {} {}{}{}",
271 emojis::MOVE_FILE,
272 style::white("move file:"),
273 style::white("{"),
274 short_path(src_root.to_str().unwrap()),
275 style::white("->"),
276 short_path(target_root),
277 style::white("}"),
278 rel_path,
279 )
280 }
281}
282
283#[async_trait]
284impl Action for MoveFile {
285 async fn execute(&self, ctx: Ctx) -> Result<()> {
286 if !ctx.dry_run {
287 rename(&self.src, &self.dst).await?;
288 }
289 Ok(())
290 }
291}