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

'Render' colours in REPL #514

Open
tecosaur opened this issue Sep 2, 2021 · 15 comments
Open

'Render' colours in REPL #514

tecosaur opened this issue Sep 2, 2021 · 15 comments

Comments

@tecosaur
Copy link

tecosaur commented Sep 2, 2021

Hello, I was having trouble imagining what various colours looked like in the REPL so I cooked up a little code to show me the colours. Should there be interest, I'd be happy to turn this into a PR.

Result

image

Code

colorcode(rgb::Vector{<:Integer}, background=false) =
    string("\e[", if background "48" else "38" end,
           ";2;", rgb[1], ";", rgb[2], ";", rgb[3], "m")

colorcode(c::RGB, background=false) =
    colorcode(reinterpret.([red(c), green(c), blue(c)]), background)

colorcode(c::RGB{Float64}, background=false) =
    colorcode(round.(Int, 255 .* [red(c), green(c), blue(c)]), background)

colorcode(c::Colorant, background=false) =
    colorcode(color("#"*hex(c, :rrggbb)), background)

if ENV["COLORTERM"] == "truecolor"
    import Base.show
    function show(io::IO, ::MIME"text/plain", c::Colorant)
        print(colorcode(c, true), "  \e[0m ")
        show(io, c)
    end
    function show(io::IO, ::MIME"text/plain", cs::Vector{<:Colorant})
        println(join(colorcode.(cs,true), if length(cs) <= 40 "  " else " " end))
    end
    function show(io::IO, ::MIME"text/plain", cs::Array{<:Colorant, 2})
        for csrow in eachrow(cs)
            println(join(colorcode.(csrow,true), if size(cs,2) <= 40 "  " else " " end))
        end
    end
end
@kimikage
Copy link
Collaborator

kimikage commented Sep 2, 2021

I think this is a good idea, but the similar feature is available in ImageInTerminal.jl.

For example, if you want to copy and paste the output into a GitHub issue, the current "plain" text is more beneficial.
I think there needs to be an easy way for users to specify which output they want, and loading or not loading the package is one of the easiest ways.

Also, the conversion to ANSI 256 (240) colors will be supported in Colors v0.13. (cf. issue #473)

cc: @johnnychen94

@kimikage
Copy link
Collaborator

kimikage commented Sep 3, 2021

BTW, @tecosaur, where did you learn the usage of color("#"*hex(c, :rrggbb))? It was deprecated 6 years ago and is going to be removed in v0.13 (cf. PR #484). I'm worried that there might be some obsolete documentation left.

@tecosaur
Copy link
Author

tecosaur commented Sep 4, 2021

I think this is a good idea, but the similar feature is available in ImageInTerminal.jl.

Ah, I hadn't heard of that. Looking at it, it seems like a fancier version of what I've done here, particularly with the Sixel support.

Given how easy it is to add this or something like it though, I personally feel like it would be nice to have some basic color preview support when just using Colors.jl. ImageInTerminal.jl would then be handy if you say wanted to see images with Sixels, which does seem outside what you'd want to have in Colors.jl.

if you want to copy and paste the output into a GitHub issue, the current "plain" text is more beneficial.

Mmm, though for copy-pasting the single-colour versions are basically the same - there are just 3 extra spaces at the start, so for that representation at least I see no downsides. It is also beneficial when seeing a range of colours, though much less compact of course.

image

As no information is lost here, I'd advocate for this being part of Colors.jl, if nothing else.

I think there needs to be an easy way for users to specify which output they want

I've actually just had an idea, inspired by how vectors are displayed. The above color-on-each-line-with-info is analogous to
image
with that in mind, I consider the adjoint
image
it's a rather useless way of representing colours AFAICT (usually only 2-3 are shown), so perhaps when showing them "sideways" we could squash the colour info out for the more compact view? I feel like it could be nice to trivially be able to view both representations.
image

All one needs to make it work like this is change the type signature in the last two show functions, e.g. to show(io::IO, ::MIME"text/plain", cs::Adjoint{<:Colorant, <:Vector{<:Colorant}}).

Also, the conversion to ANSI 256 (240) colors will be supported in Colors v0.13

Ah, nice. When I implemented this in a package of my own I just copied tmux's color conversion, and that's worked well for me so I think :)

where did you learn the usage of color("#"*hex(c, :rrggbb))

Well, I want r, g, and b values for any colour and convert(RGB, colorant"hsl(120, 100%, 25%)") didn't work so I tried ?hex noticed the hex(..., :rrbbgg) example, tried it, and so wrapped it with color("#"* ... ). I assume there's a better way to do it?

@kimikage
Copy link
Collaborator

kimikage commented Sep 5, 2021

I personally feel like it would be nice to have some basic color preview support when just using Colors.jl

I agree with you. So, I think the key issue is how to avoid annoying troubles.

As you said, the issue of loss of information (i.e. copy-pasting issue) will not be a trouble as long as the color swatches and text are written together. In the case of "sideways", why not also add one line to display both?

The main trouble here is unintentional output of ANSI escape codes in environments that do not support color display.

At least, we would need to follow the :color IO property (get(io, :color, false)). However, even if color display is partially supported, there is no reliable way to determine whether 256 or 24-bit colors is supported.
Referencing environment variables, as in your example, is one way to do it. Even with that way, the evaluation of the variables should be done at "run-time".

Another trouble is compatibility with ImageInTerminal.jl (e.g. the redefinition of methods), but this can be solved with additional modifications on the ImageInTerminal.jl side.

, and so wrapped it with color("#"* ... ). I assume there's a better way to do it?

color("string") should be replaced with parse(Colorant, "string"). However, there is no need to convert a color via a string for this feature. You can just convert the color to RGB (RGB24).

@tecosaur
Copy link
Author

tecosaur commented Sep 5, 2021

This doesn't do anything for 256-colors, but other than that I think I've come up with a model that should resolve all the other concerns.

When get(io, :color, false) == true and ENV["COLORTERM"] == "truecolor"

image

I feel like this strikes a nice balance between being compact/swatchy and informative/copy-pastable. This may not improve the experience for people on terminals that don't set COLORTERM appropriately, or only support 256-color, but their experience is no worse, just not improved — so I'd consider this a "win" overall.

Otherwise

image

As far as I can tell, this is identical to the current behaviour.

Code

colorcode(rgb::Vector{<:Integer}, background=false) =
    string("\e[", if background "48" else "38" end,
           ";2;", rgb[1], ";", rgb[2], ";", rgb[3], "m")

colorcode(c::Union{RGB, RGB24}, background=false) =
    colorcode(reinterpret.([red(c), green(c), blue(c)]), background)

colorcode(c::RGB{Float64}, background=false) =
    colorcode(round.(Int, 255 .* [red(c), green(c), blue(c)]), background)

colorcode(c::Colorant, background=false) =
    colorcode(convert(RGB24, c), background)

import Base.show
function show(io::IO, ::MIME"text/plain", c::Colorant)
    if get(io, :color, false) && ENV["COLORTERM"] == "truecolor"
        print(colorcode(c, true), "  \e[0m ")
    end
    show(io, c)
end
using LinearAlgebra # for the Adjoint type
function show(io::IO, ::MIME"text/plain", cs::Adjoint{<:Colorant, <:Vector{<:Colorant}})
    summary(io, cs)
    print(":\n")
    if get(io, :color, false) && ENV["COLORTERM"] == "truecolor"
        print(" ", join(colorcode.(cs,true), if length(cs) <= 40 "  " else " " end),
              if length(cs) <= 40 "  \e[0m" else " \e[0m" end)
    else
        Base.print_array(IOContext(io, :SHOWN_SET => cs), cs)
    end
end
function show(io::IO, ::MIME"text/plain", cs::Matrix{<:Colorant})
    summary(io, cs)
    print(":\n")
    if get(io, :color, false) && ENV["COLORTERM"] == "truecolor"
        for csrow in eachrow(cs)
            println(" ", join(colorcode.(csrow,true),
                              if size(cs,2) <= 40 "  " else " " end),
                    if size(cs,2) <= 40 "  \e[0m" else " \e[0m" end)
        end
    else
        Base.print_array(IOContext(io, :SHOWN_SET => cs), cs)
    end
end

@johnnychen94
Copy link
Member

johnnychen94 commented Sep 5, 2021

I like the new ANSI color + value view. ImageInTerminal and perhaps UnicodePlots (cc: @t-bltg) could also benefit from this proposal if the color encoder is implemented here (or maybe https://github.com/KristofferC/Crayons.jl).

There are two things to note from my ImageInTerminal maintenance experience:

The show method, however, should perhaps live in ImageInTerminal because Colors.jl focus on pixel/colorant level, also because the sixel encoding only makes sense at the array level. If we want to have show method here then we need to provide a way to allow IIT to override the behavior using Sixel encoding.

The ANSI color + value view only makes sense if we're looking at a very small image.

@kimikage
Copy link
Collaborator

kimikage commented Sep 5, 2021

I don't have a clear opinion on whether show for text/plain should be implemented in Colors.jl. However, I think it is too tricky to specialize show for Adjoint anyway.
(The implementation issues of the colorcode and ENV["COLORTERM"] above should be reviewed in the PR stage.)

@tecosaur
Copy link
Author

tecosaur commented Sep 9, 2021

Sixel stuff should definitely be left to ImageInTerminal, and I think it would be nice if Colours.jl had some swatch-ing. Beyond that, I have no strong opinions. Let me know what you'd like me to put in a PR, and I'll make one.

@kimikage
Copy link
Collaborator

IMO, it would be a good idea to submit a PR to add a swatch using ANSI escape codes for "single" Color first.

In that PR, you could design the logic and API to determine whether or not to add a swatch, and whether it uses 24-bit colors or 256 colors.
ImageInTerminal.jl (indirectly) depends on Colors.jl, so ImageInTerminal.jl should be able to reuse that API. In other words, an API that is compatible with the current API of ImageInTerminal.jl would be preferable.
(Also, supporting the environment variables defined by Crayons.jl may improve the usability.)

@kimikage
Copy link
Collaborator

kimikage commented Sep 10, 2021

Perhaps it might be useful to define a function like:

print_swatch([io,] color::Union{Color, AbstractArray{<:Color}}; width=2, mode=:auto)

to replace:

print(colorcode(c, true), " \e[0m ")

@FedeClaudi
Copy link

FedeClaudi commented Apr 2, 2022

You could use Term to build a widget to visualize colors too.

See: https://fedeclaudi.github.io/Term.jl/stable/basics/colors/

Playing around with things I was able to make this:
image

It should be very easy to crate the kind of visualizations you're after.

@kimikage
Copy link
Collaborator

Currently, there are already multiple means of displaying colors on the terminal.
And with the addition of StyledStrings to stdlib, the terminal will become even more colorful in the future.

In view of this, wouldn't it be a hindrance to customize the show in Colors.jl?
As mentioned above, there is no problem with adding Colors.jl "original" APIs such as print_swatch.

@tecosaur
Copy link
Author

tecosaur commented May 6, 2024

As I see it, it would be nice for Colors.jl to do something a bit nicer OOTB, and with StyledStrings it is possible to do so while resolving quite a few of the concerns from earlier. For instance,

At least, we would need to follow the :color IO property (get(io, :color, false)). However, even if color display is partially supported, there is no reliable way to determine whether 256 or 24-bit colors is supported.

is now fully handled by StyledStrings + the new terminfo handling.

This doesn't preclude other packages from doing even fancier colour rendering at all, they can implement custom display methods and engage in method overwriting. What we do get though is a prettier OOTB experience 🙂

@kimikage
Copy link
Collaborator

kimikage commented May 6, 2024

they can implement custom display methods and engage in method overwriting.

Generally they cannot. That is my concern.
One possibility would be to use Preferences.jl. (cf. #535)

@tecosaur
Copy link
Author

tecosaur commented May 6, 2024

they can implement custom display methods and engage in method overwriting.

Generally they cannot. That is my concern.

Hmmm? You absolutely can, just not during precompilation.

One possibility would be to use Preferences.jl

If there's really a need to be able to turn it off, then that does seem like a reasonable approach 👍.

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

No branches or pull requests

4 participants