feat: check and refresh the file list when the terminal gains focus (#3561)

This commit is contained in:
三咲雅 misaki masa 2026-01-13 16:53:00 +08:00 committed by GitHub
parent 41e5717930
commit 0c8c973e6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 95 additions and 69 deletions

View file

@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Tree view for the preset archive previewer ([#3525])
- Support compressed tarballs (`.tar.gz`, `.tar.bz2`, etc.) in the preset archive previewer ([#3518])
- Check and refresh the file list when the terminal gains focus ([#3561])
- New `Path.os()` API creates an OS-native `Path` ([#3541])
### Fixed
@ -1601,3 +1602,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3532]: https://github.com/sxyazi/yazi/pull/3532
[#3540]: https://github.com/sxyazi/yazi/pull/3540
[#3541]: https://github.com/sxyazi/yazi/pull/3541
[#3561]: https://github.com/sxyazi/yazi/pull/3561

View file

@ -21,11 +21,12 @@ impl Actions {
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, " Init : {}", Self::config_state("init.lua"))?;
writeln!(s, " Yazi : {}", Self::config_state("yazi.toml"))?;
writeln!(s, " Keymap : {}", Self::config_state("keymap.toml"))?;
writeln!(s, " Theme : {}", Self::config_state("theme.toml"))?;
writeln!(s, " VFS : {}", Self::config_state("vfs.toml"))?;
writeln!(s, " Package : {}", Self::config_state("package.toml"))?;
writeln!(s, " Dark/light flavor: {:?} / {:?}", THEME.flavor.dark, THEME.flavor.light)?;
writeln!(s, "\nEmulator")?;
@ -125,7 +126,7 @@ impl Actions {
}
fn config_state(name: &str) -> String {
let p = Xdg::config_dir().join(format!("{name}.toml"));
let p = Xdg::config_dir().join(name);
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()),

View file

@ -0,0 +1,15 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::act;
use yazi_parser::VoidOpt;
use yazi_shared::data::Data;
use crate::app::App;
impl App {
pub fn focus(&mut self, _: VoidOpt) -> Result<Data> {
let cx = &mut Ctx::active(&mut self.core);
act!(mgr:refresh, cx)
}
}

View file

@ -2,6 +2,7 @@ yazi_macro::mod_flat!(
accept_payload
bootstrap
deprecate
focus
mouse
notify
plugin

View file

@ -28,6 +28,7 @@ impl<'a> Dispatcher<'a> {
Event::Key(key) => self.dispatch_key(key),
Event::Mouse(mouse) => act!(mouse, self.app, mouse),
Event::Resize => act!(resize, self.app),
Event::Focus => act!(focus, self.app),
Event::Paste(str) => self.dispatch_paste(str),
Event::Quit(opt) => act!(quit, self.app, opt),
};

View file

@ -64,8 +64,9 @@ impl Signals {
Event::Mouse(mouse).emit();
}
}
CrosstermEvent::Paste(str) => Event::Paste(str).emit(),
CrosstermEvent::Resize(..) => Event::Resize.emit(),
CrosstermEvent::FocusGained => Event::Focus.emit(),
CrosstermEvent::Paste(str) => Event::Paste(str).emit(),
_ => {}
}
}

View file

@ -1,7 +1,7 @@
use std::{io, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, Ordering}};
use anyhow::Result;
use crossterm::{event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode}};
use crossterm::{event::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode}};
use ratatui::{CompletedFrame, Frame, Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect};
use yazi_adapter::{Emulator, Mux, TMUX};
use yazi_config::{THEME, YAZI};
@ -40,6 +40,7 @@ impl Term {
yazi_term::If(TMUX.get(), EnterAlternateScreen),
yazi_term::SetBackground(true, THEME.app.bg_color()), // Set app background
EnableBracketedPaste,
EnableFocusChange,
yazi_term::If(!YAZI.mgr.mouse_events.get().is_empty(), EnableMouseCapture),
)?;
@ -81,6 +82,7 @@ impl Term {
TTY.writer(),
yazi_term::If(!YAZI.mgr.mouse_events.get().is_empty(), DisableMouseCapture),
yazi_term::RestoreCursor,
DisableFocusChange,
DisableBracketedPaste,
LeaveAlternateScreen,
)?;
@ -103,6 +105,7 @@ impl Term {
yazi_term::If(!YAZI.mgr.mouse_events.get().is_empty(), DisableMouseCapture),
yazi_term::SetBackground(false, THEME.app.bg_color()),
yazi_term::RestoreCursor,
DisableFocusChange,
DisableBracketedPaste,
LeaveAlternateScreen,
crossterm::cursor::Show

View file

@ -455,6 +455,7 @@ pub(crate) enum FileOutUpload {
Deform(String),
Succ,
Fail(String),
Clean,
}
impl From<anyhow::Error> for FileOutUpload {
@ -481,6 +482,9 @@ impl FileOutUpload {
prog.collected = Some(false);
task.log(reason);
}
Self::Clean => {
prog.cleaned = Some(true);
}
}
}
}

View file

@ -321,6 +321,7 @@ pub struct FileProgUpload {
pub total_bytes: u64,
pub processed_bytes: u64,
pub collected: Option<bool>,
pub cleaned: Option<bool>,
}
impl From<FileProgUpload> for TaskSummary {
@ -340,14 +341,16 @@ impl FileProgUpload {
}
pub fn running(self) -> bool {
self.collected.is_none() || self.success_files + self.failed_files != self.total_files
self.collected.is_none()
|| self.success_files + self.failed_files != self.total_files
|| (self.cleaned.is_none() && self.cooked())
}
pub fn success(self) -> bool { self.cooked() }
pub fn success(self) -> bool { self.cleaned == Some(true) && self.cooked() }
pub fn failed(self) -> bool { self.collected == Some(false) }
pub fn failed(self) -> bool { self.cleaned == Some(false) || self.collected == Some(false) }
pub fn cleaned(self) -> Option<bool> { None }
pub fn cleaned(self) -> Option<bool> { self.cleaned }
pub fn percent(self) -> Option<f32> {
Some(if self.success() {

View file

@ -7,7 +7,7 @@ use yazi_fs::ok_or_not_found;
use yazi_proxy::TasksProxy;
use yazi_vfs::provider;
use crate::{Ongoing, TaskOp, TaskOps, file::{FileOutCopy, FileOutCut, FileOutDelete, FileOutDownload, FileOutTrash}, hook::{HookInOutCopy, HookInOutCut, HookInOutDelete, HookInOutDownload, HookInOutTrash}};
use crate::{Ongoing, TaskOp, TaskOps, file::{FileOutCopy, FileOutCut, FileOutDelete, FileOutDownload, FileOutTrash, FileOutUpload}, hook::{HookInDelete, HookInDownload, HookInOutCopy, HookInOutCut, HookInTrash, HookInUpload}};
pub(crate) struct Hook {
ops: TaskOps,
@ -41,7 +41,7 @@ impl Hook {
self.ops.out(task.id, FileOutCopy::Clean);
}
pub(crate) async fn delete(&self, task: HookInOutDelete) {
pub(crate) async fn delete(&self, task: HookInDelete) {
if !self.ongoing.lock().intact(task.id) {
return self.ops.out(task.id, FileOutDelete::Clean(Ok(())));
}
@ -53,7 +53,7 @@ impl Hook {
self.ops.out(task.id, FileOutDelete::Clean(result));
}
pub(crate) async fn trash(&self, task: HookInOutTrash) {
pub(crate) async fn trash(&self, task: HookInTrash) {
let intact = self.ongoing.lock().intact(task.id);
if intact {
TasksProxy::update_succeed([&task.target]);
@ -62,7 +62,15 @@ impl Hook {
self.ops.out(task.id, FileOutTrash::Clean);
}
pub(crate) async fn download(&self, task: HookInOutDownload) {
pub(crate) async fn download(&self, task: HookInDownload) {
self.ops.out(task.id, FileOutDownload::Clean);
}
pub(crate) async fn upload(&self, task: HookInUpload) {
let intact = self.ongoing.lock().intact(task.id);
if intact {
TasksProxy::update_succeed([task.target]);
}
self.ops.out(task.id, FileOutUpload::Clean);
}
}

View file

@ -48,44 +48,27 @@ impl HookInOutCut {
// --- Delete
#[derive(Debug)]
pub(crate) struct HookInOutDelete {
pub(crate) struct HookInDelete {
pub(crate) id: Id,
pub(crate) target: UrlBuf,
}
impl HookInOutDelete {
pub(crate) fn reduce(self, task: &mut Task) {
if let TaskProg::FileDelete(_) = &task.prog {
task.hook = Some(self.into());
}
}
}
// --- Trash
#[derive(Debug)]
pub(crate) struct HookInOutTrash {
pub(crate) struct HookInTrash {
pub(crate) id: Id,
pub(crate) target: UrlBuf,
}
impl HookInOutTrash {
pub(crate) fn reduce(self, task: &mut Task) {
if let TaskProg::FileTrash(_) = &task.prog {
task.hook = Some(self.into());
}
}
}
// --- Download
#[derive(Debug)]
pub(crate) struct HookInOutDownload {
pub(crate) struct HookInDownload {
pub(crate) id: Id,
}
impl HookInOutDownload {
pub(crate) fn reduce(self, task: &mut Task) {
if let TaskProg::FileDownload(_) = &task.prog {
task.hook = Some(self.into());
}
}
// --- Upload
#[derive(Debug)]
pub(crate) struct HookInUpload {
pub(crate) id: Id,
pub(crate) target: UrlBuf,
}

View file

@ -1,6 +1,6 @@
use yazi_shared::Id;
use crate::{file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload}, hook::{HookInOutCopy, HookInOutCut, HookInOutDelete, HookInOutDownload, HookInOutTrash}, impl_from_in, plugin::PluginInEntry, prework::{PreworkInFetch, PreworkInLoad, PreworkInSize}, process::{ProcessInBg, ProcessInBlock, ProcessInOrphan}};
use crate::{file::{FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload}, hook::{HookInDelete, HookInDownload, HookInOutCopy, HookInOutCut, HookInTrash, HookInUpload}, impl_from_in, plugin::PluginInEntry, prework::{PreworkInFetch, PreworkInLoad, PreworkInSize}, process::{ProcessInBg, ProcessInBlock, ProcessInOrphan}};
#[derive(Debug)]
pub(crate) enum TaskIn {
@ -26,9 +26,10 @@ pub(crate) enum TaskIn {
// Hook
HookCopy(HookInOutCopy),
HookCut(HookInOutCut),
HookDelete(HookInOutDelete),
HookTrash(HookInOutTrash),
HookDownload(HookInOutDownload),
HookDelete(HookInDelete),
HookTrash(HookInTrash),
HookDownload(HookInDownload),
HookUpload(HookInUpload),
}
impl_from_in! {
@ -41,7 +42,7 @@ impl_from_in! {
// Process
ProcessBlock(ProcessInBlock), ProcessOrphan(ProcessInOrphan), ProcessBg(ProcessInBg),
// Hook
HookCopy(HookInOutCopy), HookCut(HookInOutCut), HookDelete(HookInOutDelete), HookTrash(HookInOutTrash), HookDownload(HookInOutDownload),
HookCopy(HookInOutCopy), HookCut(HookInOutCut), HookDelete(HookInDelete), HookTrash(HookInTrash), HookDownload(HookInDownload), HookUpload(HookInUpload),
}
impl TaskIn {
@ -72,6 +73,7 @@ impl TaskIn {
Self::HookDelete(r#in) => r#in.id,
Self::HookTrash(r#in) => r#in.id,
Self::HookDownload(r#in) => r#in.id,
Self::HookUpload(r#in) => r#in.id,
}
}
@ -102,6 +104,7 @@ impl TaskIn {
Self::HookDelete(_) => true,
Self::HookTrash(_) => true,
Self::HookDownload(_) => true,
Self::HookUpload(_) => true,
}
}
}

View file

@ -1,4 +1,4 @@
use crate::{Task, file::{FileOutCopy, FileOutCopyDo, FileOutCut, FileOutCutDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutTrash, FileOutUpload, FileOutUploadDo}, hook::{HookInOutCopy, HookInOutCut, HookInOutDelete, HookInOutDownload, HookInOutTrash}, impl_from_out, plugin::PluginOutEntry, prework::{PreworkOutFetch, PreworkOutLoad, PreworkOutSize}, process::{ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}};
use crate::{Task, file::{FileOutCopy, FileOutCopyDo, FileOutCut, FileOutCutDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutTrash, FileOutUpload, FileOutUploadDo}, hook::{HookInOutCopy, HookInOutCut}, impl_from_out, plugin::PluginOutEntry, prework::{PreworkOutFetch, PreworkOutLoad, PreworkOutSize}, process::{ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}};
#[derive(Debug)]
pub(super) enum TaskOut {
@ -30,9 +30,6 @@ pub(super) enum TaskOut {
// Hook
HookCopy(HookInOutCopy),
HookCut(HookInOutCut),
HookDelete(HookInOutDelete),
HookTrash(HookInOutTrash),
HookDownload(HookInOutDownload),
}
impl_from_out! {
@ -45,7 +42,7 @@ impl_from_out! {
// Process
ProcessBlock(ProcessOutBlock), ProcessOrphan(ProcessOutOrphan), ProcessBg(ProcessOutBg),
// Hook
HookCopy(HookInOutCopy), HookCut(HookInOutCut), HookDelete(HookInOutDelete), HookTrash(HookInOutTrash), HookDownload(HookInOutDownload),
HookCopy(HookInOutCopy), HookCut(HookInOutCut),
}
impl TaskOut {
@ -79,9 +76,6 @@ impl TaskOut {
// Hook
Self::HookCopy(out) => out.reduce(task),
Self::HookCut(out) => out.reduce(task),
Self::HookDelete(out) => out.reduce(task),
Self::HookTrash(out) => out.reduce(task),
Self::HookDownload(out) => out.reduce(task),
}
}
}

View file

@ -39,6 +39,7 @@ impl Runner {
TaskIn::HookDelete(r#in) => Ok(self.hook.delete(r#in).await),
TaskIn::HookTrash(r#in) => Ok(self.hook.trash(r#in).await),
TaskIn::HookDownload(r#in) => Ok(self.hook.download(r#in).await),
TaskIn::HookUpload(r#in) => Ok(self.hook.upload(r#in).await),
}
}
@ -69,6 +70,7 @@ impl Runner {
TaskIn::HookDelete(_in) => unreachable!(),
TaskIn::HookTrash(_in) => unreachable!(),
TaskIn::HookDownload(_in) => unreachable!(),
TaskIn::HookUpload(_in) => unreachable!(),
}
}
}

View file

@ -7,7 +7,7 @@ use yazi_parser::{app::PluginOpt, tasks::ProcessOpenOpt};
use yazi_shared::{CompletionToken, Id, Throttle, url::{UrlBuf, UrlLike}};
use super::{Ongoing, TaskOp};
use crate::{HIGH, LOW, NORMAL, Runner, TaskIn, TaskOps, file::{File, FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDownload, FileOutHardlink, FileOutUpload, FileProgCopy, FileProgCut, FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgTrash, FileProgUpload}, hook::{Hook, HookInOutDelete, HookInOutDownload, HookInOutTrash}, plugin::{Plugin, PluginInEntry, PluginProgEntry}, prework::{Prework, PreworkInFetch, PreworkInLoad, PreworkInSize, PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{Process, ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}};
use crate::{HIGH, LOW, NORMAL, Runner, TaskIn, TaskOps, file::{File, FileInCopy, FileInCut, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInTrash, FileInUpload, FileOutCopy, FileOutCut, FileOutDownload, FileOutHardlink, FileOutUpload, FileProgCopy, FileProgCut, FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgTrash, FileProgUpload}, hook::{Hook, HookInDelete, HookInDownload, HookInTrash, HookInUpload}, plugin::{Plugin, PluginInEntry, PluginProgEntry}, prework::{Prework, PreworkInFetch, PreworkInLoad, PreworkInSize, PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{Process, ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}};
pub struct Scheduler {
ops: TaskOps,
@ -163,7 +163,7 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock();
let task = ongoing.add::<FileProgDelete>(format!("Delete {}", target.display()));
task.set_hook(HookInOutDelete { id: task.id, target: target.clone() });
task.set_hook(HookInDelete { id: task.id, target: target.clone() });
self.queue(FileInDelete { id: task.id, target, cha: None }, LOW);
}
@ -171,7 +171,7 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock();
let task = ongoing.add::<FileProgTrash>(format!("Trash {}", target.display()));
task.set_hook(HookInOutTrash { id: task.id, target: target.clone() });
task.set_hook(HookInTrash { id: task.id, target: target.clone() });
self.queue(FileInTrash { id: task.id, target }, LOW);
}
@ -179,8 +179,8 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock();
let task = ongoing.add::<FileProgDownload>(format!("Download {}", url.display()));
task.set_hook(HookInOutDownload { id: task.id });
if url.kind().is_remote() {
task.set_hook(HookInDownload { id: task.id });
self.queue(
FileInDownload { id: task.id, url, cha: None, retry: 0, done: task.done.clone() },
LOW,
@ -202,6 +202,7 @@ impl Scheduler {
.out(task.id, FileOutUpload::Fail("Cannot upload non-remote file".to_owned()));
};
task.set_hook(HookInUpload { id: task.id, target: url.clone() });
self.queue(
FileInUpload { id: task.id, url, cha: None, cache: None, done: task.done.clone() },
LOW,

View file

@ -15,6 +15,7 @@ pub enum Event {
Key(KeyEvent),
Mouse(MouseEvent),
Resize,
Focus,
Paste(String),
Quit(EventQuit),
}

View file

@ -1,7 +1,7 @@
use std::time::{Duration, SystemTime};
use anyhow::Result;
use hashbrown::HashSet;
use hashbrown::HashMap;
use tokio::{pin, sync::mpsc::UnboundedReceiver};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_fs::{File, FilesOp};
@ -14,7 +14,7 @@ use crate::{Reporter, WATCHER};
pub(crate) struct Remote;
impl Remote {
pub(crate) fn serve(rx: UnboundedReceiver<UrlBuf>, _reporter: Reporter) -> Self {
pub(crate) fn serve(rx: UnboundedReceiver<(UrlBuf, bool)>, _reporter: Reporter) -> Self {
tokio::spawn(Self::changed(rx));
Self
@ -24,19 +24,22 @@ impl Remote {
pub(crate) fn unwatch(&mut self, _url: Url) -> Result<()> { Ok(()) }
async fn changed(rx: UnboundedReceiver<UrlBuf>) {
async fn changed(rx: UnboundedReceiver<(UrlBuf, bool)>) {
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));
pin!(rx);
while let Some(chunk) = rx.next().await {
let urls: HashSet<_> = chunk.into_iter().collect();
let mut urls = HashMap::with_capacity(chunk.len());
for (u, upload) in chunk {
urls.entry(u).and_modify(|b| *b |= upload).or_insert(upload);
}
let _permit = WATCHER.acquire().await.unwrap();
let mut ops = Vec::with_capacity(urls.len());
let mut ups = Vec::with_capacity(urls.len());
for u in urls {
for (u, upload) in urls {
let Some((parent, urn)) = u.pair() else { continue };
let Ok(mut file) = File::new(&u).await else {
ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into()));
@ -47,7 +50,7 @@ impl Remote {
file.cha.ctime = Some(SystemTime::now());
ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()));
if is_file {
if upload && is_file {
ups.push(u);
}
}

View file

@ -9,7 +9,7 @@ use crate::{WATCHED, local::LINKED};
#[derive(Clone)]
pub(crate) struct Reporter {
pub(super) local_tx: mpsc::UnboundedSender<UrlBuf>,
pub(super) remote_tx: mpsc::UnboundedSender<UrlBuf>,
pub(super) remote_tx: mpsc::UnboundedSender<(UrlBuf, bool)>,
}
impl Reporter {
@ -49,9 +49,9 @@ impl Reporter {
let Some(dir) = watched.find_by_cache(parent.loc()) else { continue };
let Some(name) = url.name() else { continue };
if let Ok(u) = dir.try_join(Cow::from(percent_decode(name.encoded_bytes()))) {
self.remote_tx.send(u).ok();
self.remote_tx.send((u, true)).ok();
}
self.remote_tx.send(dir).ok();
self.remote_tx.send((dir, false)).ok();
}
}
@ -61,7 +61,7 @@ impl Reporter {
return;
}
self.remote_tx.send(parent.to_owned()).ok();
self.remote_tx.send(url.into_owned()).ok();
self.remote_tx.send((parent.to_owned(), false)).ok();
self.remote_tx.send((url.into_owned(), false)).ok();
}
}