1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
use crate::{config::Config, package::Package, repo::Repo};
use anyhow::{anyhow, Context, Result};
use dialoguer::{Confirm, MultiSelect};
use glob::glob;
use log::{debug, error, warn};
#[cfg(unix)]
use std::os::unix::fs::symlink;
#[cfg(not(unix))]
use std::os::windows::fs::symlink_file as symlink;
use std::{fs, io::ErrorKind, process::Command};
pub fn install() -> Result<()> {
// Get the config
let config = Config::from_default_file_or_new().context("retrieving config")?;
// Get the repository from the config
let repo = Repo::new(config.clone(), true).context("opening repository")?;
// Read the packages from the repository
let packages = repo.read().context("reading packages to install")?;
println!("Checking which packages haven't been installed yet..");
let packages_to_install: Vec<&Package> = packages
.iter()
// Only keep packages where we have the package manager of
.filter(|package| package.is_available())
// Only keep packages that can actually be installed
.filter(|package| !package.is_installed().expect("Could not perform command"))
// Make it a vector again
.collect::<_>();
let package_names = packages_to_install
.iter()
// Get the names
.map(|package| package.color_full_name())
// Make it a vector again
.collect::<Vec<String>>();
let package_names = package_names
// Convert the String into a &str
.iter()
.map(|name| name.as_str())
.collect::<Vec<&str>>();
// If there's nothing to install just return
if package_names.is_empty() {
println!("Nothing to install.");
} else {
// Prompt the user for which package to install
let selections = MultiSelect::new()
.with_prompt("Select the packages you want to install (space to add)")
.items(&package_names[..])
.interact()
.context("failed constructing checkboxes")?;
// Install the selected packages
for selection in selections {
let package = packages_to_install[selection];
println!("Installing: {}.", package.color_full_name());
let install_command = package.install_command();
debug!("Installing: {}.", install_command);
match call(install_command.split_ascii_whitespace().collect()) {
Ok(_) => println!("{} installed successfully.", package.color_full_name()),
Err(err) => error!("\"{}\": {:?}", package.install_command(), err),
};
}
}
// Create the symbolic links if applicable
config
.symlinks
.iter()
// Create the full path for source and return it together with the destination
.map(|symlink| {
(
config.folder_path().join(&symlink.source),
&symlink.source,
symlink.expanded_destination(),
)
})
// Expand the globs of the destination
.flat_map(
|(source, short_source, destination)| match glob(&destination) {
Ok(destination) => destination
.filter_map(move |destination| match destination {
Ok(destination) => {
Some((source.clone(), short_source.clone(), destination))
}
Err(err) => {
error!("Error handling glob: {}", err);
None
}
})
.collect(),
Err(err) => {
error!("Error handling glob: {}", err);
vec![]
}
},
)
// Only create symbolic references when there's no file
.filter(|(source, short_source, destination)| {
match destination.symlink_metadata() {
Ok(metadata) => {
// If it already is a symbolic link follow it
if metadata.file_type().is_symlink() {
// Follow the link and check if it's already the correct one
if &fs::read_link(destination).unwrap_or_else(|_| "".into()) == source {
// It's already correct, no need to do anything
return false;
} else {
warn!(
"Symlink already exists for \"{}\" to \"{}\".",
short_source,
destination.to_str().unwrap_or("invalid path")
);
}
} else if metadata.is_dir() {
// It's a directory, print the error and do nothing
error!(
"Symlink destination \"{}\" is a directory, please remove it manually.",
destination.to_str().unwrap_or("invalid path")
);
return false;
} else {
// It's a file
warn!(
"Symlink destination \"{}\" seems to be a file.",
destination.to_str().unwrap_or("invalid path")
);
}
// Ask the user if they want to remove the old file
match Confirm::new()
.with_prompt(&format!(
"Do you want to remove \"{}\" and replace it with a symbolic reference?",
destination.to_str().unwrap_or("invalid path")
))
.interact()
{
Ok(ok) => {
if ok {
// Attempt to remove the file
if let Err(err) = fs::remove_file(destination) {
error!("Removing file failed: {}", err);
false
} else {
// The file was removed successfully, create the symlink
true
}
} else {
// The user doesn't want to remove the file
false
}
}
Err(err) => {
// Something went wrong with the dialogue
error!("Dialoguer error: {}", err);
false
}
}
}
Err(err) => {
if err.kind() == ErrorKind::NotFound {
// The file doesn't exist, create a symlink
true
} else {
// Something is wrong with the path
error!(
"Symlink destination path \"{}\" has error: {}",
destination.to_str().unwrap_or("invalid path"),
err
);
// Do nothing for this path
false
}
}
}
})
.for_each(|(full_source, short_source, destination)| {
// Return an error when the file doesn't exist in the repository
if !full_source.exists() {
error!(
"Could not create symbolic reference for \"{}\", file does not exist.",
full_source.to_str().unwrap_or("invalid_path")
);
return;
}
println!("Creating symbolic reference for \"{}\".", short_source);
// Create the symbolic reference
if let Err(err) = symlink(&full_source, &destination) {
error!(
"Could not create symbolic reference to \"{}\": {}",
destination.to_str().unwrap_or("invalid_path"),
err
);
return;
}
});
Ok(())
}
fn call(command: Vec<&str>) -> Result<()> {
let mut iter = command.iter();
let cmd_name = iter.next().unwrap();
let mut cmd = Command::new(cmd_name);
for arg in iter {
if !arg.is_empty() {
cmd.arg(arg);
}
}
let result = cmd.output()?;
// Return stderr when the command failed
if result.status.success() {
Ok(())
} else {
let stderr = String::from_utf8(result.stderr)?;
Err(anyhow!("{}", stderr))
}
}