1use std::{
2 borrow::Cow,
3 collections::BTreeMap,
4 fs::File,
5 path::{Path, PathBuf},
6 process::exit,
7};
8
9use serde_yaml_ng::{from_reader, from_value, Value};
10use url::Url;
11
12use crate::common::{tilde, CONFIG_PATH, PLUGIN_LIBSO_PATH, REPOSITORIES_PATH};
13use crate::io::execute_and_capture_output_with_path;
14
15pub fn install_plugin(url: &str) {
37 println!("Installing {url} from its github repository");
38 let (hostname, author, plugin) = parse_hostname_author_plugin(url);
40 let repositories_path = tilde(REPOSITORIES_PATH);
42 let repositories_path = Path::new(repositories_path.as_ref());
43 create_clone_directory(repositories_path);
45 clone_repository(hostname, repositories_path, &author, &plugin);
47 let plugin_path = cargo_build_release(repositories_path, &plugin);
49 let Some(libso_path) = find_compiled_target(plugin_path, &plugin) else {
51 remove_repo_directory().expect("Couldn't delete plugin repository");
52 exit(6);
53 };
54 if _add_plugin(&libso_path) {
55 remove_repo_directory().expect("Couldn't delete plugin repository");
57 println!("Installation done");
58 }
59}
60
61fn parse_hostname_author_plugin(url: &str) -> (String, String, String) {
67 let nb_slashes = url.chars().filter(|c| *c == '/').count();
68
69 let mut author_plugin = url.to_string();
70 let hostname: String;
71 if nb_slashes == 1 {
72 println!("No host provided, using github.com");
74 hostname = "github.com".to_string();
75 } else if nb_slashes == 4 {
76 hostname = match Url::parse(&author_plugin) {
78 Ok(url) => {
79 if url.cannot_be_a_base() {
80 eprintln!("{author_plugin} isn't a valid url.");
81 exit(2);
82 } else {
83 let Some(hostname) = url.host_str() else {
84 eprintln!("{author_plugin} has no hostname");
85 exit(2);
86 };
87 author_plugin = url.path().to_string().chars().skip(1).collect();
88 hostname.to_string()
89 }
90 }
91 Err(_) => {
92 eprintln!("Cannot parse {author_plugin} as an url.");
93 exit(2);
94 }
95 };
96 } else {
97 eprintln!("Can't parse {url}. It should have 1 or 4 '/' like \"qkzk/bat_previewer\" or \"https://github.com/qkzk/bat_previewer\"");
98 exit(2);
99 }
100
101 let mut split = author_plugin.split('/');
102 let Some(author) = split.next() else {
103 eprintln!(
104 "Error installing plugin {author_plugin} isn't valid. Please use author/plugin format."
105 );
106 exit(2);
107 };
108 let Some(plugin) = split.next() else {
109 eprintln!(
110 "Error installing plugin {author_plugin} isn't valid. Please use author/plugin format."
111 );
112 exit(2);
113 };
114 (hostname, author.to_string(), plugin.to_string())
115}
116
117fn create_clone_directory(repositories_path: &Path) {
122 match std::fs::create_dir_all(repositories_path) {
123 Ok(()) => println!(
124 "- Created {repositories_path}",
125 repositories_path = repositories_path.display()
126 ),
127 Err(error) => {
128 eprintln!("Error creating directories for repostories: {error:?}");
129 exit(3);
130 }
131 }
132}
133
134fn clone_repository(hostname: String, plugin_repositories: &Path, author: &str, plugin: &str) {
140 let args = [
141 "clone",
142 "--depth",
143 "1",
144 &format!("git@{hostname}:{author}/{plugin}.git"),
145 ];
146 let output = execute_and_capture_output_with_path("git", plugin_repositories, &args);
147 match output {
148 Ok(stdout) => println!("- Cloned {author}/{plugin} git repository - {stdout}"),
149 Err(stderr) => {
150 eprintln!("Error cloning the repository :");
151 eprintln!("{}", stderr);
152 let _ = remove_repo_directory();
153 exit(4);
154 }
155 }
156}
157
158fn cargo_build_release(plugin_path: &Path, plugin: &str) -> PathBuf {
165 let args = ["build", "--release"];
167 let mut plugin_path = plugin_path.to_path_buf();
168 plugin_path.push(plugin);
169 let output = execute_and_capture_output_with_path("cargo", &plugin_path, &args);
170 match output {
171 Ok(stdout) => {
172 println!("- Compiled plugin {plugin} libso file");
173 if !stdout.is_empty() {
174 println!("- {stdout}")
175 }
176 }
177 Err(stderr) => {
178 eprintln!("Error compiling the plugin :");
179 eprintln!("{}", stderr);
180 remove_repo_directory().expect("Couldn't delete plugin repository");
181 exit(5);
182 }
183 }
184 plugin_path
185}
186
187fn find_compiled_target(mut plugin_path: PathBuf, plugin: &str) -> Option<PathBuf> {
189 let ext = format!("target/release/lib{plugin}.so");
190 plugin_path.push(ext);
191 if plugin_path.exists() {
192 Some(plugin_path)
193 } else {
194 None
195 }
196}
197
198pub fn add_plugin<P>(path: P)
210where
211 P: AsRef<Path>,
212{
213 println!("Installing {path}...", path = path.as_ref().display());
214 if _add_plugin(&path) {
215 println!(
216 "Plugin {path} added to configuration file.",
217 path = path.as_ref().display()
218 );
219 } else {
220 eprintln!(
221 "Something went wrong installing {path}.",
222 path = path.as_ref().display()
223 );
224 exit(1);
225 }
226}
227
228pub fn _add_plugin<P>(path: P) -> bool
233where
234 P: AsRef<Path>,
235{
236 let source = path.as_ref();
237 if !source.exists() {
238 eprintln!(
239 "Error installing plugin {path}. File doesn't exist.",
240 path = path.as_ref().display()
241 );
242 exit(1);
243 }
244 let dest = build_libso_destination_path(source);
245 copy_source_to_dest(source, &dest);
246 println!("- Copied libso file to {dest}", dest = dest.display());
247 let plugin_name = get_plugin_name(source);
248 add_to_config(&plugin_name, &dest);
249 println!(
250 "- Added {plugin_name}: {dest} to config file.",
251 dest = dest.display()
252 );
253 true
254}
255
256fn build_libso_destination_path(source: &Path) -> PathBuf {
262 let mut dest = PathBuf::from(tilde(PLUGIN_LIBSO_PATH).as_ref());
263 if let Err(error) = std::fs::create_dir_all(&dest) {
264 eprintln!("Couldn't create {PLUGIN_LIBSO_PATH}");
265 eprintln!("Error: {error:?}");
266 exit(1);
267 };
268
269 let Some(filename) = source.file_name() else {
270 eprintln!("Error: couldn't extract filename");
271 exit(1);
272 };
273 dest.push(filename);
274 dest
275}
276
277fn copy_source_to_dest(source: &Path, dest: &Path) {
282 if let Err(error) = std::fs::copy(source, dest) {
283 eprintln!("Error copying the libsofile: {error}");
284 exit(1);
285 }
286}
287
288fn get_plugin_name(source: &Path) -> String {
294 let filename = source.file_name().expect("source should have a filename");
295 let mut plugin_name = filename.to_string_lossy().to_string();
296 if plugin_name.starts_with("lib") {
297 plugin_name = plugin_name
298 .strip_prefix("lib")
299 .expect("Should start with lib")
300 .to_owned();
301 }
302 if plugin_name.ends_with(".so") {
303 plugin_name = plugin_name
304 .strip_suffix(".so")
305 .expect("Should end with .so")
306 .to_owned();
307 }
308 plugin_name
309}
310
311pub fn remove_plugin(removed_name: &str) {
324 let _ = remove_repo_directory();
325 remove_libso_file(removed_name);
326 remove_lib_from_config(&config_path(), removed_name);
327}
328
329fn remove_repo_directory() -> std::io::Result<()> {
334 match std::fs::remove_dir_all(REPOSITORIES_PATH) {
335 Ok(()) => {
336 println!("- Removed repository");
337 Ok(())
338 }
339 Err(err) => {
340 eprintln!("Coudln't remove repository: {err:?}",);
341 Err(err)
342 }
343 }
344}
345
346fn remove_libso_file(removed_name: &str) {
352 let mut found_in_config = false;
353 for (installed_name, path, exist) in list_plugins_details().iter() {
354 if installed_name == removed_name && *exist {
355 found_in_config = true;
356 match std::fs::remove_file(path) {
357 Ok(()) => println!("Removed {path}"),
358 Err(e) => eprintln!("Couldn't remove {path}: {e:?}"),
359 };
360 }
361 }
362 if !found_in_config {
363 eprintln!("Didn't find {removed_name} in config file. Run `fm plugin list` to see installed plugins.");
364 exit(1);
365 }
366}
367
368pub fn list_plugins() {
383 println!("Installed plugins:");
384 for (name, path, exist) in list_plugins_details().iter() {
385 let exists = if *exist { "ok" } else { "??" };
386 println!("[{exists}]: {name}: {path}");
387 }
388}
389
390fn list_plugins_details() -> Vec<(String, String, bool)> {
392 let config_file = File::open(config_path().as_ref()).expect("Couldn't open config file");
393 let mut installed = vec![];
394
395 let config_values: Value =
396 from_reader(&config_file).expect("Couldn't read config file as yaml");
397 let plugins = &config_values["plugins"]["previewer"];
398 let Ok(dmap) = from_value::<BTreeMap<String, String>>(plugins.to_owned()) else {
399 return vec![];
400 };
401 for (plugin, path) in dmap.into_iter() {
402 let exists = Path::new(&path).exists();
403 installed.push((plugin, path, exists))
404 }
405 installed
406}
407
408fn config_path() -> Cow<'static, str> {
410 tilde(CONFIG_PATH)
411}
412
413fn add_to_config(plugin_name: &str, dest: &Path) {
415 let config_path = config_path();
416 if is_plugin_name_in_config(&config_path, plugin_name) {
417 println!("- Config file {config_path} already contains a plugin called \"{plugin_name}\"");
418 return;
419 }
420 add_lib_to_config(&config_path, plugin_name, dest);
421}
422
423fn is_plugin_name_in_config(config_path: &str, plugin_name: &str) -> bool {
425 let config_file = File::open(config_path).expect("Couldn't open config file");
426 let config_values: Value =
427 from_reader(&config_file).expect("Couldn't read config file as yaml");
428 let plugins = &config_values["plugins"]["previewer"];
429 let Ok(dmap) = from_value::<BTreeMap<String, String>>(plugins.to_owned()) else {
430 return false;
431 };
432 dmap.contains_key(plugin_name)
433}
434
435fn add_lib_to_config(config_path: &str, plugin_name: &str, dest: &Path) {
443 let mut lines = extract_config_lines(config_path);
444 let new_line = format!(" '{plugin_name}': \"{d}\"", d = dest.display());
445
446 complete_lines_with_required_parts(&mut lines, new_line);
447
448 let new_content = lines.join("\n");
449 if let Err(e) = std::fs::write(config_path, new_content) {
450 eprintln!("Error installing {plugin_name}. Couldn't write to config file: {e:?}");
451 exit(1);
452 }
453}
454
455fn complete_lines_with_required_parts(lines: &mut Vec<String>, new_line: String) {
465 match find_dest_index(lines) {
466 Some(index) => {
467 if index >= lines.len() {
468 lines.push(new_line)
469 } else {
470 lines.insert(index, new_line)
471 }
472 }
473 None => {
474 if lines.iter().all(|s| s != "plugins:") {
475 lines.push("plugins:".to_string());
476 }
477 lines.push(" previewer:".to_string());
478 lines.push(new_line);
479 }
480 }
481}
482
483fn extract_config_lines(config_path: &str) -> Vec<String> {
486 let config_content = std::fs::read_to_string(config_path).expect("Couldn't read config file");
487 config_content
488 .lines()
489 .map(|line| line.to_string())
490 .collect()
491}
492
493fn find_dest_index(lines: &[String]) -> Option<usize> {
495 for (plugin_index, line) in lines.iter().enumerate() {
498 if line.starts_with("plugins:") {
499 for (previewer_index, line) in lines.iter().enumerate().skip(plugin_index) {
500 if line.starts_with(" previewer:") {
501 return Some(previewer_index + 1);
502 }
503 }
504 break;
505 }
506 }
507 None
508}
509
510fn remove_lib_from_config(config_path: &str, plugin_name: &str) {
518 let config_content = std::fs::read_to_string(config_path).expect("Couldn't read config file");
519 let mut lines: Vec<_> = config_content.lines().map(|l| l.to_string()).collect();
520 for index in 0..lines.len() {
521 let line = &lines[index];
522 if line.starts_with(&format!(" '{plugin_name}': ",)) {
523 lines.remove(index);
524 break;
525 }
526 }
527 let new_content = lines.join("\n");
528 match std::fs::write(config_path, new_content) {
529 Ok(()) => println!("Removed {plugin_name} from config file"),
530 Err(e) => {
531 eprintln!("Error removing {plugin_name}. Couldn't write to config file: {e:?}");
532 exit(1);
533 }
534 }
535}