diff --git a/app/src/app.rs b/app/src/app.rs index 83968502..6ed4b11d 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -130,12 +130,10 @@ impl App { manager.refresh(); } Event::Files(op) => { - let calc = matches!(op, FilesOp::Full(..) | FilesOp::Part(..)); + let calc = !matches!(op, FilesOp::Size(..) | FilesOp::IOErr(_)); let b = match op { - FilesOp::Full(..) => manager.update_read(op), - FilesOp::Part(..) => manager.update_read(op), - FilesOp::Size(..) => manager.update_read(op), FilesOp::IOErr(..) => manager.update_ioerr(op), + _ => manager.update_read(op), }; if b { emit!(Render); @@ -156,8 +154,8 @@ impl App { emit!(Peek); } } - Event::Hover(file) => { - if manager.update_hover(file) { + Event::Hover(url) => { + if manager.current_mut().repos(url) { emit!(Render); } emit!(Peek); diff --git a/core/src/event.rs b/core/src/event.rs index 6c4f0cc9..6d32ad89 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -6,7 +6,7 @@ use crossterm::event::KeyEvent; use shared::{InputError, RoCell, Url}; use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot}; -use super::{files::{File, FilesOp}, input::InputOpt, select::SelectOpt}; +use super::{files::FilesOp, input::InputOpt, select::SelectOpt}; use crate::{manager::PreviewLock, tasks::TasksProgress}; static TX: RoCell> = RoCell::new(); @@ -26,7 +26,7 @@ pub enum Event { Files(FilesOp), Pages(usize), Mimetype(BTreeMap), - Hover(Option), + Hover(Option), Peek(Option<(usize, Url)>), Preview(PreviewLock), diff --git a/core/src/files/file.rs b/core/src/files/file.rs index bb710dcb..c21b0c5c 100644 --- a/core/src/files/file.rs +++ b/core/src/files/file.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, ffi::OsStr, fs::Metadata}; +use std::{borrow::Cow, collections::BTreeMap, ffi::OsStr, fs::Metadata}; use anyhow::Result; use shared::Url; @@ -34,6 +34,13 @@ impl File { let is_hidden = url.file_name().map(|s| s.to_string_lossy().starts_with('.')).unwrap_or(false); Self { url, meta, length, link_to, is_link, is_hidden } } + + #[inline] + pub fn into_map(self) -> BTreeMap { + let mut map = BTreeMap::new(); + map.insert(self.url.clone(), self); + map + } } impl File { diff --git a/core/src/files/files.rs b/core/src/files/files.rs index 2781740f..e6799d5d 100644 --- a/core/src/files/files.rs +++ b/core/src/files/files.rs @@ -173,6 +173,84 @@ impl Files { } true } + + pub fn update_creating(&mut self, mut todo: BTreeMap) -> bool { + if !self.show_hidden { + todo.retain(|_, f| !f.is_hidden); + } + + let b = self.update_replacing(&mut todo); + if todo.is_empty() { + return b; + } + + self.items.extend(todo.into_values()); + self.sorter.sort(&mut self.items, &self.sizes); + self.version += 1; + true + } + + pub fn update_deleting(&mut self, mut todo: BTreeSet) -> bool { + let mut removed = Vec::with_capacity(todo.len()); + macro_rules! go { + ($name:expr) => { + removed.clear(); + for i in 0..$name.len() { + if todo.remove(&$name[i].url) { + removed.push(i); + if todo.is_empty() { + break; + } + } + } + for i in (0..removed.len()).rev() { + $name.remove(removed[i]); + } + }; + } + + let mut b = false; + if !todo.is_empty() { + go!(self.items); + b |= !removed.is_empty(); + } + + if !todo.is_empty() { + go!(self.hidden); + b |= !removed.is_empty(); + } + b + } + + pub fn update_replacing(&mut self, todo: &mut BTreeMap) -> bool { + if todo.is_empty() { + return false; + } + + macro_rules! go { + ($name:expr) => { + for i in 0..$name.len() { + if let Some(f) = todo.remove(&$name[i].url) { + $name[i] = f; + if todo.is_empty() { + self.version += 1; + return true; + } + } + } + }; + } + + let old = todo.len(); + go!(self.items); + go!(self.hidden); + + if old != todo.len() { + self.version += 1; + return true; + } + false + } } impl Files { diff --git a/core/src/files/op.rs b/core/src/files/op.rs index 92523434..6854a6cc 100644 --- a/core/src/files/op.rs +++ b/core/src/files/op.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, sync::atomic::{AtomicU64, Ordering}}; +use std::{collections::{BTreeMap, BTreeSet}, sync::atomic::{AtomicU64, Ordering}}; use shared::Url; @@ -13,6 +13,10 @@ pub enum FilesOp { Part(Url, u64, Vec), Size(Url, BTreeMap), IOErr(Url), + + Creating(Url, BTreeMap), + Deleting(Url, BTreeSet), + Replacing(Url, BTreeMap), } impl FilesOp { @@ -23,6 +27,10 @@ impl FilesOp { Self::Part(url, ..) => url, Self::Size(url, _) => url, Self::IOErr(url) => url, + + Self::Creating(url, _) => url, + Self::Deleting(url, _) => url, + Self::Replacing(url, _) => url, } } diff --git a/core/src/manager/folder.rs b/core/src/manager/folder.rs index c4750002..f60c8bca 100644 --- a/core/src/manager/folder.rs +++ b/core/src/manager/folder.rs @@ -9,11 +9,10 @@ pub struct Folder { pub cwd: Url, pub files: Files, - offset: usize, - cursor: usize, + pub offset: usize, + pub cursor: usize, - pub page: usize, - pub hovered: Option, + pub page: usize, } impl From for Folder { @@ -30,19 +29,23 @@ impl Folder { FilesOp::Full(_, items) => self.files.update_full(items), FilesOp::Part(_, ticket, items) => self.files.update_part(ticket, items), FilesOp::Size(_, items) => self.files.update_size(items), + + FilesOp::Creating(_, items) => self.files.update_creating(items), + FilesOp::Deleting(_, items) => self.files.update_deleting(items), + FilesOp::Replacing(_, mut items) => self.files.update_replacing(&mut items), _ => unreachable!(), }; if !b { return false; } - let max = self.files.len().saturating_sub(1); - self.offset = self.offset.min(max); - self.cursor = self.cursor.min(max); - self.set_page(true); + let old = self.page; + self.prev(Default::default()); + + if self.page == old { + emit!(Pages(self.page)); // Force update + } - self.hover_repos(); - self.hovered = self.files.duplicate(self.cursor); true } @@ -59,35 +62,34 @@ impl Folder { } pub fn next(&mut self, step: Step) -> bool { + let old = (self.cursor, self.offset); let len = self.files.len(); - if len == 0 { - return false; - } - let old = self.cursor; let limit = MANAGER.layout.folder_height(); - self.cursor = step.add(self.cursor, || limit).min(len - 1); - self.hovered = self.files.duplicate(self.cursor); + self.cursor = step.add(self.cursor, || limit).min(len.saturating_sub(1)); + self.offset = if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) { + len.saturating_sub(limit).min(self.offset + self.cursor - old.0) + } else { + self.offset.min(len.saturating_sub(1)) + }; + self.set_page(false); - - if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) { - self.offset = len.saturating_sub(limit).min(self.offset + self.cursor - old); - } - - old != self.cursor + old != (self.cursor, self.offset) } pub fn prev(&mut self, step: Step) -> bool { - let old = self.cursor; - self.cursor = step.add(self.cursor, || MANAGER.layout.folder_height()); - self.hovered = self.files.duplicate(self.cursor); + let old = (self.cursor, self.offset); + let max = self.files.len().saturating_sub(1); + + self.cursor = step.add(self.cursor, || MANAGER.layout.folder_height()).min(max); + self.offset = if self.cursor < self.offset + 5 { + self.offset.saturating_sub(old.0 - self.cursor) + } else { + self.offset.min(max) + }; + self.set_page(false); - - if self.cursor < self.offset + 5 { - self.offset = self.offset.saturating_sub(old - self.cursor); - } - - old != self.cursor + old != (self.cursor, self.offset) } pub fn hover(&mut self, url: &Url) -> bool { @@ -100,26 +102,14 @@ impl Folder { } #[inline] - pub fn hover_repos(&mut self) -> bool { - self.hover(&self.hovered.as_ref().map(|h| h.url_owned()).unwrap_or_default()) - } - - pub fn hover_force(&mut self, file: File) -> bool { - if self.hover(file.url()) { - return true; - } - - self.hovered = Some(file); - false + pub fn repos(&mut self, url: Option>) -> bool { + if let Some(u) = url { self.hover(u.as_ref()) } else { self.prev(Default::default()) } } } impl Folder { #[inline] - pub fn offset(&self) -> usize { self.offset } - - #[inline] - pub fn cursor(&self) -> usize { self.cursor } + pub fn hovered(&self) -> Option<&File> { self.files.get(self.cursor) } pub fn paginate(&self) -> &[File] { let len = self.files.len(); diff --git a/core/src/manager/manager.rs b/core/src/manager/manager.rs index 32aa91d7..dc17a9ce 100644 --- a/core/src/manager/manager.rs +++ b/core/src/manager/manager.rs @@ -42,10 +42,9 @@ impl Manager { let mut to_watch = BTreeSet::new(); for tab in self.tabs.iter() { to_watch.insert(&tab.current.cwd); - if let Some(ref h) = tab.current.hovered { - if h.is_dir() { - to_watch.insert(h.url()); - } + match tab.current.hovered() { + Some(h) if h.is_dir() => _ = to_watch.insert(h.url()), + _ => {} } if let Some(ref p) = tab.parent { to_watch.insert(&p.cwd); @@ -65,7 +64,7 @@ impl Manager { } if hovered.is_dir() { - let position = self.active().history(url).map(|f| (f.offset(), f.files.len())); + let position = self.active().history(url).map(|f| (f.offset, f.files.len())); self.active_mut().preview.folder(url, position, sequent); return false; } @@ -200,9 +199,11 @@ impl Manager { fs::File::create(&path).await?; } - let child = path.components().take(cwd.components().count() + 1).collect::(); - if let Ok(file) = File::from(Url::from(child)).await { - emit!(Hover(file)); + let child = + Url::from(path.components().take(cwd.components().count() + 1).collect::()); + if let Ok(f) = File::from(child.clone()).await { + emit!(Files(FilesOp::Creating(cwd, f.into_map()))); + emit!(Hover(child)); emit!(Refresh); } Ok::<(), Error>(()) @@ -219,6 +220,21 @@ impl Manager { return false; }; + async fn rename_and_hover(old: Url, new: Url) -> Result<()> { + fs::rename(&old, &new).await?; + if old.parent() != new.parent() { + return Ok(()); + } + + let parent = old.parent_url().unwrap(); + emit!(Files(FilesOp::Deleting(parent, BTreeSet::from([old])))); + + let file = File::from(new.clone()).await?; + emit!(Files(FilesOp::Creating(file.parent().unwrap(), file.into_map()))); + emit!(Hover(new)); + Ok(()) + } + tokio::spawn(async move { let mut result = emit!(Input( InputOpt::hovered("Rename:").with_value(hovered.file_name().unwrap().to_string_lossy()) @@ -230,14 +246,14 @@ impl Manager { let new = hovered.parent().unwrap().join(name); if force || fs::symlink_metadata(&new).await.is_err() { - fs::rename(&hovered, new).await.ok(); + rename_and_hover(hovered, Url::from(new)).await.ok(); return; } let mut result = emit!(Input(InputOpt::hovered("Overwrite an existing file? (y/N)"))); if let Some(Ok(choice)) = result.recv().await { if choice == "y" || choice == "Y" { - fs::rename(&hovered, new).await.ok(); + rename_and_hover(hovered, Url::from(new)).await.ok(); } }; }); @@ -404,11 +420,6 @@ impl Manager { self.mimetype.extend(mimes); true } - - #[inline] - pub fn update_hover(&mut self, file: Option) -> bool { - file.map(|f| self.current_mut().hover_force(f)) == Some(true) - } } impl Manager { @@ -437,7 +448,7 @@ impl Manager { pub fn parent(&self) -> Option<&Folder> { self.tabs.active().parent.as_ref() } #[inline] - pub fn hovered(&self) -> Option<&File> { self.tabs.active().current.hovered.as_ref() } + pub fn hovered(&self) -> Option<&File> { self.tabs.active().current.hovered() } #[inline] pub fn selected(&self) -> Vec<&File> { self.tabs.active().selected() } diff --git a/core/src/manager/tab.rs b/core/src/manager/tab.rs index e0b970d5..ac10be2f 100644 --- a/core/src/manager/tab.rs +++ b/core/src/manager/tab.rs @@ -77,7 +77,7 @@ impl Tab { // Visual selection if let Some((start, items)) = self.mode.visual_mut() { - let after = self.current.cursor(); + let after = self.current.cursor; items.clear(); for i in start.min(after)..=after.max(start) { @@ -97,14 +97,15 @@ impl Tab { let mut hovered = None; if !file.is_dir() { - hovered = Some(file); + hovered = Some(file.url_owned()); target = target.parent_url().unwrap(); + emit!(Files(FilesOp::Creating(target.clone(), file.into_map()))); } // Already in target if self.current.cwd == target { - if hovered.map(|h| self.current.hover_force(h)) == Some(true) { - emit!(Hover); + if let Some(h) = hovered { + emit!(Hover(h)); } return false; } @@ -128,7 +129,7 @@ impl Tab { // Hover the file if let Some(h) = hovered { - self.current.hover_force(h); + emit!(Hover(h)); } // Backstack @@ -153,15 +154,12 @@ impl Tab { } pub fn enter(&mut self) -> bool { - let Some(hovered) = self.current.hovered.clone() else { + let Some(hovered) = self.current.hovered().filter(|h| h.is_dir()).map(|h| h.url_owned()) else { return false; }; - if !hovered.is_dir() { - return false; - } // Current - let rep = self.history_new(hovered.url()); + let rep = self.history_new(&hovered); let rep = mem::replace(&mut self.current, rep); if rep.cwd.is_regular() { self.history.insert(rep.cwd.clone(), rep); @@ -171,10 +169,10 @@ impl Tab { if let Some(rep) = self.parent.take() { self.history.insert(rep.cwd.clone(), rep); } - self.parent = Some(self.history_new(&hovered.parent().unwrap())); + self.parent = Some(self.history_new(&hovered.parent_url().unwrap())); // Backstack - self.backstack.push(hovered.url_owned()); + self.backstack.push(hovered); emit!(Refresh); true @@ -183,8 +181,7 @@ impl Tab { pub fn leave(&mut self) -> bool { let current = self .current - .hovered - .as_ref() + .hovered() .and_then(|h| h.parent()) .filter(|p| *p != self.current.cwd) .or_else(|| self.current.cwd.parent_url()); @@ -230,8 +227,8 @@ impl Tab { } pub fn select(&mut self, state: Option) -> bool { - if let Some(ref hovered) = self.current.hovered { - return self.current.files.select(hovered.url(), state); + if let Some(u) = self.current.hovered().map(|h| h.url_owned()) { + return self.current.files.select(&u, state); } false } @@ -239,7 +236,7 @@ impl Tab { pub fn select_all(&mut self, state: Option) -> bool { self.current.files.select_all(state) } pub fn visual_mode(&mut self, unset: bool) -> bool { - let idx = self.current.cursor(); + let idx = self.current.cursor; if unset { self.mode = Mode::Unset(idx, BTreeSet::from([idx])); @@ -276,9 +273,9 @@ impl Tab { }; let step = if prev { - finder.prev(&self.current.files, self.current.cursor(), true) + finder.prev(&self.current.files, self.current.cursor, true) } else { - finder.next(&self.current.files, self.current.cursor(), true) + finder.next(&self.current.files, self.current.cursor, true) }; if let Some(step) = step { @@ -318,9 +315,9 @@ impl Tab { let b = finder.catchup(&self.current.files); let step = if prev { - finder.prev(&self.current.files, self.current.cursor(), false) + finder.prev(&self.current.files, self.current.cursor, false) } else { - finder.next(&self.current.files, self.current.cursor(), false) + finder.next(&self.current.files, self.current.cursor, false) }; b | step.is_some_and(|s| self.arrow(s.into())) @@ -427,7 +424,7 @@ impl Tab { } pub fn update_peek(&mut self, max: usize, url: Url) -> bool { - let Some(ref hovered) = self.current.hovered else { + let Some(hovered) = self.current.hovered() else { return false; }; @@ -439,7 +436,7 @@ impl Tab { } pub fn update_preview(&mut self, lock: PreviewLock) -> bool { - let Some(hovered) = self.current.hovered.as_ref().map(|h| h.url()) else { + let Some(hovered) = self.current.hovered().map(|h| h.url()) else { return self.preview_reset(); }; @@ -463,7 +460,7 @@ impl Tab { let selected = self.current.files.selected(&pending, self.mode.is_unset()); if selected.is_empty() { - self.current.hovered.as_ref().map(|h| vec![h]).unwrap_or_default() + self.current.hovered().map(|h| vec![h]).unwrap_or_default() } else { selected } @@ -513,26 +510,27 @@ impl Tab { } self.show_hidden = state; - self.apply_files_attrs(false) + if self.apply_files_attrs(false) { + emit!(Peek); + return true; + } + false } - pub fn apply_files_attrs(&mut self, only_hovered: bool) -> bool { + pub fn apply_files_attrs(&mut self, just_preview: bool) -> bool { let mut b = false; - if let Some(f) = self - .current - .hovered - .as_ref() - .filter(|h| h.is_dir()) - .and_then(|h| self.history.get_mut(h.url())) + if let Some(f) = + self.current.hovered().filter(|h| h.is_dir()).and_then(|h| self.history.get_mut(h.url())) { b |= f.files.set_show_hidden(self.show_hidden); b |= f.files.set_sorter(self.sorter); } - if only_hovered { + if just_preview { return b; } + let hovered = self.current.hovered().map(|h| h.url_owned()); b |= self.current.files.set_show_hidden(self.show_hidden); b |= self.current.files.set_sorter(self.sorter); @@ -541,7 +539,7 @@ impl Tab { b |= parent.files.set_sorter(self.sorter); } - self.current.hover_repos(); + self.current.repos(hovered); b } } diff --git a/cspell.json b/cspell.json index 33135cb1..6837af57 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand"],"language":"en","version":"0.2","flagWords":[]} +{"version":"0.2","language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan"]} diff --git a/plugin/src/bindings/active.rs b/plugin/src/bindings/active.rs index 378a7924..7414b767 100644 --- a/plugin/src/bindings/active.rs +++ b/plugin/src/bindings/active.rs @@ -26,8 +26,8 @@ impl<'a, 'b> Active<'a, 'b> { LUA.register_userdata_type::(|reg| { reg.add_field_method_get("cwd", |_, me| Ok(Url::from(&me.cwd))); - reg.add_field_method_get("offset", |_, me| Ok(me.offset())); - reg.add_field_method_get("cursor", |_, me| Ok(me.cursor())); + reg.add_field_method_get("offset", |_, me| Ok(me.offset)); + reg.add_field_method_get("cursor", |_, me| Ok(me.cursor)); reg.add_field_function_get("window", |_, me| me.named_user_value::("window")); reg.add_field_function_get("files", |_, me| me.named_user_value::("files")); @@ -63,7 +63,7 @@ impl<'a, 'b> Active<'a, 'b> { inner: &'a core::manager::Folder, window: Option<(usize, usize)>, ) -> mlua::Result> { - let window = window.unwrap_or_else(|| (inner.offset(), MANAGER.layout.folder_height())); + let window = window.unwrap_or_else(|| (inner.offset, MANAGER.layout.folder_height())); let ud = self.scope.create_any_userdata_ref(inner)?; ud.set_named_user_value( @@ -81,7 +81,7 @@ impl<'a, 'b> Active<'a, 'b> { // TODO: remove this ud.set_named_user_value( "hovered", - inner.hovered.as_ref().and_then(|h| self.file(999, h, inner).ok()), + inner.hovered().and_then(|h| self.file(999, h, inner).ok()), )?; Ok(ud) @@ -116,7 +116,7 @@ impl<'a, 'b> Active<'a, 'b> { .as_ref() .filter(|l| l.is_folder()) .and_then(|l| tab.history(&l.url)) - .and_then(|f| self.folder(f, Some((f.offset(), MANAGER.layout.preview_height()))).ok()), + .and_then(|f| self.folder(f, Some((f.offset, MANAGER.layout.preview_height()))).ok()), )?; Ok(ud) diff --git a/plugin/src/bindings/files.rs b/plugin/src/bindings/files.rs index 9ce23cb4..c81ac3ca 100644 --- a/plugin/src/bindings/files.rs +++ b/plugin/src/bindings/files.rs @@ -104,7 +104,7 @@ impl Files { reg.add_function("is_hovered", |_, me: AnyUserData| { let folder = me.named_user_value::>("folder")?; let file = me.borrow::()?; - Ok(matches!(&folder.hovered, Some(f) if f.url() == file.url())) + Ok(matches!(folder.hovered(), Some(f) if f.url() == file.url())) }); reg.add_function("is_yanked", |_, me: AnyUserData| { @@ -129,7 +129,7 @@ impl Files { selected } else { let idx: usize = me.named_user_value("idx")?; - manager.active().mode.pending(folder.offset() + idx, selected) + manager.active().mode.pending(folder.offset + idx, selected) }) });