lsd/src/display.rs

455 lines
12 KiB
Rust
Raw Normal View History

2019-02-09 10:41:42 +00:00
use crate::color::Colors;
2019-03-08 17:28:35 +00:00
use crate::flags::{Flags, Layout};
2019-02-09 10:41:42 +00:00
use crate::icon::Icons;
use crate::meta::{FileType, Meta};
2019-01-20 10:22:14 +00:00
use ansi_term::{ANSIString, ANSIStrings};
2018-12-02 16:22:51 +00:00
use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
use terminal_size::terminal_size;
use unicode_width::UnicodeWidthStr;
2018-12-02 16:22:51 +00:00
2018-12-13 15:50:47 +00:00
const EDGE: &str = "\u{251c}\u{2500}\u{2500}"; // "├──"
const LINE: &str = "\u{2502} "; // "├ "
const CORNER: &str = "\u{2514}\u{2500}\u{2500}"; // "└──"
2019-01-23 17:02:36 +00:00
const BLANK: &str = " ";
2018-12-04 13:54:56 +00:00
2019-01-20 10:22:14 +00:00
#[derive(Debug, Copy, Clone)]
struct PaddingRules {
user: usize,
group: usize,
size: (usize, usize),
date: usize,
}
pub fn one_line(metas: Vec<Meta>, flags: Flags, colors: &Colors, icons: &Icons) -> String {
inner_display_one_line(metas, flags, colors, icons, 0)
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
pub fn grid(metas: Vec<Meta>, flags: Flags, colors: &Colors, icons: &Icons) -> String {
let term_width = match terminal_size() {
Some((w, _)) => Some(w.0 as usize),
None => None,
2019-01-20 10:22:14 +00:00
};
inner_display_grid(metas, flags, colors, icons, 0, term_width)
}
pub fn tree(metas: Vec<Meta>, flags: Flags, colors: &Colors, icons: &Icons) -> String {
2019-01-23 17:02:36 +00:00
inner_display_tree(metas, flags, colors, icons, 0, "")
2019-01-20 10:22:14 +00:00
}
fn inner_display_one_line(
metas: Vec<Meta>,
flags: Flags,
colors: &Colors,
icons: &Icons,
depth: usize,
) -> String {
let mut output = String::new();
let mut padding_rules = None;
2019-03-08 17:28:35 +00:00
if let Layout::OneLine { long: true } = flags.layout {
2019-01-20 10:22:14 +00:00
// Defining the padding rules is costly and so shouldn't be done several
// times. That's why it's done outside the loop.
padding_rules = Some(PaddingRules {
user: detect_user_length(&metas),
group: detect_group_length(&metas),
size: detect_size_lengths(&metas),
date: detect_date_length(&metas, flags),
})
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
// print the files first.
for meta in &metas {
// The first iteration (depth == 0) correspond to the inputs given by
// the user. If the user enter a folder name it should not print the
// folder meta but its content.
if let (0, FileType::Directory { .. }) = (depth, meta.file_type) {
continue;
}
2019-03-08 17:28:35 +00:00
if let Layout::OneLine { long: true } = flags.layout {
2019-01-20 10:22:14 +00:00
output += &get_long_output(&meta, &colors, &icons, flags, padding_rules.unwrap());
2018-12-02 16:22:51 +00:00
} else {
2019-01-20 10:22:14 +00:00
output += &get_short_output(&meta, &colors, &icons, flags);
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
output.push('\n');
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
let should_display_folder_path = should_display_folder_path(depth, &metas);
2018-12-02 16:22:51 +00:00
2019-01-20 10:22:14 +00:00
// print the folder content
for meta in metas {
if meta.content.is_some() {
if should_display_folder_path {
output += &display_folder_path(&meta);
}
2018-12-02 16:22:51 +00:00
2019-01-20 10:22:14 +00:00
output +=
&inner_display_one_line(meta.content.unwrap(), flags, colors, icons, depth + 1);
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
}
2018-12-02 16:22:51 +00:00
2019-01-20 10:22:14 +00:00
output
}
fn inner_display_grid(
metas: Vec<Meta>,
flags: Flags,
colors: &Colors,
icons: &Icons,
depth: usize,
term_width: Option<usize>,
2019-01-20 10:22:14 +00:00
) -> String {
let mut output = String::new();
let mut grid = Grid::new(GridOptions {
filling: Filling::Spaces(2),
direction: Direction::TopToBottom,
});
// print the files first.
for meta in &metas {
// The first iteration (depth == 0) correspond to the inputs given by
// the user. If the user enter a folder name it should not print the
// folder meta but its content.
if let (0, FileType::Directory { .. }) = (depth, meta.file_type) {
continue;
2018-12-05 03:44:01 +00:00
}
2019-01-20 10:22:14 +00:00
let line_output = get_short_output(&meta, &colors, &icons, flags);
grid.add(Cell {
width: get_visible_width(&line_output),
contents: line_output,
});
}
if let Some(tw) = term_width {
if let Some(gridded_output) = grid.fit_into_width(tw) {
output += &gridded_output.to_string();
} else {
//does not fit into grid, usually because (some) filename(s)
//are longer or almost as long as term_width
//print line by line instead!
output += &grid.fit_into_columns(1).to_string();
}
2019-01-20 10:22:14 +00:00
} else {
output += &grid.fit_into_columns(1).to_string();
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
let should_display_folder_path = should_display_folder_path(depth, &metas);
2019-01-12 15:17:40 +00:00
2019-01-20 10:22:14 +00:00
// print the folder content
for meta in metas {
if meta.content.is_some() {
if should_display_folder_path {
output += &display_folder_path(&meta);
2019-01-12 15:17:40 +00:00
}
2019-01-20 10:22:14 +00:00
output += &inner_display_grid(
meta.content.unwrap(),
flags,
colors,
icons,
depth + 1,
term_width,
);
2019-01-12 15:17:40 +00:00
}
}
2019-01-20 10:22:14 +00:00
output
}
fn inner_display_tree(
metas: Vec<Meta>,
flags: Flags,
colors: &Colors,
icons: &Icons,
depth: usize,
2019-01-23 17:02:36 +00:00
prefix: &str,
2019-01-20 10:22:14 +00:00
) -> String {
let mut output = String::new();
let last_idx = metas.len();
for (idx, meta) in metas.into_iter().enumerate() {
let is_last_folder_elem = idx + 1 != last_idx;
2018-12-04 13:54:56 +00:00
2019-01-23 17:02:36 +00:00
if depth > 0 {
output += prefix;
2018-12-04 13:54:56 +00:00
2019-01-23 17:02:36 +00:00
if is_last_folder_elem {
output += EDGE;
} else {
output += CORNER;
}
output += " ";
2018-12-04 13:54:56 +00:00
}
2019-01-20 10:22:14 +00:00
output += &get_short_output(&meta, &colors, &icons, flags);
output += "\n";
2018-12-04 13:54:56 +00:00
2019-01-20 10:22:14 +00:00
if meta.content.is_some() {
2019-01-23 17:02:36 +00:00
let mut new_prefix = String::from(prefix);
if depth > 0 {
if is_last_folder_elem {
new_prefix += LINE;
} else {
new_prefix += BLANK;
}
}
output += &inner_display_tree(
meta.content.unwrap(),
flags,
colors,
icons,
depth + 1,
&new_prefix,
);
2019-01-20 10:22:14 +00:00
}
2018-12-04 13:54:56 +00:00
}
2019-01-20 10:22:14 +00:00
output
}
fn should_display_folder_path(depth: usize, metas: &[Meta]) -> bool {
if depth > 0 {
true
} else {
let folder_number = metas
.iter()
.filter(|x| match x.file_type {
FileType::Directory { .. } => true,
_ => false,
})
.count();
folder_number > 1 || folder_number < metas.len()
}
}
fn display_folder_path(meta: &Meta) -> String {
let mut output = String::new();
output.push('\n');
output += &meta.path.to_string_lossy();
output += ":\n";
output
}
fn get_short_output(meta: &Meta, colors: &Colors, icons: &Icons, flags: Flags) -> String {
let strings: &[ANSIString] = &[
meta.name.render(colors, icons),
meta.indicator.render(flags),
];
ANSIStrings(strings).to_string()
}
fn get_long_output(
meta: &Meta,
colors: &Colors,
icons: &Icons,
flags: Flags,
padding_rules: PaddingRules,
) -> String {
let strings: &[ANSIString] = &[
meta.file_type.render(colors),
meta.permissions.render(colors),
ANSIString::from(" "),
meta.owner.render_user(colors, padding_rules.user),
ANSIString::from(" "),
meta.owner.render_group(colors, padding_rules.group),
ANSIString::from(" "),
meta.size
.render(colors, padding_rules.size.0, padding_rules.size.1),
ANSIString::from(" "),
meta.date.render(colors, padding_rules.date, flags),
ANSIString::from(" "),
meta.name.render(colors, icons),
meta.indicator.render(flags),
meta.symlink.render(colors),
];
ANSIStrings(strings).to_string()
}
fn get_visible_width(input: &str) -> usize {
let mut nb_invisible_char = 0;
// Search for every instance of "\e[" indicating the start of a color code
// then count the number of charactere between the string and the first 'm'
// charactere indicating the end of the code color.
for (idx, _) in input.match_indices("\u{1b}[") {
for c in input.chars().skip(idx) {
nb_invisible_char += 1;
if c == 'm' {
break;
}
}
2019-01-20 10:22:14 +00:00
}
UnicodeWidthStr::width(input) - nb_invisible_char
}
fn detect_user_length(metas: &[Meta]) -> usize {
let mut max: usize = 0;
for meta in metas {
let user = meta.owner.user();
if user.len() > max {
max = user.len();
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
}
max
}
2018-12-02 17:16:02 +00:00
2019-01-20 10:22:14 +00:00
fn detect_group_length(metas: &[Meta]) -> usize {
let mut max: usize = 0;
for meta in metas {
let group = meta.owner.group();
if group.len() > max {
max = group.len();
}
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
max
}
fn detect_date_length(metas: &[Meta], flags: Flags) -> usize {
let mut max_value_length: usize = 0;
2018-12-02 16:22:51 +00:00
2019-01-20 10:22:14 +00:00
for meta in metas {
if meta.date.date_string(flags).len() > max_value_length {
max_value_length = meta.date.date_string(flags).len();
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
}
max_value_length
}
fn detect_size_lengths(metas: &[Meta]) -> (usize, usize) {
let mut max_value_length: usize = 0;
let mut max_unit_size: usize = 0;
2018-12-02 16:22:51 +00:00
2019-01-20 10:22:14 +00:00
for meta in metas {
if meta.size.render_value().len() > max_value_length {
max_value_length = meta.size.render_value().len();
}
2018-12-02 16:22:51 +00:00
2019-01-20 10:22:14 +00:00
if meta.size.render_unit().len() > max_unit_size {
max_unit_size = meta.size.render_unit().len();
}
2018-12-02 16:22:51 +00:00
}
2019-01-20 10:22:14 +00:00
(max_value_length, max_unit_size)
2018-12-02 16:22:51 +00:00
}
#[cfg(test)]
mod tests {
use super::*;
2019-02-09 10:41:42 +00:00
use crate::color;
use crate::color::Colors;
use crate::icon;
use crate::icon::Icons;
use crate::meta::{FileType, Name};
2018-12-20 02:51:44 +00:00
use std::path::Path;
#[test]
fn test_display_get_visible_width_without_icons() {
for (s, l) in &[
(",!", 22),
("ASCII1234-_", 11),
("制作样本。", 10),
("日本語", 6),
("샘플은 무료로 드리겠습니다", 26),
("👩🐩", 4),
("🔬", 2),
] {
let path = Path::new(s);
let name = Name::new(
&path,
FileType::File {
exec: false,
uid: false,
},
);
2018-12-20 02:51:44 +00:00
let output = name.render(
&Colors::new(color::Theme::NoColor),
&Icons::new(icon::Theme::NoIcon),
2018-12-20 02:51:44 +00:00
);
2019-01-20 10:22:14 +00:00
assert_eq!(get_visible_width(&output), *l);
}
}
#[test]
fn test_display_get_visible_width_with_icons() {
for (s, l) in &[
// Add 3 characters for the icons.
(",!", 25),
("ASCII1234-_", 14),
("制作样本。", 13),
("日本語", 9),
("샘플은 무료로 드리겠습니다", 29),
("👩🐩", 7),
("🔬", 5),
] {
let path = Path::new(s);
let name = Name::new(
&path,
FileType::File {
exec: false,
uid: false,
},
);
let output = name
.render(
&Colors::new(color::Theme::NoColor),
&Icons::new(icon::Theme::Fancy),
)
.to_string();
2019-01-20 10:22:14 +00:00
assert_eq!(get_visible_width(&output), *l);
}
}
#[test]
fn test_display_get_visible_width_with_colors() {
for (s, l) in &[
(",!", 24),
("ASCII1234-_", 11),
("制作样本。", 10),
("日本語", 6),
("샘플은 무료로 드리겠습니다", 26),
("👩🐩", 4),
("🔬", 2),
] {
let path = Path::new(s);
let name = Name::new(
&path,
FileType::File {
exec: false,
uid: false,
},
);
let output = name
.render(
&Colors::new(color::Theme::NoLscolors),
&Icons::new(icon::Theme::NoIcon),
)
.to_string();
// check if the color is present.
assert_eq!(true, output.starts_with("\u{1b}[38;5;"));
assert_eq!(true, output.ends_with("[0m"));
println!("str: {:?}", output);
2019-01-20 10:22:14 +00:00
assert_eq!(get_visible_width(&output), *l);
}
}
}