From e12ebd38e0e27e1e1fe119f7b4f3357862ef22ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Sun, 15 Oct 2023 09:09:36 +0800 Subject: [PATCH] refactor: split commands into separate files (#272) --- app/src/executor.rs | 22 +- app/src/help/layout.rs | 2 +- app/src/manager/preview.rs | 6 +- app/src/root.rs | 2 +- core/src/context.rs | 4 +- core/src/event.rs | 2 +- core/src/files/file.rs | 34 +- core/src/files/files.rs | 21 +- core/src/files/sorter.rs | 4 +- core/src/help/help.rs | 14 +- core/src/lib.rs | 2 + core/src/manager/commands/close.rs | 10 + core/src/manager/commands/create.rs | 43 ++ core/src/manager/commands/mod.rs | 19 + core/src/manager/commands/open.rs | 58 +++ core/src/manager/commands/peek.rs | 31 ++ core/src/manager/commands/quit.rs | 24 + core/src/manager/commands/refresh.rs | 31 ++ core/src/manager/commands/rename.rs | 164 +++++++ core/src/manager/commands/suspend.rs | 12 + core/src/manager/commands/yank.rs | 9 + core/src/manager/manager.rs | 372 +------------- core/src/manager/mod.rs | 13 +- core/src/manager/tab.rs | 545 --------------------- core/src/manager/tabs.rs | 10 +- core/src/manager/watcher.rs | 2 +- core/src/{manager => }/preview/mod.rs | 0 core/src/{manager => }/preview/preview.rs | 3 - core/src/{manager => }/preview/provider.rs | 0 core/src/{manager => tab}/backstack.rs | 0 core/src/tab/commands/arrow.rs | 23 + core/src/tab/commands/backstack.rs | 17 + core/src/tab/commands/cd.rs | 71 +++ core/src/tab/commands/copy.rs | 25 + core/src/tab/commands/enter.rs | 30 ++ core/src/tab/commands/escape.rs | 21 + core/src/tab/commands/find.rs | 67 +++ core/src/tab/commands/jump.rs | 24 + core/src/tab/commands/leave.rs | 39 ++ core/src/tab/commands/mod.rs | 27 + core/src/tab/commands/search.rs | 60 +++ core/src/tab/commands/select.rs | 12 + core/src/tab/commands/shell.rs | 35 ++ core/src/tab/commands/visual_mode.rs | 16 + core/src/{manager => tab}/finder.rs | 6 +- core/src/{manager => tab}/folder.rs | 0 core/src/tab/mod.rs | 12 + core/src/{manager => tab}/mode.rs | 0 core/src/tab/tab.rs | 164 +++++++ core/src/tasks/tasks.rs | 8 +- plugin/src/bindings/active.rs | 16 +- plugin/src/bindings/files.rs | 50 +- plugin/src/bindings/tabs.rs | 12 +- plugin/src/scope.rs | 2 +- 54 files changed, 1145 insertions(+), 1051 deletions(-) create mode 100644 core/src/manager/commands/close.rs create mode 100644 core/src/manager/commands/create.rs create mode 100644 core/src/manager/commands/mod.rs create mode 100644 core/src/manager/commands/open.rs create mode 100644 core/src/manager/commands/peek.rs create mode 100644 core/src/manager/commands/quit.rs create mode 100644 core/src/manager/commands/refresh.rs create mode 100644 core/src/manager/commands/rename.rs create mode 100644 core/src/manager/commands/suspend.rs create mode 100644 core/src/manager/commands/yank.rs delete mode 100644 core/src/manager/tab.rs rename core/src/{manager => }/preview/mod.rs (100%) rename core/src/{manager => }/preview/preview.rs (98%) rename core/src/{manager => }/preview/provider.rs (100%) rename core/src/{manager => tab}/backstack.rs (100%) create mode 100644 core/src/tab/commands/arrow.rs create mode 100644 core/src/tab/commands/backstack.rs create mode 100644 core/src/tab/commands/cd.rs create mode 100644 core/src/tab/commands/copy.rs create mode 100644 core/src/tab/commands/enter.rs create mode 100644 core/src/tab/commands/escape.rs create mode 100644 core/src/tab/commands/find.rs create mode 100644 core/src/tab/commands/jump.rs create mode 100644 core/src/tab/commands/leave.rs create mode 100644 core/src/tab/commands/mod.rs create mode 100644 core/src/tab/commands/search.rs create mode 100644 core/src/tab/commands/select.rs create mode 100644 core/src/tab/commands/shell.rs create mode 100644 core/src/tab/commands/visual_mode.rs rename core/src/{manager => tab}/finder.rs (95%) rename core/src/{manager => tab}/folder.rs (100%) create mode 100644 core/src/tab/mod.rs rename core/src/{manager => tab}/mode.rs (100%) create mode 100644 core/src/tab/tab.rs diff --git a/app/src/executor.rs b/app/src/executor.rs index 401b36e7..f87e7ad0 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -1,4 +1,4 @@ -use core::{emit, files::FilesSorter, input::InputMode, manager::FinderCase, Ctx}; +use core::{emit, files::FilesSorter, input::InputMode, tab::FinderCase, Ctx}; use config::{keymap::{Control, Exec, Key, KeymapLayer}, manager::SortBy, KEYMAP}; use shared::{optional_bool, Url}; @@ -97,17 +97,13 @@ impl Executor { "yank" => cx.manager.yank(exec.named.contains_key("cut")), "paste" => { let dest = cx.manager.cwd(); - let (cut, src) = cx.manager.yanked(); + let (cut, ref src) = cx.manager.yanked; let force = exec.named.contains_key("force"); - if *cut { - cx.tasks.file_cut(src, dest, force) - } else { - cx.tasks.file_copy(src, dest, force) - } + if cut { cx.tasks.file_cut(src, dest, force) } else { cx.tasks.file_copy(src, dest, force) } } "link" => { - let (cut, src) = cx.manager.yanked(); + let (cut, ref src) = cx.manager.yanked; !cut && cx.tasks.file_link( src, @@ -117,7 +113,7 @@ impl Executor { ) } "remove" => { - let targets = cx.manager.selected().into_iter().map(|f| f.url_owned()).collect(); + let targets = cx.manager.selected().into_iter().map(|f| f.url()).collect(); let force = exec.named.contains_key("force"); let permanently = exec.named.contains_key("permanently"); cx.tasks.file_remove(targets, force, permanently) @@ -182,20 +178,20 @@ impl Executor { } else { exec.args.get(0).map(Url::from).unwrap_or_else(|| Url::from("/")) }; - cx.manager.tabs_mut().create(&path) + cx.manager.tabs.create(&path) } "tab_close" => { let idx = exec.args.get(0).and_then(|i| i.parse().ok()).unwrap_or(0); - cx.manager.tabs_mut().close(idx) + cx.manager.tabs.close(idx) } "tab_switch" => { let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); let rel = exec.named.contains_key("relative"); - cx.manager.tabs_mut().switch(step, rel) + cx.manager.tabs.switch(step, rel) } "tab_swap" => { let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); - cx.manager.tabs_mut().swap(step) + cx.manager.tabs.swap(step) } // Tasks diff --git a/app/src/help/layout.rs b/app/src/help/layout.rs index 33644a17..84df3aee 100644 --- a/app/src/help/layout.rs +++ b/app/src/help/layout.rs @@ -23,7 +23,7 @@ impl<'a> Widget for Layout<'a> { Clear.render(area, buf); let help = &self.cx.help; - Paragraph::new(help.keyword().unwrap_or_else(|| format!("{}.help", help.layer()))) + Paragraph::new(help.keyword().unwrap_or_else(|| format!("{}.help", help.layer))) .style(THEME.help.footer.into()) .render(chunks[1], buf); diff --git a/app/src/manager/preview.rs b/app/src/manager/preview.rs index db3abc07..14265aec 100644 --- a/app/src/manager/preview.rs +++ b/app/src/manager/preview.rs @@ -1,4 +1,4 @@ -use core::{manager::PreviewData, Ctx}; +use core::{preview::PreviewData, Ctx}; use ansi_to_tui::IntoText; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}}; @@ -16,11 +16,11 @@ impl<'a> Preview<'a> { impl<'a> Widget for Preview<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let manager = &self.cx.manager; - let Some(hovered) = manager.hovered().map(|h| h.url()) else { + let Some(hovered) = manager.hovered().map(|h| &h.url) else { return; }; - let preview = manager.active().preview(); + let preview = &manager.active().preview; if !preview.same_path(hovered) { return; } diff --git a/app/src/root.rs b/app/src/root.rs index 7abd11ab..6d95303f 100644 --- a/app/src/root.rs +++ b/app/src/root.rs @@ -41,7 +41,7 @@ impl<'a> Widget for Root<'a> { input::Input::new(self.cx).render(area, buf); } - if self.cx.help.visible() { + if self.cx.help.visible { help::Layout::new(self.cx).render(area, buf); } diff --git a/core/src/context.rs b/core/src/context.rs index 0e031918..5bbaffda 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -38,7 +38,7 @@ impl Ctx { } Position::Hovered(rect @ Rect { mut x, y, width, height }) => { let Some(r) = - self.manager.hovered().and_then(|h| self.manager.current().rect_current(h.url())) + self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url)) else { return self.area(&Position::Top(*rect)); }; @@ -72,7 +72,7 @@ impl Ctx { pub fn layer(&self) -> KeymapLayer { if self.which.visible { KeymapLayer::Which - } else if self.help.visible() { + } else if self.help.visible { KeymapLayer::Help } else if self.input.visible { KeymapLayer::Input diff --git a/core/src/event.rs b/core/src/event.rs index 6d32ad89..cc856c27 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -7,7 +7,7 @@ use shared::{InputError, RoCell, Url}; use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot}; use super::{files::FilesOp, input::InputOpt, select::SelectOpt}; -use crate::{manager::PreviewLock, tasks::TasksProgress}; +use crate::{preview::PreviewLock, tasks::TasksProgress}; static TX: RoCell> = RoCell::new(); diff --git a/core/src/files/file.rs b/core/src/files/file.rs index c21b0c5c..d2b8a405 100644 --- a/core/src/files/file.rs +++ b/core/src/files/file.rs @@ -6,12 +6,12 @@ use tokio::fs; #[derive(Clone, Debug)] pub struct File { - pub(super) url: Url, - pub(super) meta: Metadata, - pub(super) length: u64, - pub(super) link_to: Option, - pub(super) is_link: bool, - pub(super) is_hidden: bool, + pub url: Url, + pub meta: Metadata, + pub length: u64, + pub(super) link_to: Option, + pub is_link: bool, + pub is_hidden: bool, } impl File { @@ -46,13 +46,7 @@ impl File { impl File { // --- Url #[inline] - pub fn url(&self) -> &Url { &self.url } - - #[inline] - pub fn url_mut(&mut self) -> &mut Url { &mut self.url } - - #[inline] - pub fn url_owned(&self) -> Url { self.url.clone() } + pub fn url(&self) -> Url { self.url.clone() } #[inline] pub fn name(&self) -> Option<&OsStr> { self.url.file_name() } @@ -69,27 +63,13 @@ impl File { pub fn parent(&self) -> Option { self.url.parent_url() } // --- Meta - #[inline] - pub fn meta(&self) -> &Metadata { &self.meta } - #[inline] pub fn is_file(&self) -> bool { self.meta.is_file() } #[inline] pub fn is_dir(&self) -> bool { self.meta.is_dir() } - // --- Length - #[inline] - pub fn length(&self) -> u64 { self.length } - // --- Link to / Is link #[inline] pub fn link_to(&self) -> Option<&Url> { self.link_to.as_ref() } - - #[inline] - pub fn is_link(&self) -> bool { self.is_link } - - // -- Is hidden - #[inline] - pub fn is_hidden(&self) -> bool { self.is_hidden } } diff --git a/core/src/files/files.rs b/core/src/files/files.rs index e6799d5d..4cad09d2 100644 --- a/core/src/files/files.rs +++ b/core/src/files/files.rs @@ -8,10 +8,10 @@ use tokio::{fs, select, sync::mpsc::{self, UnboundedReceiver}}; use super::{File, FilesSorter, FILES_TICKET}; pub struct Files { - items: Vec, - hidden: Vec, - ticket: u64, - version: u64, + items: Vec, + hidden: Vec, + ticket: u64, + pub(crate) version: u64, sizes: BTreeMap, selected: BTreeSet, @@ -89,7 +89,7 @@ impl Files { self.items.iter().any(|f| !self.selected.contains(&f.url)) }; - self.selected = self.iter().map(|f| f.url_owned()).collect(); + self.selected = self.iter().map(|f| f.url()).collect(); b } Some(false) => { @@ -106,7 +106,7 @@ impl Files { if self.selected.contains(&item.url) { self.selected.remove(&item.url); } else { - self.selected.insert(item.url_owned()); + self.selected.insert(item.url()); } } !self.items.is_empty() @@ -116,7 +116,7 @@ impl Files { pub fn select_index(&mut self, indices: &BTreeSet, state: Option) -> bool { let mut applied = false; - let paths: Vec<_> = self.pick(indices).iter().map(|f| f.url_owned()).collect(); + let paths: Vec<_> = self.pick(indices).iter().map(|f| f.url()).collect(); for path in paths { applied |= self.select(&path, state); @@ -268,13 +268,6 @@ impl Files { #[inline] pub fn position(&self, url: &Url) -> Option { self.iter().position(|f| &f.url == url) } - #[inline] - pub fn duplicate(&self, idx: usize) -> Option { self.items.get(idx).cloned() } - - // --- Version - #[inline] - pub fn version(&self) -> u64 { self.version } - // --- Sizes #[inline] pub fn size(&self, url: &Url) -> Option { self.sizes.get(url).copied() } diff --git a/core/src/files/sorter.rs b/core/src/files/sorter.rs index 4978bf59..47ad4a1e 100644 --- a/core/src/files/sorter.rs +++ b/core/src/files/sorter.rs @@ -56,8 +56,8 @@ impl FilesSorter { }), SortBy::Natural => self.sort_naturally(items), SortBy::Size => items.sort_unstable_by(|a, b| { - let aa = if a.is_dir() { sizes.get(a.url()).copied() } else { None }; - let bb = if b.is_dir() { sizes.get(b.url()).copied() } else { None }; + let aa = if a.is_dir() { sizes.get(&a.url).copied() } else { None }; + let bb = if b.is_dir() { sizes.get(&b.url).copied() } else { None }; self.cmp(aa.unwrap_or(a.length), bb.unwrap_or(b.length), self.promote(a, b)) }), } diff --git a/core/src/help/help.rs b/core/src/help/help.rs index 80ad1a29..69d58e16 100644 --- a/core/src/help/help.rs +++ b/core/src/help/help.rs @@ -7,9 +7,9 @@ use crate::{emit, input::Input}; #[derive(Default)] pub struct Help { - visible: bool, - layer: KeymapLayer, - bindings: Vec, + pub visible: bool, + pub layer: KeymapLayer, + bindings: Vec, // Filter keyword: Option, @@ -127,14 +127,6 @@ impl Help { } impl Help { - // --- Visible - #[inline] - pub fn visible(&self) -> bool { self.visible } - - // --- Layer - #[inline] - pub fn layer(&self) -> KeymapLayer { self.layer } - // --- Keyword #[inline] pub fn keyword(&self) -> Option { diff --git a/core/src/lib.rs b/core/src/lib.rs index 5cbd0fc9..67db2adf 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -16,8 +16,10 @@ mod highlighter; pub mod input; pub mod manager; mod position; +pub mod preview; pub mod select; mod step; +pub mod tab; pub mod tasks; pub mod which; diff --git a/core/src/manager/commands/close.rs b/core/src/manager/commands/close.rs new file mode 100644 index 00000000..7ba14adc --- /dev/null +++ b/core/src/manager/commands/close.rs @@ -0,0 +1,10 @@ +use crate::{manager::Manager, tasks::Tasks}; + +impl Manager { + pub fn close(&mut self, tasks: &Tasks) -> bool { + if self.tabs.len() > 1 { + return self.tabs.close(self.tabs.idx); + } + self.quit(tasks, false) + } +} diff --git a/core/src/manager/commands/create.rs b/core/src/manager/commands/create.rs new file mode 100644 index 00000000..e0406c86 --- /dev/null +++ b/core/src/manager/commands/create.rs @@ -0,0 +1,43 @@ +use std::path::PathBuf; + +use shared::Url; +use tokio::fs::{self}; + +use crate::{emit, files::{File, FilesOp}, input::InputOpt, manager::Manager}; + +impl Manager { + pub fn create(&self, force: bool) -> bool { + let cwd = self.cwd().to_owned(); + tokio::spawn(async move { + let mut result = emit!(Input(InputOpt::top("Create:"))); + let Some(Ok(name)) = result.recv().await else { + return Ok(()); + }; + + let path = cwd.join(&name); + if !force && fs::symlink_metadata(&path).await.is_ok() { + match emit!(Input(InputOpt::top("Overwrite an existing file? (y/N)"))).recv().await { + Some(Ok(c)) if c == "y" || c == "Y" => (), + _ => return Ok(()), + } + } + + if name.ends_with('/') { + fs::create_dir_all(&path).await?; + } else { + fs::create_dir_all(&path.parent().unwrap()).await.ok(); + fs::File::create(&path).await?; + } + + let child = + Url::from(path.components().take(cwd.components().count() + 1).collect::()); + if let Ok(f) = File::from(child.clone()).await { + emit!(Files(FilesOp::Creating(cwd, f.into_map()))); + emit!(Hover(child)); + emit!(Refresh); + } + Ok::<(), anyhow::Error>(()) + }); + false + } +} diff --git a/core/src/manager/commands/mod.rs b/core/src/manager/commands/mod.rs new file mode 100644 index 00000000..baf777a6 --- /dev/null +++ b/core/src/manager/commands/mod.rs @@ -0,0 +1,19 @@ +mod close; +mod create; +mod open; +mod peek; +mod quit; +mod refresh; +mod rename; +mod suspend; +mod yank; + +pub use close::*; +pub use create::*; +pub use open::*; +pub use peek::*; +pub use quit::*; +pub use refresh::*; +pub use rename::*; +pub use suspend::*; +pub use yank::*; diff --git a/core/src/manager/commands/open.rs b/core/src/manager/commands/open.rs new file mode 100644 index 00000000..516a3739 --- /dev/null +++ b/core/src/manager/commands/open.rs @@ -0,0 +1,58 @@ +use config::OPEN; +use shared::MIME_DIR; + +use crate::{emit, external, manager::Manager, select::SelectOpt}; + +impl Manager { + pub fn open(&mut self, interactive: bool) -> bool { + let mut files: Vec<_> = self + .selected() + .into_iter() + .map(|f| { + ( + f.url(), + f.is_dir().then(|| MIME_DIR.to_owned()).or_else(|| self.mimetype.get(&f.url).cloned()), + ) + }) + .collect(); + + if files.is_empty() { + return false; + } + + tokio::spawn(async move { + let todo: Vec<_> = files.iter().filter(|(_, m)| m.is_none()).map(|(u, _)| u).collect(); + if let Ok(mut mimes) = external::file(&todo).await { + files = files + .into_iter() + .map(|(u, m)| { + let mime = m.or_else(|| mimes.remove(&u)); + (u, mime) + }) + .collect(); + } + + let files: Vec<_> = + files.into_iter().filter_map(|(u, m)| m.map(|m| (u.into_os_string(), m))).collect(); + + if !interactive { + emit!(Open(files, None)); + return; + } + + let openers = OPEN.common_openers(&files); + if openers.is_empty() { + return; + } + + let result = emit!(Select(SelectOpt::hovered( + "Open with:", + openers.iter().map(|o| o.display_name.clone()).collect() + ))); + if let Ok(choice) = result.await { + emit!(Open(files, Some(openers[choice].clone()))); + } + }); + false + } +} diff --git a/core/src/manager/commands/peek.rs b/core/src/manager/commands/peek.rs new file mode 100644 index 00000000..fb271a1a --- /dev/null +++ b/core/src/manager/commands/peek.rs @@ -0,0 +1,31 @@ +use crate::manager::Manager; + +impl Manager { + pub fn peek(&mut self, sequent: bool, show_image: bool) -> bool { + let Some(hovered) = self.hovered().cloned() else { + return self.active_mut().preview_reset(); + }; + + let url = &hovered.url; + if !show_image { + self.active_mut().preview_reset_image(); + } + + if hovered.is_dir() { + let position = self.active().history(url).map(|f| (f.offset, f.files.len())); + self.active_mut().preview.folder(url, position, sequent); + return false; + } + + let Some(mime) = self.mimetype.get(url).cloned() else { + return false; + }; + + if sequent { + self.active_mut().preview.sequent(url, &mime, show_image); + } else { + self.active_mut().preview.go(url, &mime, show_image); + } + false + } +} diff --git a/core/src/manager/commands/quit.rs b/core/src/manager/commands/quit.rs new file mode 100644 index 00000000..5bc3dbdc --- /dev/null +++ b/core/src/manager/commands/quit.rs @@ -0,0 +1,24 @@ +use crate::{emit, input::InputOpt, manager::Manager, tasks::Tasks}; + +impl Manager { + pub fn quit(&self, tasks: &Tasks, no_cwd_file: bool) -> bool { + let tasks = tasks.len(); + if tasks == 0 { + emit!(Quit(no_cwd_file)); + return false; + } + + tokio::spawn(async move { + let mut result = emit!(Input(InputOpt::top(format!( + "There are {tasks} tasks running, sure to quit? (y/N)" + )))); + + if let Some(Ok(choice)) = result.recv().await { + if choice == "y" || choice == "Y" { + emit!(Quit(no_cwd_file)); + } + } + }); + false + } +} diff --git a/core/src/manager/commands/refresh.rs b/core/src/manager/commands/refresh.rs new file mode 100644 index 00000000..04de591a --- /dev/null +++ b/core/src/manager/commands/refresh.rs @@ -0,0 +1,31 @@ +use std::{collections::BTreeSet, env}; + +use crate::{emit, manager::Manager}; + +impl Manager { + pub fn refresh(&mut self) { + env::set_current_dir(self.cwd()).ok(); + + self.active_mut().apply_files_attrs(false); + + if let Some(f) = self.parent() { + self.watcher.trigger_dirs(&[self.cwd(), &f.cwd]); + } else { + self.watcher.trigger_dirs(&[self.cwd()]); + } + emit!(Hover); + + let mut to_watch = BTreeSet::new(); + for tab in self.tabs.iter() { + to_watch.insert(&tab.current.cwd); + match tab.current.hovered() { + Some(h) if h.is_dir() => _ = to_watch.insert(&h.url), + _ => {} + } + if let Some(ref p) = tab.parent { + to_watch.insert(&p.cwd); + } + } + self.watcher.watch(to_watch); + } +} diff --git a/core/src/manager/commands/rename.rs b/core/src/manager/commands/rename.rs new file mode 100644 index 00000000..5f276be6 --- /dev/null +++ b/core/src/manager/commands/rename.rs @@ -0,0 +1,164 @@ +use std::{collections::BTreeSet, ffi::OsStr, io::{stdout, BufWriter, Write}, path::PathBuf}; + +use anyhow::{anyhow, bail, Result}; +use config::{OPEN, PREVIEW}; +use shared::{max_common_root, Defer, Term, Url}; +use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; + +use crate::{emit, external::{self, ShellOpt}, files::{File, FilesOp}, input::InputOpt, manager::Manager, Event, BLOCKER}; + +impl Manager { + pub fn rename(&self, force: bool) -> bool { + if self.active().in_selecting() { + return self.bulk_rename(); + } + + let Some(hovered) = self.hovered().map(|h| h.url()) else { + return false; + }; + + async fn rename_and_hover(old: Url, new: Url) -> Result<()> { + fs::rename(&old, &new).await?; + if old.parent() != new.parent() { + return Ok(()); + } + + let parent = old.parent_url().unwrap(); + emit!(Files(FilesOp::Deleting(parent, BTreeSet::from([old])))); + + let file = File::from(new.clone()).await?; + emit!(Files(FilesOp::Creating(file.parent().unwrap(), file.into_map()))); + emit!(Hover(new)); + Ok(()) + } + + tokio::spawn(async move { + let mut result = emit!(Input( + InputOpt::hovered("Rename:").with_value(hovered.file_name().unwrap().to_string_lossy()) + )); + + let Some(Ok(name)) = result.recv().await else { + return; + }; + + let new = hovered.parent().unwrap().join(name); + if force || fs::symlink_metadata(&new).await.is_err() { + rename_and_hover(hovered, Url::from(new)).await.ok(); + return; + } + + let mut result = emit!(Input(InputOpt::hovered("Overwrite an existing file? (y/N)"))); + if let Some(Ok(choice)) = result.recv().await { + if choice == "y" || choice == "Y" { + rename_and_hover(hovered, Url::from(new)).await.ok(); + } + }; + }); + false + } + + pub fn bulk_rename(&self) -> bool { + let old: Vec<_> = self.selected().into_iter().map(|f| &f.url).collect(); + + let root = max_common_root(&old); + let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect(); + + let tmp = PREVIEW.tmpfile("bulk"); + tokio::spawn(async move { + let Some(opener) = OPEN.block_opener("bulk.txt", "text/plain") else { + bail!("No opener for bulk rename"); + }; + + { + let s = old.iter().map(|o| o.as_os_str()).collect::>().join(OsStr::new("\n")); + let mut f = OpenOptions::new().write(true).create_new(true).open(&tmp).await?; + #[cfg(target_os = "windows")] + { + f.write_all(s.to_string_lossy().as_bytes()).await?; + } + #[cfg(not(target_os = "windows"))] + { + use std::os::unix::ffi::OsStrExt; + f.write_all(s.as_bytes()).await?; + } + } + + let _guard = BLOCKER.acquire().await.unwrap(); + let _defer = Defer::new(|| { + Event::Stop(false, None).emit(); + tokio::spawn(fs::remove_file(tmp.clone())) + }); + emit!(Stop(true)).await; + + let mut child = external::shell(ShellOpt { + cmd: (*opener.exec).into(), + args: vec![tmp.to_owned().into()], + piped: false, + orphan: false, + })?; + child.wait().await?; + + let new: Vec<_> = fs::read_to_string(&tmp).await?.lines().map(PathBuf::from).collect(); + Self::bulk_rename_do(root, old, new).await + }); + + false + } + + async fn bulk_rename_do(root: PathBuf, old: Vec, new: Vec) -> Result<()> { + Term::clear(&mut stdout())?; + if old.len() != new.len() { + println!("Number of old and new differ, press ENTER to exit"); + stdin().read_exact(&mut [0]).await?; + return Ok(()); + } + + let todo: Vec<_> = old.into_iter().zip(new).filter(|(o, n)| o != n).collect(); + if todo.is_empty() { + return Ok(()); + } + + { + let mut stdout = BufWriter::new(stdout().lock()); + for (o, n) in &todo { + writeln!(stdout, "{} -> {}", o.display(), n.display())?; + } + write!(stdout, "Continue to rename? (y/N): ")?; + stdout.flush()?; + } + + let mut buf = [0; 10]; + stdin().read(&mut buf).await.ok(); + if buf[0] != b'y' && buf[0] != b'Y' { + return Ok(()); + } + + let mut failed = Vec::new(); + for (o, n) in todo { + if fs::metadata(&n).await.is_ok() { + failed.push((o, n, anyhow!("Destination already exists"))); + continue; + } + if let Err(e) = fs::rename(root.join(&o), root.join(&n)).await { + failed.push((o, n, e.into())); + } + } + if failed.is_empty() { + return Ok(()); + } + + Term::clear(&mut stdout())?; + { + let mut stdout = BufWriter::new(stdout().lock()); + writeln!(stdout, "Failed to rename:")?; + for (o, n, e) in failed { + writeln!(stdout, "{} -> {}: {e}", o.display(), n.display())?; + } + writeln!(stdout, "\nPress ENTER to exit")?; + stdout.flush()?; + } + + stdin().read_exact(&mut [0]).await?; + Ok(()) + } +} diff --git a/core/src/manager/commands/suspend.rs b/core/src/manager/commands/suspend.rs new file mode 100644 index 00000000..b80087d9 --- /dev/null +++ b/core/src/manager/commands/suspend.rs @@ -0,0 +1,12 @@ +use crate::{emit, manager::Manager}; + +impl Manager { + pub fn suspend(&mut self) -> bool { + #[cfg(not(target_os = "windows"))] + tokio::spawn(async move { + emit!(Stop(true)).await; + unsafe { libc::raise(libc::SIGTSTP) }; + }); + false + } +} diff --git a/core/src/manager/commands/yank.rs b/core/src/manager/commands/yank.rs new file mode 100644 index 00000000..ad92f0d4 --- /dev/null +++ b/core/src/manager/commands/yank.rs @@ -0,0 +1,9 @@ +use crate::manager::Manager; + +impl Manager { + pub fn yank(&mut self, cut: bool) -> bool { + self.yanked.0 = cut; + self.yanked.1 = self.selected().into_iter().map(|f| f.url()).collect(); + true + } +} diff --git a/core/src/manager/manager.rs b/core/src/manager/manager.rs index dc17a9ce..d661aeeb 100644 --- a/core/src/manager/manager.rs +++ b/core/src/manager/manager.rs @@ -1,19 +1,16 @@ -use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, ffi::OsStr, io::{stdout, BufWriter, Write}, path::PathBuf}; +use std::collections::{BTreeMap, HashMap, HashSet}; -use anyhow::{anyhow, bail, Error, Result}; -use config::{OPEN, PREVIEW}; -use shared::{max_common_root, Defer, Term, Url, MIME_DIR}; -use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; +use shared::Url; -use super::{Tab, Tabs, Watcher}; -use crate::{emit, external::{self, ShellOpt}, files::{File, FilesOp}, input::InputOpt, manager::Folder, select::SelectOpt, tasks::Tasks, Event, BLOCKER}; +use super::{Tabs, Watcher}; +use crate::{emit, files::{File, FilesOp}, tab::{Folder, Tab}, tasks::Tasks}; pub struct Manager { - tabs: Tabs, - yanked: (bool, HashSet), + pub tabs: Tabs, + pub yanked: (bool, HashSet), - watcher: Watcher, - pub mimetype: HashMap, + pub(super) watcher: Watcher, + pub mimetype: HashMap, } impl Manager { @@ -27,354 +24,16 @@ impl Manager { } } - pub fn refresh(&mut self) { - env::set_current_dir(self.cwd()).ok(); - - self.active_mut().apply_files_attrs(false); - - if let Some(f) = self.parent() { - self.watcher.trigger_dirs(&[self.cwd(), &f.cwd]); - } else { - self.watcher.trigger_dirs(&[self.cwd()]); - } - emit!(Hover); - - let mut to_watch = BTreeSet::new(); - for tab in self.tabs.iter() { - to_watch.insert(&tab.current.cwd); - match tab.current.hovered() { - Some(h) if h.is_dir() => _ = to_watch.insert(h.url()), - _ => {} - } - if let Some(ref p) = tab.parent { - to_watch.insert(&p.cwd); - } - } - self.watcher.watch(to_watch); - } - - pub fn peek(&mut self, sequent: bool, show_image: bool) -> bool { - let Some(hovered) = self.hovered().cloned() else { - return self.active_mut().preview_reset(); - }; - - let url = hovered.url(); - if !show_image { - self.active_mut().preview_reset_image(); - } - - if hovered.is_dir() { - let position = self.active().history(url).map(|f| (f.offset, f.files.len())); - self.active_mut().preview.folder(url, position, sequent); - return false; - } - - let Some(mime) = self.mimetype.get(url).cloned() else { - return false; - }; - - if sequent { - self.active_mut().preview.sequent(url, &mime, show_image); - } else { - self.active_mut().preview.go(url, &mime, show_image); - } - false - } - - pub fn yank(&mut self, cut: bool) -> bool { - self.yanked.0 = cut; - self.yanked.1 = self.selected().into_iter().map(|f| f.url_owned()).collect(); - true - } - - pub fn quit(&self, tasks: &Tasks, no_cwd_file: bool) -> bool { - let tasks = tasks.len(); - if tasks == 0 { - emit!(Quit(no_cwd_file)); - return false; - } - - tokio::spawn(async move { - let mut result = emit!(Input(InputOpt::top(format!( - "There are {tasks} tasks running, sure to quit? (y/N)" - )))); - - if let Some(Ok(choice)) = result.recv().await { - if choice == "y" || choice == "Y" { - emit!(Quit(no_cwd_file)); - } - } - }); - false - } - - pub fn close(&mut self, tasks: &Tasks) -> bool { - if self.tabs.len() > 1 { - return self.tabs.close(self.tabs.idx()); - } - self.quit(tasks, false) - } - - pub fn suspend(&mut self) -> bool { - #[cfg(not(target_os = "windows"))] - tokio::spawn(async move { - emit!(Stop(true)).await; - unsafe { libc::raise(libc::SIGTSTP) }; - }); - false - } - - pub fn open(&mut self, interactive: bool) -> bool { - let mut files: Vec<_> = self - .selected() - .into_iter() - .map(|f| { - ( - f.url_owned(), - f.is_dir().then(|| MIME_DIR.to_owned()).or_else(|| self.mimetype.get(f.url()).cloned()), - ) - }) - .collect(); - - if files.is_empty() { - return false; - } - - tokio::spawn(async move { - let todo: Vec<_> = files.iter().filter(|(_, m)| m.is_none()).map(|(u, _)| u).collect(); - if let Ok(mut mimes) = external::file(&todo).await { - files = files - .into_iter() - .map(|(u, m)| { - let mime = m.or_else(|| mimes.remove(&u)); - (u, mime) - }) - .collect(); - } - - let files: Vec<_> = - files.into_iter().filter_map(|(u, m)| m.map(|m| (u.into_os_string(), m))).collect(); - - if !interactive { - emit!(Open(files, None)); - return; - } - - let openers = OPEN.common_openers(&files); - if openers.is_empty() { - return; - } - - let result = emit!(Select(SelectOpt::hovered( - "Open with:", - openers.iter().map(|o| o.display_name.clone()).collect() - ))); - if let Ok(choice) = result.await { - emit!(Open(files, Some(openers[choice].clone()))); - } - }); - false - } - - pub fn create(&self, force: bool) -> bool { - let cwd = self.cwd().to_owned(); - tokio::spawn(async move { - let mut result = emit!(Input(InputOpt::top("Create:"))); - let Some(Ok(name)) = result.recv().await else { - return Ok(()); - }; - - let path = cwd.join(&name); - if !force && fs::symlink_metadata(&path).await.is_ok() { - match emit!(Input(InputOpt::top("Overwrite an existing file? (y/N)"))).recv().await { - Some(Ok(c)) if c == "y" || c == "Y" => (), - _ => return Ok(()), - } - } - - if name.ends_with('/') { - fs::create_dir_all(&path).await?; - } else { - fs::create_dir_all(&path.parent().unwrap()).await.ok(); - fs::File::create(&path).await?; - } - - let child = - Url::from(path.components().take(cwd.components().count() + 1).collect::()); - if let Ok(f) = File::from(child.clone()).await { - emit!(Files(FilesOp::Creating(cwd, f.into_map()))); - emit!(Hover(child)); - emit!(Refresh); - } - Ok::<(), Error>(()) - }); - false - } - - pub fn rename(&self, force: bool) -> bool { - if self.active().in_selecting() { - return self.bulk_rename(); - } - - let Some(hovered) = self.hovered().map(|h| h.url_owned()) else { - return false; - }; - - async fn rename_and_hover(old: Url, new: Url) -> Result<()> { - fs::rename(&old, &new).await?; - if old.parent() != new.parent() { - return Ok(()); - } - - let parent = old.parent_url().unwrap(); - emit!(Files(FilesOp::Deleting(parent, BTreeSet::from([old])))); - - let file = File::from(new.clone()).await?; - emit!(Files(FilesOp::Creating(file.parent().unwrap(), file.into_map()))); - emit!(Hover(new)); - Ok(()) - } - - tokio::spawn(async move { - let mut result = emit!(Input( - InputOpt::hovered("Rename:").with_value(hovered.file_name().unwrap().to_string_lossy()) - )); - - let Some(Ok(name)) = result.recv().await else { - return; - }; - - let new = hovered.parent().unwrap().join(name); - if force || fs::symlink_metadata(&new).await.is_err() { - rename_and_hover(hovered, Url::from(new)).await.ok(); - return; - } - - let mut result = emit!(Input(InputOpt::hovered("Overwrite an existing file? (y/N)"))); - if let Some(Ok(choice)) = result.recv().await { - if choice == "y" || choice == "Y" { - rename_and_hover(hovered, Url::from(new)).await.ok(); - } - }; - }); - false - } - - pub fn bulk_rename(&self) -> bool { - let old: Vec<_> = self.selected().into_iter().map(|f| f.url()).collect(); - - let root = max_common_root(&old); - let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect(); - - let tmp = PREVIEW.tmpfile("bulk"); - tokio::spawn(async move { - let Some(opener) = OPEN.block_opener("bulk.txt", "text/plain") else { - bail!("No opener for bulk rename"); - }; - - { - let s = old.iter().map(|o| o.as_os_str()).collect::>().join(OsStr::new("\n")); - let mut f = OpenOptions::new().write(true).create_new(true).open(&tmp).await?; - #[cfg(target_os = "windows")] - { - f.write_all(s.to_string_lossy().as_bytes()).await?; - } - #[cfg(not(target_os = "windows"))] - { - use std::os::unix::ffi::OsStrExt; - f.write_all(s.as_bytes()).await?; - } - } - - let _guard = BLOCKER.acquire().await.unwrap(); - let _defer = Defer::new(|| { - Event::Stop(false, None).emit(); - tokio::spawn(fs::remove_file(tmp.clone())) - }); - emit!(Stop(true)).await; - - let mut child = external::shell(ShellOpt { - cmd: (*opener.exec).into(), - args: vec![tmp.to_owned().into()], - piped: false, - orphan: false, - })?; - child.wait().await?; - - let new: Vec<_> = fs::read_to_string(&tmp).await?.lines().map(PathBuf::from).collect(); - Self::bulk_rename_do(root, old, new).await - }); - - false - } - - async fn bulk_rename_do(root: PathBuf, old: Vec, new: Vec) -> Result<()> { - Term::clear(&mut stdout())?; - if old.len() != new.len() { - println!("Number of old and new differ, press ENTER to exit"); - stdin().read_exact(&mut [0]).await?; - return Ok(()); - } - - let todo: Vec<_> = old.into_iter().zip(new).filter(|(o, n)| o != n).collect(); - if todo.is_empty() { - return Ok(()); - } - - { - let mut stdout = BufWriter::new(stdout().lock()); - for (o, n) in &todo { - writeln!(stdout, "{} -> {}", o.display(), n.display())?; - } - write!(stdout, "Continue to rename? (y/N): ")?; - stdout.flush()?; - } - - let mut buf = [0; 10]; - stdin().read(&mut buf).await.ok(); - if buf[0] != b'y' && buf[0] != b'Y' { - return Ok(()); - } - - let mut failed = Vec::new(); - for (o, n) in todo { - if fs::metadata(&n).await.is_ok() { - failed.push((o, n, anyhow!("Destination already exists"))); - continue; - } - if let Err(e) = fs::rename(root.join(&o), root.join(&n)).await { - failed.push((o, n, e.into())); - } - } - if failed.is_empty() { - return Ok(()); - } - - Term::clear(&mut stdout())?; - { - let mut stdout = BufWriter::new(stdout().lock()); - writeln!(stdout, "Failed to rename:")?; - for (o, n, e) in failed { - writeln!(stdout, "{} -> {}: {e}", o.display(), n.display())?; - } - writeln!(stdout, "\nPress ENTER to exit")?; - stdout.flush()?; - } - - stdin().read_exact(&mut [0]).await?; - Ok(()) - } - pub fn update_read(&mut self, op: FilesOp) -> bool { let url = op.url().clone(); let cwd = self.cwd().to_owned(); - let hovered = self.hovered().map(|h| h.url_owned()); + let hovered = self.hovered().map(|h| h.url()); let mut b = if cwd == url { self.current_mut().update(op) } else if matches!(self.parent(), Some(p) if p.cwd == url) { self.active_mut().parent.as_mut().unwrap().update(op) - } else if matches!(self.hovered(), Some(h) if h.url() == &url) { + } else if matches!(self.hovered(), Some(h) if h.url == url) { self.active_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(&url)); self.active_mut().apply_files_attrs(true); self.active_mut().history.get_mut(&url).unwrap().update(op) @@ -386,7 +45,7 @@ impl Manager { b |= self.active_mut().parent.as_mut().is_some_and(|p| p.hover(&cwd)); b |= hovered.as_ref().is_some_and(|h| self.current_mut().hover(h)); - if hovered.as_ref() != self.hovered().map(|h| h.url()) { + if hovered.as_ref() != self.hovered().map(|h| &h.url) { emit!(Hover); } b @@ -426,12 +85,6 @@ impl Manager { #[inline] pub fn cwd(&self) -> &Url { &self.current().cwd } - #[inline] - pub fn tabs(&self) -> &Tabs { &self.tabs } - - #[inline] - pub fn tabs_mut(&mut self) -> &mut Tabs { &mut self.tabs } - #[inline] pub fn active(&self) -> &Tab { self.tabs.active() } @@ -452,7 +105,4 @@ impl Manager { #[inline] pub fn selected(&self) -> Vec<&File> { self.tabs.active().selected() } - - #[inline] - pub fn yanked(&self) -> &(bool, HashSet) { &self.yanked } } diff --git a/core/src/manager/mod.rs b/core/src/manager/mod.rs index 76e321e5..8e9223e0 100644 --- a/core/src/manager/mod.rs +++ b/core/src/manager/mod.rs @@ -1,19 +1,8 @@ -mod backstack; -mod finder; -mod folder; +mod commands; mod manager; -mod mode; -mod preview; -mod tab; mod tabs; mod watcher; -pub use backstack::*; -pub use finder::*; -pub use folder::*; pub use manager::*; -pub use mode::*; -pub use preview::*; -pub use tab::*; pub use tabs::*; pub use watcher::*; diff --git a/core/src/manager/tab.rs b/core/src/manager/tab.rs deleted file mode 100644 index ac10be2f..00000000 --- a/core/src/manager/tab.rs +++ /dev/null @@ -1,545 +0,0 @@ -use std::{borrow::Cow, collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, time::Duration}; - -use anyhow::{bail, Error, Result}; -use config::{keymap::{Exec, KeymapLayer}, open::Opener, MANAGER}; -use shared::{Debounce, Defer, InputError, Url}; -use tokio::{pin, task::JoinHandle}; -use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; - -use super::{Backstack, Finder, FinderCase, Folder, Mode, Preview, PreviewLock}; -use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, FilesOp, FilesSorter}, input::InputOpt, Event, Step, BLOCKER}; - -pub struct Tab { - pub mode: Mode, - pub current: Folder, - pub parent: Option, - - pub(super) backstack: Backstack, - pub(super) history: BTreeMap, - pub(super) preview: Preview, - - finder: Option, - search: Option>>, - pub(super) sorter: FilesSorter, - pub(super) show_hidden: bool, -} - -impl From for Tab { - fn from(url: Url) -> Self { - let parent = url.parent_url().map(Folder::from); - - Self { - mode: Default::default(), - current: Folder::from(url.clone()), - parent, - - backstack: Backstack::new(url), - history: Default::default(), - preview: Default::default(), - - finder: None, - search: None, - sorter: Default::default(), - show_hidden: MANAGER.show_hidden, - } - } -} - -impl From<&Url> for Tab { - fn from(url: &Url) -> Self { Self::from(url.clone()) } -} - -impl Tab { - pub fn escape(&mut self) -> bool { - if self.finder.is_some() { - self.finder = None; - return true; - } - - if let Some((_, indices)) = self.mode.visual() { - self.current.files.select_index(indices, Some(self.mode.is_select())); - self.mode = Mode::Normal; - return true; - } - - if self.select_all(Some(false)) { - return true; - } - - self.search_stop() - } - - pub fn arrow(&mut self, step: Step) -> bool { - let ok = if step.is_positive() { self.current.next(step) } else { self.current.prev(step) }; - if !ok { - return false; - } - - // Visual selection - if let Some((start, items)) = self.mode.visual_mut() { - let after = self.current.cursor; - - items.clear(); - for i in start.min(after)..=after.max(start) { - items.insert(i); - } - } - - emit!(Hover); - true - } - - // TODO: change to sync, and remove `Event::Cd` - pub async fn cd(&mut self, mut target: Url) -> bool { - let Ok(file) = File::from(target.clone()).await else { - return false; - }; - - let mut hovered = None; - if !file.is_dir() { - hovered = Some(file.url_owned()); - target = target.parent_url().unwrap(); - emit!(Files(FilesOp::Creating(target.clone(), file.into_map()))); - } - - // Already in target - if self.current.cwd == target { - if let Some(h) = hovered { - emit!(Hover(h)); - } - return false; - } - - // Take parent to history - if let Some(rep) = self.parent.take() { - self.history.insert(rep.cwd.clone(), rep); - } - - // Current - let rep = self.history_new(&target); - let rep = mem::replace(&mut self.current, rep); - if rep.cwd.is_regular() { - self.history.insert(rep.cwd.clone(), rep); - } - - // Parent - if let Some(parent) = target.parent_url() { - self.parent = Some(self.history_new(&parent)); - } - - // Hover the file - if let Some(h) = hovered { - emit!(Hover(h)); - } - - // Backstack - if target.is_regular() { - self.backstack.push(target.clone()); - } - - emit!(Refresh); - true - } - - pub fn cd_interactive(&mut self, target: Url) -> bool { - tokio::spawn(async move { - let mut result = - emit!(Input(InputOpt::top("Change directory:").with_value(target.to_string_lossy()))); - - if let Some(Ok(s)) = result.recv().await { - emit!(Cd(Url::from(s.trim()))); - } - }); - false - } - - pub fn enter(&mut self) -> bool { - let Some(hovered) = self.current.hovered().filter(|h| h.is_dir()).map(|h| h.url_owned()) else { - return false; - }; - - // Current - let rep = self.history_new(&hovered); - let rep = mem::replace(&mut self.current, rep); - if rep.cwd.is_regular() { - self.history.insert(rep.cwd.clone(), rep); - } - - // Parent - if let Some(rep) = self.parent.take() { - self.history.insert(rep.cwd.clone(), rep); - } - self.parent = Some(self.history_new(&hovered.parent_url().unwrap())); - - // Backstack - self.backstack.push(hovered); - - emit!(Refresh); - true - } - - pub fn leave(&mut self) -> bool { - let current = self - .current - .hovered() - .and_then(|h| h.parent()) - .filter(|p| *p != self.current.cwd) - .or_else(|| self.current.cwd.parent_url()); - - let Some(current) = current else { - return false; - }; - - // Parent - if let Some(rep) = self.parent.take() { - self.history.insert(rep.cwd.clone(), rep); - } - if let Some(parent) = current.parent_url() { - self.parent = Some(self.history_new(&parent)); - } - - // Current - let rep = self.history_new(¤t); - let rep = mem::replace(&mut self.current, rep); - if rep.cwd.is_regular() { - self.history.insert(rep.cwd.clone(), rep); - } - - // Backstack - self.backstack.push(current); - - emit!(Refresh); - true - } - - pub fn back(&mut self) -> bool { - if let Some(url) = self.backstack.shift_backward().cloned() { - futures::executor::block_on(self.cd(url)); - } - false - } - - pub fn forward(&mut self) -> bool { - if let Some(url) = self.backstack.shift_forward().cloned() { - futures::executor::block_on(self.cd(url)); - } - false - } - - pub fn select(&mut self, state: Option) -> bool { - if let Some(u) = self.current.hovered().map(|h| h.url_owned()) { - return self.current.files.select(&u, state); - } - false - } - - pub fn select_all(&mut self, state: Option) -> bool { self.current.files.select_all(state) } - - pub fn visual_mode(&mut self, unset: bool) -> bool { - let idx = self.current.cursor; - - if unset { - self.mode = Mode::Unset(idx, BTreeSet::from([idx])); - } else { - self.mode = Mode::Select(idx, BTreeSet::from([idx])); - }; - true - } - - pub fn copy(&self, type_: &str) -> bool { - let mut s = OsString::new(); - let mut it = self.selected().into_iter().peekable(); - while let Some(f) = it.next() { - s.push(match type_ { - "path" => f.url().as_os_str(), - "dirname" => f.url().parent().map_or(OsStr::new(""), |p| p.as_os_str()), - "filename" => f.name().unwrap_or(OsStr::new("")), - "name_without_ext" => f.stem().unwrap_or(OsStr::new("")), - _ => return false, - }); - if it.peek().is_some() { - s.push("\n"); - } - } - - futures::executor::block_on(external::clipboard_set(s)).ok(); - false - } - - pub fn find(&mut self, query: Option<&str>, prev: bool, case: FinderCase) -> bool { - if let Some(query) = query { - let Ok(finder) = Finder::new(query, case) else { - return false; - }; - - let step = if prev { - finder.prev(&self.current.files, self.current.cursor, true) - } else { - finder.next(&self.current.files, self.current.cursor, true) - }; - - if let Some(step) = step { - self.arrow(step.into()); - } - - self.finder = Some(finder); - return true; - } - - tokio::spawn(async move { - let rx = emit!(Input( - InputOpt::top(if prev { "Find previous:" } else { "Find next:" }).with_realtime() - )); - - let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); - pin!(rx); - - while let Some(Ok(s)) | Some(Err(InputError::Typed(s))) = rx.next().await { - emit!(Call( - Exec::call("find", vec![s]) - .with_bool("previous", prev) - .with_bool("smart", case == FinderCase::Smart) - .with_bool("insensitive", case == FinderCase::Insensitive) - .vec(), - KeymapLayer::Manager - )); - } - }); - false - } - - pub fn find_arrow(&mut self, prev: bool) -> bool { - let Some(finder) = &mut self.finder else { - return false; - }; - - let b = finder.catchup(&self.current.files); - let step = if prev { - finder.prev(&self.current.files, self.current.cursor, false) - } else { - finder.next(&self.current.files, self.current.cursor, false) - }; - - b | step.is_some_and(|s| self.arrow(s.into())) - } - - pub fn search(&mut self, grep: bool) -> bool { - if let Some(handle) = self.search.take() { - handle.abort(); - } - - let mut cwd = self.current.cwd.clone(); - let hidden = self.show_hidden; - - self.search = Some(tokio::spawn(async move { - let Some(Ok(subject)) = emit!(Input(InputOpt::top("Search:"))).recv().await else { - bail!("canceled") - }; - - cwd = cwd.into_search(subject.clone()); - let rx = if grep { - external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject }) - } else { - external::fd(external::FdOpt { cwd: cwd.clone(), hidden, glob: false, subject }) - }?; - - let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(300)); - pin!(rx); - - let ticket = FilesOp::prepare(&cwd); - let mut first = true; - while let Some(chunk) = rx.next().await { - if first { - emit!(Cd(cwd.clone())); - first = false; - } - emit!(Files(FilesOp::Part(cwd.clone(), ticket, chunk))); - } - Ok(()) - })); - true - } - - pub fn search_stop(&mut self) -> bool { - if let Some(handle) = self.search.take() { - handle.abort(); - } - if self.current.cwd.is_search() { - self.preview_reset_image(); - - let rep = self.history_new(&self.current.cwd.to_regular()); - drop(mem::replace(&mut self.current, rep)); - emit!(Refresh); - } - false - } - - pub fn jump(&self, global: bool) -> bool { - let cwd = self.current.cwd.clone(); - - tokio::spawn(async move { - let _guard = BLOCKER.acquire().await.unwrap(); - let _defer = Defer::new(|| Event::Stop(false, None).emit()); - emit!(Stop(true)).await; - - let rx = - if global { external::fzf(FzfOpt { cwd }) } else { external::zoxide(ZoxideOpt { cwd }) }?; - - if let Ok(target) = rx.await? { - emit!(Cd(target)); - } - Ok::<(), Error>(()) - }); - false - } - - pub fn shell(&self, exec: &str, block: bool, confirm: bool) -> bool { - let selected: Vec<_> = self - .selected() - .into_iter() - .map(|f| (f.url().as_os_str().to_owned(), Default::default())) - .collect(); - - let mut exec = exec.to_owned(); - tokio::spawn(async move { - if !confirm || exec.is_empty() { - let mut result = emit!(Input( - InputOpt::top(if block { "Shell (block):" } else { "Shell:" }) - .with_value(&exec) - .with_highlight() - )); - match result.recv().await { - Some(Ok(e)) => exec = e, - _ => return, - } - } - - emit!(Open( - selected, - Some(Opener { exec, block, orphan: false, display_name: Default::default(), spread: true }) - )); - }); - - false - } - - pub fn update_peek(&mut self, max: usize, url: Url) -> bool { - let Some(hovered) = self.current.hovered() else { - return false; - }; - - if &url != hovered.url() { - return false; - } - - self.preview.arrow_max(max) - } - - pub fn update_preview(&mut self, lock: PreviewLock) -> bool { - let Some(hovered) = self.current.hovered().map(|h| h.url()) else { - return self.preview_reset(); - }; - - if lock.url != *hovered { - return false; - } - - self.preview.lock = Some(lock); - true - } -} - -impl Tab { - // --- Mode - #[inline] - pub fn in_selecting(&self) -> bool { self.mode.is_visual() || self.current.files.has_selected() } - - // --- Current - pub fn selected(&self) -> Vec<&File> { - let pending = self.mode.visual().map(|(_, p)| Cow::Borrowed(p)).unwrap_or_default(); - let selected = self.current.files.selected(&pending, self.mode.is_unset()); - - if selected.is_empty() { - self.current.hovered().map(|h| vec![h]).unwrap_or_default() - } else { - selected - } - } - - // --- History - #[inline] - pub fn history(&self, url: &Url) -> Option<&Folder> { self.history.get(url) } - - #[inline] - pub fn history_new(&mut self, url: &Url) -> Folder { - self.history.remove(url).unwrap_or_else(|| Folder::from(url)) - } - - // --- Preview - #[inline] - pub fn preview(&self) -> &Preview { &self.preview } - - #[inline] - pub fn preview_reset(&mut self) -> bool { self.preview.reset(|_| true) } - - #[inline] - pub fn preview_reset_image(&mut self) -> bool { self.preview.reset(|l| l.is_image()) } - - #[inline] - pub fn preview_arrow(&mut self, step: isize) -> bool { self.preview.arrow(step) } - - // --- Finder - #[inline] - pub fn finder(&self) -> Option<&Finder> { self.finder.as_ref() } - - // --- Sorter - pub fn set_sorter(&mut self, sorter: FilesSorter) -> bool { - if sorter == self.sorter { - return false; - } - - self.sorter = sorter; - self.apply_files_attrs(false) - } - - // --- Show hidden - pub fn set_show_hidden(&mut self, state: Option) -> bool { - let state = state.unwrap_or(!self.show_hidden); - if state == self.show_hidden { - return false; - } - - self.show_hidden = state; - if self.apply_files_attrs(false) { - emit!(Peek); - return true; - } - false - } - - pub fn apply_files_attrs(&mut self, just_preview: bool) -> bool { - let mut b = false; - if let Some(f) = - self.current.hovered().filter(|h| h.is_dir()).and_then(|h| self.history.get_mut(h.url())) - { - b |= f.files.set_show_hidden(self.show_hidden); - b |= f.files.set_sorter(self.sorter); - } - - if just_preview { - return b; - } - - let hovered = self.current.hovered().map(|h| h.url_owned()); - b |= self.current.files.set_show_hidden(self.show_hidden); - b |= self.current.files.set_sorter(self.sorter); - - if let Some(parent) = self.parent.as_mut() { - b |= parent.files.set_show_hidden(self.show_hidden); - b |= parent.files.set_sorter(self.sorter); - } - - self.current.repos(hovered); - b - } -} diff --git a/core/src/manager/tabs.rs b/core/src/manager/tabs.rs index 01d09cf7..a8bb3537 100644 --- a/core/src/manager/tabs.rs +++ b/core/src/manager/tabs.rs @@ -1,14 +1,13 @@ use config::BOOT; use shared::Url; -use super::Tab; -use crate::emit; +use crate::{emit, tab::Tab}; const MAX_TABS: usize = 9; pub struct Tabs { - idx: usize, - items: Vec, + pub idx: usize, + items: Vec, } impl Tabs { @@ -90,9 +89,6 @@ impl Tabs { } impl Tabs { - #[inline] - pub fn idx(&self) -> usize { self.idx } - #[inline] pub fn len(&self) -> usize { self.items.len() } diff --git a/core/src/manager/watcher.rs b/core/src/manager/watcher.rs index 45b34190..54908931 100644 --- a/core/src/manager/watcher.rs +++ b/core/src/manager/watcher.rs @@ -200,7 +200,7 @@ impl Watcher { let mut new = Vec::with_capacity(files.len()); for file in files { let mut file = file.clone(); - *file.url_mut() = ori.join(file.url().strip_prefix(url).unwrap()); + file.url = ori.join(file.url.strip_prefix(url).unwrap()); new.push(file); } new diff --git a/core/src/manager/preview/mod.rs b/core/src/preview/mod.rs similarity index 100% rename from core/src/manager/preview/mod.rs rename to core/src/preview/mod.rs diff --git a/core/src/manager/preview/preview.rs b/core/src/preview/preview.rs similarity index 98% rename from core/src/manager/preview/preview.rs rename to core/src/preview/preview.rs index e569bbe8..97ac9d70 100644 --- a/core/src/manager/preview/preview.rs +++ b/core/src/preview/preview.rs @@ -170,9 +170,6 @@ impl Preview { } impl Preview { - #[inline] - pub fn skip(&self) -> usize { self.skip } - #[inline] pub fn same(&self, url: &Url, mime: &str) -> bool { if let Some(ref lock) = self.lock { diff --git a/core/src/manager/preview/provider.rs b/core/src/preview/provider.rs similarity index 100% rename from core/src/manager/preview/provider.rs rename to core/src/preview/provider.rs diff --git a/core/src/manager/backstack.rs b/core/src/tab/backstack.rs similarity index 100% rename from core/src/manager/backstack.rs rename to core/src/tab/backstack.rs diff --git a/core/src/tab/commands/arrow.rs b/core/src/tab/commands/arrow.rs new file mode 100644 index 00000000..0e3d59d0 --- /dev/null +++ b/core/src/tab/commands/arrow.rs @@ -0,0 +1,23 @@ +use crate::{emit, tab::Tab, Step}; + +impl Tab { + pub fn arrow(&mut self, step: Step) -> bool { + let ok = if step.is_positive() { self.current.next(step) } else { self.current.prev(step) }; + if !ok { + return false; + } + + // Visual selection + if let Some((start, items)) = self.mode.visual_mut() { + let after = self.current.cursor; + + items.clear(); + for i in start.min(after)..=after.max(start) { + items.insert(i); + } + } + + emit!(Hover); + true + } +} diff --git a/core/src/tab/commands/backstack.rs b/core/src/tab/commands/backstack.rs new file mode 100644 index 00000000..a54c7633 --- /dev/null +++ b/core/src/tab/commands/backstack.rs @@ -0,0 +1,17 @@ +use crate::tab::Tab; + +impl Tab { + pub fn back(&mut self) -> bool { + if let Some(url) = self.backstack.shift_backward().cloned() { + futures::executor::block_on(self.cd(url)); + } + false + } + + pub fn forward(&mut self) -> bool { + if let Some(url) = self.backstack.shift_forward().cloned() { + futures::executor::block_on(self.cd(url)); + } + false + } +} diff --git a/core/src/tab/commands/cd.rs b/core/src/tab/commands/cd.rs new file mode 100644 index 00000000..d73e9409 --- /dev/null +++ b/core/src/tab/commands/cd.rs @@ -0,0 +1,71 @@ +use std::mem; + +use shared::Url; + +use crate::{emit, files::{File, FilesOp}, input::InputOpt, tab::Tab}; + +impl Tab { + // TODO: change to sync, and remove `Event::Cd` + pub async fn cd(&mut self, mut target: Url) -> bool { + let Ok(file) = File::from(target.clone()).await else { + return false; + }; + + let mut hovered = None; + if !file.is_dir() { + hovered = Some(file.url()); + target = target.parent_url().unwrap(); + emit!(Files(FilesOp::Creating(target.clone(), file.into_map()))); + } + + // Already in target + if self.current.cwd == target { + if let Some(h) = hovered { + emit!(Hover(h)); + } + return false; + } + + // Take parent to history + if let Some(rep) = self.parent.take() { + self.history.insert(rep.cwd.clone(), rep); + } + + // Current + let rep = self.history_new(&target); + let rep = mem::replace(&mut self.current, rep); + if rep.cwd.is_regular() { + self.history.insert(rep.cwd.clone(), rep); + } + + // Parent + if let Some(parent) = target.parent_url() { + self.parent = Some(self.history_new(&parent)); + } + + // Hover the file + if let Some(h) = hovered { + emit!(Hover(h)); + } + + // Backstack + if target.is_regular() { + self.backstack.push(target.clone()); + } + + emit!(Refresh); + true + } + + pub fn cd_interactive(&mut self, target: Url) -> bool { + tokio::spawn(async move { + let mut result = + emit!(Input(InputOpt::top("Change directory:").with_value(target.to_string_lossy()))); + + if let Some(Ok(s)) = result.recv().await { + emit!(Cd(Url::from(s.trim()))); + } + }); + false + } +} diff --git a/core/src/tab/commands/copy.rs b/core/src/tab/commands/copy.rs new file mode 100644 index 00000000..45f98cb0 --- /dev/null +++ b/core/src/tab/commands/copy.rs @@ -0,0 +1,25 @@ +use std::ffi::{OsStr, OsString}; + +use crate::{external, tab::Tab}; + +impl Tab { + pub fn copy(&self, type_: &str) -> bool { + let mut s = OsString::new(); + let mut it = self.selected().into_iter().peekable(); + while let Some(f) = it.next() { + s.push(match type_ { + "path" => f.url.as_os_str(), + "dirname" => f.url.parent().map_or(OsStr::new(""), |p| p.as_os_str()), + "filename" => f.name().unwrap_or(OsStr::new("")), + "name_without_ext" => f.stem().unwrap_or(OsStr::new("")), + _ => return false, + }); + if it.peek().is_some() { + s.push("\n"); + } + } + + futures::executor::block_on(external::clipboard_set(s)).ok(); + false + } +} diff --git a/core/src/tab/commands/enter.rs b/core/src/tab/commands/enter.rs new file mode 100644 index 00000000..b3b248f9 --- /dev/null +++ b/core/src/tab/commands/enter.rs @@ -0,0 +1,30 @@ +use std::mem; + +use crate::{emit, tab::Tab}; + +impl Tab { + pub fn enter(&mut self) -> bool { + let Some(hovered) = self.current.hovered().filter(|h| h.is_dir()).map(|h| h.url()) else { + return false; + }; + + // Current + let rep = self.history_new(&hovered); + let rep = mem::replace(&mut self.current, rep); + if rep.cwd.is_regular() { + self.history.insert(rep.cwd.clone(), rep); + } + + // Parent + if let Some(rep) = self.parent.take() { + self.history.insert(rep.cwd.clone(), rep); + } + self.parent = Some(self.history_new(&hovered.parent_url().unwrap())); + + // Backstack + self.backstack.push(hovered); + + emit!(Refresh); + true + } +} diff --git a/core/src/tab/commands/escape.rs b/core/src/tab/commands/escape.rs new file mode 100644 index 00000000..03b0dbab --- /dev/null +++ b/core/src/tab/commands/escape.rs @@ -0,0 +1,21 @@ +use crate::tab::{Mode, Tab}; + +impl Tab { + pub fn escape(&mut self) -> bool { + if self.finder.take().is_some() { + return true; + } + + if let Some((_, indices)) = self.mode.visual() { + self.current.files.select_index(indices, Some(self.mode.is_select())); + self.mode = Mode::Normal; + return true; + } + + if self.select_all(Some(false)) { + return true; + } + + self.search_stop() + } +} diff --git a/core/src/tab/commands/find.rs b/core/src/tab/commands/find.rs new file mode 100644 index 00000000..2a9b9381 --- /dev/null +++ b/core/src/tab/commands/find.rs @@ -0,0 +1,67 @@ +use std::time::Duration; + +use config::keymap::{Exec, KeymapLayer}; +use shared::{Debounce, InputError}; +use tokio::pin; +use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; + +use crate::{emit, input::InputOpt, tab::{Finder, FinderCase, Tab}}; + +impl Tab { + pub fn find(&mut self, query: Option<&str>, prev: bool, case: FinderCase) -> bool { + if let Some(query) = query { + let Ok(finder) = Finder::new(query, case) else { + return false; + }; + + let step = if prev { + finder.prev(&self.current.files, self.current.cursor, true) + } else { + finder.next(&self.current.files, self.current.cursor, true) + }; + + if let Some(step) = step { + self.arrow(step.into()); + } + + self.finder = Some(finder); + return true; + } + + tokio::spawn(async move { + let rx = emit!(Input( + InputOpt::top(if prev { "Find previous:" } else { "Find next:" }).with_realtime() + )); + + let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); + pin!(rx); + + while let Some(Ok(s)) | Some(Err(InputError::Typed(s))) = rx.next().await { + emit!(Call( + Exec::call("find", vec![s]) + .with_bool("previous", prev) + .with_bool("smart", case == FinderCase::Smart) + .with_bool("insensitive", case == FinderCase::Insensitive) + .vec(), + KeymapLayer::Manager + )); + } + }); + false + } + + pub fn find_arrow(&mut self, prev: bool) -> bool { + let Some(finder) = &mut self.finder else { + return false; + }; + + let b = finder.catchup(&self.current.files); + let step = if prev { + finder.prev(&self.current.files, self.current.cursor, false) + } else { + finder.next(&self.current.files, self.current.cursor, false) + }; + + b | step.is_some_and(|s| self.arrow(s.into())) + } +} diff --git a/core/src/tab/commands/jump.rs b/core/src/tab/commands/jump.rs new file mode 100644 index 00000000..02778ecc --- /dev/null +++ b/core/src/tab/commands/jump.rs @@ -0,0 +1,24 @@ +use shared::Defer; + +use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, tab::Tab, Event, BLOCKER}; + +impl Tab { + pub fn jump(&self, global: bool) -> bool { + let cwd = self.current.cwd.clone(); + + tokio::spawn(async move { + let _guard = BLOCKER.acquire().await.unwrap(); + let _defer = Defer::new(|| Event::Stop(false, None).emit()); + emit!(Stop(true)).await; + + let rx = + if global { external::fzf(FzfOpt { cwd }) } else { external::zoxide(ZoxideOpt { cwd }) }?; + + if let Ok(target) = rx.await? { + emit!(Cd(target)); + } + Ok::<(), anyhow::Error>(()) + }); + false + } +} diff --git a/core/src/tab/commands/leave.rs b/core/src/tab/commands/leave.rs new file mode 100644 index 00000000..2a067246 --- /dev/null +++ b/core/src/tab/commands/leave.rs @@ -0,0 +1,39 @@ +use std::mem; + +use crate::{emit, tab::Tab}; + +impl Tab { + pub fn leave(&mut self) -> bool { + let current = self + .current + .hovered() + .and_then(|h| h.parent()) + .filter(|p| *p != self.current.cwd) + .or_else(|| self.current.cwd.parent_url()); + + let Some(current) = current else { + return false; + }; + + // Parent + if let Some(rep) = self.parent.take() { + self.history.insert(rep.cwd.clone(), rep); + } + if let Some(parent) = current.parent_url() { + self.parent = Some(self.history_new(&parent)); + } + + // Current + let rep = self.history_new(¤t); + let rep = mem::replace(&mut self.current, rep); + if rep.cwd.is_regular() { + self.history.insert(rep.cwd.clone(), rep); + } + + // Backstack + self.backstack.push(current); + + emit!(Refresh); + true + } +} diff --git a/core/src/tab/commands/mod.rs b/core/src/tab/commands/mod.rs new file mode 100644 index 00000000..82f3f2a7 --- /dev/null +++ b/core/src/tab/commands/mod.rs @@ -0,0 +1,27 @@ +mod arrow; +mod backstack; +mod cd; +mod copy; +mod enter; +mod escape; +mod find; +mod jump; +mod leave; +mod search; +mod select; +mod shell; +mod visual_mode; + +pub use arrow::*; +pub use backstack::*; +pub use cd::*; +pub use copy::*; +pub use enter::*; +pub use escape::*; +pub use find::*; +pub use jump::*; +pub use leave::*; +pub use search::*; +pub use select::*; +pub use shell::*; +pub use visual_mode::*; diff --git a/core/src/tab/commands/search.rs b/core/src/tab/commands/search.rs new file mode 100644 index 00000000..901c4ce4 --- /dev/null +++ b/core/src/tab/commands/search.rs @@ -0,0 +1,60 @@ +use std::{mem, time::Duration}; + +use anyhow::bail; +use tokio::pin; +use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; + +use crate::{emit, external, files::FilesOp, input::InputOpt, tab::Tab}; + +impl Tab { + pub fn search(&mut self, grep: bool) -> bool { + if let Some(handle) = self.search.take() { + handle.abort(); + } + + let mut cwd = self.current.cwd.clone(); + let hidden = self.show_hidden; + + self.search = Some(tokio::spawn(async move { + let Some(Ok(subject)) = emit!(Input(InputOpt::top("Search:"))).recv().await else { + bail!("canceled") + }; + + cwd = cwd.into_search(subject.clone()); + let rx = if grep { + external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject }) + } else { + external::fd(external::FdOpt { cwd: cwd.clone(), hidden, glob: false, subject }) + }?; + + let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(300)); + pin!(rx); + + let ticket = FilesOp::prepare(&cwd); + let mut first = true; + while let Some(chunk) = rx.next().await { + if first { + emit!(Cd(cwd.clone())); + first = false; + } + emit!(Files(FilesOp::Part(cwd.clone(), ticket, chunk))); + } + Ok(()) + })); + true + } + + pub fn search_stop(&mut self) -> bool { + if let Some(handle) = self.search.take() { + handle.abort(); + } + if self.current.cwd.is_search() { + self.preview_reset_image(); + + let rep = self.history_new(&self.current.cwd.to_regular()); + drop(mem::replace(&mut self.current, rep)); + emit!(Refresh); + } + false + } +} diff --git a/core/src/tab/commands/select.rs b/core/src/tab/commands/select.rs new file mode 100644 index 00000000..3a6e5b53 --- /dev/null +++ b/core/src/tab/commands/select.rs @@ -0,0 +1,12 @@ +use crate::tab::Tab; + +impl Tab { + pub fn select(&mut self, state: Option) -> bool { + if let Some(u) = self.current.hovered().map(|h| h.url()) { + return self.current.files.select(&u, state); + } + false + } + + pub fn select_all(&mut self, state: Option) -> bool { self.current.files.select_all(state) } +} diff --git a/core/src/tab/commands/shell.rs b/core/src/tab/commands/shell.rs new file mode 100644 index 00000000..9a86991c --- /dev/null +++ b/core/src/tab/commands/shell.rs @@ -0,0 +1,35 @@ +use config::open::Opener; + +use crate::{emit, input::InputOpt, tab::Tab}; + +impl Tab { + pub fn shell(&self, exec: &str, block: bool, confirm: bool) -> bool { + let selected: Vec<_> = self + .selected() + .into_iter() + .map(|f| (f.url.as_os_str().to_owned(), Default::default())) + .collect(); + + let mut exec = exec.to_owned(); + tokio::spawn(async move { + if !confirm || exec.is_empty() { + let mut result = emit!(Input( + InputOpt::top(if block { "Shell (block):" } else { "Shell:" }) + .with_value(&exec) + .with_highlight() + )); + match result.recv().await { + Some(Ok(e)) => exec = e, + _ => return, + } + } + + emit!(Open( + selected, + Some(Opener { exec, block, orphan: false, display_name: Default::default(), spread: true }) + )); + }); + + false + } +} diff --git a/core/src/tab/commands/visual_mode.rs b/core/src/tab/commands/visual_mode.rs new file mode 100644 index 00000000..9cc5d3cd --- /dev/null +++ b/core/src/tab/commands/visual_mode.rs @@ -0,0 +1,16 @@ +use std::collections::BTreeSet; + +use crate::tab::{Mode, Tab}; + +impl Tab { + pub fn visual_mode(&mut self, unset: bool) -> bool { + let idx = self.current.cursor; + + if unset { + self.mode = Mode::Unset(idx, BTreeSet::from([idx])); + } else { + self.mode = Mode::Select(idx, BTreeSet::from([idx])); + }; + true + } +} diff --git a/core/src/manager/finder.rs b/core/src/tab/finder.rs similarity index 95% rename from core/src/manager/finder.rs rename to core/src/tab/finder.rs index 2ce79ab2..d2b73930 100644 --- a/core/src/manager/finder.rs +++ b/core/src/tab/finder.rs @@ -53,7 +53,7 @@ impl Finder { } pub(super) fn catchup(&mut self, files: &Files) -> bool { - if self.version == files.version() { + if self.version == files.version { return false; } self.matched.clear(); @@ -64,7 +64,7 @@ impl Finder { continue; } - self.matched.insert(file.url_owned(), i); + self.matched.insert(file.url(), i); if self.matched.len() > 99 { break; } @@ -72,7 +72,7 @@ impl Finder { i += 1; } - self.version = files.version(); + self.version = files.version; true } diff --git a/core/src/manager/folder.rs b/core/src/tab/folder.rs similarity index 100% rename from core/src/manager/folder.rs rename to core/src/tab/folder.rs diff --git a/core/src/tab/mod.rs b/core/src/tab/mod.rs new file mode 100644 index 00000000..36df8142 --- /dev/null +++ b/core/src/tab/mod.rs @@ -0,0 +1,12 @@ +mod backstack; +mod commands; +mod finder; +mod folder; +mod mode; +mod tab; + +pub use backstack::*; +pub use finder::*; +pub use folder::*; +pub use mode::*; +pub use tab::*; diff --git a/core/src/manager/mode.rs b/core/src/tab/mode.rs similarity index 100% rename from core/src/manager/mode.rs rename to core/src/tab/mode.rs diff --git a/core/src/tab/tab.rs b/core/src/tab/tab.rs new file mode 100644 index 00000000..b4e19b2f --- /dev/null +++ b/core/src/tab/tab.rs @@ -0,0 +1,164 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use anyhow::Result; +use config::MANAGER; +use shared::Url; +use tokio::task::JoinHandle; + +use super::{Backstack, Finder, Folder, Mode}; +use crate::{emit, files::{File, FilesSorter}, preview::{Preview, PreviewLock}}; + +pub struct Tab { + pub mode: Mode, + pub current: Folder, + pub parent: Option, + + pub backstack: Backstack, + pub history: BTreeMap, + pub preview: Preview, + + pub finder: Option, + pub(super) search: Option>>, + pub sorter: FilesSorter, + pub show_hidden: bool, +} + +impl From for Tab { + fn from(url: Url) -> Self { + let parent = url.parent_url().map(Folder::from); + + Self { + mode: Default::default(), + current: Folder::from(url.clone()), + parent, + + backstack: Backstack::new(url), + history: Default::default(), + preview: Default::default(), + + finder: None, + search: None, + sorter: Default::default(), + show_hidden: MANAGER.show_hidden, + } + } +} + +impl From<&Url> for Tab { + fn from(url: &Url) -> Self { Self::from(url.clone()) } +} + +impl Tab { + pub fn update_peek(&mut self, max: usize, url: Url) -> bool { + let Some(hovered) = self.current.hovered() else { + return false; + }; + + if url != hovered.url { + return false; + } + + self.preview.arrow_max(max) + } + + pub fn update_preview(&mut self, lock: PreviewLock) -> bool { + let Some(hovered) = self.current.hovered().map(|h| &h.url) else { + return self.preview_reset(); + }; + + if lock.url != *hovered { + return false; + } + + self.preview.lock = Some(lock); + true + } +} + +impl Tab { + // --- Mode + #[inline] + pub fn in_selecting(&self) -> bool { self.mode.is_visual() || self.current.files.has_selected() } + + // --- Current + pub fn selected(&self) -> Vec<&File> { + let pending = self.mode.visual().map(|(_, p)| Cow::Borrowed(p)).unwrap_or_default(); + let selected = self.current.files.selected(&pending, self.mode.is_unset()); + + if selected.is_empty() { + self.current.hovered().map(|h| vec![h]).unwrap_or_default() + } else { + selected + } + } + + // --- History + #[inline] + pub fn history(&self, url: &Url) -> Option<&Folder> { self.history.get(url) } + + #[inline] + pub fn history_new(&mut self, url: &Url) -> Folder { + self.history.remove(url).unwrap_or_else(|| Folder::from(url)) + } + + // --- Preview + #[inline] + pub fn preview_reset(&mut self) -> bool { self.preview.reset(|_| true) } + + #[inline] + pub fn preview_reset_image(&mut self) -> bool { self.preview.reset(|l| l.is_image()) } + + #[inline] + pub fn preview_arrow(&mut self, step: isize) -> bool { self.preview.arrow(step) } + + // --- Sorter + pub fn set_sorter(&mut self, sorter: FilesSorter) -> bool { + if sorter == self.sorter { + return false; + } + + self.sorter = sorter; + self.apply_files_attrs(false) + } + + // --- Show hidden + pub fn set_show_hidden(&mut self, state: Option) -> bool { + let state = state.unwrap_or(!self.show_hidden); + if state == self.show_hidden { + return false; + } + + self.show_hidden = state; + if self.apply_files_attrs(false) { + emit!(Peek); + return true; + } + false + } + + pub fn apply_files_attrs(&mut self, just_preview: bool) -> bool { + let mut b = false; + if let Some(f) = + self.current.hovered().filter(|h| h.is_dir()).and_then(|h| self.history.get_mut(&h.url)) + { + b |= f.files.set_show_hidden(self.show_hidden); + b |= f.files.set_sorter(self.sorter); + } + + if just_preview { + return b; + } + + let hovered = self.current.hovered().map(|h| h.url()); + b |= self.current.files.set_show_hidden(self.show_hidden); + b |= self.current.files.set_sorter(self.sorter); + + if let Some(parent) = self.parent.as_mut() { + b |= parent.files.set_show_hidden(self.show_hidden); + b |= parent.files.set_sorter(self.sorter); + } + + self.current.repos(hovered); + b + } +} diff --git a/core/src/tasks/tasks.rs b/core/src/tasks/tasks.rs index 3bddd0d7..0f6edd34 100644 --- a/core/src/tasks/tasks.rs +++ b/core/src/tasks/tasks.rs @@ -237,8 +237,8 @@ impl Tasks { let targets: Vec<_> = targets .iter() - .filter(|f| f.is_dir() && targets.size(f.url()).is_none()) - .map(|f| f.url()) + .filter(|f| f.is_dir() && targets.size(&f.url).is_none()) + .map(|f| &f.url) .collect(); if !targets.is_empty() { @@ -252,8 +252,8 @@ impl Tasks { pub fn precache_mime(&self, targets: &[File], mimetype: &HashMap) -> bool { let targets: Vec<_> = targets .iter() - .filter(|f| f.is_file() && !mimetype.contains_key(f.url())) - .map(|f| f.url_owned()) + .filter(|f| f.is_file() && !mimetype.contains_key(&f.url)) + .map(|f| f.url()) .collect(); if !targets.is_empty() { diff --git a/plugin/src/bindings/active.rs b/plugin/src/bindings/active.rs index 7414b767..00f3d8d4 100644 --- a/plugin/src/bindings/active.rs +++ b/plugin/src/bindings/active.rs @@ -10,12 +10,12 @@ pub struct Active<'a, 'b> { scope: &'b mlua::Scope<'a, 'a>, cx: &'a core::Ctx, - inner: &'a core::manager::Tab, + inner: &'a core::tab::Tab, } impl<'a, 'b> Active<'a, 'b> { pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { + LUA.register_userdata_type::(|reg| { reg.add_field_method_get("is_select", |_, me| Ok(me.is_select())); reg.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); reg.add_field_method_get("is_visual", |_, me| Ok(me.is_visual())); @@ -24,7 +24,7 @@ impl<'a, 'b> Active<'a, 'b> { reg.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.to_string())); })?; - LUA.register_userdata_type::(|reg| { + LUA.register_userdata_type::(|reg| { reg.add_field_method_get("cwd", |_, me| Ok(Url::from(&me.cwd))); reg.add_field_method_get("offset", |_, me| Ok(me.offset)); reg.add_field_method_get("cursor", |_, me| Ok(me.cursor)); @@ -34,7 +34,7 @@ impl<'a, 'b> Active<'a, 'b> { reg.add_field_function_get("hovered", |_, me| me.named_user_value::("hovered")); })?; - LUA.register_userdata_type::(|reg| { + LUA.register_userdata_type::(|reg| { reg.add_field_function_get("folder", |_, me| me.named_user_value::("folder")); })?; @@ -60,7 +60,7 @@ impl<'a, 'b> Active<'a, 'b> { pub(crate) fn folder( &self, - inner: &'a core::manager::Folder, + inner: &'a core::tab::Folder, window: Option<(usize, usize)>, ) -> mlua::Result> { let window = window.unwrap_or_else(|| (inner.offset, MANAGER.layout.folder_height())); @@ -95,7 +95,7 @@ impl<'a, 'b> Active<'a, 'b> { &self, idx: usize, inner: &'a core::files::File, - folder: &'a core::manager::Folder, + folder: &'a core::tab::Folder, ) -> mlua::Result> { let ud = self.scope.create_any_userdata_ref(inner)?; ud.set_named_user_value("idx", idx)?; @@ -105,8 +105,8 @@ impl<'a, 'b> Active<'a, 'b> { Ok(ud) } - fn preview(&self, tab: &'a core::manager::Tab) -> mlua::Result> { - let inner = tab.preview(); + fn preview(&self, tab: &'a core::tab::Tab) -> mlua::Result> { + let inner = &tab.preview; let ud = self.scope.create_any_userdata_ref(inner)?; ud.set_named_user_value( diff --git a/plugin/src/bindings/files.rs b/plugin/src/bindings/files.rs index c81ac3ca..02eb23d3 100644 --- a/plugin/src/bindings/files.rs +++ b/plugin/src/bindings/files.rs @@ -12,11 +12,11 @@ impl From<&core::files::File> for File { impl UserData for File { fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("url", |_, me| Ok(Url::from(me.0.url()))); - fields.add_field_method_get("length", |_, me| Ok(me.0.length())); + fields.add_field_method_get("url", |_, me| Ok(Url::from(&me.0.url))); + fields.add_field_method_get("length", |_, me| Ok(me.0.length)); fields.add_field_method_get("link_to", |_, me| Ok(me.0.link_to().map(Url::from))); - fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link())); - fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden())); + fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link)); + fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden)); } } @@ -47,33 +47,33 @@ impl Files { })?; LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("url", |_, me| Ok(Url::from(me.url()))); - reg.add_field_method_get("length", |_, me| Ok(me.length())); + reg.add_field_method_get("url", |_, me| Ok(Url::from(&me.url))); + reg.add_field_method_get("length", |_, me| Ok(me.length)); reg.add_field_method_get("link_to", |_, me| Ok(me.link_to().map(Url::from))); - reg.add_field_method_get("is_link", |_, me| Ok(me.is_link())); - reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden())); + reg.add_field_method_get("is_link", |_, me| Ok(me.is_link)); + reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden)); reg.add_field_method_get("name", |_, me| { - Ok(me.url().file_name().map(|n| n.to_string_lossy().to_string())) + Ok(me.url.file_name().map(|n| n.to_string_lossy().to_string())) }); reg.add_field_method_get("permissions", |_, me| { - Ok(shared::permissions(me.meta().permissions())) + Ok(shared::permissions(me.meta.permissions())) }); reg.add_function("mime", |_, me: AnyUserData| { let manager = me.named_user_value::>("manager")?; let file = me.borrow::()?; - Ok(manager.mimetype.get(file.url()).cloned()) + Ok(manager.mimetype.get(&file.url).cloned()) }); reg.add_function("prefix", |_, me: AnyUserData| { - let folder = me.named_user_value::>("folder")?; + let folder = me.named_user_value::>("folder")?; if !folder.cwd.is_search() { return Ok(None); } let file = me.borrow::()?; - let mut p = file.url().strip_prefix(&folder.cwd).unwrap_or(file.url()).components(); + let mut p = file.url.strip_prefix(&folder.cwd).unwrap_or(&file.url).components(); p.next_back(); Ok(Some(p.as_path().to_string_lossy().to_string())) }); @@ -83,7 +83,7 @@ impl Files { THEME .icons .iter() - .find(|&x| x.name.match_path(me.url(), Some(me.is_dir()))) + .find(|&x| x.name.match_path(&me.url, Some(me.is_dir()))) .map(|x| x.display.to_string()), ) }); @@ -91,28 +91,28 @@ impl Files { reg.add_function("style", |_, me: AnyUserData| { let manager = me.named_user_value::>("manager")?; let file = me.borrow::()?; - let mime = manager.mimetype.get(file.url()); + let mime = manager.mimetype.get(&file.url); Ok( THEME .filetypes .iter() - .find(|&x| x.matches(file.url(), mime, file.is_dir())) + .find(|&x| x.matches(&file.url, mime, file.is_dir())) .map(|x| Style::from(x.style)), ) }); reg.add_function("is_hovered", |_, me: AnyUserData| { - let folder = me.named_user_value::>("folder")?; + let folder = me.named_user_value::>("folder")?; let file = me.borrow::()?; - Ok(matches!(folder.hovered(), Some(f) if f.url() == file.url())) + Ok(matches!(folder.hovered(), Some(f) if f.url == file.url)) }); reg.add_function("is_yanked", |_, me: AnyUserData| { let manager = me.named_user_value::>("manager")?; let file = me.borrow::()?; - Ok(if !manager.yanked().1.contains(file.url()) { + Ok(if !manager.yanked.1.contains(&file.url) { 0u8 - } else if manager.yanked().0 { + } else if manager.yanked.0 { 2u8 } else { 1u8 @@ -121,10 +121,10 @@ impl Files { reg.add_function("is_selected", |_, me: AnyUserData| { let manager = me.named_user_value::>("manager")?; - let folder = me.named_user_value::>("folder")?; + let folder = me.named_user_value::>("folder")?; let file = me.borrow::()?; - let selected = folder.files.is_selected(file.url()); + let selected = folder.files.is_selected(&file.url); Ok(if !manager.active().mode.is_visual() { selected } else { @@ -135,12 +135,12 @@ impl Files { reg.add_function("found", |lua, me: AnyUserData| { let manager = me.named_user_value::>("manager")?; - let Some(finder) = manager.active().finder() else { + let Some(finder) = &manager.active().finder else { return Ok(None); }; let file = me.borrow::()?; - if let Some(idx) = finder.matched_idx(file.url()) { + if let Some(idx) = finder.matched_idx(&file.url) { return Some( lua.create_sequence_from([idx.into_lua(lua)?, finder.matched().len().into_lua(lua)?]), ) @@ -151,7 +151,7 @@ impl Files { reg.add_function("highlights", |_, me: AnyUserData| { let manager = me.named_user_value::>("manager")?; - let Some(finder) = manager.active().finder() else { + let Some(finder) = &manager.active().finder else { return Ok(None); }; diff --git a/plugin/src/bindings/tabs.rs b/plugin/src/bindings/tabs.rs index 7b6aaec9..68ca35de 100644 --- a/plugin/src/bindings/tabs.rs +++ b/plugin/src/bindings/tabs.rs @@ -11,7 +11,7 @@ pub struct Tabs<'a, 'b> { impl<'a, 'b> Tabs<'a, 'b> { pub(crate) fn init() -> mlua::Result<()> { LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("idx", |_, me| Ok(me.idx())); + reg.add_field_method_get("idx", |_, me| Ok(me.idx)); reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); reg.add_meta_function(MetaMethod::Index, |_, (me, index): (AnyUserData, usize)| { let items = me.named_user_value::>("items")?; @@ -19,7 +19,7 @@ impl<'a, 'b> Tabs<'a, 'b> { }); })?; - LUA.register_userdata_type::(|reg| { + LUA.register_userdata_type::(|reg| { reg.add_method("name", |_, me, ()| { Ok( me.current @@ -56,7 +56,7 @@ impl<'a, 'b> Tabs<'a, 'b> { Ok(ud) } - fn tab(&self, inner: &'a core::manager::Tab) -> mlua::Result> { + fn tab(&self, inner: &'a core::tab::Tab) -> mlua::Result> { let ud = self.scope.create_any_userdata_ref(inner)?; ud.set_named_user_value("parent", inner.parent.as_ref().and_then(|p| self.folder(p).ok()))?; @@ -66,7 +66,7 @@ impl<'a, 'b> Tabs<'a, 'b> { Ok(ud) } - pub(crate) fn folder(&self, inner: &'a core::manager::Folder) -> mlua::Result> { + pub(crate) fn folder(&self, inner: &'a core::tab::Folder) -> mlua::Result> { let ud = self.scope.create_any_userdata_ref(inner)?; ud.set_named_user_value("files", self.files(&inner.files)?)?; @@ -77,8 +77,8 @@ impl<'a, 'b> Tabs<'a, 'b> { self.scope.create_any_userdata_ref(inner) } - fn preview(&self, tab: &'a core::manager::Tab) -> mlua::Result> { - let inner = tab.preview(); + fn preview(&self, tab: &'a core::tab::Tab) -> mlua::Result> { + let inner = &tab.preview; let ud = self.scope.create_any_userdata_ref(inner)?; ud.set_named_user_value( diff --git a/plugin/src/scope.rs b/plugin/src/scope.rs index 8f34c35a..2357e794 100644 --- a/plugin/src/scope.rs +++ b/plugin/src/scope.rs @@ -8,7 +8,7 @@ pub fn scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) { _ = LUA.scope(|scope| { let tbl = LUA.create_table()?; tbl.set("active", bindings::Active::new(scope, cx).make()?)?; - tbl.set("tabs", bindings::Tabs::new(scope, cx.manager.tabs()).make()?)?; + tbl.set("tabs", bindings::Tabs::new(scope, &cx.manager.tabs).make()?)?; tbl.set("tasks", bindings::Tasks::new(scope, &cx.tasks).make()?)?; GLOBALS.set("cx", tbl)?;