mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: remote file management (#3396)
This commit is contained in:
parent
878b74acb7
commit
c7739c5941
19 changed files with 105 additions and 62 deletions
|
|
@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
|
|||
|
||||
### Added
|
||||
|
||||
- Remote file management ([#3166], [#3170], [#3172], [#3198], [#3201], [#3243], [#3264], [#3268])
|
||||
- Remote file management ([#3396])
|
||||
- Virtual file system ([#3034], [#3035], [#3094], [#3108], [#3187], [#3203])
|
||||
- Shell formatting ([#3232])
|
||||
- Multi-entry support for plugin system ([#3154])
|
||||
|
|
@ -83,6 +83,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
|
|||
|
||||
- Make preload tasks discardable ([#2875])
|
||||
- Reduce file change event frequency ([#2820])
|
||||
- Upload and download of a single file over SFTP in chunks concurrently ([#3393])
|
||||
- Do not listen for file changes in inactive tabs ([#2958])
|
||||
- Switch to a higher-performance hash algorithm ([#3083])
|
||||
- Sequence-based rendering merge strategy ([#2861])
|
||||
|
|
@ -1548,3 +1549,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
|
|||
[#3385]: https://github.com/sxyazi/yazi/pull/3385
|
||||
[#3387]: https://github.com/sxyazi/yazi/pull/3387
|
||||
[#3391]: https://github.com/sxyazi/yazi/pull/3391
|
||||
[#3393]: https://github.com/sxyazi/yazi/pull/3393
|
||||
[#3396]: https://github.com/sxyazi/yazi/pull/3396
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use yazi_fs::{CWD, path::expand_url, provider::{DirReader, FileHolder}};
|
|||
use yazi_macro::{act, render, succ};
|
||||
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
|
||||
use yazi_proxy::CmpProxy;
|
||||
use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{PathBufDyn, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::StrandLike, url::{UrlBuf, UrlCow, UrlLike}};
|
||||
use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{PathBufDyn, PathDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}};
|
||||
use yazi_vfs::provider;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
|
@ -69,7 +69,8 @@ impl Trigger {
|
|||
fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> {
|
||||
let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?;
|
||||
|
||||
if scheme.is_local() && path == "~" {
|
||||
tracing::debug!(?scheme, ?path);
|
||||
if scheme.is_local() && path.as_strand() == "~" {
|
||||
return None; // We don't autocomplete a `~`, but `~/`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::{Result, bail};
|
||||
use yazi_fs::FilesOp;
|
||||
use yazi_macro::{act, succ};
|
||||
use yazi_parser::{VoidOpt, mgr::{CdSource, DisplaceDoOpt}};
|
||||
use yazi_proxy::MgrProxy;
|
||||
|
|
@ -22,11 +23,10 @@ impl Actor for Displace {
|
|||
let tab = cx.tab().id;
|
||||
let from = cx.cwd().to_owned();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(to) = provider::absolute(&from).await
|
||||
&& to.is_owned()
|
||||
{
|
||||
MgrProxy::displace_do(tab, DisplaceDoOpt { to: to.into(), from });
|
||||
}
|
||||
MgrProxy::displace_do(tab, DisplaceDoOpt {
|
||||
to: provider::absolute(&from).await.map(|u| u.into_owned()),
|
||||
from,
|
||||
});
|
||||
});
|
||||
|
||||
succ!();
|
||||
|
|
@ -42,18 +42,23 @@ impl Actor for DisplaceDo {
|
|||
const NAME: &str = "displace_do";
|
||||
|
||||
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
|
||||
if !opt.to.is_absolute() {
|
||||
bail!("Target URL must be absolute");
|
||||
}
|
||||
|
||||
if cx.cwd() != opt.from {
|
||||
succ!()
|
||||
}
|
||||
|
||||
let to = match opt.to {
|
||||
Ok(url) => url,
|
||||
Err(e) => return act!(mgr:update_files, cx, FilesOp::IOErr(opt.from, e.into())),
|
||||
};
|
||||
|
||||
if !to.is_absolute() {
|
||||
bail!("Target URL must be absolute");
|
||||
} else if let Some(hovered) = cx.hovered()
|
||||
&& let Ok(url) = opt.to.try_join(hovered.urn())
|
||||
&& let Ok(url) = to.try_join(hovered.urn())
|
||||
{
|
||||
act!(mgr:reveal, cx, (url, CdSource::Displace))
|
||||
} else {
|
||||
act!(mgr:cd, cx, (opt.to, CdSource::Displace))
|
||||
act!(mgr:cd, cx, (to, CdSource::Displace))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ impl Adapter {
|
|||
}
|
||||
|
||||
impl Adapter {
|
||||
pub fn matches(emulator: Emulator) -> Self {
|
||||
pub fn matches(emulator: &Emulator) -> Self {
|
||||
let mut protocols = emulator.adapters().to_owned();
|
||||
if env_exists("ZELLIJ_SESSION_NAME") {
|
||||
protocols.retain(|p| *p == Self::Sixel);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ impl Dimension {
|
|||
}
|
||||
|
||||
pub fn cell_size() -> Option<(f64, f64)> {
|
||||
let emu = EMULATOR.get();
|
||||
let emu = &*EMULATOR;
|
||||
Some(if emu.force_16t {
|
||||
(emu.csi_16t.0 as f64, emu.csi_16t.1 as f64)
|
||||
} else if let Some(r) = Self::available().ratio() {
|
||||
|
|
|
|||
|
|
@ -10,16 +10,25 @@ use yazi_term::tty::{Handle, TTY};
|
|||
|
||||
use crate::{Adapter, Brand, Dimension, Mux, TMUX, Unknown};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Emulator {
|
||||
pub kind: Either<Brand, Unknown>,
|
||||
pub version: String,
|
||||
pub light: bool,
|
||||
pub csi_16t: (u16, u16),
|
||||
pub force_16t: bool,
|
||||
}
|
||||
|
||||
impl Default for Emulator {
|
||||
fn default() -> Self { Self::unknown() }
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kind: Either::Right(Unknown::default()),
|
||||
version: String::new(),
|
||||
light: false,
|
||||
csi_16t: (0, 0),
|
||||
force_16t: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Emulator {
|
||||
|
|
@ -60,22 +69,14 @@ impl Emulator {
|
|||
let csi_16t = Self::csi_16t(&resp).unwrap_or_default();
|
||||
Ok(Self {
|
||||
kind,
|
||||
version: Self::csi_gt_q(&resp).unwrap_or_default(),
|
||||
light: Self::light_bg(&resp).unwrap_or_default(),
|
||||
csi_16t,
|
||||
force_16t: Self::force_16t(csi_16t),
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn unknown() -> Self {
|
||||
Self {
|
||||
kind: Either::Right(Unknown::default()),
|
||||
light: false,
|
||||
csi_16t: (0, 0),
|
||||
force_16t: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn adapters(self) -> &'static [Adapter] {
|
||||
pub fn adapters(&self) -> &'static [Adapter] {
|
||||
match self.kind {
|
||||
Either::Left(brand) => brand.adapters(),
|
||||
Either::Right(unknown) => unknown.adapters(),
|
||||
|
|
@ -182,6 +183,11 @@ impl Emulator {
|
|||
Some((w.parse().ok()?, h.parse().ok()?))
|
||||
}
|
||||
|
||||
fn csi_gt_q(resp: &str) -> Option<String> {
|
||||
let (_, s) = resp.split_once("\x1bP>|")?;
|
||||
Some(s[..s.find("\x1b\\")?].to_owned())
|
||||
}
|
||||
|
||||
fn light_bg(resp: &str) -> Result<bool> {
|
||||
match resp.split_once("]11;rgb:") {
|
||||
Some((_, s)) if s.len() >= 14 => {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ yazi_macro::mod_pub!(drivers);
|
|||
|
||||
yazi_macro::mod_flat!(adapter brand dimension emulator image info mux unknown);
|
||||
|
||||
use yazi_shared::{SyncCell, in_wsl};
|
||||
use yazi_shared::{RoCell, SyncCell, in_wsl};
|
||||
|
||||
pub static EMULATOR: SyncCell<Emulator> = SyncCell::new(Emulator::unknown());
|
||||
pub static EMULATOR: RoCell<Emulator> = RoCell::new();
|
||||
pub static ADAPTOR: SyncCell<Adapter> = SyncCell::new(Adapter::Chafa);
|
||||
|
||||
// Image state
|
||||
|
|
@ -24,8 +24,8 @@ pub fn init() -> anyhow::Result<()> {
|
|||
WSL.set(in_wsl());
|
||||
|
||||
// Emulator detection
|
||||
EMULATOR.set(Emulator::detect().unwrap_or_default());
|
||||
TMUX.set(EMULATOR.get().kind.is_left_and(|&b| b == Brand::Tmux));
|
||||
let mut emulator = Emulator::detect().unwrap_or_default();
|
||||
TMUX.set(emulator.kind.is_left_and(|&b| b == Brand::Tmux));
|
||||
|
||||
// Tmux support
|
||||
if TMUX.get() {
|
||||
|
|
@ -33,12 +33,13 @@ pub fn init() -> anyhow::Result<()> {
|
|||
START.set("\x1bPtmux;\x1b\x1b");
|
||||
CLOSE.set("\x1b\\");
|
||||
Mux::tmux_passthrough();
|
||||
EMULATOR.set(Emulator::detect().unwrap_or_default());
|
||||
emulator = Emulator::detect().unwrap_or_default();
|
||||
}
|
||||
|
||||
yazi_config::init_flavor(EMULATOR.get().light)?;
|
||||
EMULATOR.init(emulator);
|
||||
yazi_config::init_flavor(EMULATOR.light)?;
|
||||
|
||||
ADAPTOR.set(Adapter::matches(EMULATOR.get()));
|
||||
ADAPTOR.set(Adapter::matches(&EMULATOR));
|
||||
ADAPTOR.get().start();
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
use crate::Adapter;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Unknown {
|
||||
pub kgp: bool,
|
||||
pub sixel: bool,
|
||||
}
|
||||
|
||||
impl Unknown {
|
||||
pub(super) const fn default() -> Self { Self { kgp: false, sixel: false } }
|
||||
|
||||
pub(super) fn adapters(self) -> &'static [Adapter] {
|
||||
use Adapter as A;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use std::{env, ffi::OsStr, fmt::Write, path::Path};
|
|||
|
||||
use regex::Regex;
|
||||
use yazi_adapter::Mux;
|
||||
use yazi_config::YAZI;
|
||||
use yazi_config::{THEME, YAZI};
|
||||
use yazi_fs::Xdg;
|
||||
use yazi_shared::timestamp_us;
|
||||
|
||||
use super::Actions;
|
||||
|
|
@ -19,12 +20,20 @@ impl Actions {
|
|||
writeln!(s, "\nYa")?;
|
||||
writeln!(s, " Version: {}", Self::process_output("ya", "--version"))?;
|
||||
|
||||
writeln!(s, "\nConfig")?;
|
||||
writeln!(s, " Yazi : {}", Self::config_state("yazi"))?;
|
||||
writeln!(s, " Keymap : {}", Self::config_state("keymap"))?;
|
||||
writeln!(s, " Theme : {}", Self::config_state("theme"))?;
|
||||
writeln!(s, " VFS : {}", Self::config_state("vfs"))?;
|
||||
writeln!(s, " Package : {}", Self::config_state("package"))?;
|
||||
writeln!(s, " Dark/light flavor: {:?} / {:?}", THEME.flavor.dark, THEME.flavor.light)?;
|
||||
|
||||
writeln!(s, "\nEmulator")?;
|
||||
writeln!(s, " TERM : {:?}", env::var_os("TERM"))?;
|
||||
writeln!(s, " TERM_PROGRAM : {:?}", env::var_os("TERM_PROGRAM"))?;
|
||||
writeln!(s, " TERM_PROGRAM_VERSION: {:?}", env::var_os("TERM_PROGRAM_VERSION"))?;
|
||||
writeln!(s, " Brand.from_env : {:?}", yazi_adapter::Brand::from_env())?;
|
||||
writeln!(s, " Emulator.detect : {:?}", yazi_adapter::EMULATOR)?;
|
||||
writeln!(s, " Emulator.detect : {:?}", &*yazi_adapter::EMULATOR)?;
|
||||
|
||||
writeln!(s, "\nAdapter")?;
|
||||
writeln!(s, " Adapter.matches : {:?}", yazi_adapter::ADAPTOR)?;
|
||||
|
|
@ -115,6 +124,16 @@ impl Actions {
|
|||
Ok(s)
|
||||
}
|
||||
|
||||
fn config_state(name: &str) -> String {
|
||||
let p = Xdg::config_dir().join(format!("{name}.toml"));
|
||||
match std::fs::read_to_string(&p) {
|
||||
Ok(s) if s.is_empty() => format!("{} (empty)", p.display()),
|
||||
Ok(s) if s.trim().is_empty() => format!("{} (whitespaces)", p.display()),
|
||||
Ok(s) => format!("{} ({} chars)", p.display(), s.chars().count()),
|
||||
Err(e) => format!("{} ({e})", p.display()),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_output(name: impl AsRef<OsStr>, arg: impl AsRef<OsStr>) -> String {
|
||||
match std::process::Command::new(&name).arg(arg).output() {
|
||||
Ok(out) if out.status.success() => {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ impl Provider {
|
|||
}
|
||||
|
||||
// --- SFTP
|
||||
#[derive(Deserialize, Hash, Serialize, Eq, PartialEq)]
|
||||
#[derive(Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct ProviderSftp {
|
||||
pub host: String,
|
||||
pub user: String,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use std::{io, path::{Path, PathBuf}};
|
||||
|
||||
pub async fn must_case_match(path: impl AsRef<Path>) -> bool {
|
||||
pub async fn match_name_case(path: impl AsRef<Path>) -> bool {
|
||||
let path = path.as_ref();
|
||||
casefold(path).await.is_ok_and(|p| p == path)
|
||||
casefold(path).await.is_ok_and(|p| p.file_name() == path.file_name())
|
||||
}
|
||||
|
||||
pub(super) async fn casefold(path: impl AsRef<Path>) -> io::Result<PathBuf> {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use yazi_shared::{event::CmdCow, url::UrlBuf};
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct DisplaceDoOpt {
|
||||
pub to: UrlBuf,
|
||||
pub to: std::io::Result<UrlBuf>,
|
||||
pub from: UrlBuf,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ impl TryFrom<CmdCow> for UpdateFilesOpt {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<FilesOp> for UpdateFilesOpt {
|
||||
fn from(op: FilesOp) -> Self { Self { op } }
|
||||
}
|
||||
|
||||
impl FromLua for UpdateFilesOpt {
|
||||
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ function M:try_with(from, pwd, to)
|
|||
if not output then
|
||||
fail("7zip failed to output when extracting '%s', error: %s", from, err)
|
||||
elseif output.status.code ~= 0 then
|
||||
fail("7zip exited when extracting '%s', error code %s", from, output.status.code)
|
||||
fail("7zip exited with error code %s when extracting '%s':\n%s", output.status.code, from, output.stderr)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ end
|
|||
|
||||
function M:spot(job)
|
||||
self.size, self.last = 0, 0
|
||||
self:spot_multi(job, false)
|
||||
|
||||
local url = job.file.url
|
||||
local it = fs.calc_size(url)
|
||||
|
|
@ -69,15 +70,15 @@ function M:spot(job)
|
|||
self:spot_multi(job, true)
|
||||
end
|
||||
|
||||
function M:spot_multi(job, force)
|
||||
function M:spot_multi(job, comp)
|
||||
local now = ya.time()
|
||||
if not force and now < self.last + 0.1 then
|
||||
if not comp and now < self.last + 0.1 then
|
||||
return
|
||||
end
|
||||
|
||||
local rows = {
|
||||
ui.Row({ "Folder" }):style(ui.Style():fg("green")),
|
||||
ui.Row { " Size:", ya.readable_size(self.size) .. (force and "" or " (?)") },
|
||||
ui.Row { " Size:", ya.readable_size(self.size) .. (comp and "" or " (?)") },
|
||||
ui.Row {},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use yazi_binding::{Composer, ComposerGet, ComposerSet};
|
|||
pub(super) fn term() -> Composer<ComposerGet, ComposerSet> {
|
||||
fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
|
||||
match key {
|
||||
b"light" => EMULATOR.get().light.into_lua(lua),
|
||||
b"light" => EMULATOR.light.into_lua(lua),
|
||||
b"cell_size" => cell_size(lua)?.into_lua(lua),
|
||||
_ => Ok(Value::Nil),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ pub(super) fn copy_with_progress_impl(
|
|||
};
|
||||
|
||||
let chunks = (cha.len + 10485760 - 1) / 10485760;
|
||||
let result = futures::stream::iter(0..chunks)
|
||||
let mut result = futures::stream::iter(0..chunks)
|
||||
.map(|i| {
|
||||
let acc_ = acc_.clone();
|
||||
let (from, to) = (from.clone(), to.clone());
|
||||
|
|
@ -79,24 +79,22 @@ pub(super) fn copy_with_progress_impl(
|
|||
copied += n as u64;
|
||||
acc_.fetch_add(n as u64, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
dist.flush().await?;
|
||||
if i == chunks - 1 {
|
||||
dist.get_ref().set_attrs(attrs).await.ok();
|
||||
}
|
||||
dist.shutdown().await.ok();
|
||||
|
||||
if copied == take {
|
||||
Ok(())
|
||||
} else {
|
||||
if copied != take {
|
||||
Err(io::Error::other(format!(
|
||||
"short copy for chunk {i}: copied {copied} bytes, expected {take}"
|
||||
)))
|
||||
} else if i == chunks - 1 {
|
||||
Ok(Some(dist.into_inner()))
|
||||
} else {
|
||||
dist.shutdown().await.ok();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
})
|
||||
.buffer_unordered(3)
|
||||
.try_for_each(|_| async { Ok(()) })
|
||||
.try_fold(None, |first, file| async { Ok(first.or(file)) })
|
||||
.await;
|
||||
|
||||
let n = acc_.swap(0, Ordering::SeqCst);
|
||||
|
|
@ -104,6 +102,11 @@ pub(super) fn copy_with_progress_impl(
|
|||
prog_tx_.send(Ok(n)).await.ok();
|
||||
}
|
||||
|
||||
if let Ok(Some(file)) = &mut result {
|
||||
file.set_attrs(attrs).await.ok();
|
||||
file.shutdown().await.ok();
|
||||
}
|
||||
|
||||
if let Err(e) = result {
|
||||
prog_tx_.send(Err(e)).await.ok();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -164,9 +164,11 @@ impl Conn {
|
|||
russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?;
|
||||
|
||||
for key in keys {
|
||||
match session.authenticate_publickey_with(&self.config.user, key, None, &mut agent).await {
|
||||
let hash_alg = session.best_supported_rsa_hash().await?.flatten();
|
||||
match session.authenticate_publickey_with(&self.config.user, key, hash_alg, &mut agent).await
|
||||
{
|
||||
Ok(result) if result.success() => return Ok(session),
|
||||
Ok(_) => {}
|
||||
Ok(result) => tracing::debug!("Identity agent authentication failed: {result:?}"),
|
||||
Err(e) => tracing::error!("Identity agent authentication error: {e}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ impl Local {
|
|||
};
|
||||
|
||||
if let Some(p) = file.url.as_local()
|
||||
&& !provider::local::must_case_match(p).await
|
||||
&& !provider::local::match_name_case(p).await
|
||||
{
|
||||
ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into()));
|
||||
continue;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue