davecheney / httpstat

It's like curl -v, with colours.
MIT License
6.96k stars 382 forks source link

Increase separation of concerns in colour formatting code #45

Closed davecheney closed 7 years ago

davecheney commented 7 years ago

Currently the formatting of coloured output is handled before passing the string to fmt.Printf. As an exercise I would like to see if it is possible to make the type of the argument passed to fmt.Printf implement fmt.Formatter and handle the colorisation of the output at the point at which it is printed.

note to implementors: this has no impact on the performance or memory usage of this program, in fact it'll probably make it infinitesimally slower and larger. I'm mainly interested in exploring this to improve the separation of concerns between the code that prepares the output, and the code that colourises it.

davecheney commented 7 years ago

/cc @ChrisHines you might enjoy this challenge. I think we might end up having to create a separate type for each color (there are a limited number in use, so this isn't so gross) so that we can still have something that is a string, but also has a Format method. Something like

type Green string 

func (g Green) Format(f fmt.State, c rune) { ... }

I'd love to be able to do this with a function, but I don't know how just yet. Something like

func Green(s string) fmt.Formatter { ... }

Which sounds straight forward, but not in a way that the underlying value in the interface is directly covertable to a string, for example, if Green is a type, then we can do

greenGreeting := Green("hello") + " world"

But if it's a function that returns a fmt.Formatter, the above won't work.

ChrisHines commented 7 years ago
type Green string

func (g Green) Format(f fmt.State, c rune) { ... }

greenGreeting := Green("hello") + " world"
fmt.Println(greenGreeting)

What part of the output from the above code should be green? It seems like the intent is that only "hello" would be green, but greenGreeting has type Green, so the whole message would get formatted by Green.Format which would not be able to tell what portion should be green.

Also, the following wouldn't work, and this inconvenience seems to dilute the value of the approach somewhat.


type Green string

type Blue string

func main() {
    greeting := Green("hello") + Blue(" world") // invalid operation: Green("hello") + Blue(" world") (mismatched types Green and Blue)
    fmt.Printf(greeting)
}
davecheney commented 7 years ago

I'm sorry, what I wrote previosly kind of got me high on my own supply. What we have now is something like this (simplified to remove the unimportant parts)

name, value := "Connection", "close"
fmt.Printf("%s: %s\n", Green(name), Cyan(value))

Which is all fine and good, but what is happening is fmt.Printf is called with something like this

fmt.Printf("%s: %s\n", "\x1b32mConnection\x1b[0m", "\x1b[36mclose\x1b[0m")

Which to my mind has mixed the data and the presentation up. What I'd like to see is something equivalent to

fmt.Printf("%s: %s\n", fmt.Formatter(Green("Connection")), fmt.Formatter(Cyan("close")))

(The fmt.Formatter conversions are not necessary, just demonstrating that the thing that comes out of Green/Cyan should be a fmt.Formatter)

And if possible the result from Green("Connection") was still convertible to a string, even if it required an explicit conversion.

var s string = string(Green("hello"))

The goal is to push the decision on which terminal formatting codes to apply to the data until as late as possible, way down in the guts of the fmt.Printf.

ChrisHines commented 7 years ago

Two questions.

  1. If Green("hello") implemented fmt.Stringer (in addition to fmt.Formatter), does that qualify as "convertible to a string"?
  2. In general, does the solution need to skip color output when writing to a non-tty?
davecheney commented 7 years ago
  1. Yes, and in that case there probably isn't a requirement to implement both.
  2. Yes

On Mon, 26 Sep 2016, 00:46 Chris Hines notifications@github.com wrote:

Two questions.

1.

If Green("hello") implemented fmt.Stringer (in addition to fmt.Formatter), does that qualify as "convertible to a string"? 2.

In general, does the solution need to skip color output when writing to a non-tty?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/davecheney/httpstat/issues/45#issuecomment-249425749, or mute the thread https://github.com/notifications/unsubscribe-auth/AAAcA5v43gj30NVWpXqwDIqdatoNbYnLks5qtokzgaJpZM4KF0ZZ .

dayvonjersen commented 7 years ago

Is this along the lines of what you were thinking?

https://play.golang.org/p/REwtOS6cLs

(escape codes work in terminal but not in playground)

ChrisHines commented 7 years ago

I just remembered once seeing a package that tackled this problem by providing a specialized version of the fmt functions. The package is at https://github.com/nhooyr/color, but the README now says it's deprecated as of last month. I post it here in case it stimulates any new ideas.

davecheney commented 7 years ago

I experimented with implementing different coloured strings as types but the result was a net increase in lines and I still had too much generation of a string, adding color tags, passing it around as a string.

I'm unhappy that the presentation (colouring) logic is smeared through the business logic, but I'll suck it up as the alternative is more verbose.