use std::{
borrow::Cow,
collections::BTreeMap,
fs::File,
path::{Path, PathBuf},
process::exit,
};
use serde_yaml_ng::{from_reader, from_value, Value};
use url::Url;
use crate::common::{tilde, CONFIG_PATH, PLUGIN_LIBSO_PATH, REPOSITORIES_PATH};
use crate::io::execute_and_capture_output_with_path;
pub fn install_plugin(url: &str) {
println!("Installing {url} from its github repository");
let (hostname, author, plugin) = parse_hostname_author_plugin(url);
let repositories_path = tilde(REPOSITORIES_PATH);
let repositories_path = Path::new(repositories_path.as_ref());
create_clone_directory(repositories_path);
clone_repository(hostname, repositories_path, &author, &plugin);
let plugin_path = cargo_build_release(repositories_path, &plugin);
let Some(libso_path) = find_compiled_target(plugin_path, &plugin) else {
remove_repo_directory().expect("Couldn't delete plugin repository");
exit(6);
};
if _add_plugin(&libso_path) {
remove_repo_directory().expect("Couldn't delete plugin repository");
println!("Installation done");
}
}
fn parse_hostname_author_plugin(url: &str) -> (String, String, String) {
let nb_slashes = url.chars().filter(|c| *c == '/').count();
let mut author_plugin = url.to_string();
let hostname: String;
if nb_slashes == 1 {
println!("No host provided, using github.com");
hostname = "github.com".to_string();
} else if nb_slashes == 4 {
hostname = match Url::parse(&author_plugin) {
Ok(url) => {
if url.cannot_be_a_base() {
eprintln!("{author_plugin} isn't a valid url.");
exit(2);
} else {
let Some(hostname) = url.host_str() else {
eprintln!("{author_plugin} has no hostname");
exit(2);
};
author_plugin = url.path().to_string().chars().skip(1).collect();
hostname.to_string()
}
}
Err(_) => {
eprintln!("Cannot parse {author_plugin} as an url.");
exit(2);
}
};
} else {
eprintln!("Can't parse {url}. It should have 1 or 4 '/' like \"qkzk/bat_previewer\" or \"https://github.com/qkzk/bat_previewer\"");
exit(2);
}
let mut split = author_plugin.split('/');
let Some(author) = split.next() else {
eprintln!(
"Error installing plugin {author_plugin} isn't valid. Please use author/plugin format."
);
exit(2);
};
let Some(plugin) = split.next() else {
eprintln!(
"Error installing plugin {author_plugin} isn't valid. Please use author/plugin format."
);
exit(2);
};
(hostname, author.to_string(), plugin.to_string())
}
fn create_clone_directory(repositories_path: &Path) {
match std::fs::create_dir_all(repositories_path) {
Ok(()) => println!(
"- Created {repositories_path}",
repositories_path = repositories_path.display()
),
Err(error) => {
eprintln!("Error creating directories for repostories: {error:?}");
exit(3);
}
}
}
fn clone_repository(hostname: String, plugin_repositories: &Path, author: &str, plugin: &str) {
let args = [
"clone",
"--depth",
"1",
&format!("git@{hostname}:{author}/{plugin}.git"),
];
let output = execute_and_capture_output_with_path("git", plugin_repositories, &args);
match output {
Ok(stdout) => println!("- Cloned {author}/{plugin} git repository - {stdout}"),
Err(stderr) => {
eprintln!("Error cloning the repository :");
eprintln!("{}", stderr);
let _ = remove_repo_directory();
exit(4);
}
}
}
fn cargo_build_release(plugin_path: &Path, plugin: &str) -> PathBuf {
let args = ["build", "--release"];
let mut plugin_path = plugin_path.to_path_buf();
plugin_path.push(plugin);
let output = execute_and_capture_output_with_path("cargo", &plugin_path, &args);
match output {
Ok(stdout) => {
println!("- Compiled plugin {plugin} libso file");
if !stdout.is_empty() {
println!("- {stdout}")
}
}
Err(stderr) => {
eprintln!("Error compiling the plugin :");
eprintln!("{}", stderr);
remove_repo_directory().expect("Couldn't delete plugin repository");
exit(5);
}
}
plugin_path
}
fn find_compiled_target(mut plugin_path: PathBuf, plugin: &str) -> Option<PathBuf> {
let ext = format!("target/release/lib{plugin}.so");
plugin_path.push(ext);
if plugin_path.exists() {
Some(plugin_path)
} else {
None
}
}
pub fn add_plugin<P>(path: P)
where
P: AsRef<Path>,
{
println!("Installing {path}...", path = path.as_ref().display());
if _add_plugin(&path) {
println!(
"Plugin {path} added to configuration file.",
path = path.as_ref().display()
);
} else {
eprintln!(
"Something went wrong installing {path}.",
path = path.as_ref().display()
);
exit(1);
}
}
pub fn _add_plugin<P>(path: P) -> bool
where
P: AsRef<Path>,
{
let source = path.as_ref();
if !source.exists() {
eprintln!(
"Error installing plugin {path}. File doesn't exist.",
path = path.as_ref().display()
);
exit(1);
}
let dest = build_libso_destination_path(source);
copy_source_to_dest(source, &dest);
println!("- Copied libso file to {dest}", dest = dest.display());
let plugin_name = get_plugin_name(source);
add_to_config(&plugin_name, &dest);
println!(
"- Added {plugin_name}: {dest} to config file.",
dest = dest.display()
);
true
}
fn build_libso_destination_path(source: &Path) -> PathBuf {
let mut dest = PathBuf::from(tilde(PLUGIN_LIBSO_PATH).as_ref());
if let Err(error) = std::fs::create_dir_all(&dest) {
eprintln!("Couldn't create {PLUGIN_LIBSO_PATH}");
eprintln!("Error: {error:?}");
exit(1);
};
let Some(filename) = source.file_name() else {
eprintln!("Error: couldn't extract filename");
exit(1);
};
dest.push(filename);
dest
}
fn copy_source_to_dest(source: &Path, dest: &Path) {
if let Err(error) = std::fs::copy(source, dest) {
eprintln!("Error copying the libsofile: {error}");
exit(1);
}
}
fn get_plugin_name(source: &Path) -> String {
let filename = source.file_name().expect("source should have a filename");
let mut plugin_name = filename.to_string_lossy().to_string();
if plugin_name.starts_with("lib") {
plugin_name = plugin_name
.strip_prefix("lib")
.expect("Should start with lib")
.to_owned();
}
if plugin_name.ends_with(".so") {
plugin_name = plugin_name
.strip_suffix(".so")
.expect("Should end with .so")
.to_owned();
}
plugin_name
}
pub fn remove_plugin(removed_name: &str) {
let _ = remove_repo_directory();
remove_libso_file(removed_name);
remove_lib_from_config(&config_path(), removed_name);
}
fn remove_repo_directory() -> std::io::Result<()> {
match std::fs::remove_dir_all(REPOSITORIES_PATH) {
Ok(()) => {
println!("- Removed repository");
Ok(())
}
Err(err) => {
eprintln!("Coudln't remove repository: {err:?}",);
Err(err)
}
}
}
fn remove_libso_file(removed_name: &str) {
let mut found_in_config = false;
for (installed_name, path, exist) in list_plugins_details().iter() {
if installed_name == removed_name && *exist {
found_in_config = true;
match std::fs::remove_file(path) {
Ok(()) => println!("Removed {path}"),
Err(e) => eprintln!("Couldn't remove {path}: {e:?}"),
};
}
}
if !found_in_config {
eprintln!("Didn't find {removed_name} in config file. Run `fm plugin list` to see installed plugins.");
exit(1);
}
}
pub fn list_plugins() {
println!("Installed plugins:");
for (name, path, exist) in list_plugins_details().iter() {
let exists = if *exist { "ok" } else { "??" };
println!("[{exists}]: {name}: {path}");
}
}
fn list_plugins_details() -> Vec<(String, String, bool)> {
let config_file = File::open(config_path().as_ref()).expect("Couldn't open config file");
let mut installed = vec![];
let config_values: Value =
from_reader(&config_file).expect("Couldn't read config file as yaml");
let plugins = &config_values["plugins"]["previewer"];
let Ok(dmap) = from_value::<BTreeMap<String, String>>(plugins.to_owned()) else {
return vec![];
};
for (plugin, path) in dmap.into_iter() {
let exists = Path::new(&path).exists();
installed.push((plugin, path, exists))
}
installed
}
fn config_path() -> Cow<'static, str> {
tilde(CONFIG_PATH)
}
fn add_to_config(plugin_name: &str, dest: &Path) {
let config_path = config_path();
if is_plugin_name_in_config(&config_path, plugin_name) {
println!("- Config file {config_path} already contains a plugin called \"{plugin_name}\"");
return;
}
add_lib_to_config(&config_path, plugin_name, dest);
}
fn is_plugin_name_in_config(config_path: &str, plugin_name: &str) -> bool {
let config_file = File::open(config_path).expect("Couldn't open config file");
let config_values: Value =
from_reader(&config_file).expect("Couldn't read config file as yaml");
let plugins = &config_values["plugins"]["previewer"];
let Ok(dmap) = from_value::<BTreeMap<String, String>>(plugins.to_owned()) else {
return false;
};
dmap.contains_key(plugin_name)
}
fn add_lib_to_config(config_path: &str, plugin_name: &str, dest: &Path) {
let mut lines = extract_config_lines(config_path);
let new_line = format!(" '{plugin_name}': \"{d}\"", d = dest.display());
complete_lines_with_required_parts(&mut lines, new_line);
let new_content = lines.join("\n");
if let Err(e) = std::fs::write(config_path, new_content) {
eprintln!("Error installing {plugin_name}. Couldn't write to config file: {e:?}");
exit(1);
}
}
fn complete_lines_with_required_parts(lines: &mut Vec<String>, new_line: String) {
match find_dest_index(lines) {
Some(index) => {
if index >= lines.len() {
lines.push(new_line)
} else {
lines.insert(index, new_line)
}
}
None => {
if lines.iter().all(|s| s != "plugins:") {
lines.push("plugins:".to_string());
}
lines.push(" previewer:".to_string());
lines.push(new_line);
}
}
}
fn extract_config_lines(config_path: &str) -> Vec<String> {
let config_content = std::fs::read_to_string(config_path).expect("Couldn't read config file");
config_content
.lines()
.map(|line| line.to_string())
.collect()
}
fn find_dest_index(lines: &[String]) -> Option<usize> {
for (plugin_index, line) in lines.iter().enumerate() {
if line.starts_with("plugins:") {
for (previewer_index, line) in lines.iter().enumerate().skip(plugin_index) {
if line.starts_with(" previewer:") {
return Some(previewer_index + 1);
}
}
break;
}
}
None
}
fn remove_lib_from_config(config_path: &str, plugin_name: &str) {
let config_content = std::fs::read_to_string(config_path).expect("Couldn't read config file");
let mut lines: Vec<_> = config_content.lines().map(|l| l.to_string()).collect();
for index in 0..lines.len() {
let line = &lines[index];
if line.starts_with(&format!(" '{plugin_name}': ",)) {
lines.remove(index);
break;
}
}
let new_content = lines.join("\n");
match std::fs::write(config_path, new_content) {
Ok(()) => println!("Removed {plugin_name} from config file"),
Err(e) => {
eprintln!("Error removing {plugin_name}. Couldn't write to config file: {e:?}");
exit(1);
}
}
}