use std::path::Path;
use crate::cli::{Format, PrintingOpts};
use sibling::{Dir, Dirs};
pub(crate) fn result_string(dirs: &Dirs, next: Option<Dir<'_>>, opts: &PrintingOpts) -> String {
match opts.format {
Format::Json => json_string(dirs, next, opts.absolute),
Format::Csv => csv_string(dirs, next, opts.absolute),
Format::List => list_string(dirs, next.as_ref(), opts),
Format::Default => {
if next.is_none() {
String::new()
} else {
result_string_impl(dirs, next, opts)
}
}
}
}
fn index_of(dir: Option<&Dir<'_>>) -> i32 {
dir.map_or(-1, |d| i32::try_from(d.index()).unwrap() + 1)
}
fn json_escape(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\u{08}' => result.push_str("\\b"),
'\u{0c}' => result.push_str("\\f"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c if c < '\u{20}' => result.push_str(&format!("\\u{:04x}", c as u32)),
c => result.push(c),
}
}
result
}
fn csv_escape(s: &str) -> String {
s.replace('"', "\"\"")
}
fn json_string(dirs: &Dirs, next: Option<Dir<'_>>, absolute: bool) -> String {
let current = dirs.current();
format!(
r#"{{"current":{{"path":"{}","index":{}}},"next":{{"path":"{}","index":{}}},"total":{}}}"#,
json_escape(&path_to_string(current.as_ref().map(Dir::path), absolute)),
index_of(current.as_ref()),
json_escape(&path_to_string(next.as_ref().map(Dir::path), absolute)),
index_of(next.as_ref()),
dirs.len()
)
}
fn csv_string(dirs: &Dirs, next: Option<Dir<'_>>, absolute: bool) -> String {
let current = dirs.current();
format!(
r#""{}","{}",{},{},{}"#,
csv_escape(&path_to_string(current.as_ref().map(Dir::path), absolute)),
csv_escape(&path_to_string(next.as_ref().map(Dir::path), absolute)),
index_of(current.as_ref()),
index_of(next.as_ref()),
dirs.len()
)
}
fn list_string(dirs: &Dirs, next: Option<&Dir<'_>>, opts: &PrintingOpts) -> String {
let mut result = vec![];
let current_index = index_of(dirs.current().as_ref());
let next_index = index_of(next);
for (i, dir) in dirs.directories().enumerate() {
let index = i32::try_from(i).unwrap() + 1;
let prefix = if index == next_index {
"> "
} else if index == current_index {
"* "
} else {
" "
};
result.push(format!(
"{index:>4} {prefix}{}",
path_to_string(Some(dir), opts.absolute)
));
}
result.join("\n")
}
fn result_string_impl(dirs: &Dirs, next: Option<Dir<'_>>, opts: &PrintingOpts) -> String {
if opts.progress {
format!(
"{} ({}/{})",
path_to_string(next.as_ref().map(Dir::path), opts.absolute),
index_of(next.as_ref()),
dirs.len()
)
} else {
path_to_string(next.as_ref().map(Dir::path), opts.absolute).to_string()
}
}
pub(crate) fn path_to_string(path: Option<&Path>, absolute: bool) -> String {
match path {
Some(p) => {
if absolute {
match std::fs::canonicalize(p) {
Ok(path) => path.to_string_lossy().to_string(),
Err(e) => {
log::error!("Failed to get canonical path for {p:?}: {e}");
p.to_string_lossy().to_string()
}
}
} else {
p.to_string_lossy().to_string()
}
}
None => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::{path_to_string, result_string};
use crate::cli::{Format, PrintingOpts};
use sibling::{Dirs, NexterType, Nextable};
use std::path::{Path, PathBuf};
#[test]
fn test_path_to_string_relative() {
let p = Path::new("testdata/basic/c");
assert_eq!(path_to_string(Some(p), false), "testdata/basic/c");
}
#[test]
fn test_path_to_string_absolute() {
let p = Path::new("testdata");
let abs = std::fs::canonicalize(p).unwrap();
assert_eq!(path_to_string(Some(p), true), abs.to_string_lossy());
}
#[test]
fn test_path_to_string_none() {
assert_eq!(path_to_string(None, false), "");
assert_eq!(path_to_string(None, true), "");
}
#[test]
fn test_json_escape() {
use super::json_escape;
assert_eq!(json_escape("/tmp/plain"), "/tmp/plain");
assert_eq!(json_escape(r#"/tmp/qu"ote"#), r#"/tmp/qu\"ote"#);
assert_eq!(json_escape(r"C:\Users\tamada"), r"C:\\Users\\tamada");
assert_eq!(json_escape("/tmp/tab\tx"), r"/tmp/tab\tx");
assert_eq!(json_escape("/tmp/lf\nx"), r"/tmp/lf\nx");
assert_eq!(json_escape("/tmp/bell\u{07}x"), r"/tmp/bell\u0007x");
assert_eq!(json_escape("/tmp/back\u{08}x"), r"/tmp/back\bx");
assert_eq!(json_escape("/tmp/form\u{0c}x"), r"/tmp/form\fx");
assert_eq!(json_escape("/tmp/cr\rx"), r"/tmp/cr\rx");
assert_eq!(json_escape("/tmp/絵文字_👍"), "/tmp/絵文字_👍");
}
#[test]
fn test_csv_escape() {
use super::csv_escape;
assert_eq!(csv_escape("/tmp/plain"), "/tmp/plain");
assert_eq!(csv_escape(r#"/tmp/qu"ote"#), r#"/tmp/qu""ote"#);
assert_eq!(csv_escape(r"C:\Users\tamada"), r"C:\Users\tamada");
}
fn dirs_of(paths: &[&str]) -> Dirs {
Dirs::new(
PathBuf::from("/tmp"),
paths.iter().map(PathBuf::from).collect(),
)
}
fn opts_of(format: Format) -> PrintingOpts {
PrintingOpts {
format,
absolute: false,
progress: false,
}
}
#[test]
fn test_list_string() {
use sibling::factory::{Config, DirsFactory};
let config = Config::new_with_wd("testdata/worried", false, "testdata/worried/dir with spaces");
let dirs = DirsFactory::create_with(&config).expect("Failed to create Dirs");
let next = dirs.next(NexterType::Next);
assert_eq!(
result_string(&dirs, next, &opts_of(Format::List)),
" 1 * testdata/worried/dir with spaces\n 2 > testdata/worried/multibyte_chars_\u{1f44d}"
);
}
#[test]
fn test_list_string_without_the_current() {
let dirs = dirs_of(&["/tmp/a", "/tmp/b"]);
let next = dirs.next(NexterType::Next);
assert_eq!(
result_string(&dirs, next, &opts_of(Format::List)),
" 1 > /tmp/a\n 2 /tmp/b"
);
}
#[test]
fn test_progress() {
let dirs = dirs_of(&["/tmp/a", "/tmp/b", "/tmp/c"]);
let next = dirs.next(NexterType::Next);
let mut opts = opts_of(Format::Default);
opts.progress = true;
assert_eq!(result_string(&dirs, next, &opts), "/tmp/a (1/3)");
}
#[test]
fn test_path_to_string_absolute_of_the_missing_path() {
let p = Path::new("testdata/no_such_dir");
assert_eq!(path_to_string(Some(p), true), "testdata/no_such_dir");
}
#[test]
fn test_json_string_with_worried_names() {
let dirs = dirs_of(&[r#"/tmp/qu"ote"#, r"/tmp/back\slash"]);
let next = dirs.next(NexterType::Next);
assert_eq!(
result_string(&dirs, next, &opts_of(Format::Json)),
r#"{"current":{"path":"","index":-1},"next":{"path":"/tmp/qu\"ote","index":1},"total":2}"#
);
}
#[test]
fn test_csv_string_with_worried_names() {
let dirs = dirs_of(&[r#"/tmp/qu"ote"#, r"/tmp/back\slash"]);
let next = dirs.next(NexterType::Next);
assert_eq!(
result_string(&dirs, next, &opts_of(Format::Csv)),
r#""","/tmp/qu""ote",-1,1,2"#
);
}
}