Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support color specifiers #231

Closed
nicola-gigante opened this issue Nov 19, 2015 · 20 comments
Closed

Support color specifiers #231

nicola-gigante opened this issue Nov 19, 2015 · 20 comments

Comments

@nicola-gigante
Copy link

It would be great to be able to specify a colors using ANSI codes for messages meant to be printed to a terminal.

Something like:

fmt::print(std::cout, "Colored: {:red}", "Hello world");

I need this feature because I want my tool to be able to produce colored output for better readability.
I have something like the following working in my codebase:

fmt::print(std::cout, "Colored: {}", colored<Red>("Hello world"));

The colored function returns an helper object with a suitable operator<< overload that outputs the argument surrounded by the correct ANSI codes.

The problem with this approach is that, ideally, the tool should not produce ANSI codes if cout is not attached to a TTY, for example if the program has been launched with a shell redirection or in a pipe.
This is easily detectable with the POSIX isatty() function (and something similar on Windows), but I cannot use it because my custom operator<< is not called on the actual output stream, but rather on an intermediary string buffer (I don't know the library implementation, but I guess it is a BasicWriter or something like that, right?). This means I cannot detect the TTY from the stream operator.

Note also that the decision to turn on or off the colors cannot be done ahead for the entire program, because the output and error streams could be redirected to different devices. We may have the output stream redirected to a file and the error still connected to the terminal. In this case, messages printed on std::cerr should still print color codes, and this means the decision has to be taken at each invocation based on which stream is being used.

All of this to say that this feature is not implementable by the user outside of the library, and a built-in support for ANSI colors with proper handling of TTYs would be great. An alternative would be to expose a way for custom stream operators to get the actual output stream passed to fmt::print, but I cannot think of an easy one.

@Naios
Copy link
Contributor

Naios commented Nov 19, 2015

Amazing idea

@Spartan322
Copy link

ANSI escape codes do not work for Win32 consoles. This may create inconsistent behavior in the console (if anyone actually cares), unless you are handling those cases separately.

@nicola-gigante
Copy link
Author

I don't know about ANSI code in particular, but git for example does support colored output on Windows, so some way to achieve the same effect should exist. This is another reason why builtin support for this feature would be awesome.

Anyway, colored output is usually used to enhance readability of output, without adding information that would be lost without colors. For this reason, not supporting colors on some platforms would not be a blocking issue for this feature, I think.

@Spartan322
Copy link

ANSI escape codes are specifically not supported by Win32 consoles. The only way to guarantee support is to do it through the hardware, also, this will probably be a problem for any other OS with a console that doesn't have ANSI escape code support. (that is if you care about color on consoles being a problem, but if that is the case, you should not do strictly colors for readability)

@nicola-gigante
Copy link
Author

Yes, I understand that ANSI codes are not supported. Nevertheless, colored output is possible, just with a different mechanism than outputting ANSI codes.
This python package does exactly that on Windows and is linker from that same wikipedia article.

Anyway, I think a non colored output on windows is not a problem, if it is difficult to achieve. It is sufficient to say it in the docs.

@vitaut
Copy link
Contributor

vitaut commented Nov 19, 2015

Interesting idea. There has been some work in this direction, so you can do

fmt::print_colored(fmt::RED, "Elapsed time: {0:.2f} seconds", 1.23);

right now. However, this only works for complete messages.

I think specifying the color in the format string

fmt::print(std::cout, "Colored: {:red}", "Hello world");

might slow down parsing for the common case of not using colors and requires hardcoding all the colors in the parser, so I'd go with the second option (especially since you already have it working) which can even be shortened with an appropriate alias:

fmt::print(std::cout, "Colored: {}", fmt::red("Hello world"));

I think the information of whether the output device is a terminal can be passed via BasicWriter with small changes to this template and the print functions. I'll post some details in the next comment, because I am about to leave now.

@vitaut
Copy link
Contributor

vitaut commented Nov 19, 2015

To add some substance, here's how it could look like:

template <typename Char>
class BasicWriter {
  ...
  virtual bool isatty() const { return false; }
};

FMT_FUNC void fmt::print(std::FILE *f, CStringRef format_str, ArgList args) {
  class FileWriter : public MemoryWriter {
   private:
    std::FILE *file_;
   public:
    explicit FileWriter(std::FILE *f) : file_(f) {}
    bool isatty() const { return ::isatty(fileno(file_)); }
  };

  FileWriter w(f);
  w.write(format_str, args);
  std::fwrite(w.data(), 1, w.size(), f);
}

Then you could provide an overload of the format function for the colored type:

template <typename Char, typename T>
void format(BasicFormatter<Char> &f, const Char *&format_str, const colored<T, ?> &c) {
  // Check if writing to a terminal with f.writer().isatty() and do the formatting.
}

What do you think?

@vitaut
Copy link
Contributor

vitaut commented Nov 19, 2015

BTW there was some work on colored output for Windows in #102.

@Spartan322
Copy link

@vitaut I was simply pointing out ANSI escape codes are not supported by Win32 consoles, so you couldn't designate them that way. The next best way (that I can recall) is to force the writer to be colored while writing a certain section, but that probably results in a slower parse.

@vitaut
Copy link
Contributor

vitaut commented Nov 20, 2015

On a second thought, calling isatty for every colored argument is probably an overkill and it doesn't guarantee that the stream is associated with a terminal in a multi-threaded program anyway. It's better to do this check once per writer instead.

@nicola-gigante
Copy link
Author

@vitaut Yes, that seems a good solution!

Anyway, is it possible to redirect a file descriptor after its creation? I don't think so. In this case, checking isatty once per writer is enough!

@nicola-gigante
Copy link
Author

At the end, do we agree on the shape of this feature? I can contribute a patch myself but I'd like to know if it's ok for everyone.

@vitaut
Copy link
Contributor

vitaut commented Nov 25, 2015

I can't say that I'm completely happy about the isatty solution for two reasons:

  • It is somewhat ad hoc and introduces a method required to solve only this particular issue. It would be nice to have something more general if possible.
  • Portability: as correctly pointed out by @Spartan322, Windows doesn't support ANSI escape codes and requires use of completely different APIs.

A more general solution would be something along the lines of parameterizing the custom argument format function not only on argument type, but also on Writer type. Then we could have TerminalWriter which knows how to write colored output to a terminal in a portable way and the argument format function could use it directly. This is just an idea, I haven't thought out the details yet.

Another option that doesn't require any changes to the library is to add TerminalWriter and use dynamic cast:

template <typename Char, typename T>
void format(BasicFormatter<Char> &f, const Char *&format_str, const colored<T, ?> &c) {
    if (TerminalWriter *w = dynamic_cast<TerminalWriter*>(&f.writer())) {
      // do colored output
    } else ; // ignore color
}

@vitaut
Copy link
Contributor

vitaut commented Dec 2, 2015

Some initial work to support custom formatters/writers: https://github.com/cppformat/cppformat/tree/custom-formatter. Once complete it will be possible to extend the current formatter for colored output or anything else.

@vitaut
Copy link
Contributor

vitaut commented Dec 4, 2015

Here's an example of how to implement this feature using the new custom formatter functionality that I've just merged into the master branch:

#include "fmt/format.h"
#include <unistd.h>

class ColoredFormatter : public fmt::BasicFormatter<char> {
 private:
  bool colorize_;

 public:
  ColoredFormatter(const fmt::ArgList &args, fmt::Writer &w, bool colorize)
  : BasicFormatter<char>(args, w), colorize_(colorize) {}

  bool colorize() const { return colorize_; }
};

template <typename T>
struct Colored {
  const T &value;
  fmt::Color color;
};

template <typename T>
Colored<T> colored(const T &value, fmt::Color color) {
  Colored<T> colored = {value, color};
  return colored;
}

template <typename T>
void format(ColoredFormatter &f, const char *&format_str, const Colored<T> &c) {
  fmt::Writer &w = f.writer();
  if (f.colorize()) {
    char escape[] = "\x1b[30m";
    escape[3] = static_cast<char>('0' + c.color);
    w << escape;
  }
  f.writer() << c.value;
  if (f.colorize())
    w << "\x1b[0m";
}

void print_colored(std::FILE *f, const char *format_str, fmt::ArgList args) {
  fmt::MemoryWriter w;
  ColoredFormatter(args, w, isatty(fileno(f))).format(format_str);
  std::fputs(w.c_str(), f);
}

template <typename... Args>
void print_colored(std::FILE *f, const char *format_str, const Args & ... args) {
  typedef fmt::internal::ArgArray<sizeof...(Args)> ArgArray;
  typename ArgArray::Type array{ArgArray::template make<ColoredFormatter>(args)...};
  print_colored(f, format_str,
    fmt::ArgList(fmt::internal::make_type(args...), array));
}

int main() {
  print_colored(stdout, "{}", colored(42, fmt::RED));
}

This can be extended to work on Windows.

@nicola-gigante, will this work for you?

@nicola-gigante
Copy link
Author

@vitaut
Sorry for the lag. Yes this is exactly what I need :)

@vitaut
Copy link
Contributor

vitaut commented Dec 12, 2015

Thanks for the confirmation. I'm closing this but note that the part

  typename fmt::internal::ArgArray<sizeof...(Args)>::Type array;
  print_colored(f, format_str,
    fmt::internal::make_arg_list<ColoredFormatter>(array, args...));

will change a bit because it currently relies on some internal APIs.

@vitaut vitaut closed this as completed Dec 12, 2015
@nicola-gigante
Copy link
Author

Ok, don't worry. Thanks for your help :)

@vitaut
Copy link
Contributor

vitaut commented May 7, 2016

I've updated the example to work with the current API.

@Munken
Copy link

Munken commented Jan 25, 2017

Just a small note. In order to compile one needs to replace format() with format_arg()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants