diff --git a/fluere-plugin/Cargo.toml b/fluere-plugin/Cargo.toml index d45cfbf..4579ca8 100644 --- a/fluere-plugin/Cargo.toml +++ b/fluere-plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fluere_plugin" -version = "0.1.0" +version = "0.1.1" authors = ["Skuld Norniern "] edition = "2021" description = "Plugin API for Fluere." @@ -14,12 +14,14 @@ repository = "https://github.com/SkuldNorniern/fluere" [dependencies] +git2 = "0.18.1" tokio = { version = "1.32", features = ["full","macros", "rt-multi-thread"] } fluere-config = { version = "0.1.2", path = "../fluere-config" } #fluere-plugin-trait = { path = "../fluere-plugin-trait" } fluereflow = { version = "0.3.2", path = "../fluereflow" } dirs = "5.0.1" mlua = { version = "0.9.2", features = ["lua54", "vendored","async","send"] } +inksac = "0.4.0" [lib] name = "fluere_plugin" diff --git a/fluere-plugin/src/downloader.rs b/fluere-plugin/src/downloader.rs index e8fc737..3833a19 100644 --- a/fluere-plugin/src/downloader.rs +++ b/fluere-plugin/src/downloader.rs @@ -1,23 +1,141 @@ +use git2::{ + build::CheckoutBuilder, FetchOptions, ObjectType, Repository, ResetType, StatusOptions, +}; +use inksac::{Color, Style, Stylish}; + +use std::fmt; +use std::io; +use std::path::Path; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + use crate::util::home_cache_path; -use std::process::Command; -pub fn download_plugin_from_github(repo_name: &str) -> Result<(), std::io::Error> { - let url = format!("https://github.com/{}.git", repo_name); - let path = home_cache_path(); - let cd_cmd = format!("cd {}", path.display()); +#[derive(Debug)] +pub enum DownloadError { + Io(std::io::Error), + Git(git2::Error), + Other(String), +} + +impl fmt::Display for DownloadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DownloadError::Io(err) => write!(f, "IO error: {}", err), + DownloadError::Git(err) => write!(f, "Git error: {}", err), + DownloadError::Other(err) => write!(f, "{}", err), + } + } +} + +impl From for DownloadError { + fn from(err: std::io::Error) -> Self { + DownloadError::Io(err) + } +} + +impl From for DownloadError { + fn from(err: git2::Error) -> Self { + DownloadError::Git(err) + } +} + +impl From for DownloadError { + fn from(err: String) -> Self { + DownloadError::Other(err) + } +} + +pub fn download_plugin_from_github(repo_name: &str) -> Result<(), DownloadError> { + let url = format!("https://github.com/{}", repo_name); + let warn_style = Style::builder().foreground(Color::Yellow).build(); + let highlight_style = Style::builder().foreground(Color::Green).bold().build(); + let path = home_cache_path()?; if !path.exists() { - std::fs::create_dir_all(path.clone())?; + std::fs::create_dir_all(&path)?; } - if path.join(repo_name.split('/').last().unwrap()).exists() { - Command::new("bash") - .arg("-c") - .arg(cd_cmd + ";git fetch ;git pull") - .output()?; + + let repo_path = path.join(repo_name.split('/').last().unwrap()); + let repository_path = Path::new(&repo_path); + + let repo = match Repository::open(repository_path) { + Ok(repo) => repo, + Err(_) => Repository::clone(&url, repository_path)?, + }; + let mut remote = repo.find_remote("origin")?; + let mut fetch_options = FetchOptions::new(); + remote.fetch(&["main"], Some(&mut fetch_options), None)?; + + let fetch_head = repo.find_reference("FETCH_HEAD")?; + + let fetch_commit = fetch_head.peel(ObjectType::Commit)?.id(); + let local_commit = repo.head()?.target().unwrap(); + + if fetch_commit != local_commit { + println!( + "An update is available for '{}'. Do you want to update? [y/N] (auto skip in 5 seconds)", + repo_name.styled(highlight_style) + ); + if user_confirms()? { + if has_local_changes(&repo)? { + println!("{}: You have uncommitted changes. Updating will overwrite these changes. Continue? [y/N] (auto skip in 5 seconds)","Warning".styled(warn_style)); + if !user_confirms()? { + println!("Update skipped for {}", repo_name.styled(highlight_style)); + return Ok(()); + } + } + + // Resetting the HEAD to the fetched commit + let fetch_commit_obj = repo.find_commit(fetch_commit)?; + repo.reset(fetch_commit_obj.as_object(), ResetType::Hard, None)?; + + // Checking out the commit to update the working directory and index + let mut checkout_builder = CheckoutBuilder::new(); + let _ = checkout_builder.force(); + repo.checkout_tree(fetch_commit_obj.as_object(), Some(&mut checkout_builder))?; + repo.set_head_detached(fetch_commit)?; + + println!( + "Successfully updated to the latest version for {}", + repo_name.styled(highlight_style) + ); + } else { + println!("Update skipped for {}", repo_name.styled(highlight_style)); + } } else { - Command::new("bash") - .arg("-c") - .arg(cd_cmd + "; git clone " + &url) - .output()?; + println!("{} is up to date.", repo_name.styled(highlight_style)); } + Ok(()) } +fn user_confirms() -> Result { + let (sender, receiver) = mpsc::channel(); + + // Spawn a new thread for user input + thread::spawn(move || { + let mut input = String::new(); + match io::stdin().read_line(&mut input) { + Ok(_) => { + let _ = sender.send(input.trim().eq_ignore_ascii_case("y")); + } + Err(_) => { + let _ = sender.send(false); + } + } + }); + + // Wait for input or timeout after 5 seconds + match receiver.recv_timeout(Duration::from_secs(5)) { + Ok(result) => Ok(result), + Err(_) => { + print!("Timeout. "); + Ok(false) + } + } +} + +fn has_local_changes(repo: &Repository) -> Result { + let statuses = repo.statuses(Some(StatusOptions::new().include_untracked(true)))?; + Ok(statuses.iter().any(|s| s.status() != git2::Status::CURRENT)) +} diff --git a/fluere-plugin/src/lib.rs b/fluere-plugin/src/lib.rs index a7aad7e..9278ece 100644 --- a/fluere-plugin/src/lib.rs +++ b/fluere-plugin/src/lib.rs @@ -16,7 +16,6 @@ pub struct PluginManager { lua: Arc>, sender: mpsc::Sender, receiver: Arc>>, - //worker: Arc>>, plugins: Arc>>, } @@ -30,7 +29,6 @@ impl PluginManager { lua, sender, receiver: Arc::new(Mutex::new(receiver)), - //worker: Arc::new(Mutex::new(tokio::task::JoinHandle::new())), plugins, }) } @@ -46,7 +44,7 @@ impl PluginManager { let mut owned_path_str = path.clone(); let name_of_main_file = "/init.lua"; owned_path_str.push_str(name_of_main_file); - // println!("path: {}", owned_path_str); + match std::fs::read_to_string(owned_path_str) { Ok(code) => { let lua_clone = self.lua.clone(); @@ -103,7 +101,7 @@ impl PluginManager { None => { match download_plugin_from_github(name) { Ok(_) => { - let path = home_cache_path().join(name.split('/').last().unwrap()); + let path = home_cache_path()?.join(name.split('/').last().unwrap()); match std::fs::read_to_string(path.join("init.lua")) { Ok(code) => { let lua_clone = self.lua.clone(); @@ -151,14 +149,16 @@ impl PluginManager { plugins_guard.insert(name.clone()); println!("Loaded plugin {}", name); } - Err(_) => { + Err(eri) => { println!("Failed to read plugin: {}", name); + println!("Error: {}", eri); continue; } } } - Err(_) => { + Err(eri) => { println!("Unable to download plugin: {}", name); + println!("Error: {}", eri); } } } @@ -220,7 +220,7 @@ impl PluginManager { { lua_table .set(*key, record_vec[index].clone()) - .expect(format!("Failed to set key: {}", key).as_str()); + .unwrap_or_else(|_| panic!("Failed to set key: {}", key)); } for plugin_name in plugins.iter() { @@ -231,7 +231,7 @@ impl PluginManager { if let Ok(func) = plugin_table.get::<_, mlua::Function>("process_data") { func.call::, ()>(lua_table.clone()) - .expect(format!("Error on plugin: {}", plugin_name).as_str()); + .unwrap_or_else(|_| panic!("Error on plugin: {}", plugin_name)); } else { println!( "'process_data' function not found in plugin: {}", @@ -270,7 +270,7 @@ impl PluginManager { if let Ok(func) = plugin_table.get::<_, mlua::Function>("cleanup") { func.call::<(), ()>(()) - .expect(format!("Error on plugin: {}", plugin_name).as_str()); + .unwrap_or_else(|_| panic!("Error on plugin: {}", plugin_name)); } else { println!("cleanup function not found in plugin: {}", plugin_name); } diff --git a/fluere-plugin/src/util.rs b/fluere-plugin/src/util.rs index a4be87a..846cb89 100644 --- a/fluere-plugin/src/util.rs +++ b/fluere-plugin/src/util.rs @@ -4,7 +4,7 @@ use std::env; use std::fs; use std::path::{Path, PathBuf}; -pub fn home_cache_path() -> PathBuf { +pub fn home_cache_path() -> Result { // Check for the SUDO_USER environment variable let sudo_user = env::var("SUDO_USER"); @@ -16,13 +16,18 @@ pub fn home_cache_path() -> PathBuf { } Err(_) => { // If not running under sudo, just use the config_dir function as before - cache_dir().unwrap() + cache_dir().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + "Failed to find cache directory", + ) + })? } }; let path_config = path_base.join("fluere"); if !path_config.exists() { - fs::create_dir_all(path_config.clone()).unwrap(); + fs::create_dir_all(path_config.clone())?; } - path_config + Ok(path_config) } diff --git a/src/net/live_fluereflow.rs b/src/net/live_fluereflow.rs index 5374da3..8e70222 100644 --- a/src/net/live_fluereflow.rs +++ b/src/net/live_fluereflow.rs @@ -509,7 +509,7 @@ fn draw_ui( flow_columns[5], ); } -async fn listen_for_exit_keys() -> Result<(),std::io::Error> { +async fn listen_for_exit_keys() -> Result<(), std::io::Error> { loop { if event::poll(std::time::Duration::from_millis(100))? { if let event::Event::Key(KeyEvent {