use termion::event::Key; use notify::{INotifyWatcher, Watcher, DebouncedEvent, RecursiveMode}; use std::io::Write; use std::sync::{Arc, Mutex}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::Duration; use std::path::PathBuf; use std::collections::HashMap; use crate::files::{File, Files}; use crate::listview::ListView; use crate::miller_columns::MillerColumns; use crate::widget::Widget; use crate::tabview::{TabView, Tabbable}; use crate::preview::{Previewer, WillBeWidget}; use crate::fail::{HResult, HError, ErrorLog}; use crate::widget::{Events, WidgetCore}; use crate::proclist::ProcView; #[derive(PartialEq)] pub enum FileBrowserWidgets { FileList(WillBeWidget>), Previewer(Previewer), } impl Widget for FileBrowserWidgets { fn get_core(&self) -> HResult<&WidgetCore> { match self { FileBrowserWidgets::FileList(widget) => widget.get_core(), FileBrowserWidgets::Previewer(widget) => widget.get_core() } } fn get_core_mut(&mut self) -> HResult<&mut WidgetCore> { match self { FileBrowserWidgets::FileList(widget) => widget.get_core_mut(), FileBrowserWidgets::Previewer(widget) => widget.get_core_mut() } } fn refresh(&mut self) -> HResult<()> { match self { FileBrowserWidgets::FileList(widget) => widget.refresh(), FileBrowserWidgets::Previewer(widget) => widget.refresh() } } fn get_drawlist(&self) -> HResult { match self { FileBrowserWidgets::FileList(widget) => widget.get_drawlist(), FileBrowserWidgets::Previewer(widget) => widget.get_drawlist() } } } pub struct FileBrowser { pub columns: MillerColumns, pub cwd: File, selections: HashMap, cached_files: HashMap, core: WidgetCore, watcher: INotifyWatcher, watches: Vec, dir_events: Arc>>, proc_view: Arc>, } impl Tabbable for TabView { fn new_tab(&mut self) -> HResult<()> { let mut tab = FileBrowser::new_cored(&self.active_tab_().core)?; let proc_view = self.active_tab_().proc_view.clone(); tab.proc_view = proc_view; self.push_widget(tab)?; self.active += 1; Ok(()) } fn close_tab(&mut self) -> HResult<()> { self.close_tab_() } fn next_tab(&mut self) -> HResult<()> { self.next_tab_(); Ok(()) } fn get_tab_names(&self) -> Vec> { self.widgets.iter().map(|filebrowser| { let path = filebrowser.cwd.path(); let last_dir = path.components().last().unwrap(); let dir_name = last_dir.as_os_str().to_string_lossy().to_string(); Some(dir_name) }).collect() } fn active_tab(& self) -> & dyn Widget { self.active_tab_() } fn active_tab_mut(&mut self) -> &mut dyn Widget { self.active_tab_mut_() } fn on_next_tab(&mut self) -> HResult<()> { self.active_tab_mut().refresh() } fn on_key_sub(&mut self, key: Key) -> HResult<()> { match key { Key::Char('!') => { let tab_dirs = self.widgets.iter().map(|w| w.cwd.clone()) .collect::>(); self.widgets[self.active].exec_cmd(tab_dirs) } _ => { self.active_tab_mut().on_key(key) } } } } fn watch_dir(rx: Receiver, dir_events: Arc>>, sender: Sender) { std::thread::spawn(move || { for event in rx.iter() { dir_events.lock().unwrap().push(event); sender.send(Events::WidgetReady).unwrap(); } }); } impl FileBrowser { pub fn new_cored(core: &WidgetCore) -> HResult { let cwd = std::env::current_dir().unwrap(); let mut core_m = core.clone(); let mut core_l = core.clone(); let mut core_p = core.clone(); let mut miller = MillerColumns::new(core); miller.set_ratios(vec![20,30,49]); let list_coords = miller.calculate_coordinates()?; core_l.coordinates = list_coords[0].clone(); core_m.coordinates = list_coords[1].clone(); core_p.coordinates = list_coords[2].clone(); let main_path = cwd.ancestors() .take(1) .map(|path| { std::path::PathBuf::from(path) }).last()?; let left_path = main_path.parent().map(|p| p.to_path_buf()); let main_widget = WillBeWidget::new(&core, Box::new(move |_| { let mut list = ListView::new(&core_m, Files::new_from_path(&main_path)?); list.animate_slide_up().log(); Ok(list) })); if let Some(left_path) = left_path { let left_widget = WillBeWidget::new(&core, Box::new(move |_| { let mut list = ListView::new(&core_l, Files::new_from_path(&left_path)?); list.animate_slide_up().log(); Ok(list) })); let left_widget = FileBrowserWidgets::FileList(left_widget); miller.push_widget(left_widget); } let previewer = Previewer::new(&core_p); miller.push_widget(FileBrowserWidgets::FileList(main_widget)); miller.push_widget(FileBrowserWidgets::Previewer(previewer)); miller.refresh().log(); let cwd = File::new_from_path(&cwd).unwrap(); let dir_events = Arc::new(Mutex::new(vec![])); let (tx_watch, rx_watch) = channel(); let watcher = INotifyWatcher::new(tx_watch, Duration::from_secs(2)).unwrap(); watch_dir(rx_watch, dir_events.clone(), core.get_sender()); let proc_view = ProcView::new(core); Ok(FileBrowser { columns: miller, cwd: cwd, selections: HashMap::new(), cached_files: HashMap::new(), core: core.clone(), watcher: watcher, watches: vec![], dir_events: dir_events, proc_view: Arc::new(Mutex::new(proc_view)) }) } pub fn enter_dir(&mut self) -> HResult<()> { let file = self.selected_file()?; if file.is_dir() { self.main_widget_goto(&file).log(); } else { let status = std::process::Command::new("rifle") .args(file.path.file_name()) .status(); match status { Ok(status) => self.show_status(&format!("\"{}\" exited with {}", "rifle", status)).log(), Err(err) => self.show_status(&format!("Can't run this \"{}\": {}", "rifle", err)).log() } } Ok(()) } pub fn main_widget_goto(&mut self, dir: &File) -> HResult<()> { self.cwd = dir.clone(); let dir = dir.clone(); let selected_file = self.get_selection(&dir).ok().cloned(); self.get_files().and_then(|files| self.cache_files(files)).log(); let cached_files = self.get_cached_files(&dir).ok(); let main_widget = self.main_widget_mut()?; main_widget.change_to(Box::new(move |stale, core| { let path = dir.path(); let cached_files = cached_files.clone(); let files = cached_files.or_else(|| { Files::new_from_path_cancellable(&path, stale).ok() })?; let mut list = ListView::new(&core, files); if let Some(file) = &selected_file { list.select_file(file); } Ok(list) })).log(); if let Ok(grand_parent) = self.cwd()?.parent_as_file() { self.left_widget_goto(&grand_parent).log(); } else { self.left_widget_mut()?.set_stale().log(); } Ok(()) } pub fn left_widget_goto(&mut self, dir: &File) -> HResult<()> { self.get_left_files().and_then(|files| self.cache_files(files)).log(); let cached_files = self.get_cached_files(&dir).ok(); let dir = dir.clone(); let left_widget = self.left_widget_mut()?; left_widget.change_to(Box::new(move |stale, core| { let path = dir.path(); let cached_files = cached_files.clone(); let files = cached_files.or_else(|| { Files::new_from_path_cancellable(&path, stale).ok() })?; let list = ListView::new(&core, files); Ok(list) }))?; Ok(()) } pub fn go_back(&mut self) -> HResult<()> { if let Ok(new_cwd) = self.cwd.parent_as_file() { self.main_widget_goto(&new_cwd).log(); } self.refresh() } pub fn update_preview(&mut self) -> HResult<()> { if !self.main_widget()?.ready() { return Ok(()) } let file = self.selected_file()?.clone(); let selection = self.get_selection(&file).ok().cloned(); let cached_files = self.get_cached_files(&file).ok(); let preview = self.preview_widget_mut()?; preview.set_file(&file, selection, cached_files); Ok(()) } pub fn set_left_selection(&mut self) -> HResult<()> { if !self.left_widget()?.ready() { return Ok(()) } let parent = self.cwd()?.parent_as_file(); let left_selection = self.get_selection(&parent?)?; self.left_widget()?.widget()?.lock()?.as_mut()?.select_file(&left_selection); Ok(()) } pub fn get_selection(&self, dir: &File) -> HResult<&File> { Ok(self.selections.get(dir)?) } pub fn get_files(&mut self) -> HResult { Ok(self.main_widget()?.widget()?.lock()?.as_ref()?.content.clone()) } pub fn get_left_files(&mut self) -> HResult { Ok(self.left_widget()?.widget()?.lock()?.as_ref()?.content.clone()) } pub fn cache_files(&mut self, files: Files) -> HResult<()> { let dir = files.directory.clone(); self.cached_files.insert(dir, files); Ok(()) } pub fn get_cached_files(&mut self, dir: &File) -> HResult { Ok(self.cached_files.get(dir)?.clone()) } pub fn save_selection(&mut self) -> HResult<()> { let cwd = self.cwd()?.clone(); if let Ok(main_selection) = self.selected_file() { self.selections.insert(cwd.clone(), main_selection); } if let Ok(left_dir) = self.cwd()?.parent_as_file() { self.selections.insert(left_dir, cwd); } Ok(()) } pub fn cwd(&self) -> HResult<&File> { Ok(&self.cwd) } pub fn set_cwd(&mut self) -> HResult<()> { let cwd = self.cwd()?; std::env::set_current_dir(&cwd.path)?; Ok(()) } pub fn left_dir(&self) -> HResult { let widget = self.left_widget()?.widget()?; let dir = widget.lock()?.as_ref()?.content.directory.clone(); Ok(dir) } fn update_watches(&mut self) -> HResult<()> { if !self.left_widget()?.ready() || !self.main_widget()?.ready() { return Ok(()) } let watched_dirs = self.watches.clone(); let cwd = self.cwd()?.clone(); let left_dir = self.left_dir()?; let preview_dir = self.selected_file().ok().map(|f| f.path); for watched_dir in watched_dirs.iter() { if watched_dir != &cwd.path && watched_dir != &left_dir.path && Some(watched_dir.clone()) != preview_dir { self.watcher.unwatch(&watched_dir).ok(); self.watches.remove_item(&watched_dir); } } if !watched_dirs.contains(&cwd.path) { self.watcher.watch(&cwd.path, RecursiveMode::NonRecursive).unwrap(); self.watches.push(cwd.path); } if !watched_dirs.contains(&left_dir.path) { self.watcher.watch(&left_dir.path, RecursiveMode::NonRecursive).unwrap(); self.watches.push(left_dir.path); } if let Some(preview_dir) = preview_dir { if !watched_dirs.contains(&preview_dir) && preview_dir.is_dir() { self.watcher.watch(&preview_dir, RecursiveMode::NonRecursive).unwrap(); self.watches.push(preview_dir); } } Ok(()) } fn handle_dir_events(&mut self) -> HResult<()> { let dir_events = self.dir_events.clone(); for event in dir_events.lock()?.iter() { let main_widget = self.main_widget()?.widget()?; let mut main_widget = main_widget.lock()?; let main_result = main_widget.as_mut()?.content.handle_event(event); let left_widget = self.left_widget()?.widget()?; let mut left_files = left_widget.lock()?; let left_result = left_files.as_mut()?.content.handle_event(event); match main_result { Err(HError::WrongDirectoryError { .. }) => { match left_result { Err(HError::WrongDirectoryError { .. }) => { let preview = self.preview_widget_mut()?; preview.reload(); }, _ => {} } }, _ => {} } } dir_events.lock()?.clear(); Ok(()) } pub fn selected_file(&self) -> HResult { let widget = self.main_widget()?.widget()?; let file = widget.lock()?.as_ref()?.selected_file().clone(); Ok(file) } pub fn main_widget(&self) -> HResult<&WillBeWidget>> { let widget = match self.columns.get_main_widget()? { FileBrowserWidgets::FileList(filelist) => Ok(filelist), _ => { return HError::wrong_widget("previewer", "filelist"); } }; widget } pub fn main_widget_mut(&mut self) -> HResult<&mut WillBeWidget>> { let widget = match self.columns.get_main_widget_mut()? { FileBrowserWidgets::FileList(filelist) => Ok(filelist), _ => { return HError::wrong_widget("previewer", "filelist"); } }; widget } pub fn left_widget(&self) -> HResult<&WillBeWidget>> { let widget = match self.columns.get_left_widget()? { FileBrowserWidgets::FileList(filelist) => Ok(filelist), _ => { return HError::wrong_widget("previewer", "filelist"); } }; widget } pub fn left_widget_mut(&mut self) -> HResult<&mut WillBeWidget>> { let widget = match self.columns.get_left_widget_mut()? { FileBrowserWidgets::FileList(filelist) => Ok(filelist), _ => { return HError::wrong_widget("previewer", "filelist"); } }; widget } pub fn preview_widget(&self) -> HResult<&Previewer> { match self.columns.get_right_widget()? { FileBrowserWidgets::Previewer(previewer) => Ok(previewer), _ => { return HError::wrong_widget("filelist", "previewer"); } } } pub fn preview_widget_mut(&mut self) -> HResult<&mut Previewer> { match self.columns.get_right_widget_mut()? { FileBrowserWidgets::Previewer(previewer) => Ok(previewer), _ => { return HError::wrong_widget("filelist", "previewer"); } } } pub fn quit_with_dir(&self) -> HResult<()> { let cwd = self.cwd()?.clone().path; let selected_file = self.selected_file()?; let selected_file = selected_file.path.to_string_lossy(); let mut filepath = dirs_2::home_dir()?; filepath.push(".hunter_cwd"); let output = format!("HUNTER_CWD=\"{}\"\nF=\"{}\"", cwd.to_str()?, selected_file); let mut file = std::fs::File::create(filepath)?; file.write(output.as_bytes())?; HError::quit() } pub fn turbo_cd(&mut self) -> HResult<()> { let dir = self.minibuffer("cd: "); match dir { Ok(dir) => { self.columns.widgets.widgets.clear(); let cwd = File::new_from_path(&std::path::PathBuf::from(&dir))?; self.cwd = cwd; let dir = std::path::PathBuf::from(&dir); let left_dir = std::path::PathBuf::from(&dir); let mcore = self.main_widget()?.get_core()?.clone(); let lcore = self.left_widget()?.get_core()?.clone();; let middle = WillBeWidget::new(&self.core, Box::new(move |_| { let files = Files::new_from_path(&dir.clone())?; let listview = ListView::new(&mcore, files); Ok(listview) })); let middle = FileBrowserWidgets::FileList(middle); let left = WillBeWidget::new(&self.core, Box::new(move |_| { let files = Files::new_from_path(&left_dir.parent()?)?; let listview = ListView::new(&lcore, files); Ok(listview) })); let left = FileBrowserWidgets::FileList(left); self.columns.push_widget(left); self.columns.push_widget(middle); }, Err(_) => {} } Ok(()) } fn exec_cmd(&mut self, tab_dirs: Vec) -> HResult<()> { let filename = self.selected_file()?.name.clone(); let widget = self.main_widget()?.widget()?; let widget = widget.lock()?; let selected_files = widget.as_ref()?.content.get_selected(); let file_names = selected_files.iter().map(|f| f.name.clone()).collect::>(); let cmd = self.minibuffer("exec:")?; self.show_status(&format!("Running: \"{}\"", &cmd)).log(); let mut cmd = if file_names.len() == 0 { cmd.replace("$s", &format!("{}", &filename)) } else { let args = file_names.iter().map(|f| { format!(" \"{}\" ", f) }).collect::(); cmd.replace("$s", &args) }; for (i, tab_dir) in tab_dirs.iter().enumerate() { let tab_identifier = format!("${}", i); let tab_path = tab_dir.path.to_string_lossy(); cmd = cmd.replace(&tab_identifier, &tab_path); } self.proc_view.lock()?.run_proc(&cmd)?; Ok(()) } } impl Widget for FileBrowser { fn get_core(&self) -> HResult<&WidgetCore> { Ok(&self.core) } fn get_core_mut(&mut self) -> HResult<&mut WidgetCore> { Ok(&mut self.core) } fn render_header(&self) -> HResult { let xsize = self.get_coordinates()?.xsize(); let file = self.selected_file()?; let name = &file.name; let color = if file.is_dir() || file.color.is_none() { crate::term::highlight_color() } else { crate::term::from_lscolor(file.color.as_ref().unwrap()) }; let path = file.path.parent()?.to_string_lossy().to_string(); let pretty_path = format!("{}/{}{}", path, &color, name ); let sized_path = crate::term::sized_string(&pretty_path, xsize); Ok(sized_path) } fn render_footer(&self) -> HResult { let xsize = self.get_coordinates()?.xsize(); let ypos = self.get_coordinates()?.position().y(); let file = self.selected_file()?; let permissions = file.pretty_print_permissions().unwrap_or("NOPERMS".into()); let user = file.pretty_user().unwrap_or("NOUSER".into()); let group = file.pretty_group().unwrap_or("NOGROUP".into()); let mtime = file.pretty_mtime().unwrap_or("NOMTIME".into()); let main_widget = self.main_widget()?.widget()?; let selection = main_widget.lock()?.as_ref().unwrap().get_selection(); let file_count = main_widget.lock()?.as_ref().unwrap().content.len(); let file_count = format!("{}", file_count); let digits = file_count.len(); let file_count = format!("{:digits$}/{:digits$}", selection, file_count, digits = digits); let count_xpos = xsize - file_count.len() as u16; let count_ypos = ypos + self.get_coordinates()?.ysize(); Ok(format!("{} {}:{} {} {} {}", permissions, user, group, mtime, crate::term::goto_xy(count_xpos, count_ypos), file_count)) } fn refresh(&mut self) -> HResult<()> { //self.proc_view.lock()?.set_coordinates(self.get_coordinates()?); self.handle_dir_events().ok(); self.columns.refresh().ok(); self.set_left_selection().log(); self.save_selection().log(); self.set_cwd().ok(); self.update_watches().ok(); self.update_preview().ok(); self.columns.refresh().ok(); Ok(()) } fn get_drawlist(&self) -> HResult { let left = self.left_widget()?.get_drawlist()?; let main = self.main_widget()?.get_drawlist()?; let prev = self.preview_widget()?.get_drawlist()?; Ok(left + &main + &prev) } fn on_key(&mut self, key: Key) -> HResult<()> { match key { Key::Char('/') => { self.turbo_cd()?; }, Key::Char('Q') => { self.quit_with_dir()?; }, Key::Right | Key::Char('f') => { self.enter_dir()?; }, Key::Left | Key::Char('b') => { self.go_back()?; }, Key::Char('w') => { self.proc_view.lock()?.popup()?; } , _ => { self.main_widget_mut()?.on_key(key)?; }, } self.update_preview()?; Ok(()) } } impl PartialEq for FileBrowser { fn eq(&self, other: &FileBrowser) -> bool { if self.columns == other.columns && self.cwd == other.cwd { true } else { false } } }