diff --git a/src/diagram_formatting.rs b/src/diagram_formatting.rs index 53b7e7d..b6de767 100644 --- a/src/diagram_formatting.rs +++ b/src/diagram_formatting.rs @@ -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, // "└── " @@ -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, @@ -18,7 +43,12 @@ pub struct FormattedEntry { pub link: Option, } -fn make_prefix(tree: &FileTree, file: &File, format_history: &HashMap) -> String { +fn make_prefix( + tree: &FileTree, + file: &File, + format_history: &HashMap, + charset: Charset, +) -> String { let mut segments = Vec::new(); let mut current = file; if let Some(ancestor) = tree.get_parent(file) { @@ -42,14 +72,9 @@ fn make_prefix(tree: &FileTree, file: &File, format_history: &HashMap "└── ", - PrefixSegment::ShapeT => "├── ", - PrefixSegment::ShapeI => "│ ", - PrefixSegment::Empty => " ", - } - }) + segments + .iter() + .fold(String::new(), |s, seg| s + seg.as_str(charset)) } fn format_file( @@ -58,8 +83,9 @@ fn format_file( format_history: &mut HashMap, result: &mut Vec, 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 { @@ -91,6 +117,7 @@ fn format_file( format_history, result, make_absolute, + charset, ); } } @@ -100,13 +127,21 @@ pub fn format_paths( root_path: &str, children: Vec<(String, FileType)>, make_absolute: bool, + charset: Charset, ) -> Vec { 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(), @@ -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); @@ -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); + } } diff --git a/src/tre.rs b/src/tre.rs index 6295fd5..f87b142 100644 --- a/src/tre.rs +++ b/src/tre.rs @@ -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 diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 77489f6..b86b22f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -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> { let mut tre = process::Command::cargo_bin("tre")?; @@ -24,15 +35,15 @@ fn respect_git_ignore() -> Result<(), Box> { 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(()) } @@ -56,15 +67,15 @@ fn ignore_hidden() -> Result<(), Box> { 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(()) } @@ -87,15 +98,15 @@ fn all_files() -> Result<(), Box> { 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(()) }