zellij/default-plugins/strider/src/file_list_view.rs
Aram Drevekenin ee16a4b8c3
feat(plugins): session manager cwd and new filepicker (#3200)
* prototype

* folder selection ui in session manager

* overhaul strider

* scan folder host command

* get strider to work from the cli and some cli pipe fixes

* some ux improvements to strider

* improve strider's ui

* make strider ui responsive

* make session-manager new ui parts responsive

* fix tests

* style(fmt): rustfmt
2024-03-18 09:19:58 +01:00

191 lines
6.2 KiB
Rust

use crate::shared::{calculate_list_bounds, render_list_tip};
use crate::state::{refresh_directory, ROOT};
use pretty_bytes::converter::convert as pretty_bytes;
use std::collections::HashMap;
use std::path::PathBuf;
use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*;
#[derive(Debug, Clone)]
pub struct FileListView {
pub path: PathBuf,
pub path_is_dir: bool,
pub files: Vec<FsEntry>,
pub cursor_hist: HashMap<PathBuf, usize>,
}
impl Default for FileListView {
fn default() -> Self {
FileListView {
path_is_dir: true,
path: Default::default(),
files: Default::default(),
cursor_hist: Default::default(),
}
}
}
impl FileListView {
pub fn descend_to_previous_path(&mut self) {
self.path.pop();
self.path_is_dir = true;
self.files.clear();
self.reset_selected();
refresh_directory(&self.path);
}
pub fn descend_to_root_path(&mut self) {
self.path.clear();
self.path_is_dir = true;
self.files.clear();
self.reset_selected();
refresh_directory(&self.path);
}
pub fn enter_dir(&mut self, entry: &FsEntry) {
let is_dir = entry.is_folder();
let path = entry.get_pathbuf_without_root_prefix();
self.path = path;
self.path_is_dir = is_dir;
self.files.clear();
self.reset_selected();
}
pub fn reset_selected(&mut self) {
*self.selected_mut() = self.selected().unwrap_or(0);
}
pub fn update_files(
&mut self,
paths: Vec<(PathBuf, Option<FileMetadata>)>,
hide_hidden_files: bool,
) {
let mut files = vec![];
for (entry, entry_metadata) in paths {
if entry_metadata.map(|e| e.is_symlink).unwrap_or(false) {
continue; // ignore symlinks
}
let entry = if entry_metadata.map(|e| e.is_dir).unwrap_or(false) {
FsEntry::Dir(entry)
} else {
let size = entry_metadata.map(|e| e.len).unwrap_or(0);
FsEntry::File(entry, size)
};
if !entry.is_hidden_file() || !hide_hidden_files {
files.push(entry);
}
}
self.files = files;
self.files.sort_unstable();
}
pub fn get_selected_entry(&self) -> Option<FsEntry> {
self.selected().and_then(|f| self.files.get(f).cloned())
}
pub fn selected_mut(&mut self) -> &mut usize {
self.cursor_hist.entry(self.path.clone()).or_default()
}
pub fn selected(&self) -> Option<usize> {
self.cursor_hist.get(&self.path).copied()
}
pub fn move_selection_up(&mut self) {
if let Some(selected) = self.selected() {
*self.selected_mut() = selected.saturating_sub(1);
}
}
pub fn move_selection_down(&mut self) {
if let Some(selected) = self.selected() {
let next = selected.saturating_add(1);
*self.selected_mut() = std::cmp::min(self.files.len().saturating_sub(1), next);
} else {
*self.selected_mut() = 0;
}
}
pub fn render(&mut self, rows: usize, cols: usize) {
let (start_index, selected_index_in_range, end_index) =
calculate_list_bounds(self.files.len(), rows.saturating_sub(1), self.selected());
render_list_tip(3, cols);
for i in start_index..end_index {
if let Some(entry) = self.files.get(i) {
let is_selected = Some(i) == selected_index_in_range;
let mut file_or_folder_name = entry.name();
let size = entry
.size()
.map(|s| pretty_bytes(s as f64))
.unwrap_or("".to_owned());
if entry.is_folder() {
file_or_folder_name.push('/');
}
let file_or_folder_name_width = file_or_folder_name.width();
let size_width = size.width();
let text = if file_or_folder_name_width + size_width < cols {
let padding = " ".repeat(
cols.saturating_sub(file_or_folder_name_width)
.saturating_sub(size_width),
);
format!("{}{}{}", file_or_folder_name, padding, size)
} else {
// drop the size, no room for it
let padding = " ".repeat(cols.saturating_sub(file_or_folder_name_width));
format!("{}{}", file_or_folder_name, padding)
};
let mut text_element = if is_selected {
Text::new(text).selected()
} else {
Text::new(text)
};
if entry.is_folder() {
text_element = text_element.color_range(0, ..);
}
print_text_with_coordinates(
text_element,
0,
4 + i.saturating_sub(start_index),
Some(cols),
None,
);
}
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
pub enum FsEntry {
Dir(PathBuf),
File(PathBuf, u64),
}
impl FsEntry {
pub fn name(&self) -> String {
let path = match self {
FsEntry::Dir(p) => p,
FsEntry::File(p, _) => p,
};
path.file_name().unwrap().to_string_lossy().into_owned()
}
pub fn size(&self) -> Option<u64> {
match self {
FsEntry::Dir(p) => None,
FsEntry::File(_, size) => Some(*size),
}
}
pub fn get_pathbuf_without_root_prefix(&self) -> PathBuf {
match self {
FsEntry::Dir(p) => p
.strip_prefix(ROOT)
.map(|p| p.to_path_buf())
.unwrap_or_else(|_| p.clone()),
FsEntry::File(p, _) => p
.strip_prefix(ROOT)
.map(|p| p.to_path_buf())
.unwrap_or_else(|_| p.clone()),
}
}
pub fn is_hidden_file(&self) -> bool {
self.name().starts_with('.')
}
pub fn is_folder(&self) -> bool {
match self {
FsEntry::Dir(_) => true,
_ => false,
}
}
}