mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
refactor: split commands into separate files (#272)
This commit is contained in:
parent
b840dcccd5
commit
e12ebd38e0
54 changed files with 1145 additions and 1051 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<UnboundedSender<Event>> = RoCell::new();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Url>,
|
||||
pub(super) is_link: bool,
|
||||
pub(super) is_hidden: bool,
|
||||
pub url: Url,
|
||||
pub meta: Metadata,
|
||||
pub length: u64,
|
||||
pub(super) link_to: Option<Url>,
|
||||
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<Url> { 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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ use tokio::{fs, select, sync::mpsc::{self, UnboundedReceiver}};
|
|||
use super::{File, FilesSorter, FILES_TICKET};
|
||||
|
||||
pub struct Files {
|
||||
items: Vec<File>,
|
||||
hidden: Vec<File>,
|
||||
ticket: u64,
|
||||
version: u64,
|
||||
items: Vec<File>,
|
||||
hidden: Vec<File>,
|
||||
ticket: u64,
|
||||
pub(crate) version: u64,
|
||||
|
||||
sizes: BTreeMap<Url, u64>,
|
||||
selected: BTreeSet<Url>,
|
||||
|
|
@ -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<usize>, state: Option<bool>) -> 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<usize> { self.iter().position(|f| &f.url == url) }
|
||||
|
||||
#[inline]
|
||||
pub fn duplicate(&self, idx: usize) -> Option<File> { self.items.get(idx).cloned() }
|
||||
|
||||
// --- Version
|
||||
#[inline]
|
||||
pub fn version(&self) -> u64 { self.version }
|
||||
|
||||
// --- Sizes
|
||||
#[inline]
|
||||
pub fn size(&self, url: &Url) -> Option<u64> { self.sizes.get(url).copied() }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ use crate::{emit, input::Input};
|
|||
|
||||
#[derive(Default)]
|
||||
pub struct Help {
|
||||
visible: bool,
|
||||
layer: KeymapLayer,
|
||||
bindings: Vec<Control>,
|
||||
pub visible: bool,
|
||||
pub layer: KeymapLayer,
|
||||
bindings: Vec<Control>,
|
||||
|
||||
// Filter
|
||||
keyword: Option<String>,
|
||||
|
|
@ -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<String> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
10
core/src/manager/commands/close.rs
Normal file
10
core/src/manager/commands/close.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
43
core/src/manager/commands/create.rs
Normal file
43
core/src/manager/commands/create.rs
Normal file
|
|
@ -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::<PathBuf>());
|
||||
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
|
||||
}
|
||||
}
|
||||
19
core/src/manager/commands/mod.rs
Normal file
19
core/src/manager/commands/mod.rs
Normal file
|
|
@ -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::*;
|
||||
58
core/src/manager/commands/open.rs
Normal file
58
core/src/manager/commands/open.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
31
core/src/manager/commands/peek.rs
Normal file
31
core/src/manager/commands/peek.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
24
core/src/manager/commands/quit.rs
Normal file
24
core/src/manager/commands/quit.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
31
core/src/manager/commands/refresh.rs
Normal file
31
core/src/manager/commands/refresh.rs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
164
core/src/manager/commands/rename.rs
Normal file
164
core/src/manager/commands/rename.rs
Normal file
|
|
@ -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::<Vec<_>>().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<PathBuf>, new: Vec<PathBuf>) -> 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(())
|
||||
}
|
||||
}
|
||||
12
core/src/manager/commands/suspend.rs
Normal file
12
core/src/manager/commands/suspend.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
9
core/src/manager/commands/yank.rs
Normal file
9
core/src/manager/commands/yank.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Url>),
|
||||
pub tabs: Tabs,
|
||||
pub yanked: (bool, HashSet<Url>),
|
||||
|
||||
watcher: Watcher,
|
||||
pub mimetype: HashMap<Url, String>,
|
||||
pub(super) watcher: Watcher,
|
||||
pub mimetype: HashMap<Url, String>,
|
||||
}
|
||||
|
||||
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::<PathBuf>());
|
||||
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::<Vec<_>>().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<PathBuf>, new: Vec<PathBuf>) -> 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<Url>) { &self.yanked }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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<Folder>,
|
||||
|
||||
pub(super) backstack: Backstack<Url>,
|
||||
pub(super) history: BTreeMap<Url, Folder>,
|
||||
pub(super) preview: Preview,
|
||||
|
||||
finder: Option<Finder>,
|
||||
search: Option<JoinHandle<Result<()>>>,
|
||||
pub(super) sorter: FilesSorter,
|
||||
pub(super) show_hidden: bool,
|
||||
}
|
||||
|
||||
impl From<Url> 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>) -> 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>) -> 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>) -> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Tab>,
|
||||
pub idx: usize,
|
||||
items: Vec<Tab>,
|
||||
}
|
||||
|
||||
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() }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
23
core/src/tab/commands/arrow.rs
Normal file
23
core/src/tab/commands/arrow.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
17
core/src/tab/commands/backstack.rs
Normal file
17
core/src/tab/commands/backstack.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
71
core/src/tab/commands/cd.rs
Normal file
71
core/src/tab/commands/cd.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
25
core/src/tab/commands/copy.rs
Normal file
25
core/src/tab/commands/copy.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
30
core/src/tab/commands/enter.rs
Normal file
30
core/src/tab/commands/enter.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
21
core/src/tab/commands/escape.rs
Normal file
21
core/src/tab/commands/escape.rs
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
67
core/src/tab/commands/find.rs
Normal file
67
core/src/tab/commands/find.rs
Normal file
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
24
core/src/tab/commands/jump.rs
Normal file
24
core/src/tab/commands/jump.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
39
core/src/tab/commands/leave.rs
Normal file
39
core/src/tab/commands/leave.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
27
core/src/tab/commands/mod.rs
Normal file
27
core/src/tab/commands/mod.rs
Normal file
|
|
@ -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::*;
|
||||
60
core/src/tab/commands/search.rs
Normal file
60
core/src/tab/commands/search.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
12
core/src/tab/commands/select.rs
Normal file
12
core/src/tab/commands/select.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use crate::tab::Tab;
|
||||
|
||||
impl Tab {
|
||||
pub fn select(&mut self, state: Option<bool>) -> 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>) -> bool { self.current.files.select_all(state) }
|
||||
}
|
||||
35
core/src/tab/commands/shell.rs
Normal file
35
core/src/tab/commands/shell.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
16
core/src/tab/commands/visual_mode.rs
Normal file
16
core/src/tab/commands/visual_mode.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
12
core/src/tab/mod.rs
Normal file
12
core/src/tab/mod.rs
Normal file
|
|
@ -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::*;
|
||||
164
core/src/tab/tab.rs
Normal file
164
core/src/tab/tab.rs
Normal file
|
|
@ -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<Folder>,
|
||||
|
||||
pub backstack: Backstack<Url>,
|
||||
pub history: BTreeMap<Url, Folder>,
|
||||
pub preview: Preview,
|
||||
|
||||
pub finder: Option<Finder>,
|
||||
pub(super) search: Option<JoinHandle<Result<()>>>,
|
||||
pub sorter: FilesSorter,
|
||||
pub show_hidden: bool,
|
||||
}
|
||||
|
||||
impl From<Url> 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>) -> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Url, String>) -> 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() {
|
||||
|
|
|
|||
|
|
@ -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::<core::manager::Mode>(|reg| {
|
||||
LUA.register_userdata_type::<core::tab::Mode>(|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::<core::manager::Folder>(|reg| {
|
||||
LUA.register_userdata_type::<core::tab::Folder>(|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::<Value>("hovered"));
|
||||
})?;
|
||||
|
||||
LUA.register_userdata_type::<core::manager::Preview>(|reg| {
|
||||
LUA.register_userdata_type::<core::preview::Preview>(|reg| {
|
||||
reg.add_field_function_get("folder", |_, me| me.named_user_value::<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<AnyUserData<'a>> {
|
||||
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<AnyUserData<'a>> {
|
||||
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<AnyUserData<'a>> {
|
||||
let inner = tab.preview();
|
||||
fn preview(&self, tab: &'a core::tab::Tab) -> mlua::Result<AnyUserData<'a>> {
|
||||
let inner = &tab.preview;
|
||||
|
||||
let ud = self.scope.create_any_userdata_ref(inner)?;
|
||||
ud.set_named_user_value(
|
||||
|
|
|
|||
|
|
@ -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::<core::files::File>(|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::<UserDataRef<core::manager::Manager>>("manager")?;
|
||||
let file = me.borrow::<core::files::File>()?;
|
||||
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::<UserDataRef<core::manager::Folder>>("folder")?;
|
||||
let folder = me.named_user_value::<UserDataRef<core::tab::Folder>>("folder")?;
|
||||
if !folder.cwd.is_search() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let file = me.borrow::<core::files::File>()?;
|
||||
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::<UserDataRef<core::manager::Manager>>("manager")?;
|
||||
let file = me.borrow::<core::files::File>()?;
|
||||
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::<UserDataRef<core::manager::Folder>>("folder")?;
|
||||
let folder = me.named_user_value::<UserDataRef<core::tab::Folder>>("folder")?;
|
||||
let file = me.borrow::<core::files::File>()?;
|
||||
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::<UserDataRef<core::manager::Manager>>("manager")?;
|
||||
let file = me.borrow::<core::files::File>()?;
|
||||
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::<UserDataRef<core::manager::Manager>>("manager")?;
|
||||
let folder = me.named_user_value::<UserDataRef<core::manager::Folder>>("folder")?;
|
||||
let folder = me.named_user_value::<UserDataRef<core::tab::Folder>>("folder")?;
|
||||
let file = me.borrow::<core::files::File>()?;
|
||||
|
||||
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::<UserDataRef<core::manager::Manager>>("manager")?;
|
||||
let Some(finder) = manager.active().finder() else {
|
||||
let Some(finder) = &manager.active().finder else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let file = me.borrow::<core::files::File>()?;
|
||||
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::<UserDataRef<core::manager::Manager>>("manager")?;
|
||||
let Some(finder) = manager.active().finder() else {
|
||||
let Some(finder) = &manager.active().finder else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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::<core::manager::Tabs>(|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::<Vec<AnyUserData>>("items")?;
|
||||
|
|
@ -19,7 +19,7 @@ impl<'a, 'b> Tabs<'a, 'b> {
|
|||
});
|
||||
})?;
|
||||
|
||||
LUA.register_userdata_type::<core::manager::Tab>(|reg| {
|
||||
LUA.register_userdata_type::<core::tab::Tab>(|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<AnyUserData<'a>> {
|
||||
fn tab(&self, inner: &'a core::tab::Tab) -> mlua::Result<AnyUserData<'a>> {
|
||||
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<AnyUserData<'a>> {
|
||||
pub(crate) fn folder(&self, inner: &'a core::tab::Folder) -> mlua::Result<AnyUserData<'a>> {
|
||||
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<AnyUserData<'a>> {
|
||||
let inner = tab.preview();
|
||||
fn preview(&self, tab: &'a core::tab::Tab) -> mlua::Result<AnyUserData<'a>> {
|
||||
let inner = &tab.preview;
|
||||
|
||||
let ud = self.scope.create_any_userdata_ref(inner)?;
|
||||
ud.set_named_user_value(
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue