refactor: improve code formatting in `mod.rs` and `logger.rs`
[ouch.git] / src / utils / formatting.rs
blob9ebef9698e253eceb899420600dda99ad424bfaa
1 use std::{borrow::Cow, cmp, ffi::OsStr, fmt::Display, path::Path};
3 use crate::CURRENT_DIRECTORY;
5 /// Converts invalid UTF-8 bytes to the Unicode replacement codepoint (�) in its Display implementation.
6 pub struct EscapedPathDisplay<'a> {
7     path: &'a Path,
10 impl<'a> EscapedPathDisplay<'a> {
11     pub fn new(path: &'a Path) -> Self {
12         Self { path }
13     }
16 #[cfg(unix)]
17 impl Display for EscapedPathDisplay<'_> {
18     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19         use std::os::unix::prelude::OsStrExt;
21         let bstr = bstr::BStr::new(self.path.as_os_str().as_bytes());
23         write!(f, "{bstr}")
24     }
27 #[cfg(windows)]
28 impl Display for EscapedPathDisplay<'_> {
29     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30         use std::{char, fmt::Write, os::windows::prelude::OsStrExt};
32         let utf16 = self.path.as_os_str().encode_wide();
33         let chars = char::decode_utf16(utf16).map(|decoded| decoded.unwrap_or(char::REPLACEMENT_CHARACTER));
35         for char in chars {
36             f.write_char(char)?;
37         }
39         Ok(())
40     }
43 /// Converts an OsStr to utf8 with custom formatting.
44 ///
45 /// This is different from [`Path::display`].
46 ///
47 /// See <https://gist.github.com/marcospb19/ebce5572be26397cf08bbd0fd3b65ac1> for a comparison.
48 pub fn path_to_str(path: &Path) -> Cow<str> {
49     os_str_to_str(path.as_ref())
52 pub fn os_str_to_str(os_str: &OsStr) -> Cow<str> {
53     let format = || {
54         let text = format!("{os_str:?}");
55         Cow::Owned(text.trim_matches('"').to_string())
56     };
58     os_str.to_str().map_or_else(format, Cow::Borrowed)
61 /// Removes the current dir from the beginning of a path as it's redundant information,
62 /// useful for presentation sake.
63 pub fn strip_cur_dir(source_path: &Path) -> &Path {
64     let current_dir = &*CURRENT_DIRECTORY;
66     source_path.strip_prefix(current_dir).unwrap_or(source_path)
69 /// Converts a slice of `AsRef<OsStr>` to comma separated String
70 ///
71 /// Panics if the slice is empty.
72 pub fn pretty_format_list_of_paths(paths: &[impl AsRef<Path>]) -> String {
73     let mut iter = paths.iter().map(AsRef::as_ref);
75     let first_path = iter.next().unwrap();
76     let mut string = path_to_str(first_path).into_owned();
78     for path in iter {
79         string += ", ";
80         string += &path_to_str(path);
81     }
82     string
85 /// Display the directory name, but use "current directory" when necessary.
86 pub fn nice_directory_display(path: &Path) -> Cow<str> {
87     if path == Path::new(".") {
88         Cow::Borrowed("current directory")
89     } else {
90         path_to_str(path)
91     }
94 /// Struct useful to printing bytes as kB, MB, GB, etc.
95 pub struct Bytes(f64);
97 impl Bytes {
98     const UNIT_PREFIXES: [&'static str; 6] = ["", "ki", "Mi", "Gi", "Ti", "Pi"];
100     /// Create a new Bytes.
101     pub fn new(bytes: u64) -> Self {
102         Self(bytes as f64)
103     }
106 impl std::fmt::Display for Bytes {
107     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108         let &Self(num) = self;
110         debug_assert!(num >= 0.0);
111         if num < 1_f64 {
112             return write!(f, "{} B", num);
113         }
115         let delimiter = 1000_f64;
116         let exponent = cmp::min((num.ln() / 6.90775).floor() as i32, 4);
118         write!(
119             f,
120             "{:.2} {}B",
121             num / delimiter.powi(exponent),
122             Bytes::UNIT_PREFIXES[exponent as usize]
123         )
124     }
127 #[cfg(test)]
128 mod tests {
129     use super::*;
131     #[test]
132     fn test_pretty_bytes_formatting() {
133         fn format_bytes(bytes: u64) -> String {
134             format!("{}", Bytes::new(bytes))
135         }
136         let b = 1;
137         let kb = b * 1000;
138         let mb = kb * 1000;
139         let gb = mb * 1000;
141         assert_eq!("0 B", format_bytes(0)); // This is weird
142         assert_eq!("1.00 B", format_bytes(b));
143         assert_eq!("999.00 B", format_bytes(b * 999));
144         assert_eq!("12.00 MiB", format_bytes(mb * 12));
145         assert_eq!("123.00 MiB", format_bytes(mb * 123));
146         assert_eq!("5.50 MiB", format_bytes(mb * 5 + kb * 500));
147         assert_eq!("7.54 GiB", format_bytes(gb * 7 + 540 * mb));
148         assert_eq!("1.20 TiB", format_bytes(gb * 1200));
150         // bytes
151         assert_eq!("234.00 B", format_bytes(234));
152         assert_eq!("999.00 B", format_bytes(999));
153         // kilobytes
154         assert_eq!("2.23 kiB", format_bytes(2234));
155         assert_eq!("62.50 kiB", format_bytes(62500));
156         assert_eq!("329.99 kiB", format_bytes(329990));
157         // megabytes
158         assert_eq!("2.75 MiB", format_bytes(2750000));
159         assert_eq!("55.00 MiB", format_bytes(55000000));
160         assert_eq!("987.65 MiB", format_bytes(987654321));
161         // gigabytes
162         assert_eq!("5.28 GiB", format_bytes(5280000000));
163         assert_eq!("95.20 GiB", format_bytes(95200000000));
164         assert_eq!("302.00 GiB", format_bytes(302000000000));
165         assert_eq!("302.99 GiB", format_bytes(302990000000));
166         // Weird aproximation cases:
167         assert_eq!("999.90 GiB", format_bytes(999900000000));
168         assert_eq!("1.00 TiB", format_bytes(999990000000));
169     }