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 std::ffi::OsString; use crate::files::{File, Files, PathBufExt}; use crate::listview::ListView; use crate::hbox::HBox; use crate::widget::Widget; use crate::dirty::Dirtyable; use crate::tabview::{TabView, Tabbable}; use crate::preview::{Previewer, AsyncWidget}; use crate::fail::{HResult, HError, ErrorLog}; use crate::widget::{Events, WidgetCore}; use crate::proclist::ProcView; use crate::bookmarks::BMPopup; use crate::term; use crate::term::ScreenExt; use crate::foldview::LogView; use crate::coordinates::Coordinates; #[derive(PartialEq)] pub enum FileBrowserWidgets { FileList(AsyncWidget>), 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 set_coordinates(&mut self, coordinates: &Coordinates) -> HResult<()> { match self { FileBrowserWidgets::FileList(widget) => widget.set_coordinates(coordinates), FileBrowserWidgets::Previewer(widget) => widget.set_coordinates(coordinates), } } 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: HBox, pub cwd: File, pub prev_cwd: Option, selections: HashMap, cached_files: HashMap, core: WidgetCore, watcher: INotifyWatcher, watches: Vec, dir_events: Arc>>, proc_view: Arc>, bookmarks: Arc>, log_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(); let bookmarks = self.active_tab_().bookmarks.clone(); let log_view = self.active_tab_().log_view.clone(); tab.proc_view = proc_view; tab.bookmarks = bookmarks; tab.log_view = log_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 goto_tab(&mut self, index: usize) -> HResult<()> { self.goto_tab_(index) } 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_tab_switch(&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::>(); let selected_files = self .widgets .iter() .map(|w| { w.selected_files() .map_err(|_| Vec::::new()) .unwrap() }).collect(); self.widgets[self.active].exec_cmd(tab_dirs, selected_files) } _ => { 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 columns = HBox::new(core); columns.set_ratios(vec![20,30,49]); let list_coords = columns.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 = AsyncWidget::new(&core, Box::new(move |_| { let mut list = ListView::new(&core_m, Files::new_from_path(&main_path)?); list.animate_slide_up().log(); list.content.meta_all(); Ok(list) })); if let Some(left_path) = left_path { let left_widget = AsyncWidget::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); columns.push_widget(left_widget); } let previewer = Previewer::new(&core_p); columns.push_widget(FileBrowserWidgets::FileList(main_widget)); columns.push_widget(FileBrowserWidgets::Previewer(previewer)); columns.set_active(1).log(); columns.refresh().log(); let cwd = File::new_from_path(&cwd, None).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); let bookmarks = BMPopup::new(&core); let log_view = LogView::new(&core, vec![]); Ok(FileBrowser { columns: columns, cwd: cwd, prev_cwd: None, 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)), bookmarks: Arc::new(Mutex::new(bookmarks)), log_view: Arc::new(Mutex::new(log_view)) }) } pub fn enter_dir(&mut self) -> HResult<()> { let file = self.selected_file()?; if file.is_dir() { match file.is_readable() { Ok(true) => {}, Ok(false) => { let status = format!("{}Stop right there, cowboy! Check your permisions!", term::color_red()); self.show_status(&status).log(); return Ok(()); } err @ Err(_) => err.log() } self.main_widget_goto(&file).log(); } else { self.core.get_sender().send(Events::InputEnabled(false))?; let status = std::process::Command::new("rifle") .args(file.path.file_name()) .status(); self.clear().log(); self.core.screen.cursor_hide().log(); self.core.get_sender().send(Events::InputEnabled(true))?; 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 open_bg(&mut self) -> HResult<()> { let cwd = self.cwd()?; let file = self.selected_file()?; let cmd = crate::proclist::Cmd { cmd: OsString::from(file.strip_prefix(&cwd)), short_cmd: None, args: None, cwd: cwd.clone(), cwd_files: None, tab_files: None, tab_paths: None }; self.proc_view.lock()?.run_proc_raw(cmd)?; Ok(()) } pub fn main_widget_goto(&mut self, dir: &File) -> HResult<()> { let dir = dir.clone(); let selected_file = self.get_selection(&dir).ok().cloned(); self.get_files().and_then(|files| self.cache_files(files)).log(); self.get_left_files().and_then(|files| self.cache_files(files)).log(); let cached_files = self.get_cached_files(&dir).ok(); self.prev_cwd = Some(self.cwd.clone()); self.cwd = dir.clone(); let main_async_widget = self.main_async_widget_mut()?; main_async_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.clone()).ok() })?; let mut list = ListView::new(&core, files); list.content.meta_set_fresh().log(); 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_async_widget_mut()?.clear().log(); self.screen()?.flush(); self.left_async_widget_mut()?.set_stale().log(); } Ok(()) } pub fn left_widget_goto(&mut self, dir: &File) -> HResult<()> { let cached_files = self.get_cached_files(&dir).ok(); let dir = dir.clone(); let left_async_widget = self.left_async_widget_mut()?; left_async_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 goto_prev_cwd(&mut self) -> HResult<()> { let prev_cwd = self.prev_cwd.take()?; self.main_widget_goto(&prev_cwd)?; Ok(()) } fn get_boomark(&mut self) -> HResult { let cwd = &match self.prev_cwd.as_ref() { Some(cwd) => cwd, None => &self.cwd }.path.to_string_lossy().to_string(); loop { let bookmark = self.bookmarks.lock()?.pick(cwd.to_string()); if let Err(HError::TerminalResizedError) = bookmark { self.core.screen.clear().log(); self.resize().log(); self.refresh().log(); self.draw().log(); continue; } return bookmark; } } pub fn goto_bookmark(&mut self) -> HResult<()> { let path = self.get_boomark()?; let path = File::new_from_path(&PathBuf::from(path), None)?; self.main_widget_goto(&path)?; Ok(()) } pub fn add_bookmark(&mut self) -> HResult<()> { let cwd = self.cwd.path.to_string_lossy().to_string(); self.bookmarks.lock()?.add(&cwd)?; Ok(()) } pub fn set_title(&self) -> HResult<()> { let path = self.cwd.short_string(); self.screen()?.set_title(&path)?; Ok(()) } pub fn update_preview(&mut self) -> HResult<()> { if !self.main_async_widget_mut()?.ready() { return Ok(()) } if self.main_widget()? .content .len() == 0 { self.preview_widget_mut()?.set_stale().log(); 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).log(); Ok(()) } pub fn set_left_selection(&mut self) -> HResult<()> { if !self.left_async_widget_mut()?.ready() { return Ok(()) } if self.cwd.parent().is_none() { return Ok(()) } let parent = self.cwd()?.parent_as_file(); let left_selection = self.get_selection(&parent?)?.clone(); self.left_widget_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()?.content.clone()) } pub fn get_left_files(&mut self) -> HResult { Ok(self.left_widget()?.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()?; let dir = widget.content.directory.clone(); Ok(dir) } fn update_watches(&mut self) -> HResult<()> { if !self.left_async_widget_mut()?.ready() || !self.main_async_widget_mut()?.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)?; self.watches.push(cwd.path); } if !watched_dirs.contains(&left_dir.path) { self.watcher.watch(&left_dir.path, RecursiveMode::NonRecursive)?; self.watches.push(left_dir.path); } if let Some(preview_dir) = preview_dir { if !watched_dirs.contains(&preview_dir) && preview_dir.is_dir() { match self.watcher.watch(&preview_dir, RecursiveMode::NonRecursive) { Ok(_) => self.watches.push(preview_dir), Err(notify::Error::Io(ioerr)) => { if ioerr.kind() != std::io::ErrorKind::PermissionDenied { Err(ioerr)? } } err @ _ => err? } } } 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_mut()?; let main_result = main_widget.content.handle_event(event); let left_widget = self.left_widget_mut()?; let left_result = left_widget.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()?; let file = widget.selected_file().clone(); Ok(file) } pub fn selected_files(&self) -> HResult> { let widget = self.main_widget()?; let files = widget.content.get_selected().into_iter().map(|f| { f.clone() }).collect(); Ok(files) } pub fn main_async_widget_mut(&mut self) -> HResult<&mut AsyncWidget>> { let widget = self.columns.active_widget_mut()?; let widget = match widget { FileBrowserWidgets::FileList(filelist) => filelist, _ => { HError::wrong_widget("previewer", "filelist")? } }; Ok(widget) } pub fn main_widget(&self) -> HResult<&ListView> { let widget = self.columns.active_widget()?; let widget = match widget { FileBrowserWidgets::FileList(filelist) => filelist.widget(), _ => { HError::wrong_widget("previewer", "filelist")? } }; widget } pub fn main_widget_mut(&mut self) -> HResult<&mut ListView> { let widget = self.columns.active_widget_mut()?; let widget = match widget { FileBrowserWidgets::FileList(filelist) => filelist.widget_mut(), _ => { HError::wrong_widget("previewer", "filelist")? } }; widget } pub fn left_async_widget_mut(&mut self) -> HResult<&mut AsyncWidget>> { let widget = match self.columns.widgets.get_mut(0)? { FileBrowserWidgets::FileList(filelist) => filelist, _ => { return HError::wrong_widget("previewer", "filelist"); } }; Ok(widget) } pub fn left_widget(&self) -> HResult<&ListView> { let widget = match self.columns.widgets.get(0)? { FileBrowserWidgets::FileList(filelist) => filelist.widget(), _ => { return HError::wrong_widget("previewer", "filelist"); } }; widget } pub fn left_widget_mut(&mut self) -> HResult<&mut ListView> { let widget = match self.columns.widgets.get_mut(0)? { FileBrowserWidgets::FileList(filelist) => filelist.widget_mut(), _ => { return HError::wrong_widget("previewer", "filelist"); } }; widget } // pub fn preview_widget(&self) -> HResult<&Previewer> { // match self.columns.widgets.get(2)? { // FileBrowserWidgets::Previewer(previewer) => Ok(previewer), // _ => { return HError::wrong_widget("filelist", "previewer"); } // } // } pub fn preview_widget_mut(&mut self) -> HResult<&mut Previewer> { match self.columns.widgets.get_mut(2)? { FileBrowserWidgets::Previewer(previewer) => Ok(previewer), _ => { return HError::wrong_widget("filelist", "previewer"); } } } pub fn toggle_colums(&mut self) { self.columns.toggle_zoom().log(); } 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.clear(); let cwd = File::new_from_path(&std::path::PathBuf::from(&dir), None)?; 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 = AsyncWidget::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 = AsyncWidget::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, tab_files: Vec>) -> HResult<()> { let cwd = self.cwd()?.clone(); let selected_file = self.selected_file()?; let selected_files = self.selected_files()?; let cmd = self.minibuffer("exec")?.trim_start().to_string() + " "; let cwd_files = if selected_files.len() == 0 { vec![selected_file] } else { selected_files }; let cmd = crate::proclist::Cmd { cmd: OsString::from(cmd), short_cmd: None, args: None, cwd: cwd, cwd_files: Some(cwd_files), tab_files: Some(tab_files), tab_paths: Some(tab_dirs) }; self.proc_view.lock()?.run_proc_subshell(cmd)?; Ok(()) } pub fn run_subshell(&mut self) -> HResult<()> { self.core.get_sender().send(Events::InputEnabled(false))?; self.core.screen.cursor_show().log(); self.core.screen.drop_screen(); let shell = std::env::var("SHELL").unwrap_or("bash".into()); let status = std::process::Command::new(&shell).status(); self.core.screen.reset_screen().log(); self.core.get_sender().send(Events::InputEnabled(true))?; match status { Ok(status) => self.show_status(&format!("\"{}\" exited with {}", shell, status)).log(), Err(err) => self.show_status(&format!("Can't run this \"{}\": {}", shell, err)).log() } Ok(()) } pub fn get_footer(&self) -> HResult { let xsize = self.get_coordinates()?.xsize(); let ypos = self.get_coordinates()?.position().y(); let pos = self.main_widget()?.get_selection(); let file = self.main_widget()?.content.files.get(pos)?; 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 target = if let Some(target) = &file.target { "--> ".to_string() + &target.short_string() } else { "".to_string() }; let main_widget = self.main_widget()?; let selection = main_widget.get_selection(); let file_count = main_widget.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(); let status = format!("{} {}:{} {}{} {}{}{} {}{}", permissions, user, group, crate::term::header_color(), mtime, crate::term::color_yellow(), target, crate::term::header_color(), crate::term::goto_xy(count_xpos, count_ypos), file_count); Ok(status) } } 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 set_coordinates(&mut self, coordinates: &Coordinates) -> HResult<()> { self.core.coordinates = coordinates.clone(); self.columns.set_coordinates(&coordinates).log(); self.proc_view.lock()?.set_coordinates(&coordinates).log(); self.log_view.lock()?.set_coordinates(&coordinates).log(); self.bookmarks.lock()?.set_coordinates(&coordinates).log(); Ok(()) } 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 = self.cwd.short_string(); let mut path = path; if &path == "" { path.clear(); } if &path == "~/" { path.pop(); } if &path == "/" { path.pop(); } 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 { match self.get_core()?.status_bar_content.lock()?.as_mut().take() { Some(status) => Ok(status.clone()), _ => { self.get_footer() }, } } fn refresh(&mut self) -> HResult<()> { //self.proc_view.lock()?.set_coordinates(self.get_coordinates()?); self.set_title().log(); self.handle_dir_events().log(); self.columns.refresh().log(); self.set_left_selection().log(); self.save_selection().log(); self.set_cwd().log(); self.update_watches().log(); if !self.columns.zoom_active { self.update_preview().log(); } self.columns.refresh().log(); Ok(()) } fn get_drawlist(&self) -> HResult { self.columns.get_drawlist() } 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::Char('F') => { self.open_bg()?; }, Key::Left | Key::Char('b') => { self.go_back()?; }, Key::Char('-') => { self.goto_prev_cwd()?; }, Key::Char('`') => { self.goto_bookmark()?; }, Key::Char('m') => { self.add_bookmark()?; }, Key::Char('w') => { self.proc_view.lock()?.popup()?; }, Key::Char('l') => self.log_view.lock()?.popup()?, Key::Char('z') => self.run_subshell()?, Key::Char('c') => self.toggle_colums(), _ => { self.main_widget_mut()?.on_key(key)?; }, } if !self.columns.zoom_active { self.update_preview().log(); } Ok(()) } } impl PartialEq for FileBrowser { fn eq(&self, other: &FileBrowser) -> bool { if self.columns == other.columns && self.cwd == other.cwd { true } else { false } } }