summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrabite <rabite@posteo.de>2019-05-28 01:14:53 +0200
committerrabite <rabite@posteo.de>2019-05-28 01:25:46 +0200
commit2a2fc2bc6df906554b8899641fd197d6354cf972 (patch)
treec38205a9d646b5f869a469cc21097372170c7b82
parent8b89183f74a3adefe0f7bcc6b4975afe5a982ec6 (diff)
add quick_actions.rs
-rw-r--r--src/quick_actions.rs521
1 files changed, 521 insertions, 0 deletions
diff --git a/src/quick_actions.rs b/src/quick_actions.rs
new file mode 100644
index 0000000..1a65c8a
--- /dev/null
+++ b/src/quick_actions.rs
@@ -0,0 +1,521 @@
+use mime_guess::Mime;
+use termion::event::Key;
+
+use async_value::Async;
+
+use std::path::PathBuf;
+use std::sync::{
+ Arc, Mutex,
+ mpsc::Sender,
+};
+use std::ffi::OsString;
+use std::str::FromStr;
+
+
+use crate::fail::{HResult, HError};
+use crate::widget::{Widget, WidgetCore, Events};
+use crate::foldview::{Foldable, FoldableWidgetExt};
+use crate::listview::ListView;
+use crate::proclist::ProcView;
+use crate::files::File;
+use crate::paths;
+use crate::term;
+use crate::term::ScreenExt;
+
+
+pub type QuickActionView = ListView<Vec<QuickActions>>;
+
+impl FoldableWidgetExt for ListView<Vec<QuickActions>> {
+ fn on_refresh(&mut self) -> HResult<()> {
+ for action in self.content.iter_mut() {
+ action.actions.pull_async().ok();
+ let content = action.actions
+ .get()
+ .map(|actions| {
+ actions
+ .iter()
+ .map(|action| {
+ let queries = action.queries
+ .iter()
+ .map(|q| String::from(":") + &q.to_string() + "?")
+ .collect::<String>();
+ format!("{}{}",
+ crate::term::highlight_color(),
+ action.title.clone() + &queries + "\n")
+ })
+ .collect::<String>()
+ });
+
+ if let Ok(content) = content {
+ let content = format!("{}{}\n{}",
+ crate::term::status_bg(),
+ action.description, content);
+ let lines = content.lines().count();
+ action.content = Some(content);
+ action.lines = lines;
+ }
+ }
+
+
+ Ok(())
+ }
+
+ fn render_header(&self) -> HResult<String> {
+ let mime = &self.content.get(0)?.mime;
+ Ok(format!("QuickActions for MIME: {}", mime))
+ }
+ fn render_footer(&self) -> HResult<String> {
+ Ok(String::from(""))
+ }
+
+ fn on_key(&mut self, key: Key) -> HResult<()> {
+ match key {
+ Key::Char('a') |
+ Key::Char('h') => HError::popup_finnished()?,
+ // undefined key causes parent to handle move up/down
+ Key::Char('j') => HError::undefined_key(key)?,
+ Key::Char('k') => HError::undefined_key(key)?,
+ Key::Char('l') => self.run_action(None),
+ key @ Key::Char(_) => {
+ let chr = match key {
+ Key::Char(key) => key,
+ // some other key that becomes None with letter_to_num()
+ _ => 'x'
+ };
+
+ let num = self.letter_to_num(chr);
+
+ if let Some(num) = num {
+ // only select the action at first, to prevent accidents
+ if self.get_selection() != num {
+ self.set_selection(num);
+ return Ok(());
+ // activate the action the second time the key is pressed
+ } else {
+ if self.is_description_selected() {
+ self.toggle_fold()?;
+ } else {
+ self.run_action(Some(num))?;
+ HError::popup_finnished()?
+ }
+ }
+ }
+
+ // Was a valid key, but not used, don't handle at parent
+ return Ok(());
+ }
+ _ => HError::undefined_key(key)?
+ }?;
+
+ HError::popup_finnished()?
+ }
+
+ fn render(&self) -> Vec<String> {
+ let (xsize, _) = self.core.coordinates.size_u();
+ self.content
+ .iter()
+ .fold(Vec::<String>::new(), |mut acc, atype| {
+ let mut alist = atype.render()
+ .iter()
+ .enumerate()
+ .map(|(i, line)| {
+ term::sized_string_u(&format!("[{}]: {}",
+ self.num_to_letter(acc.len() + i),
+ line),
+ xsize)
+ })
+ .collect::<Vec<_>>();
+
+ acc.append(&mut alist);
+ acc
+ })
+ }
+}
+
+
+impl ListView<Vec<QuickActions>> {
+ fn render(&self) -> Vec<String> {
+ vec![]
+ }
+
+ fn is_description_selected(&self) -> bool {
+ if let Some(current_fold) = self.current_fold() {
+ let fold_start_pos = self.fold_start_pos(current_fold);
+ let selection = self.get_selection();
+ selection == fold_start_pos
+ } else {
+ false
+ }
+ }
+
+ fn run_action(&mut self, num: Option<usize>) -> HResult<()> {
+ num.map(|num| self.set_selection(num));
+
+ let current_fold = self.current_fold()?;
+ let fold_start_pos = self.fold_start_pos(current_fold);
+ let selection = self.get_selection();
+ let selected_action_index = selection - fold_start_pos;
+
+ self.content[current_fold]
+ .actions
+ // -1 because fold description takes one slot
+ .get()?[selected_action_index-1]
+ .run(self.content[0].files.clone(),
+ &self.core,
+ self.content[0].proc_view.clone())?;
+
+ self.core.screen()?.clear()?;
+ Ok(())
+ }
+
+ fn num_to_letter(&self, num: usize) -> String {
+ if num > 9 && num < (CHARS.chars().count() + 10) {
+ // subtract number keys
+ CHARS.chars()
+ .skip(num-10)
+ .take(1)
+ .collect()
+ } else if num < 10{
+ format!("{}", num)
+ } else {
+ String::from("..")
+ }
+
+ }
+
+ fn letter_to_num(&self, letter: char) -> Option<usize> {
+ CHARS.chars()
+ .position(|ch| ch == letter)
+ .map(|pos| pos + 10)
+ .or_else(||
+ format!("{}", letter)
+ .parse::<usize>()
+ .ok())
+ }
+}
+
+// shouldn't contain keys used for navigation/activation
+static CHARS: &str = "bcdefgimoqrstuvxyz";
+
+impl QuickActions {
+ pub fn new(files: Vec<File>,
+ mime: mime::Mime,
+ subpath: &str,
+ description: String,
+ sender: Sender<Events>,
+ proc_view: Arc<Mutex<ProcView>>) -> HResult<QuickActions> {
+ let mut actions = files.get_actions(mime.clone(), dbg!(subpath.to_string()));
+
+ actions.on_ready(move |_,_| {
+ sender.send(Events::WidgetReady).ok();
+ Ok(())
+ })?;
+
+ actions.run()?;
+
+
+ Ok(QuickActions {
+ description: description,
+ files: files,
+ mime: mime,
+ content: None,
+ lines: 1,
+ folded: false,
+ actions: actions,
+ proc_view: proc_view
+ })
+ }
+}
+
+pub fn open(files: Vec<File>,
+ sender: Sender<Events>,
+ core: WidgetCore,
+ proc_view: Arc<Mutex<ProcView>>) -> HResult<()> {
+ let mime = files.common_mime()
+ .unwrap_or_else(|| Mime::from_str("*/").unwrap());
+
+
+ let act = QuickActions::new(files.clone(),
+ mime.clone(),
+ "",
+ String::from("UniActions"),
+ sender.clone(),
+ proc_view.clone()).unwrap();
+
+ let mut action_view: QuickActionView = ListView::new(&core, vec![]);
+ action_view.content = vec![act];
+
+
+ let subdir = mime.type_().as_str();
+ let act_base = QuickActions::new(files.clone(),
+ mime.clone(),
+ subdir,
+ String::from("BaseActions"),
+ sender.clone(),
+ proc_view.clone());
+
+ let subdir = &format!("{}/{}",
+ mime.type_().as_str(),
+ mime.subtype().as_str());
+ let act_sub = QuickActions::new(files,
+ mime.clone(),
+ subdir,
+ String::from("SubActions"),
+ sender,
+ proc_view);
+
+ act_base.map(|act| action_view.content.push(act)).ok();
+ act_sub.map(|act| action_view.content.push(act)).ok();
+
+ action_view.popup()
+}
+
+
+#[derive(Debug)]
+pub struct QuickActions {
+ description: String,
+ files: Vec<File>,
+ mime: mime::Mime,
+ content: Option<String>,
+ lines: usize,
+ folded: bool,
+ actions: Async<Vec<QuickAction>>,
+ proc_view: Arc<Mutex<ProcView>>
+}
+
+impl Foldable for QuickActions {
+ fn description(&self) -> &str {
+ &self.description
+ }
+
+ fn render_description(&self) -> String {
+ format!("{}{}",
+ term::status_bg(),
+ &self.description)
+ }
+
+ fn content(&self) -> Option<&String> {
+ self.content.as_ref()
+ }
+
+ fn lines(&self) -> usize {
+ if self.folded
+ { 1 } else
+ { self.lines }
+ }
+
+ fn toggle_fold(&mut self) {
+ self.folded = !self.folded;
+ }
+
+ fn is_folded(&self) -> bool {
+ self.folded
+ }
+}
+
+
+
+
+
+#[derive(Debug)]
+pub struct QuickAction {
+ path: PathBuf,
+ title: String,
+ queries: Vec<String>,
+ sync: bool,
+ mime: mime::Mime
+}
+
+impl QuickAction {
+ fn new(path: PathBuf, mime: mime::Mime) -> QuickAction {
+ let title = path.get_title();
+ let queries = dbg!(path.get_queries());
+ let sync = dbg!(path.get_sync());
+
+ QuickAction {
+ path,
+ title,
+ queries,
+ sync,
+ mime
+ }
+ }
+
+ fn run(&self,
+ files: Vec<File>,
+ core: &WidgetCore,
+ proc_view: Arc<Mutex<ProcView>>) -> HResult<()> {
+
+ let answers = self.queries
+ .iter()
+ .fold(Ok(vec![]), |mut acc, query| {
+ // If error occured/input was cancelled just skip querying
+ if acc.is_err() { return acc; }
+
+ match core.minibuffer(query) {
+ Err(HError::MiniBufferEmptyInput) => {
+ acc.as_mut().map(|acc| acc.push((OsString::from(query),
+ OsString::from("")))).ok();
+ acc
+ }
+ Ok(input) => {
+ acc.as_mut().map(|acc| acc.push((OsString::from(query),
+ OsString::from(input)))).ok();
+ acc
+ }
+ Err(err) => Err(err)
+ }
+ })?;
+
+ let cwd = files.get(0)?.parent_as_file()?;
+
+ let files = files.iter()
+ .map(|f| OsString::from(&f.path))
+ .collect();
+
+
+
+ if self.sync {
+ std::process::Command::new(&self.path)
+ .args(files)
+ .envs(answers)
+ .spawn()?
+ .wait()?;
+ Ok(())
+ } else {
+ let cmd = crate::proclist::Cmd {
+ cmd: std::ffi::OsString::from(&self.path),
+ args: Some(files),
+ vars: Some(answers),
+ short_cmd: None,
+ cwd: cwd,
+ cwd_files: None,
+ tab_files: None,
+ tab_paths: None
+ };
+
+ proc_view
+ .lock()
+ .map(|mut proc_view| {
+ proc_view.run_proc_raw(cmd)
+ })??;
+
+ Ok(())
+ }
+ }
+}
+
+
+
+pub trait QuickFiles {
+ fn common_mime(&self) -> Option<Mime>;
+ fn get_actions(&self, mime: mime::Mime, subpath: String) -> Async<Vec<QuickAction>>;
+}
+
+impl QuickFiles for Vec<File> {
+ // Compute the most specific MIME shared by all files
+ fn common_mime(&self) -> Option<Mime> {
+ let first_mime = self
+ .get(0)?
+ .get_mime();
+
+
+ self.iter()
+ .fold(first_mime, |common_mime, file| {
+ let cur_mime = file.get_mime();
+
+ if &cur_mime == &common_mime {
+ cur_mime
+ } else {
+
+ // MIMEs differ, find common base
+
+ match (cur_mime, common_mime) {
+ (Some(cur_mime), Some(common_mime)) => {
+ // Differ in suffix?
+
+ if cur_mime.type_() == common_mime.type_()
+ && cur_mime.subtype() == common_mime.subtype()
+ {
+ Mime::from_str(&format!("{}/{}",
+ cur_mime.type_().as_str(),
+ cur_mime.subtype().as_str()))
+ .ok()
+ }
+
+ // Differ in subtype?
+
+ else if cur_mime.type_() == common_mime.type_() {
+ Mime::from_str(&format!("{}/",
+ cur_mime.type_()
+ .as_str()))
+ .ok()
+
+ // Completely different MIME types
+
+ } else {
+ None
+ }
+ }
+ _ => None
+ }
+ }
+ })
+ }
+
+ fn get_actions(&self, mime: mime::Mime, subpath: String) -> Async<Vec<QuickAction>> {
+ Async::new(move |_| {
+ let mut apath = paths::actions_path()?;
+ apath.push(subpath);
+ Ok(std::fs::read_dir(apath)?
+ .filter_map(|file| {
+ let path = file.ok()?.path();
+ if !path.is_dir() {
+ Some(QuickAction::new(path, mime.clone()))
+ } else {
+ None
+ }
+ }).collect())
+ })
+ }
+}
+
+
+pub trait QuickPath {
+ fn get_title(&self) -> String;
+ fn get_queries(&self) -> Vec<String>;
+ fn get_sync(&self) -> bool;
+}
+
+impl QuickPath for PathBuf {
+ fn get_title(&self) -> String {
+ self.file_stem()
+ .map(|stem| stem
+ .to_string_lossy()
+ .splitn(2, "?")
+ .collect::<Vec<&str>>()[0]
+ .to_string())
+ .unwrap_or_else(|| String::from("Filename missing!"))
+ }
+
+ fn get_queries(&self) -> Vec<String> {
+ self.file_stem()
+ .map(|stem| stem
+ .to_string_lossy()
+ .split("?")
+ .collect::<Vec<&str>>()
+ .iter()
+ .skip(1)
+ .map(|q| q.to_string())
+ .collect())
+ .unwrap_or_else(|| vec![])
+ }
+
+ fn get_sync(&self) -> bool {
+ self.file_stem()
+ .map(|stem| stem
+ .to_string_lossy()
+ .ends_with("!"))
+ .unwrap_or(false)
+ }
+}