yazi/core/src/manager/tab.rs
2023-08-26 17:28:43 +08:00

405 lines
9.4 KiB
Rust

use std::{collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, path::{Path, PathBuf}};
use anyhow::{Error, Result};
use config::{open::Opener, MANAGER};
use futures::StreamExt;
use shared::{Defer, MIME_DIR};
use tokio::task::JoinHandle;
use super::{Folder, Mode, Preview, PreviewLock};
use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, Files, FilesOp}, input::InputOpt, Event, BLOCKER};
pub struct Tab {
pub(super) mode: Mode,
pub(super) current: Folder,
pub(super) parent: Option<Folder>,
search: Option<JoinHandle<Result<()>>>,
pub(super) history: BTreeMap<PathBuf, Folder>,
pub(super) preview: Preview,
}
impl Tab {
pub fn new(path: &Path) -> Self {
Self {
mode: Default::default(),
current: Folder::new(path),
parent: path.parent().map(Folder::new),
search: None,
history: Default::default(),
preview: Default::default(),
}
}
pub fn escape(&mut self) -> bool {
if let Some((_, indices)) = self.mode.visual() {
let b = matches!(self.mode, Mode::Select(..));
for idx in indices.iter() {
self.current.select(Some(*idx), Some(b));
}
self.mode = Mode::Normal;
return true;
}
if self.select_all(Some(false)) {
return true;
}
self.search_stop()
}
pub fn arrow(&mut self, step: isize) -> bool {
let ok = if step > 0 {
self.current.next(step as usize)
} else {
self.current.prev(step.unsigned_abs())
};
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
}
pub async fn cd(&mut self, mut target: PathBuf) -> bool {
let Ok(file) = File::from(&target).await else {
return false;
};
let mut hovered = None;
if !file.meta.is_dir() {
hovered = Some(file);
target = target.parent().unwrap().to_path_buf();
}
if self.current.cwd == target {
if hovered.map(|h| self.current.hover_force(h)).unwrap_or(false) {
emit!(Hover);
}
return false;
}
if let Some(rep) = self.parent.take() {
self.history.insert(rep.cwd.clone(), rep);
}
let rep = self.history_new(&target);
let rep = mem::replace(&mut self.current, rep);
if !rep.in_search {
self.history.insert(rep.cwd.clone(), rep);
}
if let Some(parent) = target.parent() {
self.parent = Some(self.history_new(parent));
}
if let Some(h) = hovered {
self.current.hover_force(h);
}
emit!(Refresh);
true
}
pub fn cd_interactive(&mut self, target: PathBuf) -> bool {
tokio::spawn(async move {
let result =
emit!(Input(InputOpt::top("Change directory:").with_value(target.to_string_lossy())));
if let Ok(target) = result.await {
emit!(Cd(PathBuf::from(target)));
}
});
false
}
pub fn enter(&mut self) -> bool {
let Some(hovered) = self.current.hovered.clone() else {
return false;
};
if !hovered.meta.is_dir() {
return false;
}
let rep = self.history_new(&hovered.path);
let rep = mem::replace(&mut self.current, rep);
if !rep.in_search {
self.history.insert(rep.cwd.clone(), rep);
}
if let Some(rep) = self.parent.take() {
self.history.insert(rep.cwd.clone(), rep);
}
self.parent = Some(self.history_new(hovered.path.parent().unwrap()));
emit!(Refresh);
true
}
pub fn leave(&mut self) -> bool {
let current = self
.current
.hovered
.as_ref()
.and_then(|h| h.path.parent())
.and_then(|p| if p == self.current.cwd { None } else { Some(p) })
.or_else(|| self.current.cwd.parent());
let Some(current) = current.map(Path::to_path_buf) else {
return false;
};
if let Some(rep) = self.parent.take() {
self.history.insert(rep.cwd.clone(), rep);
}
if let Some(parent) = current.parent() {
self.parent = Some(self.history_new(parent));
}
let rep = self.history_new(&current);
let rep = mem::replace(&mut self.current, rep);
if !rep.in_search {
self.history.insert(rep.cwd.clone(), rep);
}
emit!(Refresh);
true
}
// TODO
pub fn back(&mut self) -> bool { false }
// TODO
pub fn forward(&mut self) -> bool { false }
pub fn select(&mut self, state: Option<bool>) -> bool {
let idx = Some(self.current.cursor());
self.current.select(idx, state)
}
pub fn select_all(&mut self, state: Option<bool>) -> bool { self.current.select(None, 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.path.as_os_str(),
"dirname" => f.path.parent().map_or(OsStr::new(""), |p| p.as_os_str()),
"filename" => f.path.file_name().unwrap_or(OsStr::new("")),
"name_without_ext" => f.path.file_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 search(&mut self, grep: bool) -> bool {
if let Some(handle) = self.search.take() {
handle.abort();
}
let cwd = self.current.cwd.clone();
let hidden = self.current.files.show_hidden;
self.search = Some(tokio::spawn(async move {
let subject = emit!(Input(InputOpt::top("Search:"))).await?;
let mut rx = if grep {
external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject })
} else {
external::fd(external::FdOpt { cwd: cwd.clone(), hidden, glob: false, subject })
}?;
emit!(Files(FilesOp::search_empty(&cwd)));
while let Some(chunk) = rx.next().await {
emit!(Files(FilesOp::Search(cwd.clone(), Files::read(chunk).await)));
}
Ok(())
}));
true
}
pub fn search_stop(&mut self) -> bool {
if let Some(handle) = self.search.take() {
handle.abort();
}
if self.current.in_search {
self.preview_reset_image();
let cwd = self.current.cwd.clone();
let rep = self.history_new(&cwd);
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.path.as_os_str().to_owned(), Default::default()))
.collect();
let mut exec = exec.to_owned();
tokio::spawn(async move {
if !confirm || exec.is_empty() {
let result = emit!(Input(InputOpt::top("Shell:").with_value(&exec).with_highlight()));
match result.await {
Ok(e) => exec = e,
Err(_) => return,
}
}
emit!(Open(
selected,
Some(Opener { exec, block, display_name: Default::default(), spread: true })
));
});
false
}
pub fn update_peek(&mut self, step: isize, path: Option<PathBuf>) {
let Some(ref hovered) = self.current.hovered else {
return;
};
if path.as_ref().map(|p| *p != hovered.path).unwrap_or(false) {
return;
} else if !self.preview.arrow(step, path.is_some()) {
return;
} else if !matches!(&self.preview.lock, Some(l) if l.mime == MIME_DIR) {
return;
}
let path = &self.preview.lock.as_ref().unwrap().path;
if let Some(folder) = self.history(path) {
let max = folder.files.len().saturating_sub(MANAGER.layout.preview_height());
if self.preview.skip() > max {
self.preview.arrow(max as isize, true);
}
}
}
pub fn update_preview(&mut self, lock: PreviewLock) -> bool {
let Some(hovered) = self.current.hovered.as_ref().map(|h| &h.path) else {
return self.preview.reset();
};
if lock.path != *hovered {
return false;
}
self.preview.lock = Some(lock);
true
}
}
impl Tab {
#[inline]
pub fn mode(&self) -> &Mode { &self.mode }
#[inline]
pub fn name(&self) -> &str {
self
.current
.cwd
.file_name()
.and_then(|n| n.to_str())
.or_else(|| self.current.cwd.to_str())
.unwrap_or_default()
}
pub fn selected(&self) -> Vec<&File> {
let mode = self.mode();
let files = &self.current.files;
let selected: Vec<_> = if !mode.is_visual() {
files.iter().filter(|(_, f)| f.is_selected).map(|(_, f)| f).collect()
} else {
files
.iter()
.enumerate()
.filter(|(i, (_, f))| mode.pending(*i, f.is_selected))
.map(|(_, (_, f))| f)
.collect()
};
if selected.is_empty() {
self.current.hovered.as_ref().map(|h| vec![h]).unwrap_or_default()
} else {
selected
}
}
#[inline]
pub fn in_selecting(&self) -> bool { self.mode().is_visual() || self.current.has_selected() }
#[inline]
pub fn history(&self, path: &Path) -> Option<&Folder> { self.history.get(path) }
#[inline]
pub fn history_new(&mut self, path: &Path) -> Folder {
self.history.remove(path).unwrap_or_else(|| Folder::new(path))
}
#[inline]
pub fn preview(&self) -> &Preview { &self.preview }
#[inline]
pub fn preview_reset(&mut self) -> bool { self.preview.reset() }
#[inline]
pub fn preview_reset_image(&mut self) -> bool { self.preview.reset_image() }
}