diff --git a/Cargo.lock b/Cargo.lock index 5cefe3b..6d3c7e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,6 +22,7 @@ dependencies = [ "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -106,6 +107,11 @@ name = "custom_derive" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "dtoa" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "env_logger" version = "0.4.3" @@ -277,6 +283,11 @@ name = "htmlescape" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "itoa" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "lazy_static" version = "0.2.8" @@ -496,6 +507,17 @@ dependencies = [ "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde_json" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "siphasher" version = "0.2.2" @@ -585,6 +607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" "checksum conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" "checksum custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" +"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum env_logger 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3ddf21e73e016298f5cb37d6ef8e8da8e39f91f9ec8b0df44b7deb16a9f8cd5b" "checksum gdk 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f30018ecbbb1e6f1d59c4024ec08675850744b799abc5420be0629ac9ba0abd2" "checksum gdk-pixbuf 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "caf05dab73febcc6e90abaff8f24cfe1cf1bd2222cd648ddfe337bf3b994489f" @@ -598,6 +621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum gtk 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce17f98e7dcdc9d06b3a5f7621d796a24937c04953481205b1be267c5a02697a" "checksum gtk-sys 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "854b56ce6d6b05945f7735651482835c5ac1f8582142ce67306726259a3dafb0" "checksum htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" +"checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" "checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf" "checksum libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "8a014d9226c2cc402676fbe9ea2e15dd5222cd1dd57f576b5b283178c944a264" "checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b" @@ -624,6 +648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum serde 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "f7726f29ddf9731b17ff113c461e362c381d9d69433f79de4f3dd572488823e9" "checksum serde_derive 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cf823e706be268e73e7747b147aa31c8f633ab4ba31f115efb57e5047c3a76dd" "checksum serde_derive_internals 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37aee4e0da52d801acfbc0cc219eb1eda7142112339726e427926a6f6ee65d3a" +"checksum serde_json 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1e67ce320daa7e494c578e34d4b00689f23bb94512fe0ca0dfaf02ea53fb67" "checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" diff --git a/Cargo.toml b/Cargo.toml index bef06e6..80af648 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ htmlescape = "0.3" serde = "1.0" serde_derive = "1.0" toml = "0.4" +serde_json = "1.0" #[dependencies.neovim-lib] #git = "https://github.com/daa84/neovim-lib" diff --git a/README.md b/README.md index 3bacf14..1f2d133 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ git clone https://aur.archlinux.org/neovim-gtk-git.git cd neovim-gtk-git makepkg -si ``` - +## flatpak +Flatpak package available [here](https://github.com/daa84/neovim-gtk-flatpak) # Build ## Linux diff --git a/build.rs b/build.rs index 023ff59..2d792df 100644 --- a/build.rs +++ b/build.rs @@ -37,6 +37,7 @@ fn main() { .entry("Return", "\"CR\"") .entry("Escape", "\"Esc\"") .entry("Delete", "\"Del\"") + .entry("Insert", "\"Insert\"") .entry("Page_Up", "\"PageUp\"") .entry("Page_Down", "\"PageDown\"") .entry("Enter", "\"CR\"") diff --git a/src/cursor.rs b/src/cursor.rs index 0de86c0..500c7af 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -89,15 +89,21 @@ impl Cursor { } pub fn reset_state(&mut self) { - self.start(); + if self.state.borrow().anim_phase != AnimPhase::Busy { + self.start(); + } } pub fn enter_focus(&mut self) { - self.start(); + if self.state.borrow().anim_phase != AnimPhase::Busy { + self.start(); + } } pub fn leave_focus(&mut self) { - self.state.borrow_mut().reset_to(AnimPhase::NoFocus); + if self.state.borrow().anim_phase != AnimPhase::Busy { + self.state.borrow_mut().reset_to(AnimPhase::NoFocus); + } } pub fn busy_on(&mut self) { diff --git a/src/dirs.rs b/src/dirs.rs new file mode 100644 index 0000000..3380a65 --- /dev/null +++ b/src/dirs.rs @@ -0,0 +1,33 @@ +use std; +use std::path::PathBuf; + +pub fn get_app_config_dir_create() -> Result { + let config_dir = get_app_config_dir()?; + + std::fs::create_dir_all(&config_dir).map_err( + |e| format!("{}", e), + )?; + + Ok(config_dir) +} + +pub fn get_app_config_dir() -> Result { + let mut config_dir = get_xdg_config_dir()?; + + config_dir.push("nvim-gtk"); + + Ok(config_dir) +} + +fn get_xdg_config_dir() -> Result { + if let Ok(config_path) = std::env::var("XDG_CONFIG_HOME") { + return Ok(PathBuf::from(config_path)); + } + + let mut home_dir = std::env::home_dir().ok_or( + "Impossible to get your home dir!", + )?; + home_dir.push(".config"); + Ok(home_dir) +} + diff --git a/src/main.rs b/src/main.rs index e2cf6c1..81a8c2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,18 +18,23 @@ extern crate log; extern crate env_logger; extern crate htmlescape; +extern crate serde; #[macro_use] extern crate serde_derive; extern crate toml; +extern crate serde_json; mod sys; +mod nvim_config; +mod dirs; mod color; mod value; mod mode; mod ui_model; #[macro_use] mod ui; +mod plug_manager; mod nvim; mod render; mod shell; @@ -71,6 +76,8 @@ fn main() { app.connect_open(open); } + gtk::Window::set_default_icon_name("org.daa.NeovimGtk"); + let args: Vec = env::args().collect(); let argv: Vec<&str> = args.iter() .filter(|a| !a.starts_with(BIN_PATH_ARG)) diff --git a/src/nvim/mod.rs b/src/nvim/mod.rs index 23eae1c..f9fb7f6 100644 --- a/src/nvim/mod.rs +++ b/src/nvim/mod.rs @@ -21,6 +21,7 @@ use neovim_lib::{Neovim, NeovimApi, Session, UiAttachOptions, CallError}; use ui::UiMutex; use shell; +use nvim_config::NvimConfig; #[derive(Debug)] pub struct NvimInitError { @@ -107,6 +108,12 @@ pub fn start( cmd.arg("--cmd").arg("let &rtp.=',runtime'"); } + if let Some(nvim_config) = NvimConfig::config_path() { + if let Some(path) = nvim_config.to_str() { + cmd.arg("--cmd").arg(format!("source {}", path)); + } + } + let session = Session::new_child_cmd(&mut cmd); let session = match session { @@ -149,16 +156,23 @@ pub fn post_start_init( } -pub trait ErrorReport { +pub trait ErrorReport { fn report_err(&self, nvim: &mut NeovimApi); + + fn ok_and_report(&self, nvim: &mut NeovimApi) -> Option<&T>; } -impl ErrorReport for result::Result { +impl ErrorReport for result::Result { fn report_err(&self, _: &mut NeovimApi) { if let Err(ref err) = *self { println!("{}", err); //nvim.report_error(&err_msg).expect("Error report error :)"); } } + + fn ok_and_report(&self, nvim: &mut NeovimApi) -> Option<&T> { + self.report_err(nvim); + self.as_ref().ok() + } } diff --git a/src/nvim_config.rs b/src/nvim_config.rs new file mode 100644 index 0000000..b4a1891 --- /dev/null +++ b/src/nvim_config.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; +use std::fs::{remove_file, OpenOptions}; +use std::io::Write; + +use dirs; +use plug_manager; + +#[derive(Clone)] +pub struct NvimConfig { + plug_config: Option, +} + +impl NvimConfig { + const CONFIG_PATH: &'static str = "settings.vim"; + + pub fn new(plug_config: Option) -> Self { + NvimConfig { plug_config } + } + + pub fn generate_config(&self) -> Option { + if self.plug_config.is_some() { + match self.write_file() { + Err(err) => { + error!("{}", err); + None + } + Ok(file) => Some(file), + } + } else { + NvimConfig::config_path().map(remove_file); + None + } + } + + pub fn config_path() -> Option { + if let Ok(mut path) = dirs::get_app_config_dir() { + path.push(NvimConfig::CONFIG_PATH); + if path.is_file() { + return Some(path); + } + } + + None + } + + fn write_file(&self) -> Result { + let mut config_dir = dirs::get_app_config_dir_create()?; + config_dir.push(NvimConfig::CONFIG_PATH); + + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&config_dir) + .map_err(|e| format!("{}", e))?; + + let content = &self.plug_config.as_ref().unwrap().source; + if !content.is_empty() { + debug!("{}", content); + file.write_all(content.as_bytes()).map_err( + |e| format!("{}", e), + )?; + } + + file.sync_all().map_err(|e| format!("{}", e))?; + Ok(config_dir) + } +} diff --git a/src/plug_manager/manager.rs b/src/plug_manager/manager.rs new file mode 100644 index 0000000..ae499a8 --- /dev/null +++ b/src/plug_manager/manager.rs @@ -0,0 +1,118 @@ +use std::rc::Rc; +use std::cell::RefCell; + +use super::vim_plug; +use super::store::{Store, PlugInfo}; + +use nvim::NeovimClient; + +pub struct Manager { + pub vim_plug: vim_plug::Manager, + pub store: Store, + pub plug_manage_state: PlugManageState, +} + +impl Manager { + pub fn new() -> Self { + let (plug_manage_state, store) = if Store::is_config_exists() { + (PlugManageState::NvimGtk, Store::load()) + } else { + (PlugManageState::Unknown, Store::empty()) + }; + + Manager { + vim_plug: vim_plug::Manager::new(), + plug_manage_state, + store, + } + } + + pub fn generate_config(&self) -> Option { + if self.store.is_enabled() { + Some(PlugManagerConfigSource::new(&self.store)) + } else { + None + } + } + + pub fn init_nvim_client(&mut self, nvim: Rc>) { + self.vim_plug.initialize(nvim); + } + + pub fn reload_store(&mut self) { + match self.plug_manage_state { + PlugManageState::Unknown => { + if self.vim_plug.is_loaded() { + self.store = Store::load_from_plug(&self.vim_plug); + self.plug_manage_state = PlugManageState::VimPlug; + } else { + self.store = Store::empty(); + } + } + PlugManageState::NvimGtk => { + if Store::is_config_exists() { + self.store = Store::load(); + } else { + self.store = Store::empty(); + } + } + PlugManageState::VimPlug => { + if Store::is_config_exists() { + self.store = Store::load(); + self.plug_manage_state = PlugManageState::NvimGtk; + } else { + self.store = Store::empty(); + } + } + } + if let PlugManageState::Unknown = self.plug_manage_state { + if self.vim_plug.is_loaded() { + self.store = Store::load_from_plug(&self.vim_plug); + self.plug_manage_state = PlugManageState::VimPlug; + } + } + } + + pub fn save(&self) { + self.store.save(); + } + + pub fn clear_removed(&mut self) { + self.store.clear_removed(); + } + + pub fn add_plug(&mut self, plug: PlugInfo) -> bool { + self.store.add_plug(plug) + } + + pub fn move_item(&mut self, idx: usize, offset: i32) { + self.store.move_item(idx, offset); + } +} + +pub enum PlugManageState { + NvimGtk, + VimPlug, + Unknown, +} + +#[derive(Clone)] +pub struct PlugManagerConfigSource { + pub source: String, +} + +impl PlugManagerConfigSource { + pub fn new(store: &Store) -> Self { + let mut builder = "call plug#begin()\n".to_owned(); + + for plug in store.get_plugs() { + if !plug.removed { + builder += &format!("Plug '{}', {{ 'as': '{}' }}\n", plug.get_plug_path(), plug.name); + } + } + + builder += "call plug#end()\n"; + + PlugManagerConfigSource { source: builder } + } +} diff --git a/src/plug_manager/mod.rs b/src/plug_manager/mod.rs new file mode 100644 index 0000000..f318017 --- /dev/null +++ b/src/plug_manager/mod.rs @@ -0,0 +1,9 @@ +mod ui; +mod vim_plug; +mod store; +mod manager; +mod plugin_settings_dlg; +mod vimawesome; + +pub use self::ui::Ui; +pub use self::manager::{Manager, PlugManagerConfigSource}; diff --git a/src/plug_manager/plugin_settings_dlg.rs b/src/plug_manager/plugin_settings_dlg.rs new file mode 100644 index 0000000..3860f71 --- /dev/null +++ b/src/plug_manager/plugin_settings_dlg.rs @@ -0,0 +1,114 @@ +use gtk; +use gtk::prelude::*; + +use super::store; + +pub struct Builder<'a> { + title: &'a str, +} + +impl<'a> Builder<'a> { + pub fn new(title: &'a str) -> Self { + Builder { title } + } + + pub fn show>(&self, parent: &F) -> Option { + let dlg = gtk::Dialog::new_with_buttons( + Some(self.title), + Some(parent), + gtk::DIALOG_USE_HEADER_BAR | gtk::DIALOG_DESTROY_WITH_PARENT, + &[ + ("Cancel", gtk::ResponseType::Cancel.into()), + ("Ok", gtk::ResponseType::Ok.into()), + ], + ); + + let content = dlg.get_content_area(); + let border = gtk::Box::new(gtk::Orientation::Horizontal, 0); + border.set_border_width(12); + + let list = gtk::ListBox::new(); + list.set_selection_mode(gtk::SelectionMode::None); + + let path = gtk::Box::new(gtk::Orientation::Horizontal, 5); + path.set_border_width(5); + let path_lbl = gtk::Label::new("Repo"); + let path_e = gtk::Entry::new(); + path_e.set_placeholder_text("user_name/repo_name"); + + path.pack_start(&path_lbl, true, true, 0); + path.pack_end(&path_e, false, true, 0); + + list.add(&path); + + + let name = gtk::Box::new(gtk::Orientation::Horizontal, 5); + name.set_border_width(5); + let name_lbl = gtk::Label::new("Name"); + let name_e = gtk::Entry::new(); + + name.pack_start(&name_lbl, true, true, 0); + name.pack_end(&name_e, false, true, 0); + + list.add(&name); + + border.pack_start(&list, true, true, 0); + content.add(&border); + content.show_all(); + + path_e.connect_changed(clone!(name_e => move |p| { + if let Some(name) = p.get_text().and_then(|t| extract_name(&t)) { + name_e.set_text(&name); + } + })); + + let ok: i32 = gtk::ResponseType::Ok.into(); + let res = if dlg.run() == ok { + path_e.get_text().map(|path| { + let name = name_e + .get_text() + .and_then(|name| if name.trim().is_empty() { + None + } else { + Some(name) + }) + .or_else(|| extract_name(&path)) + .unwrap_or_else(|| path.clone()); + + store::PlugInfo::new(name.to_owned(), path.to_owned()) + }) + } else { + None + }; + + dlg.destroy(); + + res + } +} + +fn extract_name(path: &str) -> Option { + if let Some(idx) = path.rfind(|c| c == '/' || c == '\\') { + if idx < path.len() - 1 { + let path = path.trim_right_matches(".git"); + Some(path[idx + 1..].to_owned()) + } else { + None + } + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_name() { + assert_eq!( + Some("plugin_name".to_owned()), + extract_name("http://github.com/somebody/plugin_name.git") + ); + } +} diff --git a/src/plug_manager/store.rs b/src/plug_manager/store.rs new file mode 100644 index 0000000..39e8903 --- /dev/null +++ b/src/plug_manager/store.rs @@ -0,0 +1,155 @@ +use toml; + +use settings::SettingsLoader; +use super::vim_plug; + +pub struct Store { + settings: Settings, +} + +impl Store { + pub fn is_config_exists() -> bool { + Settings::is_file_exists() + } + + pub fn is_enabled(&self) -> bool { + self.settings.enabled + } + + pub fn load() -> Self { + Store { settings: Settings::load() } + } + + pub fn empty() -> Self { + Store { settings: Settings::empty() } + } + + pub fn load_from_plug(vim_plug: &vim_plug::Manager) -> Self { + let settings = match vim_plug.get_plugs() { + Err(msg) => { + error!("{}", msg); + Settings::empty() + } + Ok(plugs) => { + let plugs = plugs + .iter() + .map(|vpi| PlugInfo::new(vpi.name.to_owned(), vpi.uri.to_owned())) + .collect(); + Settings::new(plugs) + } + }; + + Store { settings } + } + + pub fn get_plugs(&self) -> &[PlugInfo] { + &self.settings.plugs + } + + pub fn set_enabled(&mut self, enabled: bool) { + self.settings.enabled = enabled; + } + + pub fn clear_removed(&mut self) { + self.settings.plugs.retain(|p| !p.removed); + } + + pub fn save(&self) { + self.settings.save(); + } + + pub fn remove_plug(&mut self, idx: usize) { + self.settings.plugs[idx].removed = true; + } + + pub fn restore_plug(&mut self, idx: usize) { + self.settings.plugs[idx].removed = false; + } + + pub fn add_plug(&mut self, plug: PlugInfo) -> bool { + let path = plug.get_plug_path(); + if self.settings.plugs.iter().any(|p| p.get_plug_path() == path || p.name == plug.name) { + return false; + } + self.settings.plugs.push(plug); + true + } + + pub fn plugs_count(&self) -> usize { + self.settings.plugs.len() + } + + pub fn move_item(&mut self, idx: usize, offset: i32) { + let plug = self.settings.plugs.remove(idx); + self.settings.plugs.insert((idx as i32 + offset) as usize, plug); + } +} + +#[derive(Serialize, Deserialize)] +struct Settings { + enabled: bool, + plugs: Vec, +} + +impl Settings { + fn new(plugs: Vec) -> Self { + Settings { plugs, enabled: false } + } +} + +impl SettingsLoader for Settings { + const SETTINGS_FILE: &'static str = "plugs.toml"; + + fn empty() -> Self { + Settings { plugs: vec![], enabled: false } + } + + fn from_str(s: &str) -> Result { + toml::from_str(&s).map_err(|e| format!("{}", e)) + } +} + +#[derive(Serialize, Deserialize)] +pub struct PlugInfo { + pub name: String, + pub url: String, + pub removed: bool, +} + +impl PlugInfo { + pub fn new(name: String, url: String) -> Self { + PlugInfo { + name, + url, + removed: false, + } + } + + pub fn get_plug_path(&self) -> String { + if self.url.contains("github.com") { + let mut path_comps: Vec<&str> = self.url + .trim_right_matches(".git") + .rsplit('/') + .take(2) + .collect(); + path_comps.reverse(); + path_comps.join("/") + } else { + self.url.clone() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_plug_path() { + let plug = PlugInfo::new( + "rust.vim".to_owned(), + "https://git::@github.com/rust-lang/rust.vim.git".to_owned(), + ); + assert_eq!("rust-lang/rust.vim".to_owned(), plug.get_plug_path()); + } +} diff --git a/src/plug_manager/ui.rs b/src/plug_manager/ui.rs new file mode 100644 index 0000000..80d21f9 --- /dev/null +++ b/src/plug_manager/ui.rs @@ -0,0 +1,441 @@ +use std::sync::Arc; +use std::rc::Rc; +use std::cell::RefCell; +use std::ops::Deref; + +use ui::UiMutex; + +use gtk; +use gtk::prelude::*; +use gtk_sys; + +use super::manager; +use super::store::{Store, PlugInfo}; +use super::plugin_settings_dlg; +use super::vimawesome; +use nvim_config::NvimConfig; + +pub struct Ui<'a> { + manager: &'a Arc>, +} + +impl<'a> Ui<'a> { + pub fn new(manager: &'a Arc>) -> Ui<'a> { + manager.borrow_mut().reload_store(); + + Ui { manager } + } + + pub fn show>(&mut self, parent: &T) { + let dlg = gtk::Dialog::new_with_buttons( + Some("Plug"), + Some(parent), + gtk::DIALOG_DESTROY_WITH_PARENT, + &[ + ("Cancel", gtk::ResponseType::Cancel.into()), + ("Ok", gtk::ResponseType::Ok.into()), + ], + ); + + dlg.set_default_size(800, 600); + let content = dlg.get_content_area(); + + let header_bar = gtk::HeaderBar::new(); + + let add_plug_btn = gtk::Button::new_with_label("Add.."); + add_plug_btn.get_style_context().map(|c| c.add_class("suggested-action")); + header_bar.pack_end(&add_plug_btn); + + + let enable_swc = gtk::Switch::new(); + enable_swc.set_valign(gtk::Align::Center); + enable_swc.show(); + + header_bar.pack_end(&enable_swc); + + header_bar.set_title("Plug"); + header_bar.set_show_close_button(true); + header_bar.show(); + + dlg.set_titlebar(&header_bar); + + let pages = SettingsPages::new( + clone!(add_plug_btn => move |row_name| if row_name == "plugins" { + add_plug_btn.show(); + } else { + add_plug_btn.hide(); + }), + ); + + enable_swc.set_state(self.manager.borrow().store.is_enabled()); + + let plugins = gtk::Box::new(gtk::Orientation::Vertical, 3); + let plugs_panel = self.fill_plugin_list(&plugins, &self.manager.borrow().store); + + add_vimawesome_tab(&pages, &self.manager, &plugs_panel); + + + let plugins_lbl = gtk::Label::new("Plugins"); + pages.add_page(&plugins_lbl, &plugins, "plugins"); + + add_help_tab( + &pages, + &format!( + "NeovimGtk plugin manager is a GUI for vim-plug.\n\ + It can load plugins from vim-plug configuration if vim-plug sarted and NeovimGtk manager settings is empty.\n\ + When enabled it generate and load vim-plug as simple vim file at startup before init.vim is processed.\n\ + So after enabling this manager you must disable vim-plug configuration in init.vim.\n\ + This manager currently only manage vim-plug configuration and do not any actions on plugin management.\n\ + So you must call all vim-plug (PlugInstall, PlugUpdate, PlugClean) commands manually.\n\ + Current configuration source is {}", + match self.manager.borrow().plug_manage_state { + manager::PlugManageState::NvimGtk => "NeovimGtk config file", + manager::PlugManageState::VimPlug => "loaded from vim-plug", + manager::PlugManageState::Unknown => "Unknown", + } + ), + ); + + let manager_ref = self.manager.clone(); + enable_swc.connect_state_set(move |_, state| { + manager_ref.borrow_mut().store.set_enabled(state); + Inhibit(false) + }); + + let manager_ref = self.manager.clone(); + add_plug_btn.connect_clicked(clone!(dlg => move |_| { + show_add_plug_dlg(&dlg, &manager_ref, &plugs_panel); + })); + + content.pack_start(&*pages, true, true, 0); + content.show_all(); + + + let ok: i32 = gtk::ResponseType::Ok.into(); + if dlg.run() == ok { + let mut manager = self.manager.borrow_mut(); + manager.clear_removed(); + manager.save(); + if let Some(config_path) = NvimConfig::new(manager.generate_config()).generate_config() { + if let Some(path) = config_path.to_str() { + manager.vim_plug.reload(path); + } + } + } + + dlg.destroy(); + } + + fn fill_plugin_list(&self, panel: >k::Box, store: &Store) -> gtk::ListBox { + let scroll = gtk::ScrolledWindow::new(None, None); + scroll.get_style_context().map(|c| c.add_class("view")); + let plugs_panel = gtk::ListBox::new(); + + for (idx, plug_info) in store.get_plugs().iter().enumerate() { + let row = create_plug_row(idx, plug_info, &self.manager); + + plugs_panel.add(&row); + } + + scroll.add(&plugs_panel); + panel.pack_start(&scroll, true, true, 5); + + panel.pack_start( + &create_up_down_btns(&plugs_panel, &self.manager), + false, + true, + 0, + ); + + plugs_panel + } +} + +fn create_up_down_btns( + plugs_panel: >k::ListBox, + manager: &Arc>, +) -> gtk::Box { + let buttons_panel = gtk::Box::new(gtk::Orientation::Horizontal, 5); + let up_btn = gtk::Button::new_from_icon_name("go-up-symbolic", gtk_sys::GTK_ICON_SIZE_BUTTON as i32); + let down_btn = gtk::Button::new_from_icon_name("go-down-symbolic", gtk_sys::GTK_ICON_SIZE_BUTTON as i32); + + up_btn.connect_clicked(clone!(plugs_panel, manager => move |_| { + if let Some(row) = plugs_panel.get_selected_row() { + let idx = row.get_index(); + if idx > 0 { + plugs_panel.unselect_row(&row); + plugs_panel.remove(&row); + plugs_panel.insert(&row, idx - 1); + plugs_panel.select_row(&row); + manager.borrow_mut().move_item(idx as usize, -1); + } + } + })); + + + down_btn.connect_clicked(clone!(plugs_panel, manager => move |_| { + if let Some(row) = plugs_panel.get_selected_row() { + let idx = row.get_index(); + let mut manager = manager.borrow_mut(); + if idx >= 0 && idx < manager.store.plugs_count() as i32 { + plugs_panel.unselect_row(&row); + plugs_panel.remove(&row); + plugs_panel.insert(&row, idx + 1); + plugs_panel.select_row(&row); + manager.move_item(idx as usize, 1); + } + } + })); + + buttons_panel.pack_start(&up_btn, false, true, 0); + buttons_panel.pack_start(&down_btn, false, true, 0); + buttons_panel.set_halign(gtk::Align::Center); + + buttons_panel +} + +fn populate_get_plugins( + query: Option, + get_plugins: >k::Box, + manager: Arc>, + plugs_panel: gtk::ListBox, +) { + let plugs_panel = UiMutex::new(plugs_panel); + let get_plugins = UiMutex::new(get_plugins.clone()); + vimawesome::call(query, move |res| { + let panel = get_plugins.borrow(); + for child in panel.get_children() { + panel.remove(&child); + } + match res { + Ok(list) => { + let result = vimawesome::build_result_panel(&list, move |new_plug| { + add_plugin(&manager, &*plugs_panel.borrow(), new_plug); + }); + panel.pack_start(&result, true, true, 0); + } + Err(e) => { + panel.pack_start(>k::Label::new(format!("{}", e).as_str()), false, true, 0); + error!("{}", e) + } + } + }); +} + +fn create_plug_row( + plug_idx: usize, + plug_info: &PlugInfo, + manager: &Arc>, +) -> gtk::ListBoxRow { + let row = gtk::ListBoxRow::new(); + let row_container = gtk::Box::new(gtk::Orientation::Vertical, 5); + row_container.set_border_width(5); + let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 5); + let label_box = create_plug_label(plug_info); + + + let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + button_box.set_halign(gtk::Align::End); + + let exists_button_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); + + let remove_btn = gtk::Button::new_with_label("Remove"); + exists_button_box.pack_start(&remove_btn, false, true, 0); + + let undo_btn = gtk::Button::new_with_label("Undo"); + + + row_container.pack_start(&hbox, true, true, 0); + hbox.pack_start(&label_box, true, true, 0); + button_box.pack_start(&exists_button_box, false, true, 0); + hbox.pack_start(&button_box, false, true, 0); + + row.add(&row_container); + + + remove_btn.connect_clicked( + clone!(manager, label_box, button_box, exists_button_box, undo_btn => move |_| { + label_box.set_sensitive(false); + button_box.remove(&exists_button_box); + button_box.pack_start(&undo_btn, false, true, 0); + button_box.show_all(); + manager.borrow_mut().store.remove_plug(plug_idx); + }), + ); + + undo_btn.connect_clicked( + clone!(manager, label_box, button_box, exists_button_box, undo_btn => move |_| { + label_box.set_sensitive(true); + button_box.remove(&undo_btn); + button_box.pack_start(&exists_button_box, false, true, 0); + button_box.show_all(); + manager.borrow_mut().store.restore_plug(plug_idx); + }), + ); + + row +} + +fn show_add_plug_dlg>( + parent: &F, + manager: &Arc>, + plugs_panel: >k::ListBox, +) { + if let Some(new_plugin) = plugin_settings_dlg::Builder::new("Add plugin").show(parent) { + add_plugin(manager, plugs_panel, new_plugin); + } +} + +fn add_plugin( + manager: &Arc>, + plugs_panel: >k::ListBox, + new_plugin: PlugInfo, +) -> bool { + let row = create_plug_row(manager.borrow().store.plugs_count(), &new_plugin, manager); + + if manager.borrow_mut().add_plug(new_plugin) { + row.show_all(); + plugs_panel.add(&row); + true + } else { + let dlg = gtk::MessageDialog::new( + None::<>k::Window>, + gtk::DialogFlags::empty(), + gtk::MessageType::Error, + gtk::ButtonsType::Ok, + "Plugin with this name or path already exists", + ); + dlg.run(); + dlg.destroy(); + false + } +} + +fn create_plug_label(plug_info: &PlugInfo) -> gtk::Box { + let label_box = gtk::Box::new(gtk::Orientation::Vertical, 5); + + let name_lbl = gtk::Label::new(None); + name_lbl.set_markup(&format!("{}", plug_info.name)); + name_lbl.set_halign(gtk::Align::Start); + let url_lbl = gtk::Label::new(Some(plug_info.get_plug_path().as_str())); + url_lbl.set_halign(gtk::Align::Start); + + + label_box.pack_start(&name_lbl, true, true, 0); + label_box.pack_start(&url_lbl, true, true, 0); + label_box +} + +fn add_vimawesome_tab( + pages: &SettingsPages, + manager: &Arc>, + plugs_panel: >k::ListBox, +) { + let get_plugins = gtk::Box::new(gtk::Orientation::Vertical, 0); + let spinner = gtk::Spinner::new(); + let get_plugins_lbl = gtk::Label::new("Get Plugins"); + pages.add_page(&get_plugins_lbl, &get_plugins, "get_plugins"); + + let list_panel = gtk::Box::new(gtk::Orientation::Vertical, 0); + let link_button = gtk::Label::new(None); + link_button.set_markup( + "Plugins are taken from: https://vimawesome.com", + ); + let search_entry = gtk::SearchEntry::new(); + + get_plugins.pack_start(&link_button, false, true, 10); + get_plugins.pack_start(&search_entry, false, true, 5); + get_plugins.pack_start(&list_panel, true, true, 0); + list_panel.pack_start(&spinner, true, true, 0); + spinner.start(); + + search_entry.connect_activate(clone!(list_panel, manager, plugs_panel => move |se| { + let spinner = gtk::Spinner::new(); + list_panel.pack_start(&spinner, false, true, 5); + spinner.show(); + spinner.start(); + populate_get_plugins(se.get_text(), &list_panel, manager.clone(), plugs_panel.clone()); + })); + + gtk::idle_add(clone!(manager, plugs_panel => move || { + populate_get_plugins(None, &list_panel, manager.clone(), plugs_panel.clone()); + Continue(false) + })); +} + +fn add_help_tab(pages: &SettingsPages, markup: &str) { + let help = gtk::Box::new(gtk::Orientation::Vertical, 3); + let label = gtk::Label::new(None); + label.set_markup(markup); + help.pack_start(&label, true, false, 0); + + let help_lbl = gtk::Label::new("Help"); + pages.add_page(&help_lbl, &help, "help"); +} + +struct SettingsPages { + categories: gtk::ListBox, + stack: gtk::Stack, + content: gtk::Box, + rows: Rc>>, +} + +impl SettingsPages { + pub fn new(row_selected: F) -> Self { + let content = gtk::Box::new(gtk::Orientation::Horizontal, 5); + let categories = gtk::ListBox::new(); + categories.get_style_context().map(|c| c.add_class("view")); + let stack = gtk::Stack::new(); + stack.set_transition_type(gtk::StackTransitionType::Crossfade); + let rows: Rc>> = + Rc::new(RefCell::new(Vec::new())); + + content.pack_start(&categories, false, true, 0); + content.pack_start(&stack, true, true, 0); + + categories.connect_row_selected( + clone!(stack, rows => move |_, row| if let &Some(ref row) = row { + if let Some(ref r) = rows.borrow().iter().find(|r| r.0 == *row) { + if let Some(child) = stack.get_child_by_name(&r.1) { + stack.set_visible_child(&child); + row_selected(&r.1); + } + + } + }), + ); + + SettingsPages { + categories, + stack, + content, + rows, + } + } + + fn add_page>( + &self, + label: >k::Label, + widget: &W, + name: &'static str, + ) { + let row = gtk::ListBoxRow::new(); + + let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 0); + hbox.set_border_width(12); + hbox.pack_start(label, false, true, 0); + row.add(&hbox); + + self.categories.add(&row); + self.stack.add_named(widget, name); + self.rows.borrow_mut().push((row, name)); + } +} + +impl Deref for SettingsPages { + type Target = gtk::Box; + + fn deref(&self) -> >k::Box { + &self.content + } +} diff --git a/src/plug_manager/vim_plug.rs b/src/plug_manager/vim_plug.rs new file mode 100644 index 0000000..e6327d8 --- /dev/null +++ b/src/plug_manager/vim_plug.rs @@ -0,0 +1,109 @@ +use std::rc::Rc; +use std::cell::{RefCell, RefMut}; + +use neovim_lib::{Neovim, NeovimApi}; + +use nvim::{NeovimClient, ErrorReport}; +use value::ValueMapExt; + +pub struct Manager { + nvim: Option>>, +} + +impl Manager { + pub fn new() -> Self { + Manager { nvim: None } + } + + pub fn initialize(&mut self, nvim: Rc>) { + self.nvim = Some(nvim); + } + + fn nvim(&self) -> Option> { + let nvim_client = self.nvim.as_ref().unwrap(); + if nvim_client.borrow().is_initialized() { + Some(RefMut::map(nvim_client.borrow_mut(), |n| n.nvim_mut())) + } else { + None + } + } + + pub fn get_plugs(&self) -> Result, String> { + if let Some(mut nvim) = self.nvim() { + let g_plugs = nvim.eval("g:plugs").map_err(|e| { + format!("Can't retrive g:plugs map: {}", e) + })?; + + let plugs_map = g_plugs + .as_map() + .ok_or("Can't retrive g:plugs map".to_owned())? + .to_attrs_map()?; + + let g_plugs_order = nvim.eval("g:plugs_order").map_err(|e| format!("{}", e))?; + + let order_arr = g_plugs_order.as_array().ok_or( + "Can't find g:plugs_order array" + .to_owned(), + )?; + + let plugs_info: Vec = order_arr + .iter() + .map(|n| n.as_str()) + .filter_map(|name| if let Some(name) = name { + plugs_map + .get(name) + .and_then(|desc| desc.as_map()) + .and_then(|desc| desc.to_attrs_map().ok()) + .and_then(|desc| { + let uri = desc.get("uri").and_then(|uri| uri.as_str()); + if let Some(uri) = uri { + Some(VimPlugInfo::new(name.to_owned(), uri.to_owned())) + } else { + None + } + }) + } else { + None + }) + .collect(); + Ok(plugs_info.into_boxed_slice()) + } else { + Err("Nvim not initialized".to_owned()) + } + } + + pub fn is_loaded(&self) -> bool { + if let Some(mut nvim) = self.nvim() { + let loaded_plug = nvim.eval("exists('g:loaded_plug')"); + loaded_plug + .ok_and_report(&mut *nvim) + .and_then(|loaded_plug| loaded_plug.as_i64()) + .map_or(false, |loaded_plug| if loaded_plug > 0 { + true + } else { + false + }) + } else { + false + } + } + + pub fn reload(&self, path: &str) { + if let Some(mut nvim) = self.nvim() { + nvim.command(&format!("source {}", path)).report_err(&mut *nvim); + } + } +} + +#[derive(Debug)] +pub struct VimPlugInfo { + pub name: String, + pub uri: String, +} + +impl VimPlugInfo { + pub fn new(name: String, uri: String) -> Self { + VimPlugInfo { name, uri } + } +} + diff --git a/src/plug_manager/vimawesome.rs b/src/plug_manager/vimawesome.rs new file mode 100644 index 0000000..78b53d3 --- /dev/null +++ b/src/plug_manager/vimawesome.rs @@ -0,0 +1,162 @@ +use std::io; +use std::thread; +use std::rc::Rc; +use std::process::{Command, Stdio}; + +use serde_json; + +use gtk; +use gtk::prelude::*; +use glib; + +use super::store::PlugInfo; + +pub fn call(query: Option, cb: F) +where + F: FnOnce(io::Result) + Send + 'static, +{ + thread::spawn(move || { + let mut result = Some(request(query.as_ref().map(|s| s.as_ref()))); + let mut cb = Some(cb); + + glib::idle_add(move || { + cb.take().unwrap()(result.take().unwrap()); + Continue(false) + }) + }); +} + +fn request(query: Option<&str>) -> io::Result { + let child = Command::new("curl") + .arg("-s") + .arg(format!( + "https://vimawesome.com/api/plugins?query={}&page=1", + query.unwrap_or("") + )) + .stdout(Stdio::piped()) + .spawn()?; + + let out = child.wait_with_output()?; + + if out.status.success() { + if out.stdout.is_empty() { + Ok(DescriptionList::empty()) + } else { + let description_list: DescriptionList = serde_json::from_slice(&out.stdout).map_err(|e| { + io::Error::new(io::ErrorKind::Other, e) + })?; + Ok(description_list) + } + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!( + "curl exit with error:\n{}", + match out.status.code() { + Some(code) => format!("Exited with status code: {}", code), + None => "Process terminated by signal".to_owned(), + } + ), + )) + } +} + +pub fn build_result_panel( + list: &DescriptionList, + add_cb: F, +) -> gtk::ScrolledWindow { + let scroll = gtk::ScrolledWindow::new(None, None); + scroll.get_style_context().map(|c| c.add_class("view")); + let panel = gtk::ListBox::new(); + + let cb_ref = Rc::new(add_cb); + for plug in list.plugins.iter() { + let row = create_plug_row(plug, cb_ref.clone()); + + panel.add(&row); + } + + scroll.add(&panel); + scroll.show_all(); + scroll +} + +fn create_plug_row( + plug: &Description, + add_cb: Rc, +) -> gtk::ListBoxRow { + let row = gtk::ListBoxRow::new(); + let row_container = gtk::Box::new(gtk::Orientation::Vertical, 5); + row_container.set_border_width(5); + let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 5); + let label_box = create_plug_label(plug); + + + let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + button_box.set_halign(gtk::Align::End); + + let add_btn = gtk::Button::new_with_label("Install"); + button_box.pack_start(&add_btn, false, true, 0); + + row_container.pack_start(&hbox, true, true, 0); + hbox.pack_start(&label_box, true, true, 0); + hbox.pack_start(&button_box, false, true, 0); + + row.add(&row_container); + + + add_btn.connect_clicked(clone!(plug => move |btn| { + if let Some(ref github_url) = plug.github_url { + btn.set_sensitive(false); + add_cb(PlugInfo::new(plug.name.clone(), github_url.clone())); + } + })); + + row +} + + +fn create_plug_label(plug: &Description) -> gtk::Box { + let label_box = gtk::Box::new(gtk::Orientation::Vertical, 5); + + let name_lbl = gtk::Label::new(None); + name_lbl.set_markup(&format!( + "{} by {}", + plug.name, + plug.author.as_ref().map(|s| s.as_ref()).unwrap_or( + "unknown", + ) + )); + name_lbl.set_halign(gtk::Align::Start); + let url_lbl = gtk::Label::new(None); + if let Some(url) = plug.github_url.as_ref() { + url_lbl.set_markup(&format!("{}", url, url)); + } + url_lbl.set_halign(gtk::Align::Start); + + + label_box.pack_start(&name_lbl, true, true, 0); + label_box.pack_start(&url_lbl, true, true, 0); + label_box +} + +#[derive(Deserialize, Debug)] +pub struct DescriptionList { + pub plugins: Box<[Description]>, +} + +impl DescriptionList { + fn empty() -> DescriptionList { + DescriptionList { + plugins: Box::new([]), + } + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Description { + pub name: String, + pub github_url: Option, + pub author: Option, + pub github_stars: Option, +} diff --git a/src/project.rs b/src/project.rs index 60fe38b..7bb838d 100644 --- a/src/project.rs +++ b/src/project.rs @@ -532,14 +532,9 @@ impl Entry { // ----- Store / Load settings // -use std::path::PathBuf; -use std::fs::File; -use std::io::prelude::*; -use std; +use settings::SettingsLoader; use toml; -const PROJECTS_SETTINGS_FILE: &str = "projects.toml"; - #[derive(Serialize, Deserialize)] struct ProjectSettings { projects: Vec, @@ -564,87 +559,21 @@ impl ProjectEntrySettings { } } -impl ProjectSettings { - fn new(projects: Vec) -> ProjectSettings { - ProjectSettings { projects } - } +impl SettingsLoader for ProjectSettings { + const SETTINGS_FILE: &'static str = "projects.toml"; fn empty() -> ProjectSettings { ProjectSettings { projects: vec![] } } - fn load_from_file(path: &Path) -> Result { - if path.exists() { - let mut file = File::open(path).map_err(|e| format!("{}", e))?; - let mut contents = String::new(); - file.read_to_string(&mut contents) - .map_err(|e| format!("{}", e))?; - toml::from_str(&contents).map_err(|e| format!("{}", e)) - } else { - Ok(ProjectSettings::empty()) - } - } - - fn load_err() -> Result { - let mut toml_path = get_app_config_dir_create()?; - toml_path.push(PROJECTS_SETTINGS_FILE); - ProjectSettings::load_from_file(&toml_path) - } - - fn load() -> ProjectSettings { - match ProjectSettings::load_err() { - Ok(settings) => settings, - Err(e) => { - println!("{}", e); - ProjectSettings::empty() - } - } - } - - fn save_err(&self) -> Result<(), String> { - let mut toml_path = get_app_config_dir_create()?; - toml_path.push(PROJECTS_SETTINGS_FILE); - let mut file = File::create(toml_path).map_err(|e| format!("{}", e))?; - - let contents = toml::to_vec(self).map_err(|e| format!("{}", e))?; - - file.write_all(&contents).map_err(|e| format!("{}", e))?; - - Ok(()) - } - - pub fn save(&self) { - match self.save_err() { - Ok(()) => (), - Err(e) => println!("{}", e), - } + fn from_str(s: &str) -> Result { + toml::from_str(&s).map_err(|e| format!("{}", e)) } } -fn get_app_config_dir_create() -> Result { - let config_dir = get_app_config_dir()?; - - std::fs::create_dir_all(&config_dir) - .map_err(|e| format!("{}", e))?; - - Ok(config_dir) -} - -fn get_app_config_dir() -> Result { - let mut config_dir = get_xdg_config_dir()?; - - config_dir.push("nvim-gtk"); - - Ok(config_dir) -} - -fn get_xdg_config_dir() -> Result { - if let Ok(config_path) = std::env::var("XDG_CONFIG_HOME") { - return Ok(PathBuf::from(config_path)); +impl ProjectSettings { + fn new(projects: Vec) -> ProjectSettings { + ProjectSettings { projects } } - - let mut home_dir = std::env::home_dir() - .ok_or("Impossible to get your home dir!")?; - home_dir.push(".config"); - Ok(home_dir) } + diff --git a/src/settings.rs b/src/settings.rs index 04d36df..6799e5a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -101,3 +101,78 @@ fn monospace_font_changed(mut shell: &mut Shell, state: &mut State) { state.update_font(&mut shell); } } + +use std::path::Path; +use std::fs::File; +use std::io::prelude::*; + +use toml; +use serde; + +use dirs; + +pub trait SettingsLoader: Sized + serde::Serialize { + const SETTINGS_FILE: &'static str; + + fn empty() -> Self; + + fn from_str(s: &str) -> Result; + + fn load() -> Self { + match load_err() { + Ok(settings) => settings, + Err(e) => { + println!("{}", e); + Self::empty() + } + } + } + + fn is_file_exists() -> bool { + if let Ok(mut toml_path) = dirs::get_app_config_dir() { + toml_path.push(Self::SETTINGS_FILE); + toml_path.is_file() + } else { + false + } + } + + fn save(&self) { + match save_err(self) { + Ok(()) => (), + Err(e) => error!("{}", e), + } + } +} + +fn load_from_file(path: &Path) -> Result { + if path.exists() { + let mut file = File::open(path).map_err(|e| format!("{}", e))?; + let mut contents = String::new(); + file.read_to_string(&mut contents).map_err( + |e| format!("{}", e), + )?; + T::from_str(&contents) + } else { + Ok(T::empty()) + } +} + +fn load_err() -> Result { + let mut toml_path = dirs::get_app_config_dir_create()?; + toml_path.push(T::SETTINGS_FILE); + load_from_file(&toml_path) +} + + +fn save_err(sl: &T) -> Result<(), String> { + let mut toml_path = dirs::get_app_config_dir_create()?; + toml_path.push(T::SETTINGS_FILE); + let mut file = File::create(toml_path).map_err(|e| format!("{}", e))?; + + let contents = toml::to_vec::(sl).map_err(|e| format!("{}", e))?; + + file.write_all(&contents).map_err(|e| format!("{}", e))?; + + Ok(()) +} diff --git a/src/shell.rs b/src/shell.rs index 5f33f85..3b912b4 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -79,6 +79,7 @@ pub struct State { options: ShellOptions, detach_cb: Option>>, + nvim_started_cb: Option>>, } impl State { @@ -112,6 +113,7 @@ impl State { options, detach_cb: None, + nvim_started_cb: None, } } @@ -156,6 +158,17 @@ impl State { } } + pub fn set_nvim_started_cb(&mut self, cb: Option) + where + F: FnMut() + Send + 'static, + { + if cb.is_some() { + self.nvim_started_cb = Some(Box::new(RefCell::new(cb.unwrap()))); + } else { + self.nvim_started_cb = None; + } + } + pub fn get_font_desc(&self) -> &FontDescription { self.font_ctx.font_description() } @@ -581,6 +594,14 @@ impl Shell { let mut state = self.state.borrow_mut(); state.set_detach_cb(cb); } + + pub fn set_nvim_started_cb(&self, cb: Option) + where + F: FnMut() + Send + 'static, + { + let mut state = self.state.borrow_mut(); + state.set_nvim_started_cb(cb); + } } impl Deref for Shell { @@ -643,10 +664,7 @@ fn gtk_button_press(shell: &mut State, ui_state: &mut UiState, ev: &EventButton) match ev.get_button() { 1 => mouse_input(shell, "LeftMouse", ev.get_state(), ev.get_position()), - 2 => { - mouse_input(shell, "LeftMouse", ev.get_state(), ev.get_position()); - shell.edit_paste("*"); - } + 2 => mouse_input(shell, "MiddleMouse", ev.get_state(), ev.get_position()), 3 => mouse_input(shell, "RightMouse", ev.get_state(), ev.get_position()), _ => (), } @@ -781,7 +799,7 @@ fn init_nvim_async( fn set_nvim_initialized(nvim: Neovim, state_arc: Arc>) { let mut nvim = Some(nvim); - glib::idle_add(move || { + glib::idle_add(clone!(state_arc => move || { let mut state = state_arc.borrow_mut(); state.nvim.borrow_mut().set_initialized( nvim.take().unwrap(), @@ -789,7 +807,10 @@ fn set_nvim_initialized(nvim: Neovim, state_arc: Arc>) { state.cursor.as_mut().unwrap().start(); Continue(false) - }); + })); + + + idle_cb_call!(state_arc.nvim_started_cb()); } fn draw_initializing(state: &State, ctx: &cairo::Context) { diff --git a/src/ui.rs b/src/ui.rs index 64c0906..28f3f6c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -14,6 +14,24 @@ use settings::Settings; use shell::{Shell, ShellOptions}; use shell_dlg; use project::Projects; +use plug_manager; + +macro_rules! clone { + (@param _) => ( _ ); + (@param $x:ident) => ( $x ); + ($($n:ident),+ => move || $body:expr) => ( + { + $( let $n = $n.clone(); )+ + move || $body + } + ); + ($($n:ident),+ => move |$($p:tt),+| $body:expr) => ( + { + $( let $n = $n.clone(); )+ + move |$(clone!(@param $p),)+| $body + } + ); +} pub struct Ui { initialized: bool, @@ -21,6 +39,7 @@ pub struct Ui { settings: Rc>, shell: Rc>, projects: Rc>, + plug_manager: Arc>, } pub struct Components { @@ -30,8 +49,8 @@ pub struct Components { impl Components { fn new() -> Components { - let save_image = Image::new_from_icon_name("document-open", - gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32); + let save_image = + Image::new_from_icon_name("document-open", gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32); Components { open_btn: ToolButton::new(Some(&save_image), "Open"), @@ -50,6 +69,9 @@ impl Components { impl Ui { pub fn new(options: ShellOptions) -> Ui { + let plug_manager = plug_manager::Manager::new(); + + let plug_manager = Arc::new(UiMutex::new(plug_manager)); let comps = Arc::new(UiMutex::new(Components::new())); let settings = Rc::new(RefCell::new(Settings::new())); let shell = Rc::new(RefCell::new(Shell::new(settings.clone(), options))); @@ -63,6 +85,7 @@ impl Ui { shell, settings, projects, + plug_manager, } } @@ -96,24 +119,29 @@ impl Ui { let projects = self.projects.clone(); header_bar.pack_start(&comps.open_btn); - comps - .open_btn - .connect_clicked(move |_| projects.borrow_mut().show()); + comps.open_btn.connect_clicked( + move |_| projects.borrow_mut().show(), + ); - let save_image = Image::new_from_icon_name("document-save", - gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32); + let save_image = Image::new_from_icon_name( + "document-save", + gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32, + ); let save_btn = ToolButton::new(Some(&save_image), "Save"); let shell = self.shell.clone(); save_btn.connect_clicked(move |_| shell.borrow_mut().edit_save_all()); header_bar.pack_start(&save_btn); - let paste_image = Image::new_from_icon_name("edit-paste", - gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32); + let paste_image = Image::new_from_icon_name( + "edit-paste", + gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32, + ); let paste_btn = ToolButton::new(Some(&paste_image), "Paste"); let shell = self.shell.clone(); paste_btn.connect_clicked(move |_| shell.borrow_mut().edit_paste()); header_bar.pack_start(&paste_btn); + header_bar.set_show_close_button(true); window.set_titlebar(Some(&header_bar)); @@ -134,29 +162,56 @@ impl Ui { shell.grab_focus(); let comps_ref = self.comps.clone(); - shell.set_detach_cb(Some(move || { + shell.set_detach_cb(Some(move || { let comps_ref = comps_ref.clone(); gtk::idle_add(move || { comps_ref.borrow().close_window(); Continue(false) }); })); + + let state_ref = self.shell.borrow().state.clone(); + let plug_manager_ref = self.plug_manager.clone(); + shell.set_nvim_started_cb(Some(move || { + plug_manager_ref.borrow_mut().init_nvim_client( + state_ref.borrow().nvim_clone(), + ); + })); } fn create_main_menu(&self, app: >k::Application) { + let comps = self.comps.clone(); + let plug_manager = self.plug_manager.clone(); + let menu = Menu::new(); + let plugs = MenuItem::new("Plugins", None); + plugs.set_detailed_action("app.Plugins"); + menu.append_item(&plugs); + let about = MenuItem::new("About", None); about.set_detailed_action("app.HelpAbout"); menu.append_item(&about); app.set_app_menu(Some(&menu)); + let plugs_action = SimpleAction::new("Plugins", None); + plugs_action.connect_activate( + clone!(comps => move |_, _| plug_manager::Ui::new(&plug_manager).show( + comps + .borrow() + .window + .as_ref() + .unwrap(), + )), + ); + let about_action = SimpleAction::new("HelpAbout", None); - let comps = self.comps.clone(); about_action.connect_activate(move |_, _| on_help_about(&*comps.borrow())); about_action.set_enabled(true); + app.add_action(&about_action); + app.add_action(&plugs_action); } } @@ -178,13 +233,13 @@ fn gtk_delete(comps: &UiMutex, shell: &RefCell) -> Inhibit { } Inhibit(if shell_dlg::can_close_window(comps, shell) { - let comps = comps.borrow(); - comps.close_window(); - shell.borrow_mut().detach_ui(); - false - } else { - true - }) + let comps = comps.borrow(); + comps.close_window(); + shell.borrow_mut().detach_ui(); + false + } else { + true + }) } pub struct UiMutex {