diff --git a/yazi-core/src/manager/commands/close.rs b/yazi-core/src/manager/commands/close.rs index 7ba14adc..45eaa08c 100644 --- a/yazi-core/src/manager/commands/close.rs +++ b/yazi-core/src/manager/commands/close.rs @@ -1,10 +1,18 @@ +use yazi_config::keymap::Exec; + use crate::{manager::Manager, tasks::Tasks}; +pub struct Opt; + +impl From<&Exec> for Opt { + fn from(_: &Exec) -> Self { Self } +} + impl Manager { - pub fn close(&mut self, tasks: &Tasks) -> bool { + pub fn close(&mut self, _: impl Into, tasks: &Tasks) -> bool { if self.tabs.len() > 1 { return self.tabs.close(self.tabs.idx); } - self.quit(tasks, false) + self.quit((), tasks) } } diff --git a/yazi-core/src/manager/commands/create.rs b/yazi-core/src/manager/commands/create.rs index cb24e53b..8dd55266 100644 --- a/yazi-core/src/manager/commands/create.rs +++ b/yazi-core/src/manager/commands/create.rs @@ -1,12 +1,22 @@ use std::path::PathBuf; use tokio::fs::{self}; +use yazi_config::keymap::Exec; use yazi_shared::Url; use crate::{emit, files::{File, FilesOp}, input::InputOpt, manager::Manager}; +pub struct Opt { + force: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self { force: e.named.contains_key("force") } } +} + impl Manager { - pub fn create(&self, force: bool) -> bool { + pub fn create(&self, opt: impl Into) -> bool { + let opt = opt.into(); let cwd = self.cwd().to_owned(); tokio::spawn(async move { let mut result = emit!(Input(InputOpt::top("Create:"))); @@ -15,7 +25,7 @@ impl Manager { }; let path = cwd.join(&name); - if !force && fs::symlink_metadata(&path).await.is_ok() { + if !opt.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(()), diff --git a/yazi-core/src/manager/commands/link.rs b/yazi-core/src/manager/commands/link.rs new file mode 100644 index 00000000..9a452e04 --- /dev/null +++ b/yazi-core/src/manager/commands/link.rs @@ -0,0 +1,22 @@ +use yazi_config::keymap::Exec; + +use crate::{manager::Manager, tasks::Tasks}; + +pub struct Opt { + relative: bool, + force: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { relative: e.named.contains_key("relative"), force: e.named.contains_key("force") } + } +} + +impl Manager { + pub fn link(&mut self, opt: impl Into, tasks: &Tasks) -> bool { + let opt = opt.into(); + let (cut, ref src) = self.yanked; + !cut && tasks.file_link(src, self.cwd(), opt.relative, opt.force) + } +} diff --git a/yazi-core/src/manager/commands/mod.rs b/yazi-core/src/manager/commands/mod.rs index fe726c10..f3db8752 100644 --- a/yazi-core/src/manager/commands/mod.rs +++ b/yazi-core/src/manager/commands/mod.rs @@ -1,9 +1,16 @@ mod close; mod create; +mod link; mod open; +mod paste; mod peek; mod quit; mod refresh; +mod remove; mod rename; mod suspend; +mod tab_close; +mod tab_create; +mod tab_swap; +mod tab_switch; mod yank; diff --git a/yazi-core/src/manager/commands/open.rs b/yazi-core/src/manager/commands/open.rs index eb860454..1f5f054b 100644 --- a/yazi-core/src/manager/commands/open.rs +++ b/yazi-core/src/manager/commands/open.rs @@ -1,10 +1,36 @@ -use yazi_config::OPEN; +use std::ffi::OsString; + +use yazi_config::{keymap::Exec, OPEN}; use yazi_shared::MIME_DIR; use crate::{emit, external, manager::Manager, select::SelectOpt}; +pub struct Opt { + interactive: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self { interactive: e.named.contains_key("interactive") } } +} + impl Manager { - pub fn open(&mut self, interactive: bool) -> bool { + async fn open_interactive(files: Vec<(OsString, String)>) { + let openers = OPEN.common_openers(&files); + if openers.is_empty() { + return; + } + + let result = emit!(Select(SelectOpt::hovered( + "Open with:", + openers.iter().map(|o| o.desc.clone()).collect() + ))); + + if let Ok(choice) = result.await { + emit!(Open(files, Some(openers[choice].clone()))); + } + } + + pub fn open(&mut self, opt: impl Into) -> bool { let mut files: Vec<_> = self .selected() .into_iter() @@ -20,6 +46,7 @@ impl Manager { return false; } + let opt = opt.into(); 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 { @@ -35,23 +62,12 @@ impl Manager { 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)); + if opt.interactive { + Self::open_interactive(files).await; 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.desc.clone()).collect() - ))); - if let Ok(choice) = result.await { - emit!(Open(files, Some(openers[choice].clone()))); - } + emit!(Open(files, None)); }); false } diff --git a/yazi-core/src/manager/commands/paste.rs b/yazi-core/src/manager/commands/paste.rs new file mode 100644 index 00000000..93bb07c1 --- /dev/null +++ b/yazi-core/src/manager/commands/paste.rs @@ -0,0 +1,21 @@ +use yazi_config::keymap::Exec; + +use crate::{manager::Manager, tasks::Tasks}; + +pub struct Opt { + force: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self { force: e.named.contains_key("force") } } +} + +impl Manager { + pub fn paste(&mut self, opt: impl Into, tasks: &Tasks) -> bool { + let dest = self.cwd(); + let (cut, ref src) = self.yanked; + + let opt = opt.into(); + if cut { tasks.file_cut(src, dest, opt.force) } else { tasks.file_copy(src, dest, opt.force) } + } +} diff --git a/yazi-core/src/manager/commands/quit.rs b/yazi-core/src/manager/commands/quit.rs index 5bc3dbdc..0141ddaf 100644 --- a/yazi-core/src/manager/commands/quit.rs +++ b/yazi-core/src/manager/commands/quit.rs @@ -1,10 +1,25 @@ +use yazi_config::keymap::Exec; + use crate::{emit, input::InputOpt, manager::Manager, tasks::Tasks}; +#[derive(Default)] +pub struct Opt { + no_cwd_file: bool, +} +impl From<()> for Opt { + fn from(_: ()) -> Self { Self::default() } +} +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self { no_cwd_file: e.named.contains_key("no-cwd-file") } } +} + impl Manager { - pub fn quit(&self, tasks: &Tasks, no_cwd_file: bool) -> bool { + pub fn quit(&self, opt: impl Into, tasks: &Tasks) -> bool { + let opt = opt.into(); + let tasks = tasks.len(); if tasks == 0 { - emit!(Quit(no_cwd_file)); + emit!(Quit(opt.no_cwd_file)); return false; } @@ -15,7 +30,7 @@ impl Manager { if let Some(Ok(choice)) = result.recv().await { if choice == "y" || choice == "Y" { - emit!(Quit(no_cwd_file)); + emit!(Quit(opt.no_cwd_file)); } } }); diff --git a/yazi-core/src/manager/commands/remove.rs b/yazi-core/src/manager/commands/remove.rs new file mode 100644 index 00000000..2e4ce08f --- /dev/null +++ b/yazi-core/src/manager/commands/remove.rs @@ -0,0 +1,25 @@ +use yazi_config::keymap::Exec; + +use crate::{manager::Manager, tasks::Tasks}; + +pub struct Opt { + force: bool, + permanently: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { + force: e.named.contains_key("force"), + permanently: e.named.contains_key("permanently"), + } + } +} + +impl Manager { + pub fn remove(&mut self, opt: impl Into, tasks: &Tasks) -> bool { + let opt = opt.into(); + let targets = self.selected().into_iter().map(|f| f.url()).collect(); + tasks.file_remove(targets, opt.force, opt.permanently) + } +} diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index 6071dd28..f2a66d23 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -2,13 +2,36 @@ use std::{collections::BTreeSet, ffi::OsStr, io::{stdout, BufWriter, Write}, pat use anyhow::{anyhow, bail, Result}; use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; -use yazi_config::{OPEN, PREVIEW}; +use yazi_config::{keymap::Exec, OPEN, PREVIEW}; use yazi_shared::{max_common_root, Defer, Term, Url}; use crate::{emit, external::{self, ShellOpt}, files::{File, FilesOp}, input::InputOpt, manager::Manager, Event, BLOCKER}; +pub struct Opt { + force: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self { force: e.named.contains_key("force") } } +} + impl Manager { - pub fn rename(&self, force: bool) -> bool { + 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(()) + } + + pub fn rename(&self, opt: impl Into) -> bool { if self.active().in_selecting() { return self.bulk_rename(); } @@ -17,21 +40,7 @@ impl Manager { 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(()) - } - + let opt = opt.into(); tokio::spawn(async move { let mut result = emit!(Input( InputOpt::hovered("Rename:").with_value(hovered.file_name().unwrap().to_string_lossy()) @@ -42,15 +51,15 @@ impl Manager { }; 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(); + if opt.force || fs::symlink_metadata(&new).await.is_err() { + Self::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(); + Self::rename_and_hover(hovered, Url::from(new)).await.ok(); } }; }); diff --git a/yazi-core/src/manager/commands/suspend.rs b/yazi-core/src/manager/commands/suspend.rs index caee38ea..f88bed06 100644 --- a/yazi-core/src/manager/commands/suspend.rs +++ b/yazi-core/src/manager/commands/suspend.rs @@ -1,7 +1,14 @@ +use yazi_config::keymap::Exec; + use crate::manager::Manager; +pub struct Opt; +impl From<&Exec> for Opt { + fn from(_: &Exec) -> Self { Self } +} + impl Manager { - pub fn suspend(&mut self) -> bool { + pub fn suspend(&mut self, _: impl Into) -> bool { #[cfg(unix)] tokio::spawn(async move { crate::emit!(Stop(true)).await; diff --git a/yazi-core/src/manager/commands/tab_close.rs b/yazi-core/src/manager/commands/tab_close.rs new file mode 100644 index 00000000..59b3b170 --- /dev/null +++ b/yazi-core/src/manager/commands/tab_close.rs @@ -0,0 +1,35 @@ +use yazi_config::keymap::Exec; + +use crate::manager::Tabs; + +pub struct Opt { + idx: usize, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { idx: e.args.first().and_then(|i| i.parse().ok()).unwrap_or(0) } + } +} + +impl From for Opt { + fn from(idx: usize) -> Self { Self { idx } } +} + +impl Tabs { + pub fn close(&mut self, opt: impl Into) -> bool { + let opt = opt.into(); + + let len = self.items.len(); + if len < 2 || opt.idx >= len { + return false; + } + + self.items.remove(opt.idx); + if opt.idx <= self.idx { + self.set_idx(self.absolute(1)); + } + + true + } +} diff --git a/yazi-core/src/manager/commands/tab_create.rs b/yazi-core/src/manager/commands/tab_create.rs new file mode 100644 index 00000000..d629cb75 --- /dev/null +++ b/yazi-core/src/manager/commands/tab_create.rs @@ -0,0 +1,41 @@ +use yazi_config::keymap::Exec; +use yazi_shared::Url; + +use crate::{manager::Tabs, tab::Tab}; + +const MAX_TABS: usize = 9; + +pub struct Opt { + url: Option, + current: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + let mut opt = Self { url: None, current: e.named.contains_key("current") }; + + if !opt.current { + opt.url = Some(e.args.first().map_or_else(|| Url::from("."), Url::from)); + } + opt + } +} + +impl Tabs { + pub fn create(&mut self, opt: impl Into) -> bool { + if self.items.len() >= MAX_TABS { + return false; + } + + let opt = opt.into(); + let url = if opt.current { self.active().current.cwd.to_owned() } else { opt.url.unwrap() }; + + let mut tab = Tab::from(url); + tab.conf = self.active().conf.clone(); + tab.apply_files_attrs(false); + + self.items.insert(self.idx + 1, tab); + self.set_idx(self.idx + 1); + true + } +} diff --git a/yazi-core/src/manager/commands/tab_swap.rs b/yazi-core/src/manager/commands/tab_swap.rs new file mode 100644 index 00000000..07f06afb --- /dev/null +++ b/yazi-core/src/manager/commands/tab_swap.rs @@ -0,0 +1,26 @@ +use yazi_config::keymap::Exec; + +use crate::manager::Tabs; + +pub struct Opt { + idx: isize, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { idx: e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0) } + } +} + +impl Tabs { + pub fn swap(&mut self, opt: impl Into) -> bool { + let idx = self.absolute(opt.into().idx); + if idx == self.idx { + return false; + } + + self.items.swap(self.idx, idx); + self.set_idx(idx); + true + } +} diff --git a/yazi-core/src/manager/commands/tab_switch.rs b/yazi-core/src/manager/commands/tab_switch.rs new file mode 100644 index 00000000..e16d168e --- /dev/null +++ b/yazi-core/src/manager/commands/tab_switch.rs @@ -0,0 +1,35 @@ +use yazi_config::keymap::Exec; + +use crate::manager::Tabs; + +pub struct Opt { + idx: isize, + rel: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { + idx: e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0), + rel: e.named.contains_key("relative"), + } + } +} + +impl Tabs { + pub fn switch(&mut self, opt: impl Into) -> bool { + let opt = opt.into(); + let idx = if opt.rel { + (self.idx as isize + opt.idx).rem_euclid(self.items.len() as isize) as usize + } else { + opt.idx as usize + }; + + if idx == self.idx || idx >= self.items.len() { + return false; + } + + self.set_idx(idx); + true + } +} diff --git a/yazi-core/src/manager/commands/yank.rs b/yazi-core/src/manager/commands/yank.rs index ad92f0d4..6638c2e2 100644 --- a/yazi-core/src/manager/commands/yank.rs +++ b/yazi-core/src/manager/commands/yank.rs @@ -1,8 +1,20 @@ +use yazi_config::keymap::Exec; + use crate::manager::Manager; +pub struct Opt { + cut: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self { cut: e.named.contains_key("cut") } } +} + impl Manager { - pub fn yank(&mut self, cut: bool) -> bool { - self.yanked.0 = cut; + pub fn yank(&mut self, opt: impl Into) -> bool { + let opt = opt.into(); + + self.yanked.0 = opt.cut; self.yanked.1 = self.selected().into_iter().map(|f| f.url()).collect(); true } diff --git a/yazi-core/src/manager/manager.rs b/yazi-core/src/manager/manager.rs index eafb7b3f..b67b911c 100644 --- a/yazi-core/src/manager/manager.rs +++ b/yazi-core/src/manager/manager.rs @@ -57,7 +57,7 @@ impl Manager { if url == self.cwd() { self.current_mut().update(op); - self.active_mut().leave(); + self.active_mut().leave(()); true } else if matches!(self.parent(), Some(p) if &p.cwd == url) { self.active_mut().parent.as_mut().unwrap().update(op) diff --git a/yazi-core/src/manager/tabs.rs b/yazi-core/src/manager/tabs.rs index edaf178d..5bd2ab6d 100644 --- a/yazi-core/src/manager/tabs.rs +++ b/yazi-core/src/manager/tabs.rs @@ -3,11 +3,9 @@ use yazi_shared::Url; use crate::{emit, tab::Tab}; -const MAX_TABS: usize = 9; - pub struct Tabs { - pub idx: usize, - items: Vec, + pub idx: usize, + pub(super) items: Vec, } impl Tabs { @@ -17,62 +15,8 @@ impl Tabs { tabs } - pub fn create(&mut self, url: &Url) -> bool { - if self.items.len() >= MAX_TABS { - return false; - } - - let mut tab = Tab::from(url); - tab.conf = self.active().conf.clone(); - tab.apply_files_attrs(false); - - self.items.insert(self.idx + 1, tab); - self.set_idx(self.idx + 1); - true - } - - pub fn switch(&mut self, idx: isize, rel: bool) -> bool { - let idx = if rel { - (self.idx as isize + idx).rem_euclid(self.items.len() as isize) as usize - } else { - idx as usize - }; - - if idx == self.idx || idx >= self.items.len() { - return false; - } - - self.set_idx(idx); - true - } - - pub fn swap(&mut self, rel: isize) -> bool { - let idx = self.absolute(rel); - if idx == self.idx { - return false; - } - - self.items.swap(self.idx, idx); - self.set_idx(idx); - true - } - - pub fn close(&mut self, idx: usize) -> bool { - let len = self.items.len(); - if len < 2 || idx >= len { - return false; - } - - self.items.remove(idx); - if idx <= self.idx { - self.set_idx(self.absolute(1)); - } - - true - } - #[inline] - fn absolute(&self, rel: isize) -> usize { + pub(super) fn absolute(&self, rel: isize) -> usize { if rel > 0 { (self.idx + rel as usize).min(self.items.len() - 1) } else { @@ -81,7 +25,7 @@ impl Tabs { } #[inline] - fn set_idx(&mut self, idx: usize) { + pub(super) fn set_idx(&mut self, idx: usize) { self.idx = idx; self.active_mut().preview.reset(|l| l.is_image()); emit!(Refresh); diff --git a/yazi-core/src/tab/commands/arrow.rs b/yazi-core/src/tab/commands/arrow.rs index 0e3d59d0..b46782d9 100644 --- a/yazi-core/src/tab/commands/arrow.rs +++ b/yazi-core/src/tab/commands/arrow.rs @@ -1,7 +1,25 @@ +use yazi_config::keymap::Exec; + use crate::{emit, tab::Tab, Step}; +pub struct Opt(Step); + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self(e.args.first().and_then(|s| s.parse().ok()).unwrap_or_default()) + } +} + +impl From for Opt +where + T: Into, +{ + fn from(t: T) -> Self { Self(t.into()) } +} + impl Tab { - pub fn arrow(&mut self, step: Step) -> bool { + pub fn arrow(&mut self, opt: impl Into) -> bool { + let step = opt.into().0; let ok = if step.is_positive() { self.current.next(step) } else { self.current.prev(step) }; if !ok { return false; diff --git a/yazi-core/src/tab/commands/backstack.rs b/yazi-core/src/tab/commands/backstack.rs index 5d11b7ab..c565ca51 100644 --- a/yazi-core/src/tab/commands/backstack.rs +++ b/yazi-core/src/tab/commands/backstack.rs @@ -1,14 +1,24 @@ +use yazi_config::keymap::Exec; + use crate::tab::Tab; +pub struct Opt; +impl From<()> for Opt { + fn from(_: ()) -> Self { Self } +} +impl From<&Exec> for Opt { + fn from(_: &Exec) -> Self { Self } +} + impl Tab { - pub fn back(&mut self) -> bool { + pub fn back(&mut self, _: impl Into) -> bool { if let Some(url) = self.backstack.shift_backward().cloned() { self.cd(url); } false } - pub fn forward(&mut self) -> bool { + pub fn forward(&mut self, _: impl Into) -> bool { if let Some(url) = self.backstack.shift_forward().cloned() { self.cd(url); } diff --git a/yazi-core/src/tab/commands/copy.rs b/yazi-core/src/tab/commands/copy.rs index 45f98cb0..59c66c1b 100644 --- a/yazi-core/src/tab/commands/copy.rs +++ b/yazi-core/src/tab/commands/copy.rs @@ -1,13 +1,25 @@ use std::ffi::{OsStr, OsString}; +use yazi_config::keymap::Exec; + use crate::{external, tab::Tab}; +pub struct Opt<'a> { + type_: &'a str, +} + +impl<'a> From<&'a Exec> for Opt<'a> { + fn from(e: &'a Exec) -> Self { Self { type_: e.args.first().map(|s| s.as_str()).unwrap_or("") } } +} + impl Tab { - pub fn copy(&self, type_: &str) -> bool { + pub fn copy<'a>(&self, opt: impl Into>) -> bool { + let opt = opt.into(); + let mut s = OsString::new(); let mut it = self.selected().into_iter().peekable(); while let Some(f) = it.next() { - s.push(match type_ { + s.push(match opt.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("")), diff --git a/yazi-core/src/tab/commands/enter.rs b/yazi-core/src/tab/commands/enter.rs index b3b248f9..d58367cb 100644 --- a/yazi-core/src/tab/commands/enter.rs +++ b/yazi-core/src/tab/commands/enter.rs @@ -1,9 +1,19 @@ use std::mem; +use yazi_config::keymap::Exec; + use crate::{emit, tab::Tab}; +pub struct Opt; +impl From<()> for Opt { + fn from(_: ()) -> Self { Self } +} +impl From<&Exec> for Opt { + fn from(_: &Exec) -> Self { Self } +} + impl Tab { - pub fn enter(&mut self) -> bool { + pub fn enter(&mut self, _: impl Into) -> bool { let Some(hovered) = self.current.hovered().filter(|h| h.is_dir()).map(|h| h.url()) else { return false; }; diff --git a/yazi-core/src/tab/commands/find.rs b/yazi-core/src/tab/commands/find.rs index 4e2937ab..72f4a58e 100644 --- a/yazi-core/src/tab/commands/find.rs +++ b/yazi-core/src/tab/commands/find.rs @@ -7,30 +7,40 @@ use yazi_shared::{Debounce, InputError}; 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; - }; +pub struct Opt<'a> { + query: Option<&'a str>, + prev: bool, + case: FinderCase, +} - 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; +impl<'a> From<&'a Exec> for Opt<'a> { + fn from(e: &'a Exec) -> Self { + Self { + query: e.args.first().map(|s| s.as_str()), + prev: e.named.contains_key("previous"), + case: match (e.named.contains_key("smart"), e.named.contains_key("insensitive")) { + (true, _) => FinderCase::Smart, + (_, false) => FinderCase::Sensitive, + (_, true) => FinderCase::Insensitive, + }, } + } +} +pub struct ArrowOpt { + prev: bool, +} + +impl From<&Exec> for ArrowOpt { + fn from(e: &Exec) -> Self { Self { prev: e.named.contains_key("previous") } } +} + +impl Tab { + pub fn find<'a>(&mut self, opt: impl Into>) -> bool { + let opt = opt.into(); tokio::spawn(async move { let rx = emit!(Input( - InputOpt::top(if prev { "Find previous:" } else { "Find next:" }).with_realtime() + InputOpt::top(if opt.prev { "Find previous:" } else { "Find next:" }).with_realtime() )); let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); @@ -38,10 +48,10 @@ impl Tab { 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) + Exec::call("find_do", vec![s]) + .with_bool("previous", opt.prev) + .with_bool("smart", opt.case == FinderCase::Smart) + .with_bool("insensitive", opt.case == FinderCase::Insensitive) .vec(), KeymapLayer::Manager )); @@ -50,18 +60,42 @@ impl Tab { false } - pub fn find_arrow(&mut self, prev: bool) -> bool { + pub fn find_do<'a>(&mut self, opt: impl Into>) -> bool { + let opt = opt.into(); + let Some(query) = opt.query else { + return false; + }; + + let Ok(finder) = Finder::new(query, opt.case) else { + return false; + }; + + let step = if opt.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); + } + + self.finder = Some(finder); + true + } + + pub fn find_arrow(&mut self, opt: impl Into) -> bool { let Some(finder) = &mut self.finder else { return false; }; let b = finder.catchup(&self.current.files); - let step = if prev { + let step = if opt.into().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())) + b | step.is_some_and(|s| self.arrow(s)) } } diff --git a/yazi-core/src/tab/commands/jump.rs b/yazi-core/src/tab/commands/jump.rs index 6a0aea4c..5ab19743 100644 --- a/yazi-core/src/tab/commands/jump.rs +++ b/yazi-core/src/tab/commands/jump.rs @@ -3,19 +3,46 @@ use yazi_shared::{ends_with_slash, 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(); +pub struct Opt { + type_: OptType, +} +#[derive(PartialEq, Eq)] +pub enum OptType { + None, + Fzf, + Zoxide, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { + type_: match e.args.first().map(|s| s.as_str()) { + Some("fzf") => OptType::Fzf, + Some("zoxide") => OptType::Zoxide, + _ => OptType::None, + }, + } + } +} + +impl Tab { + pub fn jump(&self, opt: impl Into) -> bool { + let opt = opt.into(); + if opt.type_ == OptType::None { + return false; + } + + 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 url = if global { - external::fzf(FzfOpt { cwd }).await + let rx = if opt.type_ == OptType::Fzf { + external::fzf(FzfOpt { cwd }) } else { - external::zoxide(ZoxideOpt { cwd }).await + external::zoxide(ZoxideOpt { cwd }) }?; let op = if global && !ends_with_slash(&url) { "reveal" } else { "cd" }; diff --git a/yazi-core/src/tab/commands/leave.rs b/yazi-core/src/tab/commands/leave.rs index 2a067246..842422f1 100644 --- a/yazi-core/src/tab/commands/leave.rs +++ b/yazi-core/src/tab/commands/leave.rs @@ -1,9 +1,19 @@ use std::mem; +use yazi_config::keymap::Exec; + use crate::{emit, tab::Tab}; +pub struct Opt; +impl From<()> for Opt { + fn from(_: ()) -> Self { Self } +} +impl From<&Exec> for Opt { + fn from(_: &Exec) -> Self { Self } +} + impl Tab { - pub fn leave(&mut self) -> bool { + pub fn leave(&mut self, _: impl Into) -> bool { let current = self .current .hovered() diff --git a/yazi-core/src/tab/commands/search.rs b/yazi-core/src/tab/commands/search.rs index 80191ce2..0fe5c633 100644 --- a/yazi-core/src/tab/commands/search.rs +++ b/yazi-core/src/tab/commands/search.rs @@ -7,8 +7,36 @@ use yazi_config::keymap::{Exec, KeymapLayer}; use crate::{emit, external, files::FilesOp, input::InputOpt, tab::Tab}; +pub struct Opt { + pub type_: OptType, +} + +#[derive(PartialEq, Eq)] +pub enum OptType { + None, + Rg, + Fd, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { + type_: match e.args.first().map(|s| s.as_str()) { + Some("fd") => OptType::Fd, + Some("rg") => OptType::Rg, + _ => OptType::None, + }, + } + } +} + impl Tab { - pub fn search(&mut self, grep: bool) -> bool { + pub fn search(&mut self, opt: impl Into) -> bool { + let opt = opt.into(); + if opt.type_ == OptType::None { + return self.search_stop(); + } + if let Some(handle) = self.search.take() { handle.abort(); } @@ -22,7 +50,7 @@ impl Tab { }; cwd = cwd.into_search(subject.clone()); - let rx = if grep { + let rx = if opt.type_ == OptType::Rg { external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject }) } else { external::fd(external::FdOpt { cwd: cwd.clone(), hidden, glob: false, subject }) @@ -45,7 +73,7 @@ impl Tab { true } - pub fn search_stop(&mut self) -> bool { + pub(super) fn search_stop(&mut self) -> bool { if let Some(handle) = self.search.take() { handle.abort(); } diff --git a/yazi-core/src/tab/commands/select.rs b/yazi-core/src/tab/commands/select.rs index 3a6e5b53..4637e090 100644 --- a/yazi-core/src/tab/commands/select.rs +++ b/yazi-core/src/tab/commands/select.rs @@ -1,12 +1,32 @@ +use yazi_config::keymap::Exec; + use crate::tab::Tab; +pub struct Opt(Option); + +impl From> for Opt { + fn from(b: Option) -> Self { Self(b) } +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self(match e.named.get("state").map(|s| s.as_bytes()) { + Some(b"true") => Some(true), + Some(b"false") => Some(false), + _ => None, + }) + } +} + impl Tab { - pub fn select(&mut self, state: Option) -> bool { + pub fn select(&mut self, opt: impl Into) -> bool { if let Some(u) = self.current.hovered().map(|h| h.url()) { - return self.current.files.select(&u, state); + return self.current.files.select(&u, opt.into().0); } false } - pub fn select_all(&mut self, state: Option) -> bool { self.current.files.select_all(state) } + pub fn select_all(&mut self, opt: impl Into) -> bool { + self.current.files.select_all(opt.into().0) + } } diff --git a/yazi-core/src/tab/commands/shell.rs b/yazi-core/src/tab/commands/shell.rs index 98a39c67..14f1573f 100644 --- a/yazi-core/src/tab/commands/shell.rs +++ b/yazi-core/src/tab/commands/shell.rs @@ -1,25 +1,41 @@ -use yazi_config::open::Opener; +use yazi_config::{keymap::Exec, open::Opener}; use crate::{emit, input::InputOpt, tab::Tab}; +pub struct Opt { + cmd: String, + block: bool, + confirm: bool, +} + +impl<'a> From<&'a Exec> for Opt { + fn from(e: &'a Exec) -> Self { + Self { + cmd: e.args.first().map(|e| e.to_owned()).unwrap_or_default(), + block: e.named.contains_key("block"), + confirm: e.named.contains_key("confirm"), + } + } +} + impl Tab { - pub fn shell(&self, exec: &str, block: bool, confirm: bool) -> bool { + pub fn shell(&self, opt: impl Into) -> 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(); + let mut opt = opt.into(); tokio::spawn(async move { - if !confirm || exec.is_empty() { + if !opt.confirm || opt.cmd.is_empty() { let mut result = emit!(Input( - InputOpt::top(if block { "Shell (block):" } else { "Shell:" }) - .with_value(&exec) + InputOpt::top(if opt.block { "Shell (block):" } else { "Shell:" }) + .with_value(opt.cmd) .with_highlight() )); match result.recv().await { - Some(Ok(e)) => exec = e, + Some(Ok(e)) => opt.cmd = e, _ => return, } } @@ -27,12 +43,12 @@ impl Tab { emit!(Open( selected, Some(Opener { - exec, - block, + exec: opt.cmd, + block: opt.block, orphan: false, - desc: Default::default(), - for_: None, - spread: true + desc: Default::default(), + for_: None, + spread: true, }) )); }); diff --git a/yazi-core/src/tab/commands/visual_mode.rs b/yazi-core/src/tab/commands/visual_mode.rs index 9cc5d3cd..34dc3e29 100644 --- a/yazi-core/src/tab/commands/visual_mode.rs +++ b/yazi-core/src/tab/commands/visual_mode.rs @@ -1,12 +1,23 @@ use std::collections::BTreeSet; +use yazi_config::keymap::Exec; + use crate::tab::{Mode, Tab}; +pub struct Opt { + unset: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self { unset: e.named.contains_key("unset") } } +} + impl Tab { - pub fn visual_mode(&mut self, unset: bool) -> bool { + pub fn visual_mode(&mut self, opt: impl Into) -> bool { + let opt = opt.into(); let idx = self.current.cursor; - if unset { + if opt.unset { self.mode = Mode::Unset(idx, BTreeSet::from([idx])); } else { self.mode = Mode::Select(idx, BTreeSet::from([idx])); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 9a27f06e..26a02c78 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -1,6 +1,5 @@ use yazi_config::{keymap::{Control, Exec, Key, KeymapLayer}, KEYMAP}; -use yazi_core::{input::InputMode, tab::FinderCase, Ctx}; -use yazi_shared::{expand_url, optional_bool, Url}; +use yazi_core::{input::InputMode, Ctx}; pub(super) struct Executor<'a> { cx: &'a mut Ctx, @@ -71,148 +70,76 @@ impl<'a> Executor<'a> { } fn manager(&mut self, exec: &Exec) -> bool { - match exec.cmd.as_str() { - "escape" => self.cx.manager.active_mut().escape(exec), - "quit" => self.cx.manager.quit(&self.cx.tasks, exec.named.contains_key("no-cwd-file")), - "close" => self.cx.manager.close(&self.cx.tasks), - "suspend" => self.cx.manager.suspend(), - - // Navigation - "arrow" => { - let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or_default(); - self.cx.manager.active_mut().arrow(step) - } - "peek" => { - let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); - self.cx.manager.active_mut().preview.arrow(step); - self.cx.manager.peek(true, self.cx.image_layer()) - } - "leave" => self.cx.manager.active_mut().leave(), - "enter" => self.cx.manager.active_mut().enter(), - "back" => self.cx.manager.active_mut().back(), - "forward" => self.cx.manager.active_mut().forward(), - "cd" => { - let url = exec.args.first().map(Url::from).unwrap_or_default(); - if exec.named.contains_key("interactive") { - self.cx.manager.active_mut().cd_interactive(url) - } else { - self.cx.manager.active_mut().cd(expand_url(url)) + macro_rules! on { + (MANAGER, $name:ident $(,$args:expr)*) => { + if exec.cmd == stringify!($name) { + return self.cx.manager.$name(exec, $($args),*); } - } - "reveal" => self.cx.manager.active_mut().reveal(exec), - - // Selection - "select" => { - let state = exec.named.get("state").cloned().unwrap_or("none".to_string()); - self.cx.manager.active_mut().select(optional_bool(&state)) - } - "select_all" => { - let state = exec.named.get("state").cloned().unwrap_or("none".to_string()); - self.cx.manager.active_mut().select_all(optional_bool(&state)) - } - "visual_mode" => self.cx.manager.active_mut().visual_mode(exec.named.contains_key("unset")), - - // Operation - "open" => self.cx.manager.open(exec.named.contains_key("interactive")), - "yank" => self.cx.manager.yank(exec.named.contains_key("cut")), - "paste" => { - let dest = self.cx.manager.cwd(); - let (cut, ref src) = self.cx.manager.yanked; - - let force = exec.named.contains_key("force"); - if cut { - self.cx.tasks.file_cut(src, dest, force) - } else { - self.cx.tasks.file_copy(src, dest, force) + }; + (ACTIVE, $name:ident) => { + if exec.cmd == stringify!($name) { + return self.cx.manager.active_mut().$name(exec); } - } - "link" => { - let (cut, ref src) = self.cx.manager.yanked; - !cut - && self.cx.tasks.file_link( - src, - self.cx.manager.cwd(), - exec.named.contains_key("relative"), - exec.named.contains_key("force"), - ) - } - "remove" => { - let targets = self.cx.manager.selected().into_iter().map(|f| f.url()).collect(); - let force = exec.named.contains_key("force"); - let permanently = exec.named.contains_key("permanently"); - self.cx.tasks.file_remove(targets, force, permanently) - } - "create" => self.cx.manager.create(exec.named.contains_key("force")), - "rename" => self.cx.manager.rename(exec.named.contains_key("force")), - "copy" => self.cx.manager.active().copy(exec.args.first().map(|s| s.as_str()).unwrap_or("")), - "shell" => self.cx.manager.active().shell( - exec.args.first().map(|e| e.as_str()).unwrap_or(""), - exec.named.contains_key("block"), - exec.named.contains_key("confirm"), - ), - "hidden" => self.cx.manager.active_mut().hidden(exec), - "linemode" => self.cx.manager.active_mut().linemode(exec), - "search" => match exec.args.first().map(|s| s.as_str()).unwrap_or("") { - "rg" => self.cx.manager.active_mut().search(true), - "fd" => self.cx.manager.active_mut().search(false), - _ => self.cx.manager.active_mut().search_stop(), - }, - "jump" => match exec.args.first().map(|s| s.as_str()).unwrap_or("") { - "fzf" => self.cx.manager.active_mut().jump(true), - "zoxide" => self.cx.manager.active_mut().jump(false), - _ => false, - }, + }; + (TABS, $name:ident) => { + if exec.cmd == concat!("tab_", stringify!($name)) { + return self.cx.manager.tabs.$name(exec); + } + }; + } - // Find - "find" => { - let query = exec.args.first().map(|s| s.as_str()); - let prev = exec.named.contains_key("previous"); - let case = match (exec.named.contains_key("smart"), exec.named.contains_key("insensitive")) - { - (true, _) => FinderCase::Smart, - (_, false) => FinderCase::Sensitive, - (_, true) => FinderCase::Insensitive, - }; - self.cx.manager.active_mut().find(query, prev, case) - } - "find_arrow" => self.cx.manager.active_mut().find_arrow(exec.named.contains_key("previous")), + on!(ACTIVE, escape); + on!(MANAGER, quit, &self.cx.tasks); + on!(MANAGER, close, &self.cx.tasks); + on!(MANAGER, suspend); - // Sorting - "sort" => { - let b = self.cx.manager.active_mut().sort(exec); - self.cx.tasks.precache_size(&self.cx.manager.current().files); - b - } + // Navigation + on!(ACTIVE, arrow); + // on!(T, peek); + on!(ACTIVE, leave); + on!(ACTIVE, enter); + on!(ACTIVE, back); + on!(ACTIVE, forward); + // on!(A, cd); - // Tabs - "tab_create" => { - let path = if exec.named.contains_key("current") { - self.cx.manager.cwd().to_owned() - } else { - exec.args.first().map(Url::from).unwrap_or_else(|| Url::from("/")) - }; - self.cx.manager.tabs.create(&path) - } - "tab_close" => { - let idx = exec.args.first().and_then(|i| i.parse().ok()).unwrap_or(0); - self.cx.manager.tabs.close(idx) - } - "tab_switch" => { - let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); - let rel = exec.named.contains_key("relative"); - self.cx.manager.tabs.switch(step, rel) - } - "tab_swap" => { - let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); - self.cx.manager.tabs.swap(step) - } + // Selection + on!(ACTIVE, select); + on!(ACTIVE, select_all); + on!(ACTIVE, visual_mode); + // Operation + on!(MANAGER, open); + on!(MANAGER, yank); + on!(MANAGER, paste, &self.cx.tasks); + on!(MANAGER, link, &self.cx.tasks); + on!(MANAGER, remove, &self.cx.tasks); + on!(MANAGER, create); + on!(MANAGER, rename); + on!(ACTIVE, copy); + on!(ACTIVE, shell); + on!(ACTIVE, hidden); + on!(ACTIVE, linemode); + on!(ACTIVE, search); + on!(ACTIVE, jump); + + // Find + on!(ACTIVE, find); + on!(ACTIVE, find_arrow); + + // Sorting + on!(ACTIVE, sort); + + // Tabs + on!(TABS, create); + on!(TABS, close); + on!(TABS, switch); + on!(TABS, swap); + + match exec.cmd.as_bytes() { // Tasks - "tasks_show" => self.cx.tasks.toggle(), - + b"tasks_show" => self.cx.tasks.toggle(), // Help - "help" => self.cx.help.toggle(KeymapLayer::Manager), - + b"help" => self.cx.help.toggle(KeymapLayer::Manager), _ => false, } } diff --git a/yazi-plugin/preset/ui.lua b/yazi-plugin/preset/ui.lua index 534ed366..b408de4b 100644 --- a/yazi-plugin/preset/ui.lua +++ b/yazi-plugin/preset/ui.lua @@ -56,7 +56,6 @@ function ui.highlight_ranges(s, ranges) if r[1] > last then spans[#spans + 1] = ui.Span(s:sub(last + 1, r[1])) end - -- TODO: use a customable style spans[#spans + 1] = ui.Span(s:sub(r[1] + 1, r[2])):style(THEME.manager.find_keyword) last = r[2] end diff --git a/yazi-shared/src/fns.rs b/yazi-shared/src/fns.rs index 59e0e24a..22ee445e 100644 --- a/yazi-shared/src/fns.rs +++ b/yazi-shared/src/fns.rs @@ -141,15 +141,6 @@ pub fn path_relative_to<'a>(path: &'a Path, root: &Path) -> Cow<'a, Path> { Cow::from(buf) } -#[inline] -pub fn optional_bool(s: &str) -> Option { - match s { - "true" => Some(true), - "false" => Some(false), - _ => None, - } -} - #[cfg(test)] mod tests { use std::{borrow::Cow, path::Path};