blob: d36329dee3fe30c419500860abb1748f6cc39a3b [file] [log] [blame]
/// Terminal-styling container
///
/// For now, this is the same as a [`Str`][crate::builder::Str]. This exists to reserve space in
/// the API for exposing terminal styling.
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct StyledStr {
#[cfg(feature = "color")]
pieces: Vec<(Option<Style>, String)>,
#[cfg(not(feature = "color"))]
pieces: String,
}
impl StyledStr {
/// Create an empty buffer
#[cfg(feature = "color")]
pub const fn new() -> Self {
Self { pieces: Vec::new() }
}
/// Create an empty buffer
#[cfg(not(feature = "color"))]
pub const fn new() -> Self {
Self {
pieces: String::new(),
}
}
/// Display using [ANSI Escape Code](https://en.wikipedia.org/wiki/ANSI_escape_code) styling
#[cfg(feature = "color")]
pub fn ansi(&self) -> impl std::fmt::Display + '_ {
AnsiDisplay { styled: self }
}
pub(crate) fn header(&mut self, msg: impl Into<String>) {
self.stylize_(Some(Style::Header), msg.into());
}
pub(crate) fn literal(&mut self, msg: impl Into<String>) {
self.stylize_(Some(Style::Literal), msg.into());
}
pub(crate) fn placeholder(&mut self, msg: impl Into<String>) {
self.stylize_(Some(Style::Placeholder), msg.into());
}
#[cfg_attr(not(feature = "error-context"), allow(dead_code))]
pub(crate) fn good(&mut self, msg: impl Into<String>) {
self.stylize_(Some(Style::Good), msg.into());
}
#[cfg_attr(not(feature = "error-context"), allow(dead_code))]
pub(crate) fn warning(&mut self, msg: impl Into<String>) {
self.stylize_(Some(Style::Warning), msg.into());
}
pub(crate) fn error(&mut self, msg: impl Into<String>) {
self.stylize_(Some(Style::Error), msg.into());
}
#[allow(dead_code)]
pub(crate) fn hint(&mut self, msg: impl Into<String>) {
self.stylize_(Some(Style::Hint), msg.into());
}
pub(crate) fn none(&mut self, msg: impl Into<String>) {
self.stylize_(None, msg.into());
}
pub(crate) fn stylize(&mut self, style: impl Into<Option<Style>>, msg: impl Into<String>) {
self.stylize_(style.into(), msg.into());
}
pub(crate) fn trim(&mut self) {
self.trim_start();
self.trim_end();
}
pub(crate) fn trim_start(&mut self) {
if let Some((_, item)) = self.iter_mut().next() {
*item = item.trim_start().to_owned();
}
}
#[cfg(feature = "color")]
pub(crate) fn trim_end(&mut self) {
if let Some((_, item)) = self.pieces.last_mut() {
*item = item.trim_end().to_owned();
}
}
#[cfg(not(feature = "color"))]
pub(crate) fn trim_end(&mut self) {
self.pieces = self.pieces.trim_end().to_owned();
}
#[cfg(feature = "help")]
pub(crate) fn indent(&mut self, initial: &str, trailing: &str) {
if let Some((_, first)) = self.iter_mut().next() {
first.insert_str(0, initial);
}
let mut line_sep = "\n".to_owned();
line_sep.push_str(trailing);
for (_, content) in self.iter_mut() {
*content = content.replace('\n', &line_sep);
}
}
#[cfg(all(not(feature = "wrap_help"), feature = "help"))]
pub(crate) fn wrap(&mut self, _hard_width: usize) {}
#[cfg(feature = "wrap_help")]
pub(crate) fn wrap(&mut self, hard_width: usize) {
let mut wrapper = crate::output::textwrap::wrap_algorithms::LineWrapper::new(hard_width);
for (_, content) in self.iter_mut() {
let mut total = Vec::new();
for (i, line) in content.split_inclusive('\n').enumerate() {
if 0 < i {
// start of a section does not imply newline
wrapper.reset();
}
let line = crate::output::textwrap::word_separators::find_words_ascii_space(line)
.collect::<Vec<_>>();
total.extend(wrapper.wrap(line));
}
let total = total.join("");
*content = total;
}
self.trim_end();
}
#[cfg(feature = "color")]
fn stylize_(&mut self, style: Option<Style>, msg: String) {
if !msg.is_empty() {
self.pieces.push((style, msg));
}
}
#[cfg(not(feature = "color"))]
fn stylize_(&mut self, _style: Option<Style>, msg: String) {
self.pieces.push_str(&msg);
}
#[inline(never)]
#[cfg(feature = "help")]
pub(crate) fn display_width(&self) -> usize {
let mut width = 0;
for (_, c) in self.iter() {
width += crate::output::display_width(c);
}
width
}
#[cfg(feature = "help")]
pub(crate) fn is_empty(&self) -> bool {
self.pieces.is_empty()
}
#[cfg(feature = "color")]
pub(crate) fn iter(&self) -> impl Iterator<Item = (Option<Style>, &str)> {
self.pieces.iter().map(|(s, c)| (*s, c.as_str()))
}
#[cfg(not(feature = "color"))]
pub(crate) fn iter(&self) -> impl Iterator<Item = (Option<Style>, &str)> {
[(None, self.pieces.as_str())].into_iter()
}
#[cfg(feature = "color")]
pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = (Option<Style>, &mut String)> {
self.pieces.iter_mut().map(|(s, c)| (*s, c))
}
#[cfg(not(feature = "color"))]
pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = (Option<Style>, &mut String)> {
[(None, &mut self.pieces)].into_iter()
}
#[cfg(feature = "color")]
pub(crate) fn into_iter(self) -> impl Iterator<Item = (Option<Style>, String)> {
self.pieces.into_iter()
}
#[cfg(not(feature = "color"))]
pub(crate) fn into_iter(self) -> impl Iterator<Item = (Option<Style>, String)> {
[(None, self.pieces)].into_iter()
}
pub(crate) fn extend(
&mut self,
other: impl IntoIterator<Item = (impl Into<Option<Style>>, impl Into<String>)>,
) {
for (style, content) in other {
self.stylize(style.into(), content.into());
}
}
#[cfg(feature = "color")]
pub(crate) fn write_colored(&self, buffer: &mut termcolor::Buffer) -> std::io::Result<()> {
use std::io::Write;
use termcolor::WriteColor;
for (style, content) in &self.pieces {
let mut color = termcolor::ColorSpec::new();
match style {
Some(Style::Header) => {
color.set_bold(true);
color.set_underline(true);
}
Some(Style::Literal) => {
color.set_bold(true);
}
Some(Style::Placeholder) => {}
Some(Style::Good) => {
color.set_fg(Some(termcolor::Color::Green));
}
Some(Style::Warning) => {
color.set_fg(Some(termcolor::Color::Yellow));
}
Some(Style::Error) => {
color.set_fg(Some(termcolor::Color::Red));
color.set_bold(true);
}
Some(Style::Hint) => {
color.set_dimmed(true);
}
None => {}
}
ok!(buffer.set_color(&color));
ok!(buffer.write_all(content.as_bytes()));
ok!(buffer.reset());
}
Ok(())
}
}
impl Default for &'_ StyledStr {
fn default() -> Self {
static DEFAULT: StyledStr = StyledStr::new();
&DEFAULT
}
}
impl From<std::string::String> for StyledStr {
fn from(name: std::string::String) -> Self {
let mut styled = StyledStr::new();
styled.none(name);
styled
}
}
impl From<&'_ std::string::String> for StyledStr {
fn from(name: &'_ std::string::String) -> Self {
let mut styled = StyledStr::new();
styled.none(name);
styled
}
}
impl From<&'static str> for StyledStr {
fn from(name: &'static str) -> Self {
let mut styled = StyledStr::new();
styled.none(name);
styled
}
}
impl From<&'_ &'static str> for StyledStr {
fn from(name: &'_ &'static str) -> Self {
StyledStr::from(*name)
}
}
impl PartialOrd for StyledStr {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StyledStr {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.iter().map(cmp_key).cmp(other.iter().map(cmp_key))
}
}
fn cmp_key(c: (Option<Style>, &str)) -> (Option<usize>, &str) {
let style = c.0.map(|s| s.as_usize());
let content = c.1;
(style, content)
}
/// Color-unaware printing. Never uses coloring.
impl std::fmt::Display for StyledStr {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
for (_, content) in self.iter() {
ok!(std::fmt::Display::fmt(content, f));
}
Ok(())
}
}
#[cfg(feature = "color")]
struct AnsiDisplay<'s> {
styled: &'s StyledStr,
}
#[cfg(feature = "color")]
impl std::fmt::Display for AnsiDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut buffer = termcolor::Buffer::ansi();
ok!(self
.styled
.write_colored(&mut buffer)
.map_err(|_| std::fmt::Error));
let buffer = buffer.into_inner();
let buffer = ok!(String::from_utf8(buffer).map_err(|_| std::fmt::Error));
ok!(std::fmt::Display::fmt(&buffer, f));
Ok(())
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Style {
Header,
Literal,
Placeholder,
Good,
Warning,
Error,
Hint,
}
impl Style {
fn as_usize(&self) -> usize {
match self {
Self::Header => 0,
Self::Literal => 1,
Self::Placeholder => 2,
Self::Good => 3,
Self::Warning => 4,
Self::Error => 5,
Self::Hint => 6,
}
}
}