summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorManos Pitsidianakis <el13635@mail.ntua.gr>2019-12-27 15:20:02 +0200
committerManos Pitsidianakis <el13635@mail.ntua.gr>2019-12-27 17:57:48 +0200
commitb964a6a033ec9c197ad8cfd2e6bd4b4208cf23c4 (patch)
tree04a365a74218b1b9a51311a131b4d76d3d16b7a2
parent12509748f6cf7a34e0b15935729c59e31993eb61 (diff)
Plugins WIP #2
-rw-r--r--.gitignore3
-rw-r--r--Cargo.lock11
-rw-r--r--ui/Cargo.toml2
-rw-r--r--ui/src/components/mail/view.rs62
-rw-r--r--ui/src/components/mail/view/envelope.rs2
-rw-r--r--ui/src/components/utilities.rs12
-rw-r--r--ui/src/conf.rs18
-rw-r--r--ui/src/plugins.rs280
-rw-r--r--ui/src/plugins/backend.rs190
-rwxr-xr-xui/src/plugins/python3/ansi-plugin.py48
-rw-r--r--ui/src/plugins/python3/libmeliapi.py173
-rwxr-xr-xui/src/plugins/python3/nntp-backend.py92
-rw-r--r--ui/src/plugins/rpc.rs144
-rw-r--r--ui/src/state.rs14
-rw-r--r--ui/src/types/helpers.rs2
15 files changed, 931 insertions, 122 deletions
diff --git a/.gitignore b/.gitignore
index 8f5ed057..9b1f78b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@ target/
**/*.rs.bk
.gdb_history
*.log
+
+__pycache__/
+*.py[cod]
diff --git a/Cargo.lock b/Cargo.lock
index 8c9356fb..fbde45ea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1383,6 +1383,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"rmp 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_bytes 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1482,6 +1484,14 @@ version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "serde_bytes"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "serde_derive"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2282,6 +2292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
"checksum serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)" = "fec2851eb56d010dc9a21b89ca53ee75e6528bab60c11e89d38390904982da9f"
+"checksum serde_bytes 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "325a073952621257820e7a3469f55ba4726d8b28657e7e36653d1c36dc2c84ae"
"checksum serde_derive 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)" = "46a3223d0c9ba936b61c0d2e3e559e3217dbfb8d65d06d26e8b3c25de38bae3e"
"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d"
"checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97"
diff --git a/ui/Cargo.toml b/ui/Cargo.toml
index ae73b250..020ea1ab 100644
--- a/ui/Cargo.toml
+++ b/ui/Cargo.toml
@@ -28,7 +28,7 @@ libc = {version = "0.2.59", features = ["extra_traits",]}
nix = "0.15.0"
rusqlite = {version = "0.20.0", optional =true }
rmp = "^0.8"
-rmpv = "^0.4.2"
+rmpv = { version = "^0.4.2", features=["with-serde",] }
rmp-serde = "^0.14.0"
[features]
diff --git a/ui/src/components/mail/view.rs b/ui/src/components/mail/view.rs
index 874a3906..a6328eae 100644
--- a/ui/src/components/mail/view.rs
+++ b/ui/src/components/mail/view.rs
@@ -42,6 +42,7 @@ enum ViewMode {
Url,
Attachment(usize),
Raw,
+ Ansi(RawBuffer),
Subview,
ContactSelector(Selector<Card>),
}
@@ -53,6 +54,12 @@ impl Default for ViewMode {
}
impl ViewMode {
+ fn is_ansi(&self) -> bool {
+ match self {
+ ViewMode::Ansi(_) => true,
+ _ => false,
+ }
+ }
fn is_attachment(&self) -> bool {
match self {
ViewMode::Attachment(_) => true,
@@ -315,6 +322,7 @@ impl MailView {
ret.push_str(&attachments[aidx].text());
ret
}
+ ViewMode::Ansi(_) => "Viewing attachment. Press `r` to return \n".to_string(),
}
}
@@ -609,6 +617,17 @@ impl Component for MailView {
};
self.pager = Pager::from_string(text, Some(context), None, None);
}
+ ViewMode::Ansi(ref buf) => {
+ write_string_to_grid(
+ &format!("Viewing `{}`. Press `r` to return", buf.title()),
+ grid,
+ Color::Default,
+ Color::Default,
+ Attr::Default,
+ (set_y(upper_left, y), bottom_right),
+ Some(get_x(upper_left)),
+ );
+ }
_ => {
let text = {
self.attachment_to_text(&body, context)
@@ -633,6 +652,9 @@ impl Component for MailView {
s.draw(grid, (set_y(upper_left, y), bottom_right), context);
}
}
+ ViewMode::Ansi(ref mut buf) => {
+ buf.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
+ }
_ => {
self.pager
.draw(grid, (set_y(upper_left, y), bottom_right), context);
@@ -647,6 +669,11 @@ impl Component for MailView {
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context);
match self.mode {
+ ViewMode::Ansi(ref mut buf) => {
+ if buf.process_event(event, context) {
+ return true;
+ }
+ }
ViewMode::Subview => {
if let Some(s) = self.subview.as_mut() {
if s.process_event(event, context) {
@@ -799,6 +826,7 @@ impl Component for MailView {
}
UIEvent::Input(ref key)
if (self.mode.is_attachment()
+ || self.mode.is_ansi()
|| self.mode == ViewMode::Subview
|| self.mode == ViewMode::Url
|| self.mode == ViewMode::Raw)
@@ -952,8 +980,35 @@ impl Component for MailView {
name_opt = name.as_ref().map(|n| n.clone());
}
if let Ok(binary) = binary {
- let p =
- create_temp_file(&decode(u, None), name_opt, None, true);
+ let p = create_temp_file(
+ &decode(u, None),
+ name_opt.as_ref().map(String::as_str),
+ None,
+ true,
+ );
+ match debug!(context.plugin_manager.activate_hook(
+ "attachment-view",
+ p.path().display().to_string().into_bytes()
+ )) {
+ Ok(crate::plugins::FilterResult::Ansi(s)) => {
+ if let Some(buf) =
+ crate::terminal::ansi::ansi_to_cellbuffer(&s)
+ {
+ let raw_buf = RawBuffer::new(buf, name_opt);
+ self.mode = ViewMode::Ansi(raw_buf);
+ self.dirty = true;
+ return true;
+ }
+ }
+ Ok(crate::plugins::FilterResult::UiMessage(s)) => {
+ context.replies.push_back(UIEvent::Notification(
+ None,
+ s,
+ Some(NotificationType::ERROR),
+ ));
+ }
+ _ => {}
+ }
Command::new(&binary)
.arg(p.path())
.stdin(Stdio::piped())
@@ -1318,6 +1373,8 @@ impl Component for MailView {
|| self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
|| if let ViewMode::ContactSelector(ref s) = self.mode {
s.is_dirty()
+ } else if let ViewMode::Ansi(ref r) = self.mode {
+ r.is_dirty()
} else {
false
}
@@ -1346,6 +1403,7 @@ impl Component for MailView {
let mut our_map = context.settings.shortcuts.envelope_view.key_values();
if !(self.mode.is_attachment()
+ || self.mode.is_ansi()
|| self.mode == ViewMode::Subview
|| self.mode == ViewMode::Raw
|| self.mode == ViewMode::Url)
diff --git a/ui/src/components/mail/view/envelope.rs b/ui/src/components/mail/view/envelope.rs
index 57bfc0d0..6fbae798 100644
--- a/ui/src/components/mail/view/envelope.rs
+++ b/ui/src/components/mail/view/envelope.rs
@@ -437,7 +437,7 @@ impl Component for EnvelopeView {
if let Ok(binary) = binary {
let p = create_temp_file(
&decode(u, None),
- name.as_ref().map(|n| n.clone()),
+ name.as_ref().map(String::as_str),
None,
true,
);
diff --git a/ui/src/components/utilities.rs b/ui/src/components/utilities.rs
index 09bf30ce..c8ea24a7 100644
--- a/ui/src/components/utilities.rs
+++ b/ui/src/components/utilities.rs
@@ -2246,9 +2246,10 @@ impl<T: PartialEq + Debug + Clone + Sync + Send> Selector<T> {
}
}
-#[derive(Debug)]
+#[derive(Debug, Clone, PartialEq)]
pub struct RawBuffer {
pub buf: CellBuffer,
+ title: Option<String>,
cursor: (usize, usize),
dirty: bool,
}
@@ -2329,11 +2330,18 @@ impl Component for RawBuffer {
}
impl RawBuffer {
- pub fn new(buf: CellBuffer) -> Self {
+ pub fn new(buf: CellBuffer, title: Option<String>) -> Self {
RawBuffer {
buf,
+ title,
dirty: true,
cursor: (0, 0),
}
}
+ pub fn title(&self) -> &str {
+ self.title
+ .as_ref()
+ .map(String::as_str)
+ .unwrap_or("untitled")
+ }
}
diff --git a/ui/src/conf.rs b/ui/src/conf.rs
index f20f0eb0..ab15904b 100644
--- a/ui/src/conf.rs
+++ b/ui/src/conf.rs
@@ -148,10 +148,8 @@ impl From<FileAccount> for AccountConf {
let root_tmp = root_path
.components()
.last()
- .unwrap()
- .as_os_str()
- .to_str()
- .unwrap()
+ .and_then(|c| c.as_os_str().to_str())
+ .unwrap_or("")
.to_string();
if !acc.subscribed_folders.contains(&root_tmp) {
acc.subscribed_folders.push(root_tmp);
@@ -339,7 +337,17 @@ impl FileSettings {
e.to_string()
))
})?;
- let backends = melib::backends::Backends::new();
+ let mut backends = melib::backends::Backends::new();
+ let plugin_manager = crate::plugins::PluginManager::new();
+ for (_, p) in s.plugins.clone() {
+ if crate::plugins::PluginKind::Backend == p.kind() {
+ crate::plugins::backend::PluginBackend::register(
+ plugin_manager.listener(),
+ p.clone(),
+ &mut backends,
+ );
+ }
+ }
for (name, acc) in s.accounts {
let FileAccount {
root_folder,
diff --git a/ui/src/plugins.rs b/ui/src/plugins.rs
index a661e853..75092f8e 100644
--- a/ui/src/plugins.rs
+++ b/ui/src/plugins.rs
@@ -19,23 +19,25 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
-use crate::workers::WorkController;
use melib::error::{MeliError, Result};
-use rmpv::{Value, ValueRef};
-use std::any::TypeId;
+use rmpv::Value;
use std::collections::HashMap;
-use std::io::{self, BufRead, BufReader};
-use std::io::{Read, Write};
+use std::io::Write;
use std::os::unix::net::{UnixListener, UnixStream};
-use std::path::{Path, PathBuf};
-use std::process::{Command, Stdio};
-use std::thread;
-use std::thread::ThreadId;
+use std::process::Stdio;
+use uuid::Uuid;
-#[derive(Debug, Clone, Serialize, Deserialize)]
+pub mod backend;
+pub mod rpc;
+pub use rpc::*;
+
+pub const BACKEND_FN: i8 = 0;
+
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum PluginKind {
LongLived,
- Ephemeral,
+ Filter,
+ Backend,
}
impl Default for PluginKind {
@@ -49,13 +51,24 @@ pub struct Plugin {
kind: PluginKind,
executable: String,
name: String,
+ #[serde(default)]
+ hooks: Vec<String>,
+}
+
+impl Plugin {
+ pub fn kind(&self) -> PluginKind {
+ self.kind
+ }
}
#[derive(Debug)]
pub struct PluginManager {
plugins: HashMap<String, Plugin>,
- instances: HashMap<String, std::process::Child>,
+ sessions: HashMap<Uuid, String>,
+ instances: HashMap<Uuid, std::process::Child>,
+ streams: HashMap<Uuid, RpcChannel>,
hooks: HashMap<String, UIHook>,
+ listener: UnixListener,
}
impl Drop for PluginManager {
@@ -68,57 +81,81 @@ impl PluginManager {
pub fn new() -> Self {
let _ = std::fs::remove_file("./soworkfile");
let listener = UnixListener::bind("./soworkfile").unwrap();
- debug!("bound");
- // accept connections and process them, spawning a new thread for each one
- thread::spawn(move || {
- debug!("spawn");
- let stream = listener.accept();
- debug!("socket stream {:?}", &stream);
- match stream {
- Ok((mut stream, _)) => {
- debug!("socket stream {:?}", &stream);
- /* connection succeeded */
- thread::spawn(move || {
- debug!("socket listen {:?}", &stream);
- debug!(initialize(stream));
- //let mut response = Vec::new();
- //debug!(stream.read_to_end(&mut response));
- //loop {
- // debug!("pre-flush 1");
- // stream.flush();
- // debug!("post-flush 1");
- // if debug!(rmpv::decode::value::read_value(&mut stream)).is_err() {
- // return;
- // }
- // debug!("post-read_value");
- // //debug!("socket response {}", unsafe {
- // // String::from_utf8_lossy(&response)
- // //});
- // stream.flush();
- // debug!("post-flush 2");
- // if debug!(rmpv::encode::write_value(
- // &mut stream,
- // &rmpv::Value::String("hello 2 u 2".into())
- // ))
- // .is_err()
- // {
- // return;
- // }
- // debug!("post-write_value");
- //}
- });
- }
- Err(err) => {
- /* connection failed */
- debug!(err);
+ /*
+ debug!("bound");
+ // accept connections and process them, spawning a new thread for each one
+ thread::spawn(move || {
+ debug!("spawn");
+ let stream = listener.accept();
+ debug!("socket stream {:?}", &stream);
+ match stream {
+ Ok((mut stream, _)) => {
+ debug!("socket stream {:?}", &stream);
+ /* connection succeeded */
+ thread::spawn(move || {
+ debug!("socket listen {:?}", &stream);
+ debug!(initialize(stream));
+ //let mut response = Vec::new();
+ //debug!(stream.read_to_end(&mut response));
+ //loop {
+ // debug!("pre-flush 1");
+ // stream.flush();
+ // debug!("post-flush 1");
+ // if debug!(rmpv::decode::value::read_value(&mut stream)).is_err() {
+ // return;
+ // }
+ // debug!("post-read_value");
+ // //debug!("socket response {}", unsafe {
+ // // String::from_utf8_lossy(&response)
+ // //});
+ // stream.flush();
+ // debug!("post-flush 2");
+ // if debug!(rmpv::encode::write_value(
+ // &mut stream,
+ // &rmpv::Value::String("hello 2 u 2".into())
+ // ))
+ // .is_err()
+ // {
+ // return;
+ // }
+ // debug!("post-write_value");
+ //}
+ });
+ }
+ Err(err) => {
+ /* connection failed */
+ debug!(err);
+ }
}
- }
- });
+ });
+ */
+ let mut hooks: HashMap<String, UIHook> = Default::default();
+
+ hooks.insert(
+ "attachment-view".to_string(),
+ UIHook {
+ name: "attachment-view".to_string(),
+ wait_response: true,
+ listeners: Vec::new(),
+ },
+ );
+
+ hooks.insert(
+ "refresh-account".to_string(),
+ UIHook {
+ name: "refresh-account".to_string(),
+ wait_response: false,
+ listeners: Vec::new(),
+ },
+ );
PluginManager {
plugins: Default::default(),
+ sessions: Default::default(),
instances: Default::default(),
- hooks: Default::default(),
+ streams: Default::default(),
+ hooks,
+ listener,
}
}
@@ -128,19 +165,38 @@ impl PluginManager {
PluginKind::LongLived => {
/* spawn thread */
let parts = split_command!(&plugin.executable);
- let mut child = std::process::Command::new(&parts[0])
+ let child = std::process::Command::new(&parts[0])
.args(&parts[1..])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
+ let (mut stream, _) = self.listener.accept()?;
+ /* send init message to plugin to register hooks */
+ let session = Uuid::new_v4();
+ let channel = RpcChannel::new(stream, &session)?;
+
+ for h in &plugin.hooks {
+ self.add_listener(h, session.clone());
+ }
+
+ self.instances.insert(session.clone(), child);
+ self.sessions.insert(session.clone(), plugin.name.clone());
+ self.streams.insert(session.clone(), channel);
+ self.plugins.insert(plugin.name.clone(), plugin);
+ Ok(())
+ }
+ PluginKind::Filter => {
+ let session = Uuid::new_v4();
+ for h in &plugin.hooks {
+ self.add_listener(h, session.clone());
+ }
- /* add thread to workcontroller */
- self.instances.insert(plugin.name.clone(), child);
+ self.sessions.insert(session.clone(), plugin.name.clone());
self.plugins.insert(plugin.name.clone(), plugin);
/* send init message to plugin to register hooks */
Ok(())
}
- PluginKind::Ephemeral => {
+ PluginKind::Backend => {
self.plugins.insert(plugin.name.clone(), plugin);
/* send init message to plugin to register hooks */
Ok(())
@@ -151,57 +207,67 @@ impl PluginManager {
pub fn register_hook(&mut self, hook: UIHook) {
self.hooks.insert(hook.name.clone(), hook);
}
-}
-#[derive(Debug)]
-pub struct UIHook {
- name: String,
- listeners: Vec<String>,
- kind: TypeId,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct PluginGreeting {
- version: String,
-}
+ pub fn add_listener(&mut self, hook: &str, session: Uuid) {
+ self.hooks
+ .entry(hook.to_string())
+ .and_modify(|entry| entry.listeners.push(session));
+ }
-pub fn initialize(mut stream: UnixStream) -> Result<()> {
- let greeting: std::result::Result<PluginGreeting, _> =
- rmp_serde::decode::from_read(&mut stream);
- match debug!(greeting) {
- Ok(greeting) => {
- if greeting.version != "dev" {
- return Err("Plugin is not compatible with our API (dev)".into());
+ pub fn activate_hook(&mut self, hook: &str, bytes: Vec<u8>) -> Result<FilterResult> {
+ debug!("activate_hook {}", hook);
+ debug!("bytes {:?}", &bytes);
+ for l in &self.hooks[hook].listeners {
+ let plugin = &self.plugins[&self.sessions[l]];
+ debug!(&plugin);
+ match &plugin.kind {
+ PluginKind::LongLived => {
+ debug!("listener: {}", l);
+ let channel = self.streams.get_mut(l).unwrap();
+ channel.write_ref(&rmpv::ValueRef::Binary(bytes.as_slice()));
+ let reply: Result<FilterResult> = channel.from_read();
+ return reply;
+ }
+ PluginKind::Filter => {
+ let parts = split_command!(&plugin.executable);
+ let child = std::process::Command::new(&parts[0])
+ .args(&parts[1..])
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()?;
+ let (mut stream, _) = self.listener.accept()?;
+ let mut channel = RpcChannel::new(stream, l)?;
+ channel.write_ref(&rmpv::ValueRef::Binary(bytes.as_slice()));
+ let reply: Result<FilterResult> = channel.from_read();
+ return reply;
+ }
+ k => {
+ debug!("got plugin kind {:?} in hook {}", k, hook);
+ }
}
}
- Err(err) => {
- return Err(MeliError::new(err.to_string()));
- }
+ Err(MeliError::new("no listeners for this hook"))
}
- loop {
- debug!("pre-flush 1");
- stream.flush();
- debug!("post-flush 1");
- if debug!(rmpv::decode::value::read_value(&mut stream)).is_err() {
- break;
- }
- debug!("post-read_value");
- //debug!("socket response {}", unsafe {
- // String::from_utf8_lossy(&response)
- //});
- stream.flush();
- debug!("post-flush 2");
- if debug!(rmpv::encode::write_value(
- &mut stream,
- &rmpv::Value::String("hello 2 u 2".into())
- ))
- .is_err()
- {
- break;
- }
- debug!("post-write_value");
+ pub fn listener(&self) -> UnixListener {
+ self.listener.try_clone().unwrap()
}
+}
- return Ok(());
+#[derive(Debug)]
+pub struct UIHook {
+ name: String,
+ wait_response: bool,
+ listeners: Vec<Uuid>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+#[serde(tag = "t", content = "c")]
+pub enum FilterResult {
+ UiMessage(String),
+ Text(String),
+ Ansi(String),
+ Binary(Vec<u8>),
+ Error(String),
}
diff --git a/ui/src/plugins/backend.rs b/ui/src/plugins/backend.rs
new file mode 100644
index 00000000..2cd701c3
--- /dev/null
+++ b/ui/src/plugins/backend.rs
@@ -0,0 +1,190 @@
+/*
+ * meli - plugins
+ *
+ * Copyright 2019 Manos Pitsidianakis
+ *
+ * This file is part of meli.
+ *
+ * meli is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * meli is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with meli. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use super::*;
+use fnv::FnvHashMap;
+use melib::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext};
+use melib::backends::FolderHash;
+use melib::backends::{
+ Backend, BackendOp, Backends, Folder, MailBackend, RefreshEvent, RefreshEventConsumer,
+};
+use melib::conf::AccountSettings;
+use melib::email::{Envelope, EnvelopeHash, Flag};
+use melib::error::{MeliError, Result};
+use std::collections::BTreeMap;
+use std::sync::{Arc, Mutex, RwLock};
+
+#[derive(Debug)]
+pub struct PluginBackend {
+ plugin: Plugin,
+ child: std::process::Child,
+ channel: Arc<Mutex<RpcChannel>>,
+ is_online: Arc<Mutex<(std::time::Instant, Result<()>)>>,
+}
+
+impl MailBackend for PluginBackend {
+ fn is_online(&self) -> Result<()> {
+ if let Ok(mut is_online) = self.is_online.try_lock() {
+ let now = std::time::Instant::now();
+ if now.duration_since(is_online.0) >= std::time::Duration::new(2, 0) {
+ let mut channel = self.channel.lock().unwrap();
+ channel.write_ref(&rmpv::ValueRef::Ext(BACKEND_FN, b"is_online"))?;
+ debug!(channel.expect_ack())?;
+ let ret: PluginResult<()> = debug!(channel.from_read())?;
+ is_online.0 = now;
+ is_online.1 = ret.into();
+ }
+ is_online.1.clone()
+ } else {
+ Err(MeliError::new("busy"))
+ }
+ }
+
+ fn connect(&mut self) {}
+
+ fn get(&mut self, folder: &Folder) -> Async<Result<Vec<Envelope>>> {
+ let mut w = AsyncBuilder::new();
+ let folder_hash = folder.hash();
+ let channel = self.channel.clone();
+ let handle = {
+ let tx = w.tx();
+ let closure = move |_work_context| {
+ let mut channel = channel.lock().unwrap();
+ channel
+ .write_ref(&rmpv::ValueRef::Ext(BACKEND_FN, b"get"))
+ .unwrap();
+ channel.expect_ack().unwrap();
+ loop {
+ let read_val: Result<PluginResult<Option<Vec<String>>>> =
+ debug!(channel.from_read());
+ match read_val.map(Into::into).and_then(std::convert::identity) {
+ Ok(Some(a)) => {
+ tx.send(AsyncStatus::Payload(Ok(a
+ .into_iter()
+ .filter_map(|s| Envelope::from_bytes(s.as_bytes(), None).ok())
+ .collect::<Vec<Envelope>>())))
+ .unwrap();
+ }
+ Ok(None) => {
+ tx.send(AsyncStatus::Finished).unwrap();
+ return;
+ }
+ Err(err) => {
+ tx.send(AsyncStatus::Payload(Err(err))).unwrap();
+ tx.send(AsyncStatus::Finished).unwrap();
+ return;
+ }
+ };
+ }
+ };
+ Box::new(closure)
+ };
+ w.build(handle)
+ }
+
+ fn refresh(
+ &mut self,
+ _folder_hash: FolderHash,
+ _sender: RefreshEventConsumer,
+ ) -> Result<Async<Result<Vec<RefreshEvent>>>> {
+ Err(MeliError::new("Unimplemented."))
+ }
+ fn watch(
+ &self,
+ sender: RefreshEventConsumer,
+ work_context: WorkContext,
+ ) -> Result<std::thread::ThreadId> {
+ Err(MeliError::new("Unimplemented."))
+ }
+ fn folders(&self) -> Result<FnvHashMap<FolderHash, Folder>> {
+ let mut ret: FnvHashMap<FolderHash, Folder> = Default::default();
+ ret.insert(0, Folder::default());
+ Ok(ret)
+ }
+ fn operation(&self, hash: EnvelopeHash) -> Box<dyn BackendOp> {
+ unimplemented!()
+ }
+
+ fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()> {
+ Err(MeliError::new("Unimplemented."))
+ }
+ fn create_folder(&mut self, name: String) -> Result<Folder> {
+ Err(MeliError::new("Unimplemented."))
+ }
+ fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
+ None
+ }
+ fn as_any(&self) -> &dyn::std::any::Any {
+ self
+ }
+}
+
+impl PluginBackend {
+ pub fn new(
+ listener: UnixListener,
+ plugin: Plugin,
+ _s: &AccountSettings,
+ _is_subscribed: Box<dyn Fn(&str) -> bool>,
+ ) -> Result<Box<dyn MailBackend>> {
+ if plugin.kind != PluginKind::Backend {
+ return Err(MeliError::new(format!(
+ "Error: Plugin `{}` is not a mail backend plugin, it's `{:?}`",
+ &plugin.name, &plugin.kind
+ )));
+ }
+ let parts = split_command!(&plugin.executable);
+ let child = std::process::Command::new(&parts[0])
+ .args(&parts[1..])
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()?;
+ let (mut stream, _) = listener.accept()?;
+ /* send init message to plugin to register hooks */
+ let session = Uuid::new_v4();
+ let channel = RpcChannel::new(stream, &session)?;
+ let now = std::time::Instant::now() - std::time::Duration::from_secs(5);
+
+ Ok(Box::new(PluginBackend {
+ child,
+ plugin,
+ channel: Arc::new(Mutex::new(channel)),
+ is_online: Arc::new(Mutex::new((now, Err(MeliError::new("Unitialized"))))),
+ }))
+ }
+
+ pub fn register(listener: UnixListener, plugin: Plugin, backends: &mut Backends) {
+ backends.register(
+ plugin.name.clone(),
+ Backend {
+ create_fn: Box::new(move || {
+ let plugin = plugin.clone();
+ let listener = listener.try_clone().unwrap();
+ Box::new(move |f, i| {
+ let plugin = plugin.clone();
+ let listener = listener.try_clone().unwrap();
+ PluginBackend::new(listener, plugin, f, i)
+ })
+ }),
+ validate_conf_fn: Box::new(|_| Ok(())),
+ },
+ );
+ }
+}
diff --git a/ui/src/plugins/python3/ansi-plugin.py b/ui/src/plugins/python3/ansi-plugin.py
new file mode 100755
index 00000000..507cae72
--- /dev/null
+++ b/ui/src/plugins/python3/ansi-plugin.py
@@ -0,0 +1,48 @@
+#! /usr/bin/env python3
+"""
+meli - sample plugin
+
+Copyright 2019 Manos Pitsidianakis
+
+This file is part of meli.
+
+meli is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+meli is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with meli. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+import sys
+import subprocess
+print(sys.path, file=sys.stderr)
+from libmeliapi import Client
+
+if __name__ == "__main__":