lib/common/
mod.rs

1pub mod util;
2
3use contracts::*;
4use derive_getters::Getters;
5use failure::Fail;
6use itertools::Itertools;
7use std::{
8    convert::{AsRef, From},
9    fmt::{self, Display},
10    iter::IntoIterator,
11    ops::Deref,
12    path::{Path, PathBuf},
13    str::FromStr,
14};
15use strum::IntoEnumIterator;
16use strum_macros::EnumIter;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
19pub enum Platform {
20    Windows,
21    Macos,
22    Linux,
23    Wsl,
24}
25use Platform::*;
26
27impl Platform {
28    /// Returns the valid strings corresponding to `self`
29    pub fn strs(&self) -> &[&'static str] {
30        match self {
31            Windows => &["win", "windows"],
32            Macos => &["mac", "macos"],
33            Linux => &["linux"],
34            Wsl => &["wsl"],
35        }
36    }
37}
38
39#[derive(Debug, Fail)]
40#[fail(display = "unsupported platform \"{}\"", input)]
41pub struct PlatformParseError {
42    input: String,
43}
44
45impl FromStr for Platform {
46    type Err = PlatformParseError;
47
48    fn from_str(s: &str) -> Result<Platform, Self::Err> {
49        let s = s.trim().to_lowercase();
50
51        for platform in Platform::iter() {
52            if platform.strs().contains(&s.as_str()) {
53                return Ok(platform);
54            }
55        }
56
57        Err(PlatformParseError { input: s })
58    }
59}
60
61/// Represents an (owned) path which must be absolute
62#[derive(Clone, Debug, Eq, Hash, PartialEq)]
63pub struct AbsolutePath {
64    path: PathBuf,
65}
66
67impl Display for AbsolutePath {
68    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
69        f.pad(&util::home_to_tilde(&self.path).display().to_string())
70    }
71}
72
73// I'd like to have a blanket `impl From<P> where P: AsRef<Path> for
74// AbsolutePath`, but that won't work until you can add a `P != AbsolutePath`
75// constraint. Otherwise, you run up against the blanket `impl From<T> for T`.
76// See https://github.com/rust-lang/rfcs/issues/1834.
77//
78// For now, I'll just have to deal with writing relevant impls by hand.
79
80impl From<PathBuf> for AbsolutePath {
81    #[pre(path.is_absolute())]
82    fn from(path: PathBuf) -> Self {
83        AbsolutePath { path }
84    }
85}
86
87impl From<&Path> for AbsolutePath {
88    #[pre(path.is_absolute())]
89    fn from(path: &Path) -> Self {
90        AbsolutePath {
91            path: path.to_path_buf(),
92        }
93    }
94}
95
96impl From<&str> for AbsolutePath {
97    fn from(path: &str) -> Self {
98        AbsolutePath::from(Path::new(path))
99    }
100}
101
102impl Deref for AbsolutePath {
103    type Target = PathBuf;
104
105    fn deref(&self) -> &Self::Target {
106        &self.path
107    }
108}
109
110impl AsRef<Path> for AbsolutePath {
111    fn as_ref(&self) -> &Path {
112        self.path.as_ref()
113    }
114}
115
116/// Represents the location of a dotfile (the source) and the
117/// location of the symlink pointing to the source (the destination) as a pair
118/// of absolute paths to the two files.
119#[derive(Debug, Getters)]
120pub struct Item {
121    source: AbsolutePath,
122    dest: AbsolutePath,
123}
124
125impl Item {
126    pub fn new(source: impl Into<AbsolutePath>, dest: impl Into<AbsolutePath>) -> Self {
127        Item {
128            source: source.into(),
129            dest: dest.into(),
130        }
131    }
132}
133
134/// Just a wrapper for pretty-printing `Item`s
135///
136/// This type is not meant to be constructed directly. Instead,
137/// use `FormattedItems::from_items`.
138#[derive(Debug)]
139pub struct FormattedItem {
140    item: Item,
141    width: usize,
142}
143
144impl Deref for FormattedItem {
145    type Target = Item;
146
147    fn deref(&self) -> &Self::Target {
148        &self.item
149    }
150}
151
152impl Display for FormattedItem {
153    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
154        f.pad(&format!(
155            "{:width$}  ->    {}",
156            self.source(),
157            self.dest(),
158            width = self.width
159        ))
160    }
161}
162
163// Just a convenient wrapper for multiple `FormattedItem`s
164#[derive(Debug)]
165pub struct FormattedItems {
166    formatted_items: Vec<FormattedItem>,
167}
168
169impl FormattedItems {
170    /// Formats items in a group to ensure uniform formatting.
171    ///
172    /// For example:
173    /// ```
174    /// # use crate::lib::common::{Item, FormattedItems};
175    /// let items_short = vec![
176    ///     Item::new("/home/tkadur/.dotfiles/file1", "/home/tkadur/.file1"),
177    ///     Item::new("/home/tkadur/.dotfiles/file2", "/home/tkadur/.file2"),
178    /// ];
179    ///
180    /// # let str_short_expected = [
181    /// #     "/home/tkadur/.dotfiles/file1  ->    /home/tkadur/.file1",
182    /// #     "/home/tkadur/.dotfiles/file2  ->    /home/tkadur/.file2"
183    /// # ].join("\n");
184    ///
185    /// // Produces the following:
186    /// //
187    /// // /home/tkadur/.dotfiles/file1  ->    /home/tkadur/.file1
188    /// // /home/tkadur/.dotfiles/file2  ->    /home/tkadur/.file2
189    /// let str_short = FormattedItems::from_items(items_short).to_string();
190    ///
191    /// # assert_eq!(
192    /// # str_short_expected,
193    /// #     str_short,
194    /// # );
195    ///
196    /// let items_long = vec![
197    ///     Item::new("/home/tkadur/.dotfiles/file1", "/home/tkadur/.file1"),
198    ///     Item::new("/home/tkadur/.dotfiles/file2", "/home/tkadur/.file2"),
199    ///     Item::new(
200    ///         "/home/tkadur/.dotfiles/file_long",
201    ///         "/home/tkadur/.file_long",
202    ///     ),
203    ///     Item::new(
204    ///         "/home/tkadur/.dotfiles/file_even_longer",
205    ///         "/home/tkadur/.file_even_longer",
206    ///     ),
207    /// ];
208    ///
209    /// // Produces the following:
210    /// //
211    /// // /home/tkadur/.dotfiles/file1             ->    /home/tkadur/.file1
212    /// // /home/tkadur/.dotfiles/file2             ->    /home/tkadur/.file2
213    /// // /home/tkadur/.dotfiles/file_long         ->    /home/tkadur/.file_long
214    /// // /home/tkadur/.dotfiles/file_even_longer  ->    /home/tkadur/.file_even_longer
215    /// let str_long = FormattedItems::from_items(items_long).to_string();
216    ///
217    /// # let str_long_expected = [
218    /// #     "/home/tkadur/.dotfiles/file1             ->    /home/tkadur/.file1",
219    /// #     "/home/tkadur/.dotfiles/file2             ->    /home/tkadur/.file2",
220    /// #     "/home/tkadur/.dotfiles/file_long         ->    /home/tkadur/.file_long",
221    /// #     "/home/tkadur/.dotfiles/file_even_longer  ->    /home/tkadur/.file_even_longer",
222    /// # ]
223    /// # .join("\n");
224    ///
225    /// # assert_eq!(
226    /// #     str_long_expected,
227    /// #     str_long,
228    /// # );
229    /// ```
230    pub fn from_items(items: Vec<Item>) -> Self {
231        let width = items
232            .iter()
233            .map(|item| item.source().to_string().len())
234            .max()
235            .unwrap_or(0);
236
237        let formatted_items = items
238            .into_iter()
239            .map(|item| FormattedItem { item, width })
240            .collect();
241
242        FormattedItems { formatted_items }
243    }
244}
245
246impl IntoIterator for FormattedItems {
247    type IntoIter = <Vec<FormattedItem> as IntoIterator>::IntoIter;
248    type Item = <Vec<FormattedItem> as IntoIterator>::Item;
249
250    fn into_iter(self) -> Self::IntoIter {
251        self.formatted_items.into_iter()
252    }
253}
254
255impl<'a> IntoIterator for &'a FormattedItems {
256    type IntoIter = <&'a Vec<FormattedItem> as IntoIterator>::IntoIter;
257    type Item = <&'a Vec<FormattedItem> as IntoIterator>::Item;
258
259    fn into_iter(self) -> Self::IntoIter {
260        self.formatted_items.iter()
261    }
262}
263
264impl Display for FormattedItems {
265    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
266        f.pad(&self.formatted_items.iter().join("\n"))
267    }
268}