diff --git a/core/src/external/clipboard.rs b/core/src/external/clipboard.rs index 0560d6f00..86e770b5d 100644 --- a/core/src/external/clipboard.rs +++ b/core/src/external/clipboard.rs @@ -86,5 +86,5 @@ pub async fn clipboard_set(s: impl AsRef) -> Result<()> { let result = tokio::task::spawn_blocking(move || set_clipboard(formats::Unicode, s.to_string_lossy())); - Ok(result.await?.map_err(|_| anyhow!("failed to set clipboard"))?) + result.await?.map_err(|_| anyhow!("failed to set clipboard")) } diff --git a/core/src/manager/backstack.rs b/core/src/manager/backstack.rs new file mode 100644 index 000000000..c65375b9b --- /dev/null +++ b/core/src/manager/backstack.rs @@ -0,0 +1,82 @@ +pub struct Backstack { + cursor: usize, + stack: Vec, +} + +impl Backstack { + pub fn new(item: T) -> Self { Self { cursor: 0, stack: vec![item] } } + + pub fn push(&mut self, item: T) { + if self.stack[self.cursor] == item { + return; + } + + self.cursor += 1; + if self.cursor == self.stack.len() { + self.stack.push(item); + } else { + self.stack[self.cursor] = item; + self.stack.truncate(self.cursor + 1); + } + + // Only keep 30 items before the cursor, the cleanup threshold is 60 + if self.stack.len() > 60 { + let start = self.cursor.saturating_sub(30); + self.stack.drain(..start); + self.cursor -= start; + } + } + + #[cfg(test)] + #[inline] + pub fn current(&self) -> &T { &self.stack[self.cursor] } + + pub fn shift_backward(&mut self) -> Option<&T> { + if self.cursor > 0 { + self.cursor -= 1; + Some(&self.stack[self.cursor]) + } else { + None + } + } + + pub fn shift_forward(&mut self) -> Option<&T> { + if self.cursor + 1 == self.stack.len() { + None + } else { + self.cursor += 1; + Some(&self.stack[self.cursor]) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_backstack() { + let mut backstack = Backstack::::new(1); + assert_eq!(backstack.current(), &1); + + backstack.push(2); + backstack.push(3); + assert_eq!(backstack.current(), &3); + + assert_eq!(backstack.shift_backward(), Some(&2)); + assert_eq!(backstack.shift_backward(), Some(&1)); + assert_eq!(backstack.shift_backward(), None); + assert_eq!(backstack.shift_backward(), None); + assert_eq!(backstack.current(), &1); + assert_eq!(backstack.shift_forward(), Some(&2)); + assert_eq!(backstack.shift_forward(), Some(&3)); + assert_eq!(backstack.shift_forward(), None); + + backstack.shift_backward(); + backstack.push(4); + + assert_eq!(backstack.current(), &4); + assert_eq!(backstack.shift_forward(), None); + assert_eq!(backstack.shift_backward(), Some(&2)); + } +} diff --git a/core/src/manager/mod.rs b/core/src/manager/mod.rs index afa145490..76e321e5b 100644 --- a/core/src/manager/mod.rs +++ b/core/src/manager/mod.rs @@ -1,3 +1,4 @@ +mod backstack; mod finder; mod folder; mod manager; @@ -7,6 +8,7 @@ mod tab; mod tabs; mod watcher; +pub use backstack::*; pub use finder::*; pub use folder::*; pub use manager::*; diff --git a/core/src/manager/tab.rs b/core/src/manager/tab.rs index a8a28cebc..d522d9026 100644 --- a/core/src/manager/tab.rs +++ b/core/src/manager/tab.rs @@ -6,7 +6,7 @@ use shared::{Debounce, Defer, InputError, Url}; use tokio::{pin, task::JoinHandle}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; -use super::{Finder, Folder, Mode, Preview, PreviewLock}; +use super::{Backstack, Finder, Folder, Mode, Preview, PreviewLock}; use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, FilesOp, FilesSorter}, input::InputOpt, Event, Step, BLOCKER}; pub struct Tab { @@ -14,8 +14,9 @@ pub struct Tab { pub(super) current: Folder, pub(super) parent: Option, - pub(super) history: BTreeMap, - pub(super) preview: Preview, + pub(super) backstack: Backstack, + pub(super) history: BTreeMap, + pub(super) preview: Preview, finder: Option, search: Option>>, @@ -29,9 +30,10 @@ impl From for Tab { Self { mode: Default::default(), - current: Folder::from(url), + current: Folder::from(url.clone()), parent, + backstack: Backstack::new(url), history: Default::default(), preview: Default::default(), @@ -87,6 +89,7 @@ impl Tab { true } + // TODO: change to sync, and remove `Event::Cd` pub async fn cd(&mut self, mut target: Url) -> bool { let Ok(file) = File::from(target.clone()).await else { return false; @@ -98,6 +101,7 @@ impl Tab { target = target.parent_url().unwrap(); } + // Already in target if self.current.cwd == target { if hovered.map(|h| self.current.hover_force(h)) == Some(true) { emit!(Hover); @@ -105,23 +109,31 @@ impl Tab { return false; } + // Take parent to history if let Some(rep) = self.parent.take() { self.history.insert(rep.cwd.clone(), rep); } + // Current let rep = self.history_new(&target); let rep = mem::replace(&mut self.current, rep); if rep.cwd.is_regular() { self.history.insert(rep.cwd.clone(), rep); } + // Parent if let Some(parent) = target.parent_url() { self.parent = Some(self.history_new(&parent)); } + // Hover the file if let Some(h) = hovered { self.current.hover_force(h); } + + // Backstack + self.backstack.push(target.clone()); + emit!(Refresh); true } @@ -146,17 +158,22 @@ impl Tab { return false; } + // Current let rep = self.history_new(hovered.url()); let rep = mem::replace(&mut self.current, rep); if rep.cwd.is_regular() { self.history.insert(rep.cwd.clone(), rep); } + // Parent if let Some(rep) = self.parent.take() { self.history.insert(rep.cwd.clone(), rep); } self.parent = Some(self.history_new(&hovered.parent().unwrap())); + // Backstack + self.backstack.push(hovered.url_owned()); + emit!(Refresh); true } @@ -174,6 +191,7 @@ impl Tab { return false; }; + // Parent if let Some(rep) = self.parent.take() { self.history.insert(rep.cwd.clone(), rep); } @@ -181,21 +199,33 @@ impl Tab { self.parent = Some(self.history_new(&parent)); } + // Current let rep = self.history_new(¤t); let rep = mem::replace(&mut self.current, rep); if rep.cwd.is_regular() { self.history.insert(rep.cwd.clone(), rep); } + // Backstack + self.backstack.push(current); + emit!(Refresh); true } - // TODO - pub fn back(&mut self) -> bool { false } + pub fn back(&mut self) -> bool { + if let Some(url) = self.backstack.shift_backward().cloned() { + futures::executor::block_on(self.cd(url)); + } + false + } - // TODO - pub fn forward(&mut self) -> bool { false } + pub fn forward(&mut self) -> bool { + if let Some(url) = self.backstack.shift_forward().cloned() { + futures::executor::block_on(self.cd(url)); + } + false + } pub fn select(&mut self, state: Option) -> bool { if let Some(ref hovered) = self.current.hovered { diff --git a/cspell.json b/cspell.json index d6760f994..3b0031aef 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"language":"en","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"],"version":"0.2","flagWords":[]} +{"version":"0.2","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","backstack"],"flagWords":[],"language":"en"}