1/// Terminal-styling container 2/// 3/// For now, this is the same as a [`Str`][crate::builder::Str]. This exists to reserve space in 4/// the API for exposing terminal styling. 5#[derive(Clone, Default, Debug, PartialEq, Eq)] 6pub struct StyledStr { 7 #[cfg(feature = "color")] 8 pieces: Vec<(Option<Style>, String)>, 9 #[cfg(not(feature = "color"))] 10 pieces: String, 11} 12 13impl StyledStr { 14 /// Create an empty buffer 15 #[cfg(feature = "color")] 16 pub const fn new() -> Self { 17 Self { pieces: Vec::new() } 18 } 19 20 /// Create an empty buffer 21 #[cfg(not(feature = "color"))] 22 pub const fn new() -> Self { 23 Self { 24 pieces: String::new(), 25 } 26 } 27 28 /// Display using [ANSI Escape Code](https://en.wikipedia.org/wiki/ANSI_escape_code) styling 29 #[cfg(feature = "color")] 30 pub fn ansi(&self) -> impl std::fmt::Display + '_ { 31 AnsiDisplay { styled: self } 32 } 33 34 pub(crate) fn header(&mut self, msg: impl Into<String>) { 35 self.stylize_(Some(Style::Header), msg.into()); 36 } 37 38 pub(crate) fn literal(&mut self, msg: impl Into<String>) { 39 self.stylize_(Some(Style::Literal), msg.into()); 40 } 41 42 pub(crate) fn placeholder(&mut self, msg: impl Into<String>) { 43 self.stylize_(Some(Style::Placeholder), msg.into()); 44 } 45 46 #[cfg_attr(not(feature = "error-context"), allow(dead_code))] 47 pub(crate) fn good(&mut self, msg: impl Into<String>) { 48 self.stylize_(Some(Style::Good), msg.into()); 49 } 50 51 #[cfg_attr(not(feature = "error-context"), allow(dead_code))] 52 pub(crate) fn warning(&mut self, msg: impl Into<String>) { 53 self.stylize_(Some(Style::Warning), msg.into()); 54 } 55 56 pub(crate) fn error(&mut self, msg: impl Into<String>) { 57 self.stylize_(Some(Style::Error), msg.into()); 58 } 59 60 #[allow(dead_code)] 61 pub(crate) fn hint(&mut self, msg: impl Into<String>) { 62 self.stylize_(Some(Style::Hint), msg.into()); 63 } 64 65 pub(crate) fn none(&mut self, msg: impl Into<String>) { 66 self.stylize_(None, msg.into()); 67 } 68 69 pub(crate) fn stylize(&mut self, style: impl Into<Option<Style>>, msg: impl Into<String>) { 70 self.stylize_(style.into(), msg.into()); 71 } 72 73 pub(crate) fn trim(&mut self) { 74 self.trim_start(); 75 self.trim_end(); 76 } 77 78 pub(crate) fn trim_start(&mut self) { 79 if let Some((_, item)) = self.iter_mut().next() { 80 *item = item.trim_start().to_owned(); 81 } 82 } 83 84 #[cfg(feature = "color")] 85 pub(crate) fn trim_end(&mut self) { 86 if let Some((_, item)) = self.pieces.last_mut() { 87 *item = item.trim_end().to_owned(); 88 } 89 } 90 91 #[cfg(not(feature = "color"))] 92 pub(crate) fn trim_end(&mut self) { 93 self.pieces = self.pieces.trim_end().to_owned(); 94 } 95 96 #[cfg(feature = "help")] 97 pub(crate) fn indent(&mut self, initial: &str, trailing: &str) { 98 if let Some((_, first)) = self.iter_mut().next() { 99 first.insert_str(0, initial); 100 } 101 let mut line_sep = "\n".to_owned(); 102 line_sep.push_str(trailing); 103 for (_, content) in self.iter_mut() { 104 *content = content.replace('\n', &line_sep); 105 } 106 } 107 108 #[cfg(all(not(feature = "wrap_help"), feature = "help"))] 109 pub(crate) fn wrap(&mut self, _hard_width: usize) {} 110 111 #[cfg(feature = "wrap_help")] 112 pub(crate) fn wrap(&mut self, hard_width: usize) { 113 let mut wrapper = crate::output::textwrap::wrap_algorithms::LineWrapper::new(hard_width); 114 for (_, content) in self.iter_mut() { 115 let mut total = Vec::new(); 116 for (i, line) in content.split_inclusive('\n').enumerate() { 117 if 0 < i { 118 // start of a section does not imply newline 119 wrapper.reset(); 120 } 121 let line = crate::output::textwrap::word_separators::find_words_ascii_space(line) 122 .collect::<Vec<_>>(); 123 total.extend(wrapper.wrap(line)); 124 } 125 let total = total.join(""); 126 *content = total; 127 } 128 129 self.trim_end(); 130 } 131 132 #[cfg(feature = "color")] 133 fn stylize_(&mut self, style: Option<Style>, msg: String) { 134 if !msg.is_empty() { 135 self.pieces.push((style, msg)); 136 } 137 } 138 139 #[cfg(not(feature = "color"))] 140 fn stylize_(&mut self, _style: Option<Style>, msg: String) { 141 self.pieces.push_str(&msg); 142 } 143 144 #[inline(never)] 145 #[cfg(feature = "help")] 146 pub(crate) fn display_width(&self) -> usize { 147 let mut width = 0; 148 for (_, c) in self.iter() { 149 width += crate::output::display_width(c); 150 } 151 width 152 } 153 154 #[cfg(feature = "help")] 155 pub(crate) fn is_empty(&self) -> bool { 156 self.pieces.is_empty() 157 } 158 159 #[cfg(feature = "color")] 160 pub(crate) fn iter(&self) -> impl Iterator<Item = (Option<Style>, &str)> { 161 self.pieces.iter().map(|(s, c)| (*s, c.as_str())) 162 } 163 164 #[cfg(not(feature = "color"))] 165 pub(crate) fn iter(&self) -> impl Iterator<Item = (Option<Style>, &str)> { 166 [(None, self.pieces.as_str())].into_iter() 167 } 168 169 #[cfg(feature = "color")] 170 pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = (Option<Style>, &mut String)> { 171 self.pieces.iter_mut().map(|(s, c)| (*s, c)) 172 } 173 174 #[cfg(not(feature = "color"))] 175 pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = (Option<Style>, &mut String)> { 176 [(None, &mut self.pieces)].into_iter() 177 } 178 179 #[cfg(feature = "color")] 180 pub(crate) fn into_iter(self) -> impl Iterator<Item = (Option<Style>, String)> { 181 self.pieces.into_iter() 182 } 183 184 #[cfg(not(feature = "color"))] 185 pub(crate) fn into_iter(self) -> impl Iterator<Item = (Option<Style>, String)> { 186 [(None, self.pieces)].into_iter() 187 } 188 189 pub(crate) fn extend( 190 &mut self, 191 other: impl IntoIterator<Item = (impl Into<Option<Style>>, impl Into<String>)>, 192 ) { 193 for (style, content) in other { 194 self.stylize(style.into(), content.into()); 195 } 196 } 197 198 #[cfg(feature = "color")] 199 pub(crate) fn write_colored(&self, buffer: &mut termcolor::Buffer) -> std::io::Result<()> { 200 use std::io::Write; 201 use termcolor::WriteColor; 202 203 for (style, content) in &self.pieces { 204 let mut color = termcolor::ColorSpec::new(); 205 match style { 206 Some(Style::Header) => { 207 color.set_bold(true); 208 color.set_underline(true); 209 } 210 Some(Style::Literal) => { 211 color.set_bold(true); 212 } 213 Some(Style::Placeholder) => {} 214 Some(Style::Good) => { 215 color.set_fg(Some(termcolor::Color::Green)); 216 } 217 Some(Style::Warning) => { 218 color.set_fg(Some(termcolor::Color::Yellow)); 219 } 220 Some(Style::Error) => { 221 color.set_fg(Some(termcolor::Color::Red)); 222 color.set_bold(true); 223 } 224 Some(Style::Hint) => { 225 color.set_dimmed(true); 226 } 227 None => {} 228 } 229 230 ok!(buffer.set_color(&color)); 231 ok!(buffer.write_all(content.as_bytes())); 232 ok!(buffer.reset()); 233 } 234 235 Ok(()) 236 } 237} 238 239impl Default for &'_ StyledStr { 240 fn default() -> Self { 241 static DEFAULT: StyledStr = StyledStr::new(); 242 &DEFAULT 243 } 244} 245 246impl From<std::string::String> for StyledStr { 247 fn from(name: std::string::String) -> Self { 248 let mut styled = StyledStr::new(); 249 styled.none(name); 250 styled 251 } 252} 253 254impl From<&'_ std::string::String> for StyledStr { 255 fn from(name: &'_ std::string::String) -> Self { 256 let mut styled = StyledStr::new(); 257 styled.none(name); 258 styled 259 } 260} 261 262impl From<&'static str> for StyledStr { 263 fn from(name: &'static str) -> Self { 264 let mut styled = StyledStr::new(); 265 styled.none(name); 266 styled 267 } 268} 269 270impl From<&'_ &'static str> for StyledStr { 271 fn from(name: &'_ &'static str) -> Self { 272 StyledStr::from(*name) 273 } 274} 275 276impl PartialOrd for StyledStr { 277 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { 278 Some(self.cmp(other)) 279 } 280} 281 282impl Ord for StyledStr { 283 fn cmp(&self, other: &Self) -> std::cmp::Ordering { 284 self.iter().map(cmp_key).cmp(other.iter().map(cmp_key)) 285 } 286} 287 288fn cmp_key(c: (Option<Style>, &str)) -> (Option<usize>, &str) { 289 let style = c.0.map(|s| s.as_usize()); 290 let content = c.1; 291 (style, content) 292} 293 294/// Color-unaware printing. Never uses coloring. 295impl std::fmt::Display for StyledStr { 296 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 297 for (_, content) in self.iter() { 298 ok!(std::fmt::Display::fmt(content, f)); 299 } 300 301 Ok(()) 302 } 303} 304 305#[cfg(feature = "color")] 306struct AnsiDisplay<'s> { 307 styled: &'s StyledStr, 308} 309 310#[cfg(feature = "color")] 311impl std::fmt::Display for AnsiDisplay<'_> { 312 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 313 let mut buffer = termcolor::Buffer::ansi(); 314 ok!(self 315 .styled 316 .write_colored(&mut buffer) 317 .map_err(|_| std::fmt::Error)); 318 let buffer = buffer.into_inner(); 319 let buffer = ok!(String::from_utf8(buffer).map_err(|_| std::fmt::Error)); 320 ok!(std::fmt::Display::fmt(&buffer, f)); 321 322 Ok(()) 323 } 324} 325 326#[derive(Copy, Clone, Debug, PartialEq, Eq)] 327pub(crate) enum Style { 328 Header, 329 Literal, 330 Placeholder, 331 Good, 332 Warning, 333 Error, 334 Hint, 335} 336 337impl Style { 338 fn as_usize(&self) -> usize { 339 match self { 340 Self::Header => 0, 341 Self::Literal => 1, 342 Self::Placeholder => 2, 343 Self::Good => 3, 344 Self::Warning => 4, 345 Self::Error => 5, 346 Self::Hint => 6, 347 } 348 } 349} 350