Skip to content

Commit

Permalink
refactor(help): Track via style via ANSI codes
Browse files Browse the repository at this point in the history
  • Loading branch information
epage committed Mar 16, 2023
1 parent f002cb9 commit 11583e1
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 182 deletions.
2 changes: 1 addition & 1 deletion src/builder/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4149,7 +4149,7 @@ impl Arg {
styled.literal("-");
styled.literal(s);
}
styled.extend(self.stylize_arg_suffix(required).into_iter());
styled.push_styled(&self.stylize_arg_suffix(required));
styled
}

Expand Down
183 changes: 50 additions & 133 deletions src/builder/styled_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,13 @@
///
/// 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,
}
#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct StyledStr(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(),
}
Self(String::new())
}

/// Display using [ANSI Escape Code](https://en.wikipedia.org/wiki/ANSI_escape_code) styling
Expand Down Expand Up @@ -66,143 +52,111 @@ impl StyledStr {
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();
self.0 = self.0.trim().to_owned()
}

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 replace_newline_var(&mut self) {
self.0 = self.0.replace("{n}", "\n");
}

#[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);
}
self.0.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);
}
self.0 = self.0.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 new = String::with_capacity(self.0.len());

let mut last = 0;
let mut wrapper = crate::output::textwrap::wrap_algorithms::LineWrapper::new(hard_width);
for (_, content) in self.iter_mut() {
let mut total = Vec::new();
for content in self.iter_text() {
// Preserve styling
let current = content.as_ptr() as usize - self.0.as_str().as_ptr() as usize;
if last != current {
new.push_str(&self.0.as_str()[last..current]);
}
last = current + content.len();

for (i, line) in content.split_inclusive('\n').enumerate() {
if 0 < i {
// start of a section does not imply newline
// reset char count on newline, skipping the start as we might have carried
// over from a prior block of styled text
wrapper.reset();
}
let line = crate::output::textwrap::word_separators::find_words_ascii_space(line)
.collect::<Vec<_>>();
total.extend(wrapper.wrap(line));
new.extend(wrapper.wrap(line));
}
let total = total.join("");
*content = total;
}
if last != self.0.len() {
new.push_str(&self.0.as_str()[last..]);
}
new = new.trim_end().to_owned();

self.trim_end();
self.0 = new;
}

#[cfg(feature = "color")]
fn stylize_(&mut self, style: Option<Style>, msg: String) {
if !msg.is_empty() {
self.pieces.push((style, msg));
use std::fmt::Write as _;

let style = style.map(|s| s.as_style()).unwrap_or_default();
let _ = write!(self.0, "{}{}{}", style.render(), msg, style.render_reset());
}
}

#[cfg(not(feature = "color"))]
fn stylize_(&mut self, _style: Option<Style>, msg: String) {
self.pieces.push_str(&msg);
self.0.push_str(&msg);
}

#[inline(never)]
#[cfg(feature = "help")]
pub(crate) fn display_width(&self) -> usize {
let mut width = 0;
for (_, c) in self.iter() {
for c in self.iter_text() {
width += crate::output::display_width(c);
}
width
}

#[cfg(feature = "help")]
pub(crate) fn is_empty(&self) -> bool {
self.pieces.is_empty()
self.0.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 = "help")]
pub(crate) fn as_styled_str(&self) -> &str {
&self.0
}

#[cfg(feature = "color")]
pub(crate) fn into_iter(self) -> impl Iterator<Item = (Option<Style>, String)> {
self.pieces.into_iter()
pub(crate) fn iter_text(&self) -> impl Iterator<Item = &str> {
anstream::adapter::strip_str(&self.0)
}

#[cfg(not(feature = "color"))]
pub(crate) fn into_iter(self) -> impl Iterator<Item = (Option<Style>, String)> {
[(None, self.pieces)].into_iter()
pub(crate) fn iter_text(&self) -> impl Iterator<Item = &str> {
[self.0.as_str()].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());
}
pub(crate) fn push_styled(&mut self, other: &Self) {
self.0.push_str(&other.0);
}

#[cfg(feature = "color")]
pub(crate) fn write_colored(&self, buffer: &mut dyn std::io::Write) -> std::io::Result<()> {
for (style, content) in &self.pieces {
let style = style.map(|s| s.as_style()).unwrap_or_default();
ok!(style.write_to(buffer));
ok!(buffer.write_all(content.as_bytes()));
ok!(style.write_reset_to(buffer));
}
pub(crate) fn write_to(&self, buffer: &mut dyn std::io::Write) -> std::io::Result<()> {
ok!(buffer.write_all(self.0.as_bytes()));

Ok(())
}
Expand Down Expand Up @@ -245,29 +199,11 @@ impl From<&'_ &'static str> for StyledStr {
}
}

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));
for part in self.iter_text() {
part.fmt(f)?;
}

Ok(())
Expand All @@ -282,14 +218,7 @@ struct AnsiDisplay<'s> {
#[cfg(feature = "color")]
impl std::fmt::Display for AnsiDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
for (style, content) in &self.styled.pieces {
let style = style.map(|s| s.as_style()).unwrap_or_default();
ok!(style.render().fmt(f));
ok!(content.fmt(f));
ok!(style.render_reset().fmt(f));
}

Ok(())
self.styled.0.fmt(f)
}
}

Expand All @@ -305,18 +234,6 @@ pub(crate) enum Style {
}

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,
}
}

#[cfg(feature = "color")]
fn as_style(&self) -> anstyle::Style {
match self {
Expand Down
10 changes: 5 additions & 5 deletions src/error/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ impl ErrorFormatter for RichFormatter {
styled.none("\n");
styled.none(TAB);
styled.good("note: ");
styled.extend(suggestion.iter());
styled.push_styled(suggestion);
}
}

let usage = error.get(ContextKind::Usage);
if let Some(ContextValue::StyledStr(usage)) = usage {
put_usage(&mut styled, usage.clone());
put_usage(&mut styled, usage);
}

try_help(&mut styled, error.inner.help_flag);
Expand Down Expand Up @@ -377,7 +377,7 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) ->
pub(crate) fn format_error_message(
message: &str,
cmd: Option<&Command>,
usage: Option<StyledStr>,
usage: Option<&StyledStr>,
) -> StyledStr {
let mut styled = StyledStr::new();
start_error(&mut styled);
Expand All @@ -400,9 +400,9 @@ fn singular_or_plural(n: usize) -> &'static str {
}
}

fn put_usage(styled: &mut StyledStr, usage: StyledStr) {
fn put_usage(styled: &mut StyledStr, usage: &StyledStr) {
styled.none("\n\n");
styled.extend(usage.into_iter());
styled.push_styled(usage);
}

pub(crate) fn get_help_flag(cmd: &Command) -> Option<&'static str> {
Expand Down
2 changes: 1 addition & 1 deletion src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ impl Message {
let mut message = String::new();
std::mem::swap(s, &mut message);

let styled = format::format_error_message(&message, Some(cmd), usage);
let styled = format::format_error_message(&message, Some(cmd), usage.as_ref());

*self = Self::Formatted(styled);
}
Expand Down
8 changes: 3 additions & 5 deletions src/output/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,23 @@ impl Colorizer {
}
};

self.content.write_colored(writer)
self.content.write_to(writer)
}

#[cfg(not(feature = "color"))]
pub(crate) fn print(&self) -> std::io::Result<()> {
use std::io::Write;

// [e]println can't be used here because it panics
// if something went wrong. We don't want that.
match self.stream {
Stream::Stdout => {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
write!(stdout, "{}", self)
self.content.write_to(&mut stdout)
}
Stream::Stderr => {
let stderr = std::io::stderr();
let mut stderr = stderr.lock();
write!(stderr, "{}", self)
self.content.write_to(&mut stderr)
}
}
}
Expand Down
12 changes: 3 additions & 9 deletions src/output/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,15 @@ pub(crate) fn write_help(writer: &mut StyledStr, cmd: &Command, usage: &Usage<'_
debug!("write_help");

if let Some(h) = cmd.get_override_help() {
writer.extend(h.iter());
writer.push_styled(h);
} else {
#[cfg(feature = "help")]
{
use super::AutoHelp;
use super::HelpTemplate;
if let Some(tmpl) = cmd.get_help_template() {
for (style, content) in tmpl.iter() {
if style.is_none() {
HelpTemplate::new(writer, cmd, usage, use_long)
.write_templated_help(content);
} else {
writer.stylize(style, content);
}
}
HelpTemplate::new(writer, cmd, usage, use_long)
.write_templated_help(tmpl.as_styled_str());
} else {
AutoHelp::new(writer, cmd, usage, use_long).write_help();
}
Expand Down
Loading

0 comments on commit 11583e1

Please sign in to comment.