1use super::*;
2
3use indent::{indent_all_by, indent_by};
4
5pub const DEFAULT_INDENT_SPACES: usize = 4;
10
11#[must_use]
13pub fn indent_string<S: AsRef<str>>(s: S) -> String {
14 indent_by(DEFAULT_INDENT_SPACES, s.as_ref().to_owned())
15}
16
17#[must_use]
19pub fn indent_all_string<S: AsRef<str>>(s: S) -> String {
20 indent_all_by(DEFAULT_INDENT_SPACES, s.as_ref().to_owned())
21}
22
23pub trait StripTrailingNewline {
28 fn strip_trailing_newline(&self) -> &str;
30}
31
32impl<T: AsRef<str>> StripTrailingNewline for T {
33 fn strip_trailing_newline(&self) -> &str {
34 self.as_ref()
35 .strip_suffix("\r\n")
36 .or_else(|| self.as_ref().strip_suffix('\n'))
37 .unwrap_or(self.as_ref())
38 }
39}
40
41#[must_use]
46pub fn human_byte_count<T: Into<u64>>(count: T) -> String {
47 let count = count.into();
48 if count >= 1024u64 * 1024u64 * 1024u64 {
49 format!("{:.3}GB", (count / (1024u64 * 1024u64)) as f64 / 1024.0)
50 } else if count >= 1024u64 * 1024u64 {
51 format!("{:.3}MB", (count / 1024u64) as f64 / 1024.0)
52 } else if count >= 1024u64 {
53 format!("{:.3}KB", count as f64 / 1024.0)
54 } else {
55 format!("{}B", count)
56 }
57}
58
59#[must_use]
62pub fn human_byte_data<T: AsRef<[u8]>>(data: T, truncate_len: Option<usize>) -> String {
63 let data = data.as_ref();
64
65 let mut printable = true;
67 for c in data {
68 if *c < 32 || *c > 126 {
69 printable = false;
70 break;
71 }
72 }
73
74 let (data, truncated) = if let Some(truncate_len) = truncate_len {
75 if data.len() > truncate_len {
76 (&data[0..truncate_len], true)
77 } else {
78 (data, false)
79 }
80 } else {
81 (data, false)
82 };
83
84 let strdata = if printable {
85 String::from_utf8_lossy(data).to_string()
86 } else {
87 let sw = shell_words::quote(String::from_utf8_lossy(data).as_ref()).to_string();
88 let always_hex = sw
89 .chars()
90 .any(|c| !c.is_ascii() || c.is_ascii_control() || c.is_ascii_graphic());
91 let h = hex::encode(data);
92 if always_hex || h.len() < sw.len() {
93 h
94 } else {
95 sw
96 }
97 };
98 if truncated {
99 format!("{}...", strdata)
100 } else {
101 strdata
102 }
103}
104
105pub trait FormatterToString {
111 fn to_string<T: fmt::Display>(&self, value: T) -> String;
113
114 fn to_string_opt<T: fmt::Display>(&self, value: Option<T>) -> String {
116 if let Some(ts) = value {
117 self.to_string(ts)
118 } else {
119 "None".to_string()
120 }
121 }
122
123 fn to_multiline_string<'a, T, I>(&self, items: I) -> String
125 where
126 T: fmt::Display + 'a,
127 I: IntoIterator<Item = T>,
128 {
129 let mut out = Vec::new();
130 for x in items {
131 out.push(self.to_string(x));
132 }
133 out.join("\n")
134 }
135
136 fn to_multiline_indexed_string<'a, T, I>(&self, items: I) -> String
139 where
140 T: fmt::Display + 'a,
141 I: IntoIterator<Item = T>,
142 {
143 let mut out = Vec::new();
144 for (n, x) in items.into_iter().enumerate() {
145 let block = self.to_string(x);
146 if block.contains('\n') {
147 out.push(format!("{n}:"));
148 out.push(indent_all_by(DEFAULT_INDENT_SPACES, block));
149 } else {
150 out.push(format!("{n}: {block}"));
151 }
152 }
153 out.join("\n")
154 }
155
156 fn to_table_string<T: fmt::Display>(&self, items: &[T]) -> String {
158 self.to_table_string_custom(items, 32, 4, true, true)
159 }
160
161 fn to_table_string_custom<T: fmt::Display, I: IntoIterator<Item = T>>(
164 &self,
165 items: I,
166 columns: usize,
167 indent: usize,
168 brackets: bool,
169 newlines: bool,
170 ) -> String {
171 let strings: Vec<String> = items.into_iter().map(|s| self.to_string(s)).collect();
172 table_string_from_strings(&strings, columns, indent, brackets, newlines)
173 }
174}
175
176fn table_string_from_strings(
177 strings: &[String],
178 columns: usize,
179 indent: usize,
180 brackets: bool,
181 newlines: bool,
182) -> String {
183 let len = strings.len();
184 let mut col = 0usize;
185 let mut out = String::new();
186 let mut left = len;
187
188 let use_columns = columns > 0 && columns < len;
189
190 if brackets {
191 out.push('[');
192 }
193 if use_columns && newlines {
194 out.push('\n');
195 }
196 let indent_str = " ".repeat(indent);
197
198 for sc in strings {
199 if use_columns && col == 0 {
200 out.push_str(&indent_str);
201 }
202 out.push_str(sc);
203 col += 1;
204 left -= 1;
205 if left != 0 {
206 out.push(',');
207 if use_columns && col == columns {
208 col = 0;
209 out.push('\n');
210 }
211 }
212 }
213 if use_columns && newlines {
214 out.push('\n');
215 }
216 if brackets {
217 out.push(']');
218 }
219 out
220}
221
222impl FormatterToString for fmt::Formatter<'_> {
223 fn to_string<T: fmt::Display>(&self, value: T) -> String {
224 if self.alternate() {
225 format!("{:#}", value)
226 } else {
227 format!("{}", value)
228 }
229 }
230}
231
232#[derive(Debug, Clone, Copy, Default)]
234pub struct DefaultDisplayFormat;
235
236impl FormatterToString for DefaultDisplayFormat {
237 fn to_string<T: fmt::Display>(&self, value: T) -> String {
238 value.to_string()
239 }
240}
241
242#[derive(Debug, Clone, Copy, Default)]
244pub struct AlternateDisplayFormat;
245
246impl FormatterToString for AlternateDisplayFormat {
247 fn to_string<T: fmt::Display>(&self, value: T) -> String {
248 format!("{:#}", value)
249 }
250}
251
252pub trait ToMultilineString {
254 fn to_multiline_string(&self) -> String;
256 fn to_multiline_indexed_string(&self) -> String;
258}
259
260impl<T> ToMultilineString for Vec<T>
261where
262 T: fmt::Display,
263{
264 fn to_multiline_string(&self) -> String {
265 AlternateDisplayFormat.to_multiline_string(self.iter())
266 }
267
268 fn to_multiline_indexed_string(&self) -> String {
269 AlternateDisplayFormat.to_multiline_indexed_string(self.iter())
270 }
271}
272
273pub trait ToTableString {
275 fn to_table_string(&self) -> String {
277 self.to_table_string_custom(32, 4, true, true)
278 }
279 fn to_table_string_custom(
281 &self,
282 columns: usize,
283 indent: usize,
284 brackets: bool,
285 newlines: bool,
286 ) -> String;
287}
288
289impl<T> ToTableString for Vec<T>
290where
291 T: fmt::Display,
292{
293 fn to_table_string_custom(
294 &self,
295 columns: usize,
296 indent: usize,
297 brackets: bool,
298 newlines: bool,
299 ) -> String {
300 AlternateDisplayFormat.to_table_string_custom(self, columns, indent, brackets, newlines)
301 }
302}
303
304pub trait StringIfEmpty<X: ToString> {
306 fn string_if_empty(&self, empty_string: X) -> String;
308}
309
310impl<S: AsRef<str>, X: ToString> StringIfEmpty<X> for S {
311 fn string_if_empty(&self, empty_string: X) -> String {
312 let s = self.as_ref();
313 if s.is_empty() {
314 empty_string.to_string()
315 } else {
316 s.to_string()
317 }
318 }
319}
320
321pub fn split_port(name: &str) -> Result<(String, Option<u16>), String> {
328 if let Some(split) = name.rfind(':') {
329 let hoststr = &name[0..split];
330 let portstr = &name[split + 1..];
331 let port: u16 = portstr
332 .parse::<u16>()
333 .map_err(|e| format!("invalid port: {}", e))?;
334
335 Ok((hoststr.to_string(), Some(port)))
336 } else {
337 Ok((name.to_string(), None))
338 }
339}
340
341#[must_use]
343pub fn prepend_slash(s: String) -> String {
344 if s.starts_with('/') {
345 return s;
346 }
347 let mut out = "/".to_owned();
348 out.push_str(s.as_str());
349 out
350}
351
352pub fn map_to_string<X: ToString>(arg: X) -> String {
354 arg.to_string()
355}
356
357#[cfg(test)]
358mod formatter_to_string_tests {
359 use super::*;
360
361 #[derive(Clone, Copy)]
362 struct AltEcho(u32);
363
364 impl fmt::Display for AltEcho {
365 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366 if f.alternate() {
367 write!(f, "[{}]", self.0)
368 } else {
369 write!(f, "{}", self.0)
370 }
371 }
372 }
373
374 struct Wrap<'a, T: fmt::Display>(&'a T);
375
376 impl<T: fmt::Display> fmt::Display for Wrap<'_, T> {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 f.write_str(&f.to_string(self.0))
379 }
380 }
381
382 #[test]
383 fn nested_format_inherits_alternate() {
384 assert_eq!(format!("{}", Wrap(&AltEcho(3))), "3");
385 assert_eq!(format!("{:#}", Wrap(&AltEcho(3))), "[3]");
386 }
387
388 #[test]
389 fn default_table_string_matches_single_line_when_short() {
390 let v = [1u32, 2, 3];
391 assert_eq!(AlternateDisplayFormat.to_table_string(&v), "[1,2,3]");
392 }
393
394 #[test]
395 fn alternate_indexed_multiline_uses_hash_flag() {
396 let v = [AltEcho(7)];
397 let s = AlternateDisplayFormat.to_multiline_indexed_string(v.iter());
398 assert!(s.contains("[7]"), "got {:?}", s);
399 }
400}