refactor: switch to actor model (#2986)

This commit is contained in:
三咲雅 misaki masa 2025-07-14 17:36:11 +08:00 committed by sxyazi
parent 6366e46c23
commit 60a2382087
No known key found for this signature in database
337 changed files with 4166 additions and 3053 deletions

77
Cargo.lock generated
View file

@ -340,9 +340,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
@ -528,9 +528,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crc32fast"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@ -2207,9 +2207,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rgb"
version = "0.8.51"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a457e416a0f90d246a4c3288bd7a25b2304ca727f253f95be383dd17af56be8f"
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
[[package]]
name = "rustc-demangle"
@ -2739,9 +2739,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0207d6ed1852c2a124c1fbec61621acb8330d2bf969a5d0643131e9affd985a5"
checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
dependencies = [
"indexmap",
"serde",
@ -2785,18 +2785,18 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5c1c469eda89749d2230d8156a5969a69ffe0d6d01200581cdc6110674d293e"
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.0"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b679217f2848de74cabd3e8fc5e6d66f40b7da40f8e1954d92054d9010690fd5"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
[[package]]
name = "tracing"
@ -3440,9 +3440,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.11"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
@ -3471,6 +3471,33 @@ dependencies = [
"tap",
]
[[package]]
name = "yazi-actor"
version = "25.6.11"
dependencies = [
"anyhow",
"crossterm 0.29.0",
"futures",
"libc",
"paste",
"scopeguard",
"tokio",
"tokio-stream",
"tracing",
"yazi-boot",
"yazi-config",
"yazi-core",
"yazi-dds",
"yazi-fs",
"yazi-macro",
"yazi-parser",
"yazi-plugin",
"yazi-proxy",
"yazi-shared",
"yazi-term",
"yazi-widgets",
]
[[package]]
name = "yazi-adapter"
version = "25.6.11"
@ -3544,7 +3571,7 @@ dependencies = [
"serde",
"serde_json",
"tokio",
"toml 0.9.1",
"toml 0.9.2",
"twox-hash",
"vergen-gitcl",
"yazi-boot",
@ -3575,7 +3602,7 @@ dependencies = [
"ratatui",
"regex",
"serde",
"toml 0.9.1",
"toml 0.9.2",
"tracing",
"yazi-codegen",
"yazi-fs",
@ -3596,16 +3623,12 @@ dependencies = [
"notify",
"parking_lot",
"ratatui",
"scopeguard",
"serde",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
"unicode-width 0.2.0",
"yazi-adapter",
"yazi-boot",
"yazi-codegen",
"yazi-config",
"yazi-dds",
"yazi-fs",
@ -3615,7 +3638,6 @@ dependencies = [
"yazi-proxy",
"yazi-scheduler",
"yazi-shared",
"yazi-term",
"yazi-widgets",
]
@ -3678,15 +3700,16 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
"yazi-actor",
"yazi-adapter",
"yazi-binding",
"yazi-boot",
"yazi-codegen",
"yazi-config",
"yazi-core",
"yazi-dds",
"yazi-fs",
"yazi-macro",
"yazi-parser",
"yazi-plugin",
"yazi-proxy",
"yazi-shared",
@ -3734,7 +3757,9 @@ version = "25.6.11"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"crossterm 0.29.0",
"mlua",
"serde",
"tokio",
"yazi-binding",
"yazi-boot",
@ -3744,7 +3769,6 @@ dependencies = [
"yazi-macro",
"yazi-proxy",
"yazi-shared",
"yazi-widgets",
]
[[package]]
@ -3752,7 +3776,6 @@ name = "yazi-plugin"
version = "25.6.11"
dependencies = [
"anyhow",
"base64",
"crossterm 0.29.0",
"futures",
"globset",
@ -3782,7 +3805,6 @@ dependencies = [
"yazi-prebuilt",
"yazi-proxy",
"yazi-shared",
"yazi-term",
"yazi-widgets",
]
@ -3824,6 +3846,7 @@ dependencies = [
"yazi-dds",
"yazi-fs",
"yazi-macro",
"yazi-parser",
"yazi-plugin",
"yazi-proxy",
"yazi-shared",
@ -3866,6 +3889,7 @@ dependencies = [
name = "yazi-widgets"
version = "25.6.11"
dependencies = [
"anyhow",
"base64",
"clipboard-win",
"crossterm 0.29.0",
@ -3875,10 +3899,9 @@ dependencies = [
"tokio",
"unicode-width 0.2.0",
"yazi-binding",
"yazi-codegen",
"yazi-config",
"yazi-macro",
"yazi-proxy",
"yazi-parser",
"yazi-shared",
"yazi-term",
]

View file

@ -48,7 +48,7 @@ syntect = { version = "5.2.0", default-features = false, features =
tokio = { version = "1.46.1", features = [ "full" ] }
tokio-stream = "0.1.17"
tokio-util = "0.7.15"
toml = { version = "0.9.1" }
toml = { version = "0.9.2" }
tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.1", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
unicode-width = { version = "0.2.0", default-features = false }

39
yazi-actor/Cargo.toml Normal file
View file

@ -0,0 +1,39 @@
[package]
name = "yazi-actor"
version = "25.6.11"
edition = "2024"
license = "MIT"
authors = [ "sxyazi <sxyazi@gmail.com>" ]
description = "Yazi actor model"
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
[dependencies]
yazi-boot = { path = "../yazi-boot", version = "25.6.11" }
yazi-config = { path = "../yazi-config", version = "25.6.11" }
yazi-core = { path = "../yazi-core", version = "25.6.11" }
yazi-dds = { path = "../yazi-dds", version = "25.6.11" }
yazi-fs = { path = "../yazi-fs", version = "25.6.11" }
yazi-macro = { path = "../yazi-macro", version = "25.6.11" }
yazi-parser = { path = "../yazi-parser", version = "25.6.11" }
yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" }
yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
yazi-term = { path = "../yazi-term", version = "25.6.11" }
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
futures = { workspace = true }
paste = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
[target."cfg(unix)".dependencies]
libc = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

12
yazi-actor/src/actor.rs Normal file
View file

@ -0,0 +1,12 @@
use anyhow::Result;
use yazi_shared::event::Data;
use crate::Ctx;
pub trait Actor {
type Options;
const NAME: &'static str;
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data>;
}

View file

@ -0,0 +1,19 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
use yazi_widgets::Scrollable;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Options = ArrowOpt;
const NAME: &'static str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
succ!(render!(cx.cmp.scroll(opt.step)));
}
}

View file

@ -0,0 +1,26 @@
use std::mem;
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::{cmp::CloseOpt, input::CompleteOpt};
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Options = CloseOpt;
const NAME: &'static str = "close";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let cmp = &mut cx.core.cmp;
if let Some(item) = cmp.selected().filter(|_| opt.submit).cloned() {
return act!(complete, cx.core.input, CompleteOpt { item, _ticket: cmp.ticket });
}
cmp.caches.clear();
succ!(render!(mem::replace(&mut cmp.visible, false)));
}
}

View file

@ -1,40 +1,49 @@
use std::{mem, ops::ControlFlow};
use yazi_macro::render;
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::cmp::ShowOpt;
use yazi_proxy::options::CmpItem;
use yazi_shared::{osstr_contains, osstr_starts_with};
use yazi_shared::{event::Data, osstr_contains, osstr_starts_with};
use crate::cmp::Cmp;
use crate::{Actor, Ctx};
const LIMIT: usize = 30;
impl Cmp {
#[yazi_codegen::command]
pub fn show(&mut self, opt: ShowOpt) {
if self.ticket != opt.ticket {
return;
pub struct Show;
impl Actor for Show {
type Options = ShowOpt;
const NAME: &'static str = "show";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let cmp = &mut cx.cmp;
if cmp.ticket != opt.ticket {
succ!();
}
if !opt.cache.is_empty() {
self.caches.insert(opt.cache_name.clone(), opt.cache);
cmp.caches.insert(opt.cache_name.clone(), opt.cache);
}
let Some(cache) = self.caches.get(&opt.cache_name) else {
return;
let Some(cache) = cmp.caches.get(&opt.cache_name) else {
succ!();
};
self.ticket = opt.ticket;
self.cands = Self::match_candidates(&opt.word, cache);
if self.cands.is_empty() {
return render!(mem::replace(&mut self.visible, false));
cmp.ticket = opt.ticket;
cmp.cands = Self::match_candidates(&opt.word, cache);
if cmp.cands.is_empty() {
succ!(render!(mem::replace(&mut cmp.visible, false)));
}
self.offset = 0;
self.cursor = 0;
self.visible = true;
render!();
cmp.offset = 0;
cmp.cursor = 0;
cmp.visible = true;
succ!(render!());
}
}
impl Show {
fn match_candidates(word: &str, cache: &[CmpItem]) -> Vec<CmpItem> {
let smart = !word.bytes().any(|c| c.is_ascii_uppercase());

View file

@ -1,38 +1,41 @@
use std::{ffi::OsString, mem, path::{MAIN_SEPARATOR_STR, Path, PathBuf}};
use anyhow::Result;
use tokio::fs;
use yazi_fs::{CWD, expand_path};
use yazi_macro::{emit, render};
use yazi_parser::cmp::TriggerOpt;
use yazi_macro::{act, emit, render, succ};
use yazi_parser::cmp::{ShowOpt, TriggerOpt};
use yazi_proxy::options::CmpItem;
use yazi_shared::{event::Cmd, natsort};
use yazi_shared::{event::{Cmd, Data}, natsort};
use crate::cmp::Cmp;
use crate::{Actor, Ctx};
impl Cmp {
#[yazi_codegen::command]
pub fn trigger(&mut self, opt: TriggerOpt) {
pub struct Trigger;
impl Actor for Trigger {
type Options = TriggerOpt;
const NAME: &'static str = "trigger";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let cmp = &mut cx.cmp;
if let Some(t) = opt.ticket {
if t < self.ticket {
return;
if t < cmp.ticket {
succ!();
}
self.ticket = t;
cmp.ticket = t;
}
let Some((parent, word)) = Self::split_path(&opt.word) else {
return self.close(false);
return act!(cmp:close, cx, false);
};
if self.caches.contains_key(&parent) {
return self.show(
Cmd::default()
.with_any("cache-name", parent)
.with("word", word)
.with("ticket", self.ticket),
);
if cmp.caches.contains_key(&parent) {
let ticket = cmp.ticket;
return act!(cmp:show, cx, ShowOpt { cache_name: parent, word: word.into(), ticket, ..Default::default() });
}
let ticket = self.ticket;
let ticket = cmp.ticket;
tokio::spawn(async move {
let mut dir = fs::read_dir(&parent).await?;
let mut cache = vec![];
@ -65,9 +68,11 @@ impl Cmp {
Ok::<_, anyhow::Error>(())
});
render!(mem::replace(&mut self.visible, false));
succ!(render!(mem::replace(&mut cmp.visible, false)));
}
}
impl Trigger {
fn split_path(s: &str) -> Option<(PathBuf, String)> {
if s == "~" {
return None; // We don't autocomplete a `~`, but `~/`
@ -93,7 +98,7 @@ mod tests {
use super::*;
fn compare(s: &str, parent: &str, child: &str) {
let (p, c) = Cmp::split_path(s).unwrap();
let (p, c) = Trigger::split_path(s).unwrap();
let p = p.strip_prefix(yazi_fs::CWD.load().as_ref()).unwrap_or(&p);
assert_eq!((p, c.as_str()), (Path::new(parent), child));
}

View file

@ -0,0 +1,26 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Options = ArrowOpt;
const NAME: &'static str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let confirm = &mut cx.core.confirm;
let area = cx.core.mgr.area(confirm.position);
let len = confirm.list.line_count(area.width);
let old = confirm.offset;
confirm.offset = opt.step.add(confirm.offset, len, area.height as _);
succ!(render!(old != confirm.offset));
}
}

View file

@ -0,0 +1,23 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::confirm::CloseOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Options = CloseOpt;
const NAME: &'static str = "close";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if let Some(cb) = cx.confirm.callback.take() {
_ = cb.send(opt.submit);
}
cx.confirm.visible = false;
succ!(render!());
}
}

View file

@ -0,0 +1,31 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::confirm::ShowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Show;
impl Actor for Show {
type Options = ShowOpt;
const NAME: &'static str = "show";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(confirm:close, cx)?;
let confirm = &mut cx.confirm;
confirm.title = opt.cfg.title;
confirm.body = opt.cfg.body;
confirm.list = opt.cfg.list;
confirm.position = opt.cfg.position;
confirm.offset = 0;
confirm.callback = Some(opt.tx);
confirm.visible = true;
succ!(render!());
}
}

83
yazi-actor/src/context.rs Normal file
View file

@ -0,0 +1,83 @@
use std::ops::{Deref, DerefMut};
use anyhow::{Result, anyhow};
use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}, tasks::Tasks};
use yazi_fs::File;
use yazi_shared::{Id, url::Url};
pub struct Ctx<'a> {
pub core: &'a mut Core,
pub tab: usize,
}
impl Deref for Ctx<'_> {
type Target = Core;
#[inline]
fn deref(&self) -> &Self::Target { self.core }
}
impl DerefMut for Ctx<'_> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target { self.core }
}
impl<'a> Ctx<'a> {
#[inline]
pub fn new(core: &'a mut Core, tab: Option<Id>) -> Result<Self> {
let tab = if let Some(id) = tab {
core
.mgr
.tabs
.iter()
.position(|t| t.id == id)
.ok_or_else(|| anyhow!("Tab with id {id} not found"))?
} else {
core.mgr.tabs.cursor
};
Ok(Self { core, tab })
}
#[inline]
pub fn active(core: &'a mut Core) -> Self {
let tab = core.mgr.tabs.cursor;
Self { core, tab }
}
#[inline]
pub fn tabs(&self) -> &Tabs { &self.mgr.tabs }
#[inline]
pub fn tabs_mut(&mut self) -> &mut Tabs { &mut self.mgr.tabs }
#[inline]
pub fn tab(&self) -> &Tab { &self.tabs()[self.tab] }
#[inline]
pub fn tab_mut(&mut self) -> &mut Tab { &mut self.core.mgr.tabs[self.tab] }
#[inline]
pub fn cwd(&self) -> &Url { self.tab().cwd() }
#[inline]
pub fn parent(&self) -> Option<&Folder> { self.tab().parent.as_ref() }
#[inline]
pub fn parent_mut(&mut self) -> Option<&mut Folder> { self.tab_mut().parent.as_mut() }
#[inline]
pub fn current(&self) -> &Folder { &self.tab().current }
#[inline]
pub fn current_mut(&mut self) -> &mut Folder { &mut self.tab_mut().current }
#[inline]
pub fn hovered(&self) -> Option<&File> { self.tab().hovered() }
#[inline]
pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() }
#[inline]
pub fn tasks(&self) -> &Tasks { &self.tasks }
}

View file

@ -0,0 +1,19 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
use yazi_widgets::Scrollable;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Options = ArrowOpt;
const NAME: &'static str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
succ!(render!(cx.help.scroll(opt.step)));
}
}

View file

@ -0,0 +1,27 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Escape;
impl Actor for Escape {
type Options = VoidOpt;
const NAME: &'static str = "escape";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
if cx.help.keyword().is_none() {
return act!(help:toggle, cx, cx.help.layer);
}
let help = &mut cx.help;
help.keyword = String::new();
help.in_filter = None;
help.filter_apply();
succ!(render!());
}
}

View file

@ -0,0 +1,22 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Filter;
impl Actor for Filter {
type Options = VoidOpt;
const NAME: &'static str = "filter";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let help = &mut cx.help;
help.in_filter = Some(Default::default());
help.filter_apply();
succ!(render!());
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(arrow escape filter toggle);

View file

@ -0,0 +1,29 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::help::ToggleOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Toggle;
impl Actor for Toggle {
type Options = ToggleOpt;
const NAME: &'static str = "toggle";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let help = &mut cx.help;
help.visible = !help.visible;
help.layer = opt.layer;
help.keyword = String::new();
help.in_filter = None;
help.filter_apply();
help.offset = 0;
help.cursor = 0;
succ!(render!());
}
}

View file

@ -0,0 +1,28 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::input::CloseOpt;
use yazi_shared::{errors::InputError, event::Data};
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Options = CloseOpt;
const NAME: &'static str = "close";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let input = &mut cx.input;
input.visible = false;
input.ticket.next();
if let Some(tx) = input.tx.take() {
let value = input.snap().value.clone();
_ = tx.send(if opt.submit { Ok(value) } else { Err(InputError::Canceled(value)) });
}
act!(cmp:close, cx)?;
succ!(render!());
}
}

View file

@ -0,0 +1,30 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use yazi_widgets::input::InputOp;
use crate::{Actor, Ctx};
pub struct Escape;
impl Actor for Escape {
type Options = VoidOpt;
const NAME: &'static str = "escape";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
use yazi_widgets::input::InputMode as M;
let input = &mut cx.input;
let mode = input.snap().mode;
match mode {
M::Normal if input.snap_mut().op == InputOp::None => act!(input:close, cx),
M::Insert => act!(cmp:close, cx),
M::Normal | M::Replace => Ok(().into()),
}?;
act!(escape, cx.input)?;
succ!(render!());
}
}

View file

@ -0,0 +1,55 @@
use std::ops::DerefMut;
use anyhow::Result;
use yazi_config::YAZI;
use yazi_macro::{act, render, succ};
use yazi_parser::input::ShowOpt;
use yazi_shared::{errors::InputError, event::Data};
use yazi_widgets::input::InputCallback;
use crate::{Actor, Ctx};
pub struct Show;
impl Actor for Show {
type Options = ShowOpt;
const NAME: &'static str = "show";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(input:close, cx)?;
let input = &mut cx.input;
input.visible = true;
input.title = opt.cfg.title;
input.position = opt.cfg.position;
// Typing
input.tx = Some(opt.tx.clone());
let ticket = input.ticket.clone();
// Reset input
let cb: InputCallback = Box::new(move |before, after| {
if opt.cfg.realtime {
opt.tx.send(Err(InputError::Typed(format!("{before}{after}")))).ok();
} else if opt.cfg.completion {
opt.tx.send(Err(InputError::Completed(before.to_owned(), ticket.current()))).ok();
}
});
*input.deref_mut() = yazi_widgets::input::Input::new(
opt.cfg.value,
opt.cfg.position.offset.width.saturating_sub(YAZI.input.border()) as usize,
opt.cfg.obscure,
cb,
);
// Set cursor after reset
// TODO: remove this
if let Some(cursor) = opt.cfg.cursor {
input.snap_mut().cursor = cursor;
act!(r#move, input)?;
}
succ!(render!());
}
}

5
yazi-actor/src/lib.rs Normal file
View file

@ -0,0 +1,5 @@
extern crate self as yazi_actor;
yazi_macro::mod_pub!(cmp confirm help input mgr pick spot tasks which);
yazi_macro::mod_flat!(actor context);

View file

@ -0,0 +1,33 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Options = ArrowOpt;
const NAME: &'static str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
if !tab.current.arrow(opt.step) {
succ!();
}
// Visual selection
if let Some((start, items)) = tab.mode.visual_mut() {
let end = tab.current.cursor;
*items = (start.min(end)..=end.max(start)).collect();
}
act!(mgr:hover, cx, None)?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
succ!(render!());
}
}

View file

@ -0,0 +1,21 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_parser::{VoidOpt, tab::CdSource};
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Back;
impl Actor for Back {
type Options = VoidOpt;
const NAME: &'static str = "back";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
if let Some(u) = cx.tab_mut().backstack.shift_backward().cloned() {
return act!(mgr:cd, cx, (u, CdSource::Back));
}
succ!();
}
}

View file

@ -7,25 +7,32 @@ use tokio::{fs::{self, OpenOptions}, io::AsyncWriteExt};
use yazi_config::YAZI;
use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file};
use yazi_macro::err;
use yazi_macro::{err, succ};
use yazi_parser::VoidOpt;
use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER};
use yazi_shared::{terminal_clear, url::Url};
use yazi_shared::{event::Data, terminal_clear, url::Url};
use yazi_term::tty::TTY;
use crate::mgr::Mgr;
use crate::{Actor, Ctx};
impl Mgr {
pub(super) fn bulk_rename(&self) {
pub struct BulkRename;
impl Actor for BulkRename {
type Options = VoidOpt;
const NAME: &'static str = "bulk_rename";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let Some(opener) = YAZI.opener.block(YAZI.open.all("bulk-rename.txt", "text/plain")) else {
return AppProxy::notify_warn("Bulk rename", "No text opener found");
succ!(AppProxy::notify_warn("Bulk rename", "No text opener found"));
};
let old: Vec<_> = self.selected_or_hovered().collect();
let old: Vec<_> = cx.tab().selected_or_hovered().collect();
let root = max_common_root(&old);
let old: Vec<_> = old.into_iter().map(|p| p.strip_prefix(&root).unwrap().to_owned()).collect();
let cwd = self.cwd().clone();
let cwd = cx.cwd().clone();
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk");
let s = old.iter().map(|o| o.as_os_str()).collect::<Vec<_>>().join(OsStr::new("\n"));
@ -50,11 +57,14 @@ impl Mgr {
let new: Vec<_> =
fs::read_to_string(&tmp).await?.lines().take(old.len()).map(PathBuf::from).collect();
Self::bulk_rename_do(root, old, new).await
Self::r#do(root, old, new).await
});
succ!();
}
}
async fn bulk_rename_do(root: PathBuf, old: Vec<PathBuf>, new: Vec<PathBuf>) -> Result<()> {
impl BulkRename {
async fn r#do(root: PathBuf, old: Vec<PathBuf>, new: Vec<PathBuf>) -> Result<()> {
terminal_clear(TTY.writer())?;
if old.len() != new.len() {
#[rustfmt::skip]
@ -176,7 +186,7 @@ mod tests {
#[test]
fn test_sort() {
fn cmp(input: &[(&str, &str)], expected: &[(&str, &str)]) {
let sorted = Mgr::prioritized_paths(
let sorted = BulkRename::prioritized_paths(
input.iter().map(|&(o, _)| o.into()).collect(),
input.iter().map(|&(_, n)| n.into()).collect(),
);

View file

@ -1,67 +1,72 @@
use std::{mem, time::Duration};
use anyhow::Result;
use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::popup::InputCfg;
use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, expand_path};
use yazi_macro::{err, render};
use yazi_macro::{act, err, render, succ};
use yazi_parser::tab::CdOpt;
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy, TabProxy};
use yazi_shared::{Debounce, errors::InputError, url::Url};
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy};
use yazi_shared::{Debounce, errors::InputError, event::Data, url::Url};
use crate::tab::Tab;
use crate::{Actor, Ctx};
impl Tab {
#[yazi_codegen::command]
pub fn cd(&mut self, opt: CdOpt) {
if !self.try_escape_visual() {
return;
}
pub struct Cd;
impl Actor for Cd {
type Options = CdOpt;
const NAME: &'static str = "cd";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(mgr:escape_visual, cx)?;
if opt.interactive {
return self.cd_interactive();
return Self::cd_interactive();
}
if opt.target == *self.cwd() {
return;
let tab = cx.tab_mut();
if opt.target == *tab.cwd() {
succ!();
}
// Take parent to history
if let Some(rep) = self.parent.take() {
self.history.insert(rep.url.to_owned(), rep);
if let Some(rep) = tab.parent.take() {
tab.history.insert(rep.url.to_owned(), rep);
}
// Backstack
if opt.source.big_jump() {
if self.current.url.is_regular() {
self.backstack.push(&self.current.url);
if tab.current.url.is_regular() {
tab.backstack.push(&tab.current.url);
}
if opt.target.is_regular() {
self.backstack.push(&opt.target);
tab.backstack.push(&opt.target);
}
}
// Current
let rep = self.history.remove_or(&opt.target);
let rep = mem::replace(&mut self.current, rep);
let rep = tab.history.remove_or(&opt.target);
let rep = mem::replace(&mut tab.current, rep);
if rep.url.is_regular() {
self.history.insert(rep.url.to_owned(), rep);
tab.history.insert(rep.url.to_owned(), rep);
}
// Parent
if let Some(parent) = opt.target.parent_url() {
self.parent = Some(self.history.remove_or(&parent));
tab.parent = Some(tab.history.remove_or(&parent));
}
err!(Pubsub::pub_after_cd(self.id, self.cwd()));
self.hover(None);
MgrProxy::refresh();
render!();
err!(Pubsub::pub_after_cd(tab.id, tab.cwd()));
act!(mgr:hover, cx)?;
act!(mgr:refresh, cx)?;
succ!(render!());
}
}
fn cd_interactive(&mut self) {
impl Cd {
fn cd_interactive() -> Result<Data> {
let input = InputProxy::show(InputCfg::cd());
tokio::spawn(async move {
@ -75,13 +80,13 @@ impl Tab {
let Ok(file) = File::new(url.clone()).await else { return };
if file.is_dir() {
return TabProxy::cd(&url);
return MgrProxy::cd(&url);
}
if let Some(p) = url.parent_url() {
FilesOp::Upserting(p, [(url.urn_owned(), file)].into()).emit();
}
TabProxy::reveal(&url);
MgrProxy::reveal(&url);
}
Err(InputError::Completed(before, ticket)) => {
CmpProxy::trigger(&before, ticket);
@ -90,5 +95,6 @@ impl Tab {
}
}
});
succ!();
}
}

View file

@ -0,0 +1,22 @@
use anyhow::Result;
use yazi_macro::act;
use yazi_parser::mgr::CloseOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Options = CloseOpt;
const NAME: &'static str = "close";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if cx.tabs().len() > 1 {
act!(mgr:tab_close, cx, cx.tabs().cursor)
} else {
act!(mgr:quit, cx, opt.0)
}
}
}

View file

@ -1,22 +1,28 @@
use std::{ffi::OsString, path::Path};
use anyhow::{Result, bail};
use yazi_macro::{act, succ};
use yazi_parser::tab::CopyOpt;
use yazi_shared::event::Data;
use yazi_widgets::CLIPBOARD;
use crate::tab::Tab;
use crate::{Actor, Ctx};
impl Tab {
#[yazi_codegen::command]
pub fn copy(&mut self, opt: CopyOpt) {
if !self.try_escape_visual() {
return;
}
pub struct Copy;
impl Actor for Copy {
type Options = CopyOpt;
const NAME: &'static str = "copy";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(mgr:escape_visual, cx)?;
let mut s = OsString::new();
let mut it = if opt.hovered {
Box::new(self.hovered().map(|h| &h.url).into_iter())
Box::new(cx.hovered().map(|h| &h.url).into_iter())
} else {
self.selected_or_hovered()
cx.tab().selected_or_hovered()
}
.peekable();
@ -26,7 +32,7 @@ impl Tab {
"dirname" => opt.separator.transform(u.parent().unwrap_or(Path::new(""))),
"filename" => opt.separator.transform(u.name()),
"name_without_ext" => opt.separator.transform(u.file_stem().unwrap_or_default()),
_ => return,
_ => bail!("Unknown copy type: {}", opt.r#type),
});
if it.peek().is_some() {
s.push("\n");
@ -35,9 +41,10 @@ impl Tab {
// Copy the CWD path regardless even if the directory is empty
if s.is_empty() && opt.r#type == "dirname" {
s.push(self.cwd());
s.push(cx.cwd());
}
futures::executor::block_on(CLIPBOARD.set(s));
succ!();
}
}

View file

@ -2,16 +2,22 @@ use anyhow::Result;
use tokio::fs;
use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, realname};
use yazi_macro::succ;
use yazi_parser::mgr::CreateOpt;
use yazi_proxy::{ConfirmProxy, InputProxy, TabProxy, WATCHER};
use yazi_shared::url::{Url, UrnBuf};
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
use yazi_shared::{event::Data, url::{Url, UrnBuf}};
use crate::mgr::Mgr;
use crate::{Actor, Ctx};
impl Mgr {
#[yazi_codegen::command]
pub fn create(&self, opt: CreateOpt) {
let cwd = self.cwd().to_owned();
pub struct Create;
impl Actor for Create {
type Options = CreateOpt;
const NAME: &'static str = "create";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let cwd = cx.cwd().to_owned();
let mut input = InputProxy::show(InputCfg::create(opt.dir));
tokio::spawn(async move {
@ -28,11 +34,14 @@ impl Mgr {
return;
}
_ = Self::create_do(new, opt.dir || name.ends_with('/') || name.ends_with('\\')).await;
_ = Self::r#do(new, opt.dir || name.ends_with('/') || name.ends_with('\\')).await;
});
succ!();
}
}
async fn create_do(new: Url, dir: bool) -> Result<()> {
impl Create {
async fn r#do(new: Url, dir: bool) -> Result<()> {
let Some(parent) = new.parent_url() else { return Ok(()) };
let _permit = WATCHER.acquire().await.unwrap();
@ -50,7 +59,7 @@ impl Mgr {
if let Ok(f) = File::new(new.clone()).await {
FilesOp::Upserting(parent, [(f.urn_owned(), f)].into()).emit();
TabProxy::reveal(&new)
MgrProxy::reveal(&new)
}
Ok(())
}

View file

@ -0,0 +1,20 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_parser::{VoidOpt, tab::CdSource};
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Enter;
impl Actor for Enter {
type Options = VoidOpt;
const NAME: &'static str = "enter";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let Some(h) = cx.hovered().filter(|h| h.is_dir()) else { succ!() };
act!(mgr:cd, cx, (h.url.to_regular(), CdSource::Enter))
}
}

View file

@ -0,0 +1,146 @@
use anyhow::{Result, bail};
use yazi_macro::{act, render, render_and, succ};
use yazi_parser::{VoidOpt, tab::EscapeOpt};
use yazi_proxy::AppProxy;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Escape;
impl Actor for Escape {
type Options = EscapeOpt;
const NAME: &'static str = "escape";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if opt.is_empty() {
_ = act!(mgr:escape_find, cx)? != false
|| act!(mgr:escape_visual, cx)? != false
|| act!(mgr:escape_filter, cx)? != false
|| act!(mgr:escape_select, cx)? != false
|| act!(mgr:escape_search, cx)? != false;
succ!();
}
if opt.contains(EscapeOpt::FIND) {
act!(mgr:escape_find, cx)?;
}
if opt.contains(EscapeOpt::VISUAL) {
act!(mgr:escape_visual, cx)?;
}
if opt.contains(EscapeOpt::FILTER) {
act!(mgr:escape_filter, cx)?;
}
if opt.contains(EscapeOpt::SELECT) {
act!(mgr:escape_select, cx)?;
}
if opt.contains(EscapeOpt::SEARCH) {
act!(mgr:escape_search, cx)?;
}
succ!();
}
}
// --- Find
pub struct EscapeFind;
impl Actor for EscapeFind {
type Options = VoidOpt;
const NAME: &'static str = "escape_find";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
succ!(render_and!(cx.tab_mut().finder.take().is_some()))
}
}
// --- Visual
pub struct EscapeVisual;
impl Actor for EscapeVisual {
type Options = VoidOpt;
const NAME: &'static str = "escape_visual";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
let select = tab.mode.is_select();
let Some((_, indices)) = tab.mode.take_visual() else { succ!(false) };
render!();
let urls: Vec<_> =
indices.into_iter().filter_map(|i| tab.current.files.get(i)).map(|f| &f.url).collect();
if !select {
tab.selected.remove_many(&urls);
} else if tab.selected.add_many(&urls) != urls.len() {
AppProxy::notify_warn(
"Escape visual mode",
"Some files cannot be selected, due to path nesting conflict.",
);
bail!("Some files cannot be selected, due to path nesting conflict.");
}
succ!(true)
}
}
// --- Filter
pub struct EscapeFilter;
impl Actor for EscapeFilter {
type Options = VoidOpt;
const NAME: &'static str = "escape_filter";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
if cx.current_mut().files.filter().is_none() {
succ!(false);
}
act!(mgr:filter_do, cx)?;
render!();
succ!(true);
}
}
// --- Select
pub struct EscapeSelect;
impl Actor for EscapeSelect {
type Options = VoidOpt;
const NAME: &'static str = "escape_select";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
if tab.selected.is_empty() {
succ!(false);
}
tab.selected.clear();
if tab.hovered().is_some_and(|h| h.is_dir()) {
act!(mgr:peek, cx, true)?;
}
render!();
succ!(true);
}
}
// --- Search
pub struct EscapeSearch;
impl Actor for EscapeSearch {
type Options = VoidOpt;
const NAME: &'static str = "escape_search";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let b = cx.cwd().is_search();
act!(mgr:search_stop, cx)?;
succ!(render_and!(b));
}
}

View file

@ -1,19 +1,25 @@
use std::time::Duration;
use anyhow::Result;
use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::popup::InputCfg;
use yazi_fs::FilterCase;
use yazi_macro::emit;
use yazi_macro::{emit, succ};
use yazi_parser::tab::FilterOpt;
use yazi_proxy::InputProxy;
use yazi_shared::{Debounce, errors::InputError, event::Cmd};
use yazi_shared::{Debounce, errors::InputError, event::{Cmd, Data}};
use crate::tab::Tab;
use crate::{Actor, Ctx};
impl Tab {
#[yazi_codegen::command]
pub fn filter(&mut self, opt: FilterOpt) {
pub struct Filter;
impl Actor for Filter {
type Options = FilterOpt;
const NAME: &'static str = "filter";
fn act(_: &mut Ctx, opt: Self::Options) -> Result<Data> {
let input = InputProxy::show(InputCfg::filter());
tokio::spawn(async move {
@ -32,5 +38,6 @@ impl Tab {
));
}
});
succ!();
}
}

View file

@ -0,0 +1,36 @@
use anyhow::Result;
use yazi_fs::Filter;
use yazi_macro::{act, render, succ};
use yazi_parser::tab::FilterOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct FilterDo;
impl Actor for FilterDo {
type Options = FilterOpt;
const NAME: &'static str = "filter_do";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) };
let hovered = cx.hovered().map(|f| f.urn_owned());
if cx.current_mut().files.set_filter(filter) {
cx.current_mut().repos(hovered.as_ref());
}
if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(|u| u.as_urn()) {
act!(mgr:hover, cx)?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}
if opt.done {
act!(mgr:update_paged, cx)?;
}
succ!(render!());
}
}

View file

@ -1,19 +1,25 @@
use std::time::Duration;
use anyhow::Result;
use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::popup::InputCfg;
use yazi_fs::FilterCase;
use yazi_macro::emit;
use yazi_macro::{emit, succ};
use yazi_parser::tab::FindOpt;
use yazi_proxy::InputProxy;
use yazi_shared::{Debounce, errors::InputError, event::Cmd};
use yazi_shared::{Debounce, errors::InputError, event::{Cmd, Data}};
use crate::tab::Tab;
use crate::{Actor, Ctx};
impl Tab {
#[yazi_codegen::command]
pub fn find(&mut self, opt: FindOpt) {
pub struct Find;
impl Actor for Find {
type Options = FindOpt;
const NAME: &'static str = "find";
fn act(_: &mut Ctx, opt: Self::Options) -> Result<Data> {
let input = InputProxy::show(InputCfg::find(opt.prev));
tokio::spawn(async move {
@ -29,5 +35,6 @@ impl Tab {
));
}
});
succ!();
}
}

View file

@ -0,0 +1,31 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::tab::FindArrowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct FindArrow;
impl Actor for FindArrow {
type Options = FindArrowOpt;
const NAME: &'static str = "find_arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
let Some(finder) = &mut tab.finder else { succ!() };
render!(finder.catchup(&tab.current));
let offset = if opt.prev {
finder.prev(&tab.current.files, tab.current.cursor, false)
} else {
finder.next(&tab.current.files, tab.current.cursor, false)
};
if let Some(offset) = offset {
act!(mgr:arrow, cx, offset)?;
}
succ!();
}
}

View file

@ -0,0 +1,39 @@
use anyhow::Result;
use yazi_core::tab::Finder;
use yazi_macro::{act, render, succ};
use yazi_parser::tab::FindDoOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct FindDo;
impl Actor for FindDo {
type Options = FindDoOpt;
const NAME: &'static str = "find_do";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if opt.query.is_empty() {
return act!(mgr:escape_find, cx);
}
let finder = Finder::new(&opt.query, opt.case)?;
if matches!(&cx.tab().finder, Some(f) if f.filter == finder.filter) {
succ!();
}
let step = if opt.prev {
finder.prev(&cx.current().files, cx.current().cursor, true)
} else {
finder.next(&cx.current().files, cx.current().cursor, true)
};
if let Some(step) = step {
act!(mgr:arrow, cx, step)?;
}
cx.tab_mut().finder = Some(finder);
succ!(render!());
}
}

View file

@ -0,0 +1,28 @@
use anyhow::Result;
use yazi_fs::clean_url;
use yazi_macro::{act, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Follow;
impl Actor for Follow {
type Options = VoidOpt;
const NAME: &'static str = "follow";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let Some(file) = cx.hovered() else { succ!() };
let Some(link_to) = &file.link_to else { succ!() };
if link_to.is_absolute() {
act!(mgr:reveal, cx, link_to.to_owned())
} else if let Some(p) = file.url.parent_url() {
act!(mgr:reveal, cx, clean_url(&p.join(link_to)))
} else {
succ!()
}
}
}

View file

@ -0,0 +1,21 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_parser::{VoidOpt, tab::CdSource};
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Forward;
impl Actor for Forward {
type Options = VoidOpt;
const NAME: &'static str = "forward";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
match cx.tab_mut().backstack.shift_forward().cloned() {
Some(u) => act!(mgr:cd, cx, (u, CdSource::Forward)),
None => succ!(),
}
}
}

View file

@ -0,0 +1,25 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::mgr::HardlinkOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Hardlink;
impl Actor for Hardlink {
type Options = HardlinkOpt;
const NAME: &'static str = "hardlink";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let mgr = &mut cx.core.mgr;
let tab = &mgr.tabs[cx.tab];
if !mgr.yanked.cut {
cx.core.tasks.file_hardlink(&mgr.yanked, tab.cwd(), opt.force, opt.follow);
}
succ!();
}
}

View file

@ -0,0 +1,33 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_parser::mgr::HiddenOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Hidden;
impl Actor for Hidden {
type Options = HiddenOpt;
const NAME: &'static str = "hidden";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
tab.pref.show_hidden = opt.state.unwrap_or(!tab.pref.show_hidden);
let hovered = tab.hovered().map(|f| f.url_owned());
tab.apply_files_attrs();
if hovered.as_ref() != tab.hovered().map(|f| &f.url) {
act!(mgr:hover, cx, hovered)?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
} else if tab.hovered().is_some_and(|f| f.is_dir()) {
act!(mgr:peek, cx, true)?;
}
act!(mgr:update_paged, cx)?;
succ!();
}
}

View file

@ -0,0 +1,52 @@
use anyhow::Result;
use yazi_dds::Pubsub;
use yazi_macro::{act, err, render, succ};
use yazi_parser::tab::{HoverDoOpt, HoverOpt};
use yazi_shared::{event::Data, url::Urn};
use crate::{Actor, Ctx};
pub struct Hover;
impl Actor for Hover {
type Options = HoverOpt;
const NAME: &'static str = "hover";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if let Some(u) = opt.url {
act!(mgr:hover_do, cx, u)?;
} else {
cx.current_mut().arrow(0);
}
// Publish through DDS
let tab = cx.tab();
err!(Pubsub::pub_after_hover(tab.id, tab.hovered().map(|h| &h.url)));
succ!();
}
}
// --- Do
pub struct HoverDo;
impl Actor for HoverDo {
type Options = HoverDoOpt;
const NAME: &'static str = "hover_do";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
// Hover on the file
if let Ok(p) = opt.url.strip_prefix(cx.cwd()) {
render!(cx.current_mut().hover(Urn::new(p)));
}
// Turn on tracing
if cx.hovered().is_some_and(|h| h.url == opt.url) {
// `hover(Some)` occurs after user actions, such as create, rename, reveal, etc.
// At this point, it's intuitive to track the location of the file regardless.
cx.current_mut().trace = Some(opt.url.urn_owned());
}
succ!();
}
}

View file

@ -0,0 +1,24 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_parser::{VoidOpt, tab::CdSource};
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Leave;
impl Actor for Leave {
type Options = VoidOpt;
const NAME: &'static str = "leave";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let url = cx
.hovered()
.and_then(|h| h.url.parent_url())
.filter(|u| u != cx.cwd())
.or_else(|| cx.cwd().parent_url());
if let Some(u) = url { act!(mgr:cd, cx, (u.into_regular(), CdSource::Leave)) } else { succ!() }
}
}

View file

@ -0,0 +1,25 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::mgr::LinemodeOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Linemode;
impl Actor for Linemode {
type Options = LinemodeOpt;
const NAME: &'static str = "linemode";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
if opt.new != tab.pref.linemode {
tab.pref.linemode = opt.new.into_owned();
render!();
}
succ!();
}
}

View file

@ -0,0 +1,25 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::mgr::LinkOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Link;
impl Actor for Link {
type Options = LinkOpt;
const NAME: &'static str = "link";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let mgr = &mut cx.core.mgr;
let tab = &mgr.tabs[cx.tab];
if !mgr.yanked.cut {
cx.core.tasks.file_link(&mgr.yanked, tab.cwd(), opt.relative, opt.force);
}
succ!();
}
}

View file

@ -1,8 +1,25 @@
yazi_macro::mod_flat!(
arrow
back
bulk_rename
cd
close
copy
create
enter
escape
filter
filter_do
find
find_arrow
find_do
follow
forward
hardlink
hidden
hover
leave
linemode
link
open
paste
@ -11,19 +28,28 @@ yazi_macro::mod_flat!(
refresh
remove
rename
reveal
search
seek
shell
sort
spot
suspend
tab_close
tab_create
tab_swap
tab_switch
toggle
toggle_all
unyank
update_files
update_mimes
update_paged
update_peeked
update_spotted
update_tasks
update_yanked
visual_mode
watch
yank
);

View file

@ -1,29 +1,34 @@
use std::{borrow::Cow, iter};
use anyhow::Result;
use tracing::error;
use yazi_config::{YAZI, popup::PickCfg};
use yazi_core::tab::Folder;
use yazi_fs::File;
use yazi_macro::{act, succ};
use yazi_parser::mgr::OpenOpt;
use yazi_plugin::isolate;
use yazi_proxy::{MgrProxy, PickProxy, TasksProxy, options::OpenDoOpt};
use yazi_shared::{MIME_DIR, event::CmdCow, url::Url};
use yazi_shared::{MIME_DIR, event::{CmdCow, Data}, url::Url};
use crate::{mgr::Mgr, tab::Folder, tasks::Tasks};
use crate::{Actor, Ctx, mgr::Quit};
impl Mgr {
#[yazi_codegen::command]
pub fn open(&mut self, opt: OpenOpt, tasks: &Tasks) {
if !self.active_mut().try_escape_visual() {
return;
}
let Some(hovered) = self.hovered().map(|h| h.url_owned()) else {
return;
};
pub struct Open;
impl Actor for Open {
type Options = OpenOpt;
const NAME: &'static str = "open";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(mgr:escape_visual, cx)?;
let Some(hovered) = cx.hovered().map(|h| h.url_owned()) else { succ!() };
let mut selected =
if opt.hovered { Box::new(iter::once(&hovered)) } else { self.selected_or_hovered() };
if Self::quit_with_selected(opt, &mut selected) {
return;
if opt.hovered { Box::new(iter::once(&hovered)) } else { cx.tab().selected_or_hovered() };
if Quit::quit_with_selected(opt, &mut selected) {
succ!();
}
let mut todo = vec![];
@ -31,9 +36,9 @@ impl Mgr {
.cloned()
.enumerate()
.map(|(i, u)| {
if self.mimetype.contains(&u) {
if cx.mgr.mimetype.contains(&u) {
(u, "")
} else if self.guess_folder(&u) {
} else if Self::guess_folder(cx, &u) {
(u, MIME_DIR)
} else {
todo.push(i);
@ -42,10 +47,9 @@ impl Mgr {
})
.collect();
let cwd = self.cwd().clone();
let cwd = cx.cwd().clone();
if todo.is_empty() {
return self
.open_do(OpenDoOpt { cwd, hovered, targets, interactive: opt.interactive }, tasks);
return act!(mgr:open_do, cx, OpenDoOpt { cwd, hovered, targets, interactive: opt.interactive });
}
tokio::spawn(async move {
@ -64,25 +68,34 @@ impl Mgr {
MgrProxy::open_do(OpenDoOpt { cwd, hovered, targets, interactive: opt.interactive });
});
succ!();
}
}
#[yazi_codegen::command]
pub fn open_do(&mut self, opt: OpenDoOpt, tasks: &Tasks) {
// --- Do
pub struct OpenDo;
impl Actor for OpenDo {
type Options = OpenDoOpt;
const NAME: &'static str = "open_do";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let mut targets = opt.targets;
targets.iter_mut().filter(|(_, m)| m.is_empty()).for_each(|(u, m)| {
*m = self.mimetype.by_url(u).unwrap_or_default();
*m = cx.mgr.mimetype.by_url(u).unwrap_or_default();
});
targets.retain(|(_, m)| !m.is_empty());
if targets.is_empty() {
return;
succ!();
} else if !opt.interactive {
return tasks.process_from_files(opt.cwd, opt.hovered, targets);
succ!(cx.tasks.process_from_files(opt.cwd, opt.hovered, targets));
}
let openers: Vec<_> = YAZI.opener.all(YAZI.open.common(&targets).into_iter());
if openers.is_empty() {
return;
succ!();
}
let pick = PickProxy::show(PickCfg::open(openers.iter().map(|o| o.desc()).collect()));
@ -92,9 +105,12 @@ impl Mgr {
TasksProxy::open_with(Cow::Borrowed(openers[choice]), opt.cwd, urls);
}
});
succ!();
}
}
fn guess_folder(&self, url: &Url) -> bool {
impl Open {
fn guess_folder(cx: &Ctx, url: &Url) -> bool {
let Some(p) = url.parent_url() else {
return true;
};
@ -105,9 +121,9 @@ impl Mgr {
})
};
find(Some(self.current()))
|| find(self.parent())
|| find(self.hovered_folder())
|| find(self.active().history.get(&p))
find(Some(cx.current()))
|| find(cx.parent())
|| find(cx.hovered_folder())
|| find(cx.tab().history.get(&p))
}
}

View file

@ -0,0 +1,29 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_parser::mgr::PasteOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Paste;
impl Actor for Paste {
type Options = PasteOpt;
const NAME: &'static str = "paste";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let mgr = &mut cx.core.mgr;
let tab = &mgr.tabs[cx.tab];
let (src, dest) = (mgr.yanked.iter().collect::<Vec<_>>(), tab.cwd());
if mgr.yanked.cut {
cx.core.tasks.file_cut(&src, dest, opt.force);
mgr.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(&src));
act!(mgr:unyank, cx)
} else {
succ!(cx.core.tasks.file_copy(&src, dest, opt.force, opt.follow));
}
}
}

View file

@ -0,0 +1,54 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::mgr::PeekOpt;
use yazi_proxy::HIDER;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Peek;
impl Actor for Peek {
type Options = PeekOpt;
const NAME: &'static str = "peek";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let Some(hovered) = cx.hovered().cloned() else {
succ!(cx.tab_mut().preview.reset());
};
if HIDER.try_acquire().is_err() {
succ!(cx.tab_mut().preview.reset_image());
}
let mime = cx.mgr.mimetype.by_file_owned(&hovered).unwrap_or_default();
let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.cha));
if !cx.tab().preview.same_url(&hovered.url) {
cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_default();
}
if !cx.tab().preview.same_file(&hovered, &mime) {
cx.tab_mut().preview.reset();
}
if matches!(opt.only_if, Some(u) if u != hovered.url) {
succ!();
}
if let Some(skip) = opt.skip {
let preview = &mut cx.tab_mut().preview;
if opt.upper_bound {
preview.skip = preview.skip.min(skip);
} else {
preview.skip = skip;
}
}
if hovered.is_dir() {
cx.tab_mut().preview.go_folder(hovered, folder.map(|(_, cha)| cha), opt.force);
} else {
cx.tab_mut().preview.go(hovered, mime, opt.force);
}
succ!();
}
}

View file

@ -1,29 +1,34 @@
use std::{ffi::OsString, time::Duration};
use anyhow::Result;
use tokio::{select, time};
use yazi_boot::ARGS;
use yazi_config::popup::ConfirmCfg;
use yazi_macro::emit;
use yazi_macro::{emit, succ};
use yazi_parser::mgr::{OpenOpt, QuitOpt};
use yazi_proxy::ConfirmProxy;
use yazi_shared::{event::EventQuit, url::Url};
use yazi_shared::{event::{Data, EventQuit}, url::Url};
use crate::{mgr::Mgr, tasks::Tasks};
use crate::{Actor, Ctx};
impl Mgr {
#[yazi_codegen::command]
pub fn quit(&self, opt: QuitOpt, tasks: &Tasks) {
pub struct Quit;
impl Actor for Quit {
type Options = QuitOpt;
const NAME: &'static str = "quit";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let event = opt.into();
let ongoing = tasks.ongoing().clone();
let ongoing = cx.tasks().ongoing().clone();
let (left, left_names) = {
let ongoing = ongoing.lock();
(ongoing.len(), ongoing.values().take(11).map(|t| t.name.clone()).collect())
};
if left == 0 {
emit!(Quit(event));
return;
succ!(emit!(Quit(event)));
}
tokio::spawn(async move {
@ -52,8 +57,11 @@ impl Mgr {
emit!(Quit(event));
}
});
succ!();
}
}
impl Quit {
pub(super) fn quit_with_selected<'a, I>(opt: OpenOpt, selected: I) -> bool
where
I: Iterator<Item = &'a Url>,

View file

@ -0,0 +1,39 @@
use anyhow::Result;
use crossterm::{execute, terminal::SetTitle};
use yazi_config::YAZI;
use yazi_fs::CWD;
use yazi_macro::{act, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use yazi_term::tty::TTY;
use crate::{Actor, Ctx};
pub struct Refresh;
impl Actor for Refresh {
type Options = VoidOpt;
const NAME: &'static str = "refresh";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
if let (_, Some(s)) = (CWD.set(cx.cwd()), YAZI.mgr.title()) {
execute!(TTY.writer(), SetTitle(s)).ok();
}
cx.tab_mut().apply_files_attrs();
if let Some(p) = cx.parent() {
cx.mgr.watcher.trigger_dirs(&[cx.current(), p]);
} else {
cx.mgr.watcher.trigger_dirs(&[cx.current()]);
}
act!(mgr:peek, cx, false)?;
act!(mgr:watch, cx)?;
act!(mgr:update_paged, cx)?;
cx.tasks().prework_sorted(&cx.current().files);
succ!();
}
}

View file

@ -0,0 +1,70 @@
use anyhow::Result;
use yazi_config::popup::ConfirmCfg;
use yazi_macro::{act, succ};
use yazi_parser::mgr::RemoveOpt;
use yazi_proxy::{ConfirmProxy, MgrProxy};
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Remove;
impl Actor for Remove {
type Options = RemoveOpt;
const NAME: &'static str = "remove";
fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result<Data> {
act!(mgr:escape_visual, cx)?;
opt.targets = if opt.hovered {
cx.hovered().map_or(vec![], |h| vec![h.url.clone()])
} else {
cx.tab().selected_or_hovered().cloned().collect()
};
if opt.targets.is_empty() {
succ!();
} else if opt.force {
return act!(mgr:remove_do, cx, opt);
}
let confirm = ConfirmProxy::show(if opt.permanently {
ConfirmCfg::delete(&opt.targets)
} else {
ConfirmCfg::trash(&opt.targets)
});
tokio::spawn(async move {
if confirm.await {
MgrProxy::remove_do(opt.targets, opt.permanently);
}
});
succ!();
}
}
// --- Do
pub struct RemoveDo;
impl Actor for RemoveDo {
type Options = RemoveOpt;
const NAME: &'static str = "remove_do";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let mgr = &mut cx.mgr;
mgr.tabs.iter_mut().for_each(|t| {
t.selected.remove_many(&opt.targets);
});
for u in &opt.targets {
mgr.yanked.remove(u);
}
mgr.yanked.catchup_revision(false);
cx.tasks.file_remove(opt.targets, opt.permanently);
succ!();
}
}

View file

@ -3,23 +3,29 @@ use tokio::fs;
use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, paths_to_same_file, realname};
use yazi_macro::err;
use yazi_macro::{act, err, succ};
use yazi_parser::mgr::RenameOpt;
use yazi_proxy::{ConfirmProxy, InputProxy, TabProxy, WATCHER};
use yazi_shared::{Id, url::{Url, UrnBuf}};
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
use yazi_shared::{Id, event::Data, url::{Url, UrnBuf}};
use crate::mgr::Mgr;
use crate::{Actor, Ctx};
impl Mgr {
#[yazi_codegen::command]
pub fn rename(&mut self, opt: RenameOpt) {
if !self.active_mut().try_escape_visual() {
return;
} else if !opt.hovered && !self.active().selected.is_empty() {
return self.bulk_rename();
pub struct Rename;
impl Actor for Rename {
type Options = RenameOpt;
const NAME: &'static str = "rename";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(mgr:escape_visual, cx)?;
if !opt.hovered && !cx.tab_mut().selected.is_empty() {
return act!(mgr:bulk_rename, cx);
}
let Some(hovered) = self.hovered() else { return };
let Some(hovered) = cx.hovered() else { succ!() };
let name = Self::empty_url_part(&hovered.url, &opt.empty);
let cursor = match opt.cursor.as_ref() {
"start" => Some(0),
@ -33,8 +39,7 @@ impl Mgr {
_ => None,
};
let old = hovered.url_owned();
let tab = self.tabs.active().id;
let (tab, old) = (cx.tab().id, hovered.url_owned());
let mut input = InputProxy::show(InputCfg::rename().with_value(name).with_cursor(cursor));
tokio::spawn(async move {
@ -45,14 +50,17 @@ impl Mgr {
let new = Url::from(old.parent().unwrap().join(name));
if opt.force || !maybe_exists(&new).await || paths_to_same_file(&old, &new).await {
Self::rename_do(tab, old, new).await.ok();
Self::r#do(tab, old, new).await.ok();
} else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await {
Self::rename_do(tab, old, new).await.ok();
Self::r#do(tab, old, new).await.ok();
}
});
succ!();
}
}
async fn rename_do(tab: Id, old: Url, new: Url) -> Result<()> {
impl Rename {
async fn r#do(tab: Id, old: Url, new: Url) -> Result<()> {
let Some((p_old, n_old)) = old.pair() else { return Ok(()) };
let Some((p_new, n_new)) = new.pair() else { return Ok(()) };
let _permit = WATCHER.acquire().await.unwrap();
@ -73,7 +81,7 @@ impl Mgr {
FilesOp::Upserting(p_new, [(n_new, file)].into()).emit();
}
TabProxy::reveal(&new);
MgrProxy::reveal(&new);
err!(Pubsub::pub_after_rename(tab, &old, &new));
Ok(())
}

View file

@ -0,0 +1,40 @@
use anyhow::Result;
use yazi_fs::{File, FilesOp};
use yazi_macro::{act, succ};
use yazi_parser::tab::RevealOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Reveal;
impl Actor for Reveal {
type Options = RevealOpt;
const NAME: &'static str = "reveal";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let Some((parent, child)) = opt.target.pair() else { succ!() };
// Cd to the parent directory
act!(mgr:cd, cx, (parent.clone(), opt.source))?;
// Try to hover on the child file
let tab = cx.tab_mut();
tab.current.hover(child.as_urn());
// If the child is not hovered, which means it doesn't exist,
// create a dummy file
if !opt.no_dummy && tab.hovered().is_none_or(|f| &child != f.urn()) {
let op = FilesOp::Creating(parent, vec![File::from_dummy(opt.target.clone(), None)]);
tab.current.update_pub(tab.id, op);
}
// Now, we can safely hover on the target
act!(mgr:hover, cx, Some(opt.target))?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
succ!();
}
}

View file

@ -1,48 +1,60 @@
use std::{borrow::Cow, mem, time::Duration};
use anyhow::Result;
use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use tracing::error;
use yazi_config::popup::InputCfg;
use yazi_fs::{FilesOp, cha::Cha};
use yazi_macro::{act, succ};
use yazi_plugin::external;
use yazi_proxy::{AppProxy, InputProxy, MgrProxy, TabProxy, options::{SearchOpt, SearchOptVia}};
use yazi_proxy::{InputProxy, MgrProxy, options::{SearchOpt, SearchOptVia}};
use yazi_shared::event::Data;
use crate::tab::Tab;
use crate::{Actor, Ctx};
impl Tab {
pub fn search(&mut self, opt: impl TryInto<SearchOpt>) {
let Ok(mut opt): Result<SearchOpt, _> = opt.try_into() else {
return AppProxy::notify_error("Invalid `search` option", "Failed to parse search option");
};
pub struct Search;
if let Some(handle) = self.search.take() {
impl Actor for Search {
type Options = SearchOpt;
const NAME: &'static str = "search";
fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result<Data> {
if let Some(handle) = cx.tab_mut().search.take() {
handle.abort();
}
let mut input =
InputProxy::show(InputCfg::search(opt.via.into_str()).with_value(&*opt.subject));
tokio::spawn(async move {
if let Some(Ok(subject)) = input.recv().await {
opt.subject = Cow::Owned(subject);
TabProxy::search_do(opt);
MgrProxy::search_do(opt);
}
});
succ!();
}
}
pub fn search_do(&mut self, opt: impl TryInto<SearchOpt>) {
let Ok(opt): Result<SearchOpt, _> = opt.try_into() else {
return error!("Failed to parse search option for `search_do`");
};
// --- Do
pub struct SearchDo;
if let Some(handle) = self.search.take() {
impl Actor for SearchDo {
type Options = SearchOpt;
const NAME: &'static str = "search_do";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
if let Some(handle) = tab.search.take() {
handle.abort();
}
let cwd = self.cwd().to_search(opt.subject.as_ref());
let hidden = self.pref.show_hidden;
let cwd = tab.cwd().to_search(opt.subject.as_ref());
let hidden = tab.pref.show_hidden;
self.search = Some(tokio::spawn(async move {
tab.search = Some(tokio::spawn(async move {
let rx = match opt.via {
SearchOptVia::Rg => external::rg(external::RgOpt {
cwd: cwd.clone(),
@ -67,7 +79,7 @@ impl Tab {
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(5000, Duration::from_millis(500));
pin!(rx);
let ((), ticket) = (TabProxy::cd(&cwd), FilesOp::prepare(&cwd));
let ((), ticket) = (MgrProxy::cd(&cwd), FilesOp::prepare(&cwd));
while let Some(chunk) = rx.next().await {
FilesOp::Part(cwd.clone(), chunk, ticket).emit();
}
@ -75,16 +87,30 @@ impl Tab {
Ok(())
}));
}
pub(super) fn search_stop(&mut self) {
if let Some(handle) = self.search.take() {
handle.abort();
}
if self.cwd().is_search() {
let rep = self.history.remove_or(&self.cwd().to_regular());
drop(mem::replace(&mut self.current, rep));
MgrProxy::refresh();
}
succ!();
}
}
// --- Stop
pub struct SearchStop;
impl Actor for SearchStop {
type Options = ();
const NAME: &'static str = "search_stop";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
if let Some(handle) = tab.search.take() {
handle.abort();
}
if tab.cwd().is_search() {
let rep = tab.history.remove_or(&tab.cwd().to_regular());
drop(mem::replace(&mut tab.current, rep));
act!(mgr:refresh, cx)?;
}
succ!();
}
}

View file

@ -0,0 +1,33 @@
use anyhow::Result;
use yazi_config::YAZI;
use yazi_macro::succ;
use yazi_parser::mgr::SeekOpt;
use yazi_plugin::isolate;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Seek;
impl Actor for Seek {
type Options = SeekOpt;
const NAME: &'static str = "seek";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let Some(hovered) = cx.hovered() else {
succ!(cx.tab_mut().preview.reset());
};
let Some(mime) = cx.mgr.mimetype.by_file(hovered) else {
succ!(cx.tab_mut().preview.reset());
};
let Some(previewer) = YAZI.plugin.previewer(&hovered.url, mime) else {
succ!(cx.tab_mut().preview.reset());
};
isolate::seek_sync(&previewer.run, hovered.clone(), opt.units);
succ!();
}
}

View file

@ -1,24 +1,26 @@
use std::{borrow::Cow, fmt::Display};
use std::borrow::Cow;
use anyhow::Result;
use yazi_config::{opener::OpenerRule, popup::InputCfg};
use yazi_macro::{act, succ};
use yazi_parser::tab::ShellOpt;
use yazi_proxy::{AppProxy, InputProxy, TasksProxy};
use yazi_proxy::{InputProxy, TasksProxy};
use yazi_shared::event::Data;
use crate::tab::Tab;
use crate::{Actor, Ctx};
impl Tab {
pub fn shell(&mut self, opt: impl TryInto<ShellOpt, Error = impl Display>) {
if !self.try_escape_visual() {
return;
}
pub struct Shell;
let mut opt = match opt.try_into() {
Ok(o) => o as ShellOpt,
Err(e) => return AppProxy::notify_warn("`shell` command", e),
};
impl Actor for Shell {
type Options = ShellOpt;
let cwd = opt.cwd.take().unwrap_or_else(|| self.cwd().clone());
let selected = self.hovered_and_selected().cloned().collect();
const NAME: &'static str = "shell";
fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result<Data> {
act!(mgr:escape_visual, cx)?;
let cwd = opt.cwd.take().unwrap_or_else(|| cx.cwd().clone());
let selected = cx.tab().hovered_and_selected().cloned().collect();
let input = opt.interactive.then(|| {
InputProxy::show(InputCfg::shell(opt.block).with_value(&*opt.run).with_cursor(opt.cursor))
@ -48,5 +50,7 @@ impl Tab {
selected,
);
});
succ!();
}
}

View file

@ -0,0 +1,40 @@
use std::str::FromStr;
use anyhow::Result;
use yazi_fs::SortBy;
use yazi_macro::{act, succ};
use yazi_shared::event::{CmdCow, Data};
use crate::{Actor, Ctx};
pub struct Sort;
impl Actor for Sort {
type Options = CmdCow;
const NAME: &'static str = "sort";
fn act(cx: &mut Ctx, c: Self::Options) -> Result<Data> {
let mut new = cx.tab().pref.clone();
new.sort_by = c.first_str().and_then(|s| SortBy::from_str(s).ok()).unwrap_or(new.sort_by);
new.sort_reverse = c.maybe_bool("reverse").unwrap_or(new.sort_reverse);
new.sort_dir_first = c.maybe_bool("dir-first").unwrap_or(new.sort_dir_first);
new.sort_sensitive = c.maybe_bool("sensitive").unwrap_or(new.sort_sensitive);
new.sort_translit = c.maybe_bool("translit").unwrap_or(new.sort_translit);
if new == cx.tab().pref {
succ!();
}
cx.tab_mut().pref = new;
cx.tab_mut().apply_files_attrs();
act!(mgr:hover, cx)?;
cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.files);
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
act!(mgr:update_paged, cx)?;
succ!();
}
}

View file

@ -0,0 +1,32 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::mgr::SpotOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Spot;
impl Actor for Spot {
type Options = SpotOpt;
const NAME: &'static str = "spot";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let Some(hovered) = cx.hovered().cloned() else { succ!() };
let mime = cx.mgr.mimetype.by_file_owned(&hovered).unwrap_or_default();
// if !self.active().spot.same_file(&hovered, &mime) {
// self.active_mut().spot.reset();
// }
if let Some(skip) = opt.skip {
cx.tab_mut().spot.skip = skip;
} else if !cx.tab().spot.same_url(&hovered.url) {
cx.tab_mut().spot.skip = 0;
}
cx.tab_mut().spot.go(hovered, mime);
succ!();
}
}

View file

@ -0,0 +1,22 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Suspend;
impl Actor for Suspend {
type Options = VoidOpt;
const NAME: &'static str = "suspend";
fn act(_: &mut Ctx, _: Self::Options) -> Result<Data> {
#[cfg(unix)]
unsafe {
libc::raise(libc::SIGTSTP);
}
succ!();
}
}

View file

@ -0,0 +1,36 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::mgr::TabCloseOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct TabClose;
impl Actor for TabClose {
type Options = TabCloseOpt;
const NAME: &'static str = "tab_close";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let len = cx.tabs().len();
if len < 2 || opt.idx >= len {
succ!();
}
let tabs = cx.tabs_mut();
tabs.remove(opt.idx).shutdown();
if opt.idx > tabs.cursor {
tabs.set_idx(tabs.cursor);
} else {
tabs.set_idx(usize::min(tabs.cursor + 1, tabs.len() - 1));
}
let cx = &mut Ctx::active(cx.core);
act!(mgr:refresh, cx)?;
act!(mgr:peek, cx, true)?;
succ!(render!());
}
}

View file

@ -0,0 +1,55 @@
use anyhow::Result;
use yazi_core::tab::Tab;
use yazi_macro::{act, render, succ};
use yazi_parser::{mgr::TabCreateOpt, tab::CdSource};
use yazi_proxy::AppProxy;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
const MAX_TABS: usize = 9;
pub struct TabCreate;
impl Actor for TabCreate {
type Options = TabCreateOpt;
const NAME: &'static str = "tab_create";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if cx.tabs().len() >= MAX_TABS {
succ!(AppProxy::notify_warn(
"Too many tabs",
"You can only open up to 9 tabs at the same time."
));
}
let mut tab = Tab::default();
let (cd, url) = if let Some(wd) = opt.wd {
(true, wd)
} else if let Some(h) = cx.hovered() {
tab.pref = cx.tab().pref.clone();
tab.apply_files_attrs();
(false, h.url.to_regular())
} else {
tab.pref = cx.tab().pref.clone();
tab.apply_files_attrs();
(true, cx.cwd().to_regular())
};
let tabs = &mut cx.mgr.tabs;
tabs.items.insert(tabs.cursor + 1, tab);
tabs.set_idx(tabs.cursor + 1);
let cx = &mut Ctx::active(cx.core);
if cd {
act!(mgr:cd, cx, (url, CdSource::Tab))?;
} else {
act!(mgr:reveal, cx, (url, CdSource::Tab))?;
}
act!(mgr:refresh, cx)?;
act!(mgr:peek, cx, true)?;
succ!(render!());
}
}

View file

@ -0,0 +1,32 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct TabSwap;
impl Actor for TabSwap {
type Options = ArrowOpt;
const NAME: &'static str = "tab_swap";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tabs = cx.tabs_mut();
let new = opt.step.add(tabs.cursor, tabs.len(), 0);
if new == tabs.cursor {
succ!();
}
tabs.items.swap(tabs.cursor, new);
tabs.set_idx(new);
let cx = &mut Ctx::active(cx.core);
act!(mgr:refresh, cx)?;
act!(mgr:peek, cx, true)?;
succ!(render!());
}
}

View file

@ -0,0 +1,34 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::mgr::TabSwitchOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct TabSwitch;
impl Actor for TabSwitch {
type Options = TabSwitchOpt;
const NAME: &'static str = "tab_switch";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tabs = cx.tabs_mut();
let idx = if opt.relative {
opt.step.saturating_add_unsigned(tabs.cursor).rem_euclid(tabs.len() as _) as _
} else {
opt.step as usize
};
if idx == tabs.cursor || idx >= tabs.len() {
succ!();
}
tabs.set_idx(idx);
let cx = &mut Ctx::active(cx.core);
act!(mgr:refresh, cx)?;
act!(mgr:peek, cx, true)?;
succ!(render!());
}
}

View file

@ -0,0 +1,34 @@
use anyhow::Result;
use yazi_macro::{render_and, succ};
use yazi_parser::tab::ToggleOpt;
use yazi_proxy::AppProxy;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Toggle;
impl Actor for Toggle {
type Options = ToggleOpt;
const NAME: &'static str = "toggle";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
let Some(url) = opt.url.as_ref().or(tab.current.hovered().map(|h| &h.url)) else { succ!() };
let b = match opt.state {
Some(true) => render_and!(tab.selected.add(url)),
Some(false) => render_and!(tab.selected.remove(url)) | true,
None => render_and!(tab.selected.remove(url) || tab.selected.add(url)),
};
if !b {
AppProxy::notify_warn(
"Toggle",
"This file cannot be selected, due to path nesting conflict.",
);
}
succ!();
}
}

View file

@ -0,0 +1,49 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::tab::ToggleAllOpt;
use yazi_proxy::AppProxy;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct ToggleAll;
impl Actor for ToggleAll {
type Options = ToggleAllOpt;
const NAME: &'static str = "toggle_all";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
use yazi_shared::Either::*;
let tab = cx.tab_mut();
let it = tab.current.files.iter().map(|f| &f.url);
let either = match opt.state {
Some(true) if opt.urls.is_empty() => Left((vec![], it.collect())),
Some(true) => Right((vec![], opt.urls)),
Some(false) if opt.urls.is_empty() => Left((it.collect(), vec![])),
Some(false) => Right((opt.urls, vec![])),
None if opt.urls.is_empty() => Left(it.partition(|&u| tab.selected.contains_key(u))),
None => Right(opt.urls.into_iter().partition(|u| tab.selected.contains_key(u))),
};
let warn = match either {
Left((removal, addition)) => {
render!(tab.selected.remove_many(&removal) > 0);
render!(tab.selected.add_many(&addition), > 0) != addition.len()
}
Right((removal, addition)) => {
render!(tab.selected.remove_many(&removal) > 0);
render!(tab.selected.add_many(&addition), > 0) != addition.len()
}
};
if warn {
AppProxy::notify_warn(
"Toggle all",
"Some files cannot be selected, due to path nesting conflict.",
);
}
succ!();
}
}

View file

@ -0,0 +1,26 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::Actor;
pub struct Unyank;
impl Actor for Unyank {
type Options = VoidOpt;
const NAME: &'static str = "unyank";
fn act(cx: &mut crate::Ctx, _: Self::Options) -> Result<Data> {
let repeek = cx.hovered().is_some_and(|f| f.is_dir() && cx.mgr.yanked.contains_in(&f.url));
cx.mgr.yanked.clear();
render!(cx.mgr.yanked.catchup_revision(false));
if repeek {
act!(mgr:peek, cx, true)?;
}
succ!();
}
}

View file

@ -0,0 +1,112 @@
use anyhow::Result;
use yazi_core::{mgr::LINKED, tab::Folder};
use yazi_fs::FilesOp;
use yazi_macro::{act, render, succ};
use yazi_parser::mgr::UpdateFilesOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct UpdateFiles;
impl Actor for UpdateFiles {
type Options = UpdateFilesOpt;
const NAME: &'static str = "update_files";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let revision = cx.current().files.revision;
let linked: Vec<_> = LINKED.read().from_dir(opt.op.cwd()).map(|u| opt.op.rebase(u)).collect();
for op in [opt.op].into_iter().chain(linked) {
cx.mgr.yanked.apply_op(&op);
Self::update_tab(cx, op).ok();
}
render!(cx.mgr.yanked.catchup_revision(false));
cx.tab_mut().apply_files_attrs();
if revision != cx.current().files.revision {
act!(mgr:hover, cx)?;
act!(mgr:peek, cx, false)?;
act!(mgr:watch, cx)?;
act!(mgr:update_paged, cx)?;
}
succ!();
}
}
impl UpdateFiles {
fn update_tab(cx: &mut Ctx, op: FilesOp) -> Result<Data> {
let url = op.cwd();
cx.tab_mut().selected.apply_op(&op);
if url == cx.cwd() {
Self::update_current(cx, op)
} else if matches!(cx.parent(), Some(p) if *url == p.url) {
Self::update_parent(cx, op)
} else if matches!(cx.hovered(), Some(h) if *url == h.url) {
Self::update_hovered(cx, op)
} else {
Self::update_history(cx, op)
}
}
fn update_parent(cx: &mut Ctx, op: FilesOp) -> Result<Data> {
let tab = cx.tab_mut();
let urn = tab.current.url.urn();
let leave = matches!(op, FilesOp::Deleting(_, ref urns) if urns.contains(urn));
if let Some(f) = tab.parent.as_mut() {
render!(f.update_pub(tab.id, op));
render!(f.hover(urn));
}
if leave {
act!(mgr:leave, cx)?;
}
succ!();
}
fn update_current(cx: &mut Ctx, op: FilesOp) -> Result<Data> {
let calc = !matches!(op, FilesOp::Size(..) | FilesOp::Deleting(..));
let id = cx.tab().id;
if !cx.current_mut().update_pub(id, op) {
succ!();
}
if calc {
cx.tasks.prework_sorted(&cx.current().files);
}
succ!();
}
fn update_hovered(cx: &mut Ctx, op: FilesOp) -> Result<Data> {
let (id, url) = (cx.tab().id, op.cwd());
let folder = cx.tab_mut().history.entry(url.clone()).or_insert_with(|| Folder::from(url));
if folder.update_pub(id, op) {
act!(mgr:peek, cx, true)?;
}
succ!();
}
fn update_history(cx: &mut Ctx, op: FilesOp) -> Result<Data> {
let tab = &mut cx.tab_mut();
let leave = tab.parent.as_ref().and_then(|f| f.url.parent_url().map(|p| (p, f.url.urn()))).is_some_and(
|(p, n)| matches!(op, FilesOp::Deleting(ref parent, ref urns) if *parent == p && urns.contains(n)),
);
tab
.history
.entry(op.cwd().clone())
.or_insert_with(|| Folder::from(op.cwd()))
.update_pub(tab.id, op);
if leave {
act!(mgr:leave, cx)?;
}
succ!();
}
}

View file

@ -0,0 +1,57 @@
use std::collections::HashMap;
use anyhow::Result;
use yazi_core::mgr::LINKED;
use yazi_macro::{act, render, succ};
use yazi_parser::mgr::UpdateMimesOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct UpdateMimes;
impl Actor for UpdateMimes {
type Options = UpdateMimesOpt;
const NAME: &'static str = "update_mimes";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let linked = LINKED.read();
let updates = opt
.updates
.into_iter()
.flat_map(|(key, value)| key.into_url().zip(value.into_string()))
.filter(|(url, mime)| cx.mgr.mimetype.by_url(url) != Some(mime))
.fold(HashMap::new(), |mut map, (u, m)| {
for u in linked.from_file(&u) {
map.insert(u, m.to_string());
}
map.insert(u, m.into_owned());
map
});
drop(linked);
if updates.is_empty() {
succ!();
}
let affected: Vec<_> = cx
.current()
.paginate(cx.current().page)
.iter()
.filter(|&f| updates.contains_key(&f.url))
.cloned()
.collect();
let repeek = cx.hovered().is_some_and(|f| updates.contains_key(&f.url));
cx.mgr.mimetype.extend(updates);
if repeek {
act!(mgr:peek, cx, false)?;
}
cx.tasks.fetch_paged(&affected, &cx.mgr.mimetype);
cx.tasks.preload_paged(&affected, &cx.mgr.mimetype);
succ!(render!());
}
}

View file

@ -0,0 +1,27 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::mgr::UpdatePagedOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct UpdatePaged;
impl Actor for UpdatePaged {
type Options = UpdatePagedOpt;
const NAME: &'static str = "update_paged";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if opt.only_if.is_some_and(|u| u != *cx.cwd()) {
succ!();
}
let targets = cx.current().paginate(opt.page.unwrap_or(cx.current().page));
if !targets.is_empty() {
cx.tasks().fetch_paged(targets, &cx.mgr.mimetype);
cx.tasks().preload_paged(targets, &cx.mgr.mimetype);
}
succ!();
}
}

View file

@ -0,0 +1,27 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::tab::UpdatePeekedOpt;
use yazi_shared::event::Data;
use crate::Actor;
pub struct UpdatePeeked;
impl Actor for UpdatePeeked {
type Options = UpdatePeekedOpt;
const NAME: &'static str = "update_peeked";
fn act(cx: &mut crate::Ctx, opt: Self::Options) -> Result<Data> {
let Some(hovered) = cx.hovered().map(|h| &h.url) else {
succ!(cx.tab_mut().preview.reset());
};
if opt.lock.url == *hovered {
cx.tab_mut().preview.lock = Some(opt.lock);
render!();
}
succ!();
}
}

View file

@ -0,0 +1,36 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::tab::UpdateSpottedOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct UpdateSpotted;
impl Actor for UpdateSpotted {
type Options = UpdateSpottedOpt;
const NAME: &'static str = "update_spotted";
fn act(cx: &mut Ctx, mut opt: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
let Some(hovered) = tab.hovered().map(|h| &h.url) else {
succ!(tab.spot.reset());
};
if opt.lock.url != *hovered {
succ!();
}
if tab.spot.lock.as_ref().is_none_or(|l| l.id != opt.lock.id) {
tab.spot.skip = opt.lock.selected().unwrap_or_default();
} else if let Some(s) = opt.lock.selected() {
tab.spot.skip = s;
} else {
opt.lock.select(Some(tab.spot.skip));
}
tab.spot.lock = Some(opt.lock);
succ!(render!());
}
}

View file

@ -0,0 +1,19 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::mgr::UpdateTasksOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct UpdateTasks;
impl Actor for UpdateTasks {
type Options = UpdateTasksOpt;
const NAME: &'static str = "update_tasks";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
cx.mgr.watcher.push_files(opt.urls);
succ!();
}
}

View file

@ -0,0 +1,24 @@
use anyhow::Result;
use yazi_core::mgr::Yanked;
use yazi_macro::{render, succ};
use yazi_parser::mgr::UpdateYankedOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct UpdateYanked;
impl Actor for UpdateYanked {
type Options = UpdateYankedOpt;
const NAME: &'static str = "update_yanked";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if opt.urls.is_empty() && cx.mgr.yanked.is_empty() {
succ!();
}
cx.mgr.yanked = Yanked::new(opt.cut, opt.urls);
succ!(render!());
}
}

View file

@ -0,0 +1,30 @@
use std::collections::BTreeSet;
use anyhow::Result;
use yazi_core::tab::Mode;
use yazi_macro::{render, succ};
use yazi_parser::tab::VisualModeOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct VisualMode;
impl Actor for VisualMode {
type Options = VisualModeOpt;
const NAME: &'static str = "visual_mode";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
let idx = tab.current.cursor;
if opt.unset {
tab.mode = Mode::Unset(idx, BTreeSet::from([idx]));
} else {
tab.mode = Mode::Select(idx, BTreeSet::from([idx]));
};
succ!(render!());
}
}

View file

@ -0,0 +1,25 @@
use std::iter;
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Watch;
impl Actor for Watch {
type Options = VoidOpt;
const NAME: &'static str = "watch";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let it = iter::once(cx.core.mgr.tabs.active().cwd())
.chain(cx.core.mgr.tabs.parent().map(|p| &p.url))
.chain(cx.core.mgr.tabs.hovered().filter(|h| h.is_dir()).map(|h| &h.url));
cx.core.mgr.watcher.watch(it);
succ!();
}
}

View file

@ -0,0 +1,24 @@
use anyhow::Result;
use yazi_core::mgr::Yanked;
use yazi_macro::{act, render};
use yazi_parser::mgr::YankOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Yank;
impl Actor for Yank {
type Options = YankOpt;
const NAME: &'static str = "yank";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(mgr:escape_visual, cx)?;
cx.mgr.yanked = Yanked::new(opt.cut, cx.tab().selected_or_hovered().cloned().collect());
render!(cx.mgr.yanked.catchup_revision(true));
act!(mgr:escape_select, cx)
}
}

View file

@ -0,0 +1,19 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
use yazi_widgets::Scrollable;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Options = ArrowOpt;
const NAME: &'static str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
succ!(render!(cx.pick.scroll(opt.step)));
}
}

View file

@ -0,0 +1,26 @@
use anyhow::{Result, anyhow};
use yazi_macro::{render, succ};
use yazi_parser::pick::CloseOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Options = CloseOpt;
const NAME: &'static str = "close";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let pick = &mut cx.pick;
if let Some(cb) = pick.callback.take() {
_ = cb.send(if opt.submit { Ok(pick.cursor) } else { Err(anyhow!("canceled")) });
}
pick.cursor = 0;
pick.offset = 0;
pick.visible = false;
succ!(render!());
}
}

View file

@ -0,0 +1,27 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::pick::ShowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Show;
impl Actor for Show {
type Options = ShowOpt;
const NAME: &'static str = "show";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(pick:close, cx)?;
let pick = &mut cx.pick;
pick.title = opt.cfg.title;
pick.items = opt.cfg.items;
pick.position = opt.cfg.position;
pick.callback = Some(opt.tx);
pick.visible = true;
succ!(render!());
}
}

View file

@ -0,0 +1,30 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Options = ArrowOpt;
const NAME: &'static str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let spot = &mut cx.tab_mut().spot;
let Some(lock) = &mut spot.lock else { succ!() };
let new = opt.step.add(spot.skip, lock.len().unwrap_or(u16::MAX as _), 0);
let Some(old) = lock.selected() else {
return act!(mgr:spot, cx, new);
};
lock.select(Some(new));
let new = lock.selected().unwrap();
spot.skip = new;
succ!(render!(new != old));
}
}

View file

@ -0,0 +1,18 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Options = VoidOpt;
const NAME: &'static str = "close";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
succ!(cx.tab_mut().spot.reset());
}
}

View file

@ -0,0 +1,36 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::spot::CopyOpt;
use yazi_shared::event::Data;
use yazi_widgets::CLIPBOARD;
use crate::{Actor, Ctx};
pub struct Copy;
impl Actor for Copy {
type Options = CopyOpt;
const NAME: &'static str = "copy";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let spot = &cx.tab().spot;
let Some(lock) = &spot.lock else { succ!() };
let Some(table) = lock.table() else { succ!() };
let mut s = String::new();
match opt.r#type.as_ref() {
"cell" => {
let Some(cell) = table.selected_cell() else { succ!() };
s = cell.to_string();
}
"line" => {
// TODO
}
_ => {}
}
futures::executor::block_on(CLIPBOARD.set(s));
succ!();
}
}

View file

@ -0,0 +1,19 @@
use anyhow::Result;
use yazi_macro::act;
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Swipe;
impl Actor for Swipe {
type Options = ArrowOpt;
const NAME: &'static str = "swipe";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
act!(mgr:arrow, cx, opt)?;
act!(mgr:spot, cx)
}
}

View file

@ -0,0 +1,24 @@
use anyhow::Result;
use yazi_core::tasks::Tasks;
use yazi_macro::{render, succ};
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Arrow;
impl Actor for Arrow {
type Options = ArrowOpt;
const NAME: &'static str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tasks = &mut cx.tasks;
let old = tasks.cursor;
tasks.cursor = opt.step.add(tasks.cursor, tasks.summaries.len(), Tasks::limit());
succ!(render!(tasks.cursor != old));
}
}

View file

@ -0,0 +1,27 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Cancel;
impl Actor for Cancel {
type Options = VoidOpt;
const NAME: &'static str = "cancel";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let tasks = &mut cx.tasks;
let id = tasks.ongoing().lock().get_id(tasks.cursor);
if id.map(|id| tasks.scheduler.cancel(id)) != Some(true) {
succ!();
}
tasks.summaries = tasks.paginate();
act!(tasks:arrow, cx)?;
succ!(render!());
}
}

View file

@ -0,0 +1,27 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Options = VoidOpt;
const NAME: &'static str = "close";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let tasks = &mut cx.tasks;
if !tasks.visible {
succ!();
}
tasks.visible = false;
tasks.summaries = Vec::new();
act!(tasks:arrow, cx)?;
succ!(render!());
}
}

View file

@ -1,19 +1,28 @@
use std::io::Write;
use anyhow::Result;
use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode}};
use scopeguard::defer;
use tokio::{io::{AsyncReadExt, stdin}, select, sync::mpsc, time};
use yazi_macro::succ;
use yazi_parser::VoidOpt;
use yazi_proxy::{AppProxy, HIDER};
use yazi_shared::{event::CmdCow, terminal_clear};
use yazi_shared::{event::Data, terminal_clear};
use yazi_term::tty::TTY;
use crate::tasks::Tasks;
use crate::{Actor, Ctx};
impl Tasks {
pub fn inspect(&self, _: CmdCow) {
let ongoing = self.ongoing().clone();
let Some(id) = ongoing.lock().get_id(self.cursor) else {
return;
pub struct Inspect;
impl Actor for Inspect {
type Options = VoidOpt;
const NAME: &'static str = "inspect";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let ongoing = cx.tasks.ongoing().clone();
let Some(id) = ongoing.lock().get_id(cx.tasks.cursor) else {
succ!();
};
tokio::spawn(async move {
@ -67,5 +76,6 @@ impl Tasks {
answer = stdin.read_u8().await.unwrap_or(b'q');
}
});
succ!();
}
}

View file

@ -0,0 +1,22 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_proxy::options::OpenWithOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct OpenWith;
impl Actor for OpenWith {
type Options = OpenWithOpt;
const NAME: &'static str = "open_with";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
succ!(cx.tasks.process_from_opener(
opt.cwd,
opt.opener,
opt.targets.into_iter().map(|u| u.into_path().into_os_string()).collect(),
));
}
}

View file

@ -0,0 +1,18 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_proxy::options::ProcessExecOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct ProcessExec;
impl Actor for ProcessExec {
type Options = ProcessExecOpt;
const NAME: &'static str = "process_exec";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
succ!(cx.tasks.scheduler.process_open(opt));
}
}

View file

@ -0,0 +1,27 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::VoidOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Show;
impl Actor for Show {
type Options = VoidOpt;
const NAME: &'static str = "show";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let tasks = &mut cx.tasks;
if tasks.visible {
succ!();
}
tasks.visible = true;
tasks.summaries = tasks.paginate();
act!(tasks:arrow, cx)?;
succ!(render!());
}
}

View file

@ -0,0 +1,19 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::which::CallbackOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Callback;
impl Actor for Callback {
type Options = CallbackOpt;
const NAME: &'static str = "callback";
fn act(_: &mut Ctx, opt: Self::Options) -> Result<Data> {
opt.tx.try_send(opt.idx)?;
succ!();
}
}

View file

@ -0,0 +1,28 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::which::ShowOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
pub struct Show;
impl Actor for Show {
type Options = ShowOpt;
const NAME: &'static str = "show";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if opt.cands.is_empty() {
succ!();
}
let which = &mut cx.which;
which.times = 0;
which.cands = opt.cands.into_iter().map(|c| c.into()).collect();
which.visible = true;
which.silent = opt.silent;
succ!(render!());
}
}

View file

@ -10,8 +10,6 @@ repository = "https://github.com/sxyazi/yazi"
[dependencies]
yazi-adapter = { path = "../yazi-adapter", version = "25.6.11" }
yazi-boot = { path = "../yazi-boot", version = "25.6.11" }
yazi-codegen = { path = "../yazi-codegen", version = "25.6.11" }
yazi-config = { path = "../yazi-config", version = "25.6.11" }
yazi-dds = { path = "../yazi-dds", version = "25.6.11" }
yazi-fs = { path = "../yazi-fs", version = "25.6.11" }
@ -21,7 +19,6 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.6.11" }
yazi-proxy = { path = "../yazi-proxy", version = "25.6.11" }
yazi-scheduler = { path = "../yazi-scheduler", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
yazi-term = { path = "../yazi-term", version = "25.6.11" }
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
# External dependencies
@ -32,8 +29,6 @@ indexmap = { workspace = true }
notify = { version = "8.1.0", default-features = false, features = [ "macos_fsevent" ] }
parking_lot = { workspace = true }
ratatui = { workspace = true }
scopeguard = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tokio-util = { workspace = true }

View file

@ -6,13 +6,13 @@ use yazi_widgets::Scrollable;
#[derive(Default)]
pub struct Cmp {
pub(super) caches: HashMap<PathBuf, Vec<CmpItem>>,
pub(super) cands: Vec<CmpItem>,
pub(super) offset: usize,
pub cursor: usize,
pub caches: HashMap<PathBuf, Vec<CmpItem>>,
pub cands: Vec<CmpItem>,
pub offset: usize,
pub cursor: usize,
pub(super) ticket: Id,
pub visible: bool,
pub ticket: Id,
pub visible: bool,
}
impl Cmp {
@ -30,3 +30,17 @@ impl Cmp {
#[inline]
pub fn rel_cursor(&self) -> usize { self.cursor - self.offset }
}
impl Scrollable for Cmp {
#[inline]
fn total(&self) -> usize { self.cands.len() }
#[inline]
fn limit(&self) -> usize { self.cands.len().min(10) }
#[inline]
fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor }
#[inline]
fn offset_mut(&mut self) -> &mut usize { &mut self.offset }
}

Some files were not shown because too many files have changed in this diff Show more