Skip to content

Commit 3ebc225

Browse files
committed
Merge branch 'awkward-file-names'
Fixes #156.
2 parents 6b008a6 + 4249cf0 commit 3ebc225

File tree

10 files changed

+127
-24
lines changed

10 files changed

+127
-24
lines changed

Vagrantfile

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,39 @@ Vagrant.configure(2) do |config|
142142
EOF
143143

144144

145+
# File name testcases.
146+
# bash really doesn’t want you to create a file with escaped characters
147+
# in its name, so we have to resort to the echo builtin and touch!
148+
#
149+
# The double backslashes are not strictly necessary; without them, Ruby
150+
# will interpolate them instead of bash, but because Vagrant prints out
151+
# each command it runs, your *own* terminal will go “ding” from the alarm!
152+
config.vm.provision :shell, privileged: false, inline: <<-EOF
153+
set -xe
154+
mkdir "#{test_dir}/file-names"
155+
156+
echo -ne "#{test_dir}/file-names/ascii: hello" | xargs -0 touch
157+
echo -ne "#{test_dir}/file-names/emoji: [🆒]" | xargs -0 touch
158+
echo -ne "#{test_dir}/file-names/utf-8: pâté" | xargs -0 touch
159+
160+
echo -ne "#{test_dir}/file-names/bell: [\\a]" | xargs -0 touch
161+
echo -ne "#{test_dir}/file-names/backspace: [\\b]" | xargs -0 touch
162+
echo -ne "#{test_dir}/file-names/form-feed: [\\f]" | xargs -0 touch
163+
echo -ne "#{test_dir}/file-names/new-line: [\\n]" | xargs -0 touch
164+
echo -ne "#{test_dir}/file-names/return: [\\r]" | xargs -0 touch
165+
echo -ne "#{test_dir}/file-names/tab: [\\t]" | xargs -0 touch
166+
echo -ne "#{test_dir}/file-names/vertical-tab: [\\v]" | xargs -0 touch
167+
168+
echo -ne "#{test_dir}/file-names/escape: [\\033]" | xargs -0 touch
169+
echo -ne "#{test_dir}/file-names/ansi: [\\033[34mblue\\033[0m]" | xargs -0 touch
170+
171+
echo -ne "#{test_dir}/file-names/invalid-utf8-1: [\\xFF]" | xargs -0 touch
172+
echo -ne "#{test_dir}/file-names/invalid-utf8-2: [\\xc3\\x28]" | xargs -0 touch
173+
echo -ne "#{test_dir}/file-names/invalid-utf8-3: [\\xe2\\x82\\x28]" | xargs -0 touch
174+
echo -ne "#{test_dir}/file-names/invalid-utf8-4: [\\xf0\\x28\\x8c\\x28]" | xargs -0 touch
175+
EOF
176+
177+
145178
# Special file testcases.
146179
config.vm.provision :shell, privileged: false, inline: <<-EOF
147180
set -xe

src/output/cell.rs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ use std::ops::{Add, Deref, DerefMut};
55
use ansi_term::{Style, ANSIString, ANSIStrings};
66
use unicode_width::UnicodeWidthStr;
77

8-
use fs::File;
9-
108

119
/// An individual cell that holds text in a table, used in the details and
1210
/// lines views to store ANSI-terminal-formatted data before it is printed.
@@ -161,6 +159,11 @@ impl TextCellContents {
161159
pub fn strings(&self) -> ANSIStrings {
162160
ANSIStrings(&self.0)
163161
}
162+
163+
pub fn width(&self) -> DisplayWidth {
164+
let foo = self.0.iter().map(|anstr| anstr.chars().count()).sum();
165+
DisplayWidth(foo)
166+
}
164167
}
165168

166169

@@ -180,19 +183,6 @@ impl TextCellContents {
180183
#[derive(PartialEq, Debug, Clone, Copy, Default)]
181184
pub struct DisplayWidth(usize);
182185

183-
impl DisplayWidth {
184-
pub fn from_file(file: &File, classify: bool) -> DisplayWidth {
185-
let name_width = *DisplayWidth::from(&*file.name);
186-
if classify {
187-
if file.is_executable_file() || file.is_directory() ||
188-
file.is_pipe() || file.is_link() || file.is_socket() {
189-
return DisplayWidth(name_width + 1);
190-
}
191-
}
192-
DisplayWidth(name_width)
193-
}
194-
}
195-
196186
impl<'a> From<&'a str> for DisplayWidth {
197187
fn from(input: &'a str) -> DisplayWidth {
198188
DisplayWidth(UnicodeWidthStr::width(input))

src/output/colours.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub struct Colours {
2222
pub symlink_path: Style,
2323
pub broken_arrow: Style,
2424
pub broken_filename: Style,
25+
pub control_char: Style,
2526
}
2627

2728
#[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -170,7 +171,8 @@ impl Colours {
170171

171172
symlink_path: Cyan.normal(),
172173
broken_arrow: Red.normal(),
173-
broken_filename: Red.underline()
174+
broken_filename: Red.underline(),
175+
control_char: Red.normal(),
174176
}
175177
}
176178

src/output/details.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,9 @@ impl Details {
306306
for (index, egg) in file_eggs.into_iter().enumerate() {
307307
let mut files = Vec::new();
308308
let mut errors = egg.errors;
309-
let mut width = DisplayWidth::from_file(&egg.file, self.classify);
309+
310+
let filename = filename(&egg.file, &self.colours, true, self.classify);
311+
let mut width = filename.width();
310312

311313
if egg.file.dir.is_none() {
312314
if let Some(parent) = egg.file.path.parent() {
@@ -315,7 +317,7 @@ impl Details {
315317
}
316318

317319
let name = TextCell {
318-
contents: filename(&egg.file, &self.colours, true, self.classify),
320+
contents: filename,
319321
width: width,
320322
};
321323

@@ -456,7 +458,8 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
456458
}
457459

458460
pub fn filename_cell(&self, file: File, links: bool) -> TextCell {
459-
let mut width = DisplayWidth::from_file(&file, self.opts.classify);
461+
let filename = filename(&file, &self.opts.colours, links, self.opts.classify);
462+
let mut width = filename.width();
460463

461464
if file.dir.is_none() {
462465
if let Some(parent) = file.path.parent() {
@@ -465,7 +468,7 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
465468
}
466469

467470
TextCell {
468-
contents: filename(&file, &self.opts.colours, links, self.opts.classify),
471+
contents: filename,
469472
width: width,
470473
}
471474
}

src/output/grid.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,17 @@ impl Grid {
2929
grid.reserve(files.len());
3030

3131
for file in files.iter() {
32-
let mut width = DisplayWidth::from_file(file, self.classify);
32+
let filename = filename(file, &self.colours, false, self.classify);
3333

34+
let mut width = filename.width();
3435
if file.dir.is_none() {
3536
if let Some(parent) = file.path.parent() {
3637
width = width + 1 + DisplayWidth::from(parent.to_string_lossy().as_ref());
3738
}
3839
}
3940

4041
grid.add(grid::Cell {
41-
contents: filename(file, &self.colours, false, self.classify).strings().to_string(),
42+
contents: filename.strings().to_string(),
4243
width: *width,
4344
});
4445
}

src/output/mod.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ansi_term::Style;
1+
use ansi_term::{ANSIString, Style};
22

33
use fs::{File, FileTarget};
44

@@ -22,6 +22,8 @@ mod tree;
2222
pub fn filename(file: &File, colours: &Colours, links: bool, classify: bool) -> TextCellContents {
2323
let mut bits = Vec::new();
2424

25+
// TODO: This long function could do with some splitting up.
26+
2527
if file.dir.is_none() {
2628
if let Some(parent) = file.path.parent() {
2729
let coconut = parent.components().count();
@@ -37,7 +39,9 @@ pub fn filename(file: &File, colours: &Colours, links: bool, classify: bool) ->
3739
}
3840

3941
if !file.name.is_empty() {
40-
bits.push(file_colour(colours, file).paint(file.name.clone()));
42+
for bit in coloured_file_name(file, colours) {
43+
bits.push(bit);
44+
}
4145
}
4246

4347
if links && file.is_link() {
@@ -92,6 +96,44 @@ pub fn filename(file: &File, colours: &Colours, links: bool, classify: bool) ->
9296
bits.into()
9397
}
9498

99+
/// Returns at least one ANSI-highlighted string representing this file’s
100+
/// name using the given set of colours.
101+
///
102+
/// Ordinarily, this will be just one string: the file’s complete name,
103+
/// coloured according to its file type. If the name contains control
104+
/// characters such as newlines or escapes, though, we can’t just print them
105+
/// to the screen directly, because then there’ll be newlines in weird places.
106+
///
107+
/// So in that situation, those characters will be escaped and highlighted in
108+
/// a different colour.
109+
fn coloured_file_name<'a>(file: &File, colours: &Colours) -> Vec<ANSIString<'a>> {
110+
let colour = file_colour(colours, file);
111+
let mut bits = Vec::new();
112+
113+
if file.name.chars().all(|c| c >= 0x20 as char) {
114+
bits.push(colour.paint(file.name.clone()));
115+
}
116+
else {
117+
for c in file.name.chars() {
118+
// The `escape_default` method on `char` is *almost* what we want here, but
119+
// it still escapes non-ASCII UTF-8 characters, which are still printable.
120+
121+
if c >= 0x20 as char {
122+
// TODO: This allocates way too much,
123+
// hence the `all` check above.
124+
let mut s = String::new();
125+
s.push(c);
126+
bits.push(colour.paint(s));
127+
} else {
128+
let s = c.escape_default().collect::<String>();
129+
bits.push(colours.control_char.paint(s));
130+
}
131+
}
132+
}
133+
134+
bits
135+
}
136+
95137
pub fn file_colour(colours: &Colours, file: &File) -> Style {
96138
match file {
97139
f if f.is_directory() => colours.filetypes.directory,

xtests/file_names

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ansi: [\u{1b}[34mblue\u{1b}[0m] form-feed: [\u{c}] return: [\r]
2+
ascii: hello invalid-utf8-1: [�] tab: [\t]
3+
backspace: [\u{8}] invalid-utf8-2: [�(] utf-8: pâté
4+
bell: [\u{7}] invalid-utf8-3: [�(] vertical-tab: [\u{b}]
5+
emoji: [🆒] invalid-utf8-4: [�(�(]
6+
escape: [\u{1b}] new-line: [\n]

xtests/file_names_1

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
ansi: [\u{1b}[34mblue\u{1b}[0m]
2+
ascii: hello
3+
backspace: [\u{8}]
4+
bell: [\u{7}]
5+
emoji: [🆒]
6+
escape: [\u{1b}]
7+
form-feed: [\u{c}]
8+
invalid-utf8-1: [�]
9+
invalid-utf8-2: [�(]
10+
invalid-utf8-3: [�(]
11+
invalid-utf8-4: [�(�(]
12+
new-line: [\n]
13+
return: [\r]
14+
tab: [\t]
15+
utf-8: pâté
16+
vertical-tab: [\u{b}]

xtests/file_names_x

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ansi: [\u{1b}[34mblue\u{1b}[0m] ascii: hello backspace: [\u{8}]
2+
bell: [\u{7}] emoji: [🆒] escape: [\u{1b}]
3+
form-feed: [\u{c}] invalid-utf8-1: [�] invalid-utf8-2: [�(]
4+
invalid-utf8-3: [�(] invalid-utf8-4: [�(�(] new-line: [\n]
5+
return: [\r] tab: [\t] utf-8: pâté
6+
vertical-tab: [\u{b}]

xtests/run.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ $exa $testcases/passwd -lgh | diff -q - $results/passwd || exit 1
5454
sudo -u cassowary $exa $testcases/permissions -lghR 2>&1 | diff -q - $results/permissions_sudo || exit 1
5555
$exa $testcases/permissions -lghR 2>&1 | diff -q - $results/permissions || exit 1
5656

57+
# File names
58+
COLUMNS=80 $exa $testcases/file-names 2>&1 | diff -q - $results/file_names || exit 1
59+
COLUMNS=80 $exa $testcases/file-names -x 2>&1 | diff -q - $results/file_names_x || exit 1
60+
$exa $testcases/file-names -1 2>&1 | diff -q - $results/file_names_1 || exit 1
5761

5862
# File types
5963
$exa $testcases/file-names-exts -1 2>&1 | diff -q - $results/file-names-exts || exit 1

0 commit comments

Comments
 (0)