dot_over/actions/
fs.rs

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        // progress.tick();
53        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_with_message("DOne");
76    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        &current_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        // write!(f, "{} -> {}", self.source.display(), self.target.display())
140        // We operate on string as path normalization is broken in rust
141        // See:
142        //  - https://users.rust-lang.org/t/trailing-in-paths/43166/9
143        //  - https://github.com/rust-lang/rfcs/issues/2208
144        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                // TODO: handle file absorption
198                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    // pub target: PathBuf,
214}
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}