* 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
191 lines
6.2 KiB
Rust
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,
|
|
}
|
|
}
|
|
}
|