Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 118 additions & 20 deletions src/diagram_formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ use super::file_tree::{File, FileTree, FileType};
use std::collections::HashMap;
use std::fs;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Charset {
Unicode,
Ascii,
}

#[derive(Debug, Clone, PartialEq)]
enum PrefixSegment {
ShapeL, // "└── "
Expand All @@ -10,6 +16,25 @@ enum PrefixSegment {
Empty, // " "
}

impl PrefixSegment {
fn as_str(&self, charset: Charset) -> &'static str {
match charset {
Charset::Unicode => match self {
PrefixSegment::ShapeL => "└── ",
PrefixSegment::ShapeT => "├── ",
PrefixSegment::ShapeI => "│ ",
PrefixSegment::Empty => " ",
},
Charset::Ascii => match self {
PrefixSegment::ShapeL => "\\-- ",
PrefixSegment::ShapeT => "|-- ",
PrefixSegment::ShapeI => "| ",
PrefixSegment::Empty => " ",
},
}
}
}

#[derive(Debug, Clone, PartialEq)]
pub struct FormattedEntry {
pub name: String,
Expand All @@ -18,7 +43,12 @@ pub struct FormattedEntry {
pub link: Option<String>,
}

fn make_prefix(tree: &FileTree, file: &File, format_history: &HashMap<usize, usize>) -> String {
fn make_prefix(
tree: &FileTree,
file: &File,
format_history: &HashMap<usize, usize>,
charset: Charset,
) -> String {
let mut segments = Vec::new();
let mut current = file;
if let Some(ancestor) = tree.get_parent(file) {
Expand All @@ -42,14 +72,9 @@ fn make_prefix(tree: &FileTree, file: &File, format_history: &HashMap<usize, usi
}

segments.reverse();
segments.iter().fold(String::new(), |s, seg| {
s + match seg {
PrefixSegment::ShapeL => "└── ",
PrefixSegment::ShapeT => "├── ",
PrefixSegment::ShapeI => "│ ",
PrefixSegment::Empty => " ",
}
})
segments
.iter()
.fold(String::new(), |s, seg| s + seg.as_str(charset))
}

fn format_file(
Expand All @@ -58,8 +83,9 @@ fn format_file(
format_history: &mut HashMap<usize, usize>,
result: &mut Vec<FormattedEntry>,
make_absolute: bool,
charset: Charset,
) {
let prefix = make_prefix(tree, file, format_history);
let prefix = make_prefix(tree, file, format_history, charset);
let path = if make_absolute {
fs::canonicalize(&file.path).unwrap().display().to_string()
} else {
Expand Down Expand Up @@ -91,6 +117,7 @@ fn format_file(
format_history,
result,
make_absolute,
charset,
);
}
}
Expand All @@ -100,13 +127,21 @@ pub fn format_paths(
root_path: &str,
children: Vec<(String, FileType)>,
make_absolute: bool,
charset: Charset,
) -> Vec<FormattedEntry> {
let mut history = HashMap::new();
let mut result = Vec::new();
match FileTree::new(root_path, children) {
Some(tree) => {
let root = tree.get_root();
format_file(&tree, root, &mut history, &mut result, make_absolute);
format_file(
&tree,
root,
&mut history,
&mut result,
make_absolute,
charset,
);
result
}
None => Vec::new(),
Expand All @@ -115,20 +150,20 @@ pub fn format_paths(

#[cfg(test)]
mod test {
use super::FormattedEntry;
use super::{Charset, FormattedEntry};
use crate::file_tree::FileType;
use std::path;

fn test_input() -> Vec<(String, FileType)> {
vec![
("a".to_string(), FileType::File),
(format!("b{}c", path::MAIN_SEPARATOR), FileType::File),
]
}

#[test]
fn formatting_works() {
let formatted = super::format_paths(
".",
vec![
("a".to_string(), FileType::File),
(format!("b{}c", path::MAIN_SEPARATOR), FileType::File),
],
false,
);
let formatted = super::format_paths(".", test_input(), false, Charset::Unicode);

let bc_path = format!("b{}c", path::MAIN_SEPARATOR);
let b_path = format!(".{}b", path::MAIN_SEPARATOR);
Expand Down Expand Up @@ -188,4 +223,67 @@ mod test {

assert!(formatted == variant0 || formatted == variant1);
}

#[test]
fn formatting_ascii() {
let formatted = super::format_paths(".", test_input(), false, Charset::Ascii);

let bc_path = format!("b{}c", path::MAIN_SEPARATOR);
let b_path = format!(".{}b", path::MAIN_SEPARATOR);
let variant0 = vec![
FormattedEntry {
name: ".".to_string(),
path: ".".to_string(),
prefix: String::new(),
link: None,
},
FormattedEntry {
name: "a".to_string(),
path: "a".to_string(),
prefix: "|-- ".to_string(),
link: None,
},
FormattedEntry {
name: "b".to_string(),
path: b_path.clone(),
prefix: "`-- ".to_string(),
link: None,
},
FormattedEntry {
name: "c".to_string(),
path: bc_path.clone(),
prefix: " `-- ".to_string(),
link: None,
},
];

let variant1 = vec![
FormattedEntry {
name: ".".to_string(),
path: ".".to_string(),
prefix: String::new(),
link: None,
},
FormattedEntry {
name: "b".to_string(),
path: b_path.clone(),
prefix: "|-- ".to_string(),
link: None,
},
FormattedEntry {
name: "c".to_string(),
path: bc_path.clone(),
prefix: "| `-- ".to_string(),
link: None,
},
FormattedEntry {
name: "a".to_string(),
path: "a".to_string(),
prefix: "`-- ".to_string(),
link: None,
},
];

assert!(formatted == variant0 || formatted == variant1);
}
}
10 changes: 8 additions & 2 deletions src/tre.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,20 @@ pub fn run(option: RunOptions) {
if option.output_json {
println!("{}", json_formatting::format_paths(&option.root, paths));
} else {
let is_tty = atty::is(atty::Stream::Stdout);
let charset = if cfg!(windows) && !is_tty {
diagram_formatting::Charset::Ascii
} else {
diagram_formatting::Charset::Unicode
};
let format_result =
diagram_formatting::format_paths(&option.root, paths, option.portable_aliases);
diagram_formatting::format_paths(&option.root, paths, option.portable_aliases, charset);
let lscolors = LsColors::from_env().unwrap_or_default();
let coloring = match option.coloring {
cli::Coloring::Never => None,
cli::Coloring::Always => Some(&lscolors),
cli::Coloring::Automatic => {
if atty::is(atty::Stream::Stdout) {
if is_tty {
Some(&lscolors)
} else {
None
Expand Down
65 changes: 38 additions & 27 deletions tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ use std::str;
use assert_cmd::prelude::CommandCargoExt;
use std::env;

// on Windows, piped output uses ascii for formatting instead of unicode.
// this function makes the tests check for ascii formatting on Windows and
// unicode formatting on other platforms
fn has_entry(text: &str, name: &str) -> bool {
if cfg!(windows) {
text.contains(&format!("-- {}", name))
} else {
text.contains(&format!("── {}", name))
}
}

#[test]
fn respect_git_ignore() -> Result<(), Box<dyn error::Error>> {
let mut tre = process::Command::cargo_bin("tre")?;
Expand All @@ -24,15 +35,15 @@ fn respect_git_ignore() -> Result<(), Box<dyn error::Error>> {
let output = tre.output()?.stdout;
let text = str::from_utf8(&output)?;
assert!(text.contains("."));
assert!(text.contains("── .gitignore"));
assert!(text.contains("── a"));
assert!(text.contains("── b"));
assert!(text.contains("── c"));
assert!(text.contains("── d"));
assert!(text.contains("── e"));
assert!(text.contains("── h"));
assert!(text.contains("── f"));
assert!(text.contains("── g"));
assert!(has_entry(text, ".gitignore"));
assert!(has_entry(text, "a"));
assert!(has_entry(text, "b"));
assert!(has_entry(text, "c"));
assert!(has_entry(text, "d"));
assert!(has_entry(text, "e"));
assert!(has_entry(text, "h"));
assert!(has_entry(text, "f"));
assert!(has_entry(text, "g"));
assert!(!text.contains("ignore_me"));
Ok(())
}
Expand All @@ -56,15 +67,15 @@ fn ignore_hidden() -> Result<(), Box<dyn error::Error>> {
let output = tre.arg("-s").output()?.stdout;
let text = str::from_utf8(&output)?;
assert!(text.contains("."));
assert!(!text.contains("── .gitignore")); // hidden files should be hidden
assert!(text.contains("── a"));
assert!(text.contains("── b"));
assert!(text.contains("── c"));
assert!(text.contains("── d"));
assert!(text.contains("── e"));
assert!(text.contains("── h"));
assert!(text.contains("── f"));
assert!(text.contains("── g"));
assert!(!has_entry(text, ".gitignore")); // hidden files should be hidden
assert!(has_entry(text, "a"));
assert!(has_entry(text, "b"));
assert!(has_entry(text, "c"));
assert!(has_entry(text, "d"));
assert!(has_entry(text, "e"));
assert!(has_entry(text, "h"));
assert!(has_entry(text, "f"));
assert!(has_entry(text, "g"));
assert!(text.contains("ignore_me"));
Ok(())
}
Expand All @@ -87,15 +98,15 @@ fn all_files() -> Result<(), Box<dyn error::Error>> {
let output = tre.arg("-a").output()?.stdout;
let text = str::from_utf8(&output)?;
assert!(text.contains("."));
assert!(text.contains("── .gitignore")); // hidden files should be hidden
assert!(text.contains("── a"));
assert!(text.contains("── b"));
assert!(text.contains("── c"));
assert!(text.contains("── d"));
assert!(text.contains("── e"));
assert!(text.contains("── h"));
assert!(text.contains("── f"));
assert!(text.contains("── g"));
assert!(has_entry(text, ".gitignore"));
assert!(has_entry(text, "a"));
assert!(has_entry(text, "b"));
assert!(has_entry(text, "c"));
assert!(has_entry(text, "d"));
assert!(has_entry(text, "e"));
assert!(has_entry(text, "h"));
assert!(has_entry(text, "f"));
assert!(has_entry(text, "g"));
assert!(text.contains("ignore_me"));
Ok(())
}