feat: better file hover state (#269)

This commit is contained in:
三咲雅 · Misaki Masa 2023-10-14 23:12:32 +08:00 committed by GitHub
parent 0b0901823d
commit b840dcccd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 204 additions and 114 deletions

View File

@ -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);

View File

@ -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<UnboundedSender<Event>> = RoCell::new();
@ -26,7 +26,7 @@ pub enum Event {
Files(FilesOp),
Pages(usize),
Mimetype(BTreeMap<Url, String>),
Hover(Option<File>),
Hover(Option<Url>),
Peek(Option<(usize, Url)>),
Preview(PreviewLock),

View File

@ -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<Url, File> {
let mut map = BTreeMap::new();
map.insert(self.url.clone(), self);
map
}
}
impl File {

View File

@ -173,6 +173,84 @@ impl Files {
}
true
}
pub fn update_creating(&mut self, mut todo: BTreeMap<Url, File>) -> 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<Url>) -> 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<Url, File>) -> 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 {

View File

@ -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<File>),
Size(Url, BTreeMap<Url, u64>),
IOErr(Url),
Creating(Url, BTreeMap<Url, File>),
Deleting(Url, BTreeSet<Url>),
Replacing(Url, BTreeMap<Url, File>),
}
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,
}
}

View File

@ -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<File>,
pub page: usize,
}
impl From<Url> 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<impl AsRef<Url>>) -> 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();

View File

@ -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::<PathBuf>();
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::<PathBuf>());
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<File>) -> 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() }

View File

@ -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>) -> 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>) -> 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
}
}

View File

@ -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"]}

View File

@ -26,8 +26,8 @@ impl<'a, 'b> Active<'a, 'b> {
LUA.register_userdata_type::<core::manager::Folder>(|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::<Value>("window"));
reg.add_field_function_get("files", |_, me| me.named_user_value::<AnyUserData>("files"));
@ -63,7 +63,7 @@ impl<'a, 'b> Active<'a, 'b> {
inner: &'a core::manager::Folder,
window: Option<(usize, usize)>,
) -> mlua::Result<AnyUserData<'a>> {
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)

View File

@ -104,7 +104,7 @@ impl Files {
reg.add_function("is_hovered", |_, me: AnyUserData| {
let folder = me.named_user_value::<UserDataRef<core::manager::Folder>>("folder")?;
let file = me.borrow::<core::files::File>()?;
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)
})
});