refactor: split commands into separate files (#272)

This commit is contained in:
三咲雅 · Misaki Masa 2023-10-15 09:09:36 +08:00 committed by GitHub
parent b840dcccd5
commit e12ebd38e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1145 additions and 1051 deletions

View file

@ -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

View file

@ -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);

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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

View file

@ -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();

View file

@ -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 }
}

View file

@ -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() }

View file

@ -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))
}),
}

View file

@ -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> {

View file

@ -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;

View 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)
}
}

View 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
}
}

View 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::*;

View 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
}
}

View 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
}
}

View 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
}
}

View 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);
}
}

View 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(())
}
}

View 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
}
}

View 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
}
}

View file

@ -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 }
}

View file

@ -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::*;

View file

@ -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(&current);
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
}
}

View file

@ -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() }

View file

@ -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

View file

@ -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 {

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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()
}
}

View 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()))
}
}

View 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
}
}

View 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(&current);
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
}
}

View 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::*;

View 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
}
}

View 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) }
}

View 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
}
}

View 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
}
}

View file

@ -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
View 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
View 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
}
}

View file

@ -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() {

View file

@ -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(

View file

@ -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);
};

View file

@ -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(

View file

@ -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)?;