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, KeyBindError}; use crate::widget::{Widget, WidgetCore, Events}; use crate::foldview::{Foldable, FoldableWidgetExt, ActingExt}; use crate::listview::ListView; use crate::proclist::ProcView; use crate::files::File; use crate::paths; use crate::term; use crate::term::ScreenExt; use crate::keybind::{Bindings, Movement, QuickActionAction}; pub type QuickActionView = ListView>; impl FoldableWidgetExt for ListView> { 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::(); format!("{}{}", crate::term::highlight_color(), action.title.clone() + &queries + "\n") }) .collect::() }); 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 { let mime = &self.content.get(0)?.mime; Ok(format!("QuickActions for MIME: {}", mime)) } fn on_key(&mut self, key: Key) -> HResult<()> { ActingExt::do_key_ext(self,key) } fn render(&self) -> Vec { let (xsize, _) = self.core.coordinates.size_u(); self.content .iter() .fold(Vec::::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::>(); acc.append(&mut alist); acc }) } } impl ActingExt for QuickActionView { type Action = QuickActionAction; fn search_in(&self) -> Bindings { self.core.config().keybinds.quickaction } fn movement(&mut self, movement: &Movement) -> HResult<()> { match movement { Movement::Right => self.run_action(None), _ => Err(KeyBindError::MovementUndefined)? } } fn do_action(&mut self, action: &Self::Action) -> HResult<()> { use crate::keybind::QuickActionAction::*; match action { Close => self.popup_finnished(), SelectOrRun(chr) => { 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()? } } } Ok(()) } } } } impl ListView> { fn render(&self) -> Vec { 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) -> 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; // Fixes crash when "running" first description and nicer to use if self.is_description_selected() { self.toggle_fold()?; return Ok(()); } 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 { CHARS.chars() .position(|ch| ch == letter) .map(|pos| pos + 10) .or_else(|| format!("{}", letter) .parse::() .ok()) } } // shouldn't contain keys used for navigation/activation static CHARS: &str = "bcdefgimoqrstuvxyz"; impl QuickActions { pub fn new(files: Vec, mime: mime::Mime, subpath: &str, description: String, sender: Sender, proc_view: Arc>) -> HResult { let mut actions = files.get_actions(mime.clone(), 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, sender: Sender, core: WidgetCore, proc_view: Arc>) -> 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, mime: mime::Mime, content: Option, lines: usize, folded: bool, actions: Async>, proc_view: Arc> } 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, sync: bool, mime: mime::Mime } impl QuickAction { fn new(path: PathBuf, mime: mime::Mime) -> QuickAction { let title = path.get_title(); let queries = path.get_queries(); let sync = path.get_sync(); QuickAction { path, title, queries, sync, mime } } fn run(&self, files: Vec, core: &WidgetCore, proc_view: Arc>) -> HResult<()> { let answers = self.queries .iter() .fold(Ok(vec![]), |mut acc, query| { // If error occured/input was cancelled just skip querying // Turn into try_fold? 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: Vec = 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; fn get_actions(&self, mime: mime::Mime, subpath: String) -> Async>; } impl QuickFiles for Vec { // Compute the most specific MIME shared by all files fn common_mime(&self) -> Option { 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> { 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; 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::>()[0] .to_string()) .unwrap_or_else(|| String::from("Filename missing!")) } fn get_queries(&self) -> Vec { self.file_stem() .map(|stem| stem .to_string_lossy() .split("?") .collect::>() .iter() .skip(1) // Remove ! in queries from sync actions .map(|q| q.trim_end_matches("!").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) } }