diff options
author | DLFW <daniel@llin.info> | 2021-06-20 01:14:10 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-19 19:14:10 -0400 |
commit | e6359a599e804031189eb9bc067c4eff9b01a1a8 (patch) | |
tree | f94102b28145f91db5b0bded5e38869157c1946f /src/ui/widgets/tui_dirlist_detailed.rs | |
parent | 362a507ad60ee479fb68a48fc7daf4a49652f1bf (diff) |
Show symlinked files and dirs correctly (#72)
* Treat symlinks as normal files and dirs
* Dirs are recognized as dirs no matter is they are a symlink
(correct devicon)
* Permission flags shown in the footer are the permissions of the target
in case a symlink is selected
* Size of a symlinked file is the size of the target
* File display to be fixed: symlink arrow now just overrides the file
size, but both need to be shown one after the other
* Correctly show symlink arrow and file size next
This commit includes quite some refactoring. The shortening of the
left and right part of an entry in a dir list and the shortening of the
file name are separated into functions which don't directly write to the
buffer but just return strings. That way, they get testable and further
enhancements like different line modes should be easier to implement.
* fix review findings
* better file name truncation
File names are only truncated at grapheme border, while the width is
calculated on the actual nuber of terminal cells.
* more adequate test organization
* more code structure
Diffstat (limited to 'src/ui/widgets/tui_dirlist_detailed.rs')
-rw-r--r-- | src/ui/widgets/tui_dirlist_detailed.rs | 281 |
1 files changed, 182 insertions, 99 deletions
diff --git a/src/ui/widgets/tui_dirlist_detailed.rs b/src/ui/widgets/tui_dirlist_detailed.rs index 2efae8b..796ead4 100644 --- a/src/ui/widgets/tui_dirlist_detailed.rs +++ b/src/ui/widgets/tui_dirlist_detailed.rs @@ -2,14 +2,14 @@ use tui::buffer::Buffer; use tui::layout::Rect; use tui::style::{Color, Modifier, Style}; use tui::widgets::Widget; -use unicode_width::UnicodeWidthStr; -use crate::fs::{FileType, JoshutoDirEntry, JoshutoDirList}; +use crate::fs::{FileType, JoshutoDirEntry, JoshutoDirList, LinkType}; use crate::util::format; +use crate::util::string::UnicodeTruncate; use crate::util::style; +use unicode_width::UnicodeWidthStr; -const FILE_SIZE_WIDTH: usize = 8; -const SYMLINK_WIDTH: usize = 4; +const MIN_LEFT_LABEL_WIDTH: i32 = 15; const ELLIPSIS: &str = "…"; @@ -80,113 +80,196 @@ fn print_entry( (x, y): (u16, u16), drawing_width: usize, ) { - let name = entry.label(); - let name_width = name.width(); - - match entry.metadata.file_type() { - FileType::Directory => { - buf.set_stringn(x, y, name, drawing_width, style); - if name_width > drawing_width { - buf.set_string(x + drawing_width as u16 - 1, y, ELLIPSIS, style); - } - } - FileType::Symlink(_) => { - if drawing_width < SYMLINK_WIDTH { - return; - } - let file_drawing_width = drawing_width - SYMLINK_WIDTH; - - print_file_name(buf, (x, y), name, style, file_drawing_width); + let size_string = match entry.metadata.file_type() { + FileType::Directory => String::from(""), + FileType::File => format::file_size_to_string(entry.metadata.len()), + }; + let symlink_string = match entry.metadata.link_type() { + LinkType::Normal => "", + LinkType::Symlink(_) => "-> ", + }; + let left_label_original = entry.label(); + let right_label_original = format!(" {}{}", symlink_string, size_string); + let (left_label, right_label) = + factor_labels_for_entry(left_label_original, right_label_original, drawing_width); + let right_width = right_label.width(); + buf.set_stringn(x, y, left_label, drawing_width, style); + buf.set_stringn( + x + drawing_width as u16 - right_width as u16, + y, + right_label, + drawing_width, + style, + ); +} - // print arrow - buf.set_string(x + file_drawing_width as u16, y, "->", style); - if name_width >= file_drawing_width { - buf.set_string(x + file_drawing_width as u16 - 1, y, ELLIPSIS, style); - } - } - FileType::File => { - if drawing_width < FILE_SIZE_WIDTH { - return; - } - let file_drawing_width = drawing_width - FILE_SIZE_WIDTH; - - print_file_name(buf, (x, y), name, style, file_drawing_width); - - // print file size - let file_size_string = format::file_size_to_string(entry.metadata.len()); - buf.set_string(x + file_drawing_width as u16, y, " ", style); - buf.set_string( - x + file_drawing_width as u16 + 1, - y, - file_size_string, - style, - ); +fn factor_labels_for_entry( + left_label_original: &str, + right_label_original: String, + drawing_width: usize, +) -> (String, String) { + let left_width_remainder = drawing_width as i32 - right_label_original.width() as i32; + let width_remainder = left_width_remainder as i32 - left_label_original.width() as i32; + if width_remainder >= 0 { + ( + left_label_original.to_string(), + right_label_original.to_string(), + ) + } else { + if left_label_original.width() <= drawing_width { + (left_label_original.to_string(), "".to_string()) + } else if left_width_remainder < MIN_LEFT_LABEL_WIDTH { + ( + trim_file_label(left_label_original, drawing_width), + "".to_string(), + ) + } else { + ( + trim_file_label(left_label_original, left_width_remainder as usize), + right_label_original.to_string(), + ) } } } -pub fn print_file_name( - buf: &mut Buffer, - (x, y): (u16, u16), - name: &str, - style: Style, - drawing_width: usize, -) { +pub fn trim_file_label(name: &str, drawing_width: usize) -> String { + // pre-condition: string name is longer than width let (stem, extension) = match name.rfind('.') { None => (name, ""), Some(i) => name.split_at(i), }; - if stem.is_empty() { + if drawing_width < 1 { + String::from("") + } else if stem.is_empty() || extension.is_empty() { + let full = format!("{}{}", stem, extension); + let mut truncated = full.trunc(drawing_width - 1); + truncated.push_str(ELLIPSIS); + truncated + } else { let ext_width = extension.width(); - buf.set_stringn(x, y, extension, drawing_width, style); if ext_width > drawing_width { - buf.set_string(x + drawing_width as u16 - 1, y, ELLIPSIS, style); - } - } else if extension.is_empty() { - let stem_width = stem.width(); - buf.set_stringn(x, y, stem, drawing_width, style); - if stem_width > drawing_width { - buf.set_string(x + drawing_width as u16 - 1, y, ELLIPSIS, style); - } - } else { - if stem.is_empty() { - let ext_width = extension.width(); - buf.set_stringn(x, y, extension, drawing_width, style); - if ext_width > drawing_width { - buf.set_string(x + drawing_width as u16 - 1, y, ELLIPSIS, style); - } - } else if extension.is_empty() { - let stem_width = stem.width(); - buf.set_stringn(x, y, stem, drawing_width, style); - if stem_width > drawing_width { - buf.set_string(x + drawing_width as u16 - 1, y, ELLIPSIS, style); - } + // file ext does not fit + ELLIPSIS.to_string() + } else if ext_width == drawing_width { + extension.to_string().replacen('.', ELLIPSIS, 1) } else { - let stem_width = stem.width(); - let ext_width = extension.width(); - if stem_width + ext_width <= drawing_width { - // file stem and extension fits - buf.set_stringn(x, y, stem, drawing_width, style); - buf.set_string(x + stem_width as u16, y, extension, style); - } else if ext_width > drawing_width { - // file ext does not fit - buf.set_string(x, y, ELLIPSIS, style); - } else { - buf.set_stringn(x, y, stem, drawing_width, style); - // file ext fits, but file stem does not - let ext_start_idx = if drawing_width < ext_width { - 0 - } else { - (drawing_width - ext_width) as u16 - }; - buf.set_string(x + ext_start_idx, y, extension, style); - let ext_start_idx = if ext_start_idx > 0 { - ext_start_idx - 1 - } else { - 0 - }; - buf.set_string(x + ext_start_idx, y, ELLIPSIS, style); - } + let stem_width = drawing_width - ext_width; + let truncated_stem = stem.trunc(stem_width - 1); + format!("{}{}{}", truncated_stem, ELLIPSIS, extension) } } } + +#[cfg(test)] +mod test_factor_labels { + use super::{factor_labels_for_entry, MIN_LEFT_LABEL_WIDTH}; + + #[test] + fn both_labels_empty_if_drawing_width_zero() { + let left = "foo.ext"; + let right = "right"; + assert_eq!( + ("".to_string(), "".to_string()), + factor_labels_for_entry(left, right.to_string(), 0) + ); + } + + #[test] + fn nothing_changes_if_all_labels_fit_easily() { + let left = "foo.ext"; + let right = "right"; + assert_eq!( + (left.to_string(), right.to_string()), + factor_labels_for_entry(left, right.to_string(), 20) + ); + } + + #[test] + fn nothing_changes_if_all_labels_just_fit() { + let left = "foo.ext"; + let right = "right"; + assert_eq!( + (left.to_string(), right.to_string()), + factor_labels_for_entry(left, right.to_string(), 12) + ); + } + + #[test] + fn right_label_omitted_if_left_label_would_need_to_be_shortened_below_min_left_label_width() { + let left = "foobarbazfo.ext"; + let right = "right"; + assert!(left.chars().count() as i32 == MIN_LEFT_LABEL_WIDTH); + assert_eq!( + (left.to_string(), "".to_string()), + factor_labels_for_entry(left, right.to_string(), MIN_LEFT_LABEL_WIDTH as usize) + ); + } + + #[test] + fn right_label_is_kept_if_left_label_is_not_shortened_below_min_left_label_width() { + let left = "foobarbazfoobarbaz.ext"; + let right = "right"; + assert!(left.chars().count() as i32 > MIN_LEFT_LABEL_WIDTH + right.chars().count() as i32); + assert_eq!( + ("foobarbazf….ext".to_string(), right.to_string()), + factor_labels_for_entry( + left, + right.to_string(), + MIN_LEFT_LABEL_WIDTH as usize + right.chars().count() + ) + ); + } +} + +#[cfg(test)] +mod test_trim_file_label { + use super::trim_file_label; + + #[test] + fn dotfiles_get_an_ellipsis_at_the_end_if_they_dont_fit() { + let label = ".joshuto"; + assert_eq!(".jos…".to_string(), trim_file_label(label, 5)); + } + + #[test] + fn dotless_files_get_an_ellipsis_at_the_end_if_they_dont_fit() { + let label = "Desktop"; + assert_eq!("Desk…".to_string(), trim_file_label(label, 5)); + } + + #[test] + fn if_the_extension_doesnt_fit_just_an_ellipses_is_shown() { + let label = "foo.ext"; + assert_eq!("…".to_string(), trim_file_label(label, 2)); + } + + #[test] + fn if_just_the_extension_fits_its_shown_with_an_ellipsis_instead_of_a_dot() { + let left = "foo.ext"; + assert_eq!("…ext".to_string(), trim_file_label(left, 4)); + } + + #[test] + fn if_the_extension_fits_the_stem_is_truncated_with_an_appended_ellipsis_1() { + let left = "foo.ext"; + assert_eq!("….ext".to_string(), trim_file_label(left, 5)); + } + + #[test] + fn if_the_extension_fits_the_stem_is_truncated_with_an_appended_ellipsis_2() { + let left = "foo.ext"; + assert_eq!("f….ext".to_string(), trim_file_label(left, 6)); + } + + #[test] + fn if_the_name_is_truncated_after_a_full_width_character_the_ellipsis_is_shown_correctly() { + let left = "🌕🌕🌕"; + assert_eq!("🌕…".to_string(), trim_file_label(left, 4)); + } + + #[test] + fn if_the_name_is_truncated_within_a_full_width_character_the_ellipsis_is_shown_correctly() { + let left = "🌕🌕🌕"; + assert_eq!("🌕🌕…".to_string(), trim_file_label(left, 5)); + } +} |