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

color scheme support #41435

Closed
StefanKarpinski opened this issue Jul 1, 2021 · 41 comments · Fixed by #49586
Closed

color scheme support #41435

StefanKarpinski opened this issue Jul 1, 2021 · 41 comments · Fixed by #49586
Labels
display and printing Aesthetics and correctness of printed representations of objects. REPL Julia's REPL (Read Eval Print Loop)

Comments

@StefanKarpinski
Copy link
Sponsor Member

As suggested here: #41423 (comment) it would be good to support theming Julia's colors. There's also #41295 but I think that may overcomplexify things. Without @staticfloat's input, I'm not entirely clear on how using Preferences in Julia would work, whereas I have a pretty clear idea about how adding a config file to theme Julia's colors would work: Instead of hardcoding colors, if Julia is run in color mode, it would look in a file called ~/.julia/config/colors.toml that maps uses of colors as keys to some kind of color description as values. Here are some questions:

  1. What are the color keys? Presumably descriptions of how we use colors.
  2. What are the color values? #123abc strings? ANSI color names? Either?
  3. Do we need anything else in the file or is it just this map?

I think for usability we'd probably also want to have changes to this file take effect immediately, so we'd want to remember the inode, size, mtime and ctime and if any of them changes, reload the file. We also want to take care that if we're not using color the file never needs to be looked at.

@JeffBezanson JeffBezanson added display and printing Aesthetics and correctness of printed representations of objects. REPL Julia's REPL (Read Eval Print Loop) labels Jul 1, 2021
@fredrikekre
Copy link
Member

it would look in a file called ~/.julia/config/colors.toml

I suggested something like ~/.julia/config/REPL.toml, but perhaps these color settings would be used in non-interactive mode too?

@StefanKarpinski
Copy link
Sponsor Member Author

I thought it would be good to have a separate file for color settings since I feel like people will send around nice color themes and it would be good to be able to drop the file into the directory easily without overwriting other REPL settings. Perhaps that suggests a setup more like this: ~/.julia/config/colors/monokai.toml and ~/.julia/config/REPL.toml has a colors = "monokai" entry that picks a name from that directory?

@blackeneth
Copy link

blackeneth commented Jul 10, 2021

I think this is a great idea. It will:

  1. Allow for individual customization to color and font style preferences
  2. Eliminate posting of issues and PRs for aesthetic formatting preferences
  3. Increase accessibility of Julia for users with low vision

The Problems
A lot of issues and PRs are raised due to formatting with color and font styles (normal, bold, italic, underline) (#41295 #41251 #40913 #40228 #30110 #40228 #40230 #40232 #37773 #36134). What one person likes, another may not.

In #40228, for example, user @nickrobinson251 finds that he has trouble seeing the filepath/line location of errors that are printed in muted grey (:light_black). I have no reason not to believe him. If Julia had a modifiable color scheme, he would be empowered to fix it himself.

In addition, the existing color scheme is designed for people with normal vision. The incidence of color blindness is quite high - about 1 in 12 men and 1 in 200 women. 98% of those with color blindness are red-green color blind. Other people may have low vision due to contrast sensitivity or other issues.

How Color Scheme Support Solves the Problems
Users can modify the color scheme to their personal preferences, or to compensate for their particular vision issues. This would be expected to eliminate the posting of issues and pull requests related to changing colors and font weights.

Accessibility of Julia would be improved for people with low vision by allowing customization of color schemes for people with low vision. I would hope and recommend that Julia would ship with four color schemes:

  1. Julia colorful (default)
  2. Red-green colorblind
  3. Blue-yellow colorblind
  4. High-contrast

Technical Decisions

Stephen asked a few questions -- for which I'll give my opinion, plus opinions on a few more things

What needs to be specified?
Both color and font weight (normal, bold, italic, underline)

What format should the file for the color scheme be in?
Stephen mentioned .toml, which is super-simple. I suppose the other choices would be XML (barf)[1] and JSON.

What does other software use? VS Code uses JSON, as they have significant hierarchy; for example:

	workbench.colorCustomizations 
		activityBar.foreground: "#00FFFF"
		activityBar.background: "#00FF88"
		activityBarBridge.foreground: "#00FF00"

It looks like TextMate itself uses XML (barf):

	<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>name</key>
    <string>Example Color Scheme</string>
    <key>settings</key>
    <array>
        <!-- Global settings -->
        <dict>
            <key>settings</key>
            <dict>
                <key>background</key>
                <string>#222222</string>
                <key>foreground</key>
                <string>#EEEEEE</string>
                <key>caret</key>
                <string>#FFFFFF</string>
            </dict>
        </dict>

Should the user-color map completely replace the default, or just overwrite parts of it?
I would say -- it only overwrites the default. If the user only wants to overwrite 1 color, they only need to put one line in their user color scheme.

What if they specify more hierarchy and colors in their color scheme?
I think they should be loaded into the data structure with a warning. It could be they're working on a new piece of syntax that needs new coloring. Otherwise, the new hierarchy and colors would be unknown (and unused) by the existing code.

How should the color values be specified?
I would think hex values, #AABBCC. However, the color value is evaluated, so it could be a function:

struct.color = #6495ed
struct.color = RGB(100,149,237)
struct.color = HSL(0.61,0.79,0.66)
struct.color = HSV(219,58,93)
struct.color = "cornflower blue"

You could require that they wrap it in hex(), or if it comes in non-hex, just convert it for the user. Or, strings of the named colors from the Colors.jl package.
In short: ultimate flexibility

Of course, in addition to color, we have font weight as well:

    struct.fontweight = bold

Anything else in the file?
Maybe. Perhaps users want multiple color schemes in 1 file, with ability to switch between them? Why would they want this? Perhaps:
1. They hot seat swap with another programmer who is color blind
2. They program on different computers, with different quality monitors attached, which vary in their ability to display colors.
3. They want color sets that they can use if they switch the syntax formatting in VS Code -- from Monokai, to Kimbie Dark, or whatever.

Semantic Coloring
I can't say I'm an expert at this, but ... "semantic coloring" appears to run regex-like functions over the output text, then annotate it with color information. VS Code uses TextMate grammars -- see Semantic Highlight Guide. I think Highlights.jl is actually doing semantic highlighting, without saying "semantic highlighting" anywhere in the documentation.

Some issues and pull requests have been input regarding semantic tokens and highlighting - #961, #1168, #847 .

This would appear to be the best way to color output errors, etc. -- but perhaps it's too much to bite off at this point.

In Conclusion
I think this is great project, which will benefit all users, but will especially make Julia more accessible to users with low vision.

[1] I actually think I threw up in my mouth a little bit.

@oscardssmith
Copy link
Member

@blackeneth Nice writeup! The main reason to use TOML is consistency with other parts of the REPL (ie project.toml an dmanifest.toml). One thing to add is that it might provide a lot of value to let people map whole colors to other colors. This would make it much easier to customize if you basically like the built-in color profile, but want to change 1 color that is hard to read with the theme you use (looking at you solarized).

@StefanKarpinski
Copy link
Sponsor Member Author

StefanKarpinski commented Jul 11, 2021

One choice is easy: we are definitely using TOML, not any other configuration/serialization format.

  1. All Pkg and other Julia config files are in TOML;
  2. We already have a TOML parser built in and we have none of the others.

The appropriate way to factor this is to have code that uses colors ask for something with semantic meaning, like "what color/boldness is the REPL prompt for shell mode" and then answer that question based on the config file. It would make sense for this to be open-ended so that packages like OhMyREPL (cc @KristofferC) can be configured this was as well. It would probably make sense to look at how TextMate color scheme work for this.

I'm pretty strongly against putting any kind of executable code in this kind of config file. It's tempting because it's maximally flexible and avoids having to make real design decisions, but it's almost always a bad idea. For one thing, it would make this file a security attack vector 😬. For another it would require re-evaluating the entire file for every color decision since code is not generally pure so it might give different answers over time even if the file hasn't changed. Also, using Colors for this would entail making that package and its dependencies (ColorTypes, FixedPointNumbers, Reexport) all into standard libraries, which would effectively fossilize them.

We should probably also allow specifying display properties beyond than just those that are supported by terminals since we probably want to use the same color configuration mechanism for notebooks in the browser and maybe even other display mechanisms. That suggests maybe looking at CSS for display properties.

@StefanKarpinski
Copy link
Sponsor Member Author

StefanKarpinski commented Jul 11, 2021

Looking at

http://web.simmons.edu/~grabiner/comm244/weekthree/css-basic-properties.html

the properties that make sense to me are:

  • bold
  • italic
  • underline
  • color

I don't think we need to stick with CSS syntax, however. I'm also not sure how much of CSS color syntax to support, there's quite a bit of it:

https://www.w3schools.com/colors/default.asp

Does anyone know if all the ANSI color names are supported in CSS? There are 140 color names supported in CSS. How do color fallbacks work in terminals? If we ask for a specific RGB color and the terminal doesn't support it, what happens? Do we need to specify a fallback or does the terminal automatically use the closest color that it supports?

@blackeneth
Copy link

Decided:

  1. File format TOML
  2. No execution in color scheme file
  3. need colors and display properties (bold, italic, underline)[1]

Given #2, and the other discussion, I believe it is also decided that
4. colors would be specified by hex code (#RRBBGG) or string name; example:

struct.name.color = #6495ed
struct.name.color = "cornflowerblue" 

I would recommend the input be case-insensitive, so #6495Ed and "CornflowerBlue" would also be accepted.

On Color Names

ANSI color names are quite limited, and defined originally for ANSI escape sequences (and here). The ANSI colors are black, red, green, yellow, blue, purple, cyan, and white. Windows supports ANSI escape sequences and 24 bit color.

Most of the "color name lists" out there derive from the original X10R3 list, with later modifications, and is often referred as the "X11 colors". The color names of crayons were often used. The sRGB color space (24 bit) is used.

The named CSS colors are a superset of the SVG 1.1 color names.

Note the comment on the CSS name color section:

Note: these color names are standardized here, not because they are good, but because their use and implementation has been widespread for decades and the standard needs to reflect reality. Indeed, it is often hard to imagine what each name will look like (hence the list below); the names are not evenly distributed throughout the sRGB color volume, the names are not even internally consistent

See “Tomato” versus “#FF6347”—the tragicomic history of CSS color names

There are many other color name dictionaries. Two of the best are probably ISCC-NBS (only uses names, designators, and categories) and Bang (and here) (which has fanciful color names). ISCC-NBS has 267 color names, and Bang has 678. Also popular is Resene 2010 , which has 1,378 color names. You see some of these systems listed in the W3 Color Schemes.

For names, also need to decide if you're going to support "gray" (US) and "grey" (UK) names.

Or, you could keep it super simple and support a very small set of names - red, green, yellow, blue, purple, cyan (6 colors), plus light/dark modifiers for each (12 more colors), plus black, white (2 more), and 8 shades of gray/grey (8 more). Total of 28 names. Every other color can be had by specifying the hex code.


[1] What, no blink support? 😉 [for those too young to have been there, in the early days of the web, blink was over used on web pages and was very annoying -- such that every browser started ignoring the blink HTML tag]

@adigitoleo
Copy link
Contributor

I would love to see a simple way to tell the REPL to use only the basic terminal ANSI colors (either 8 or 16). I think it's more common now with RGB terminals for people to set their preferred colors for the ANSI codes. Currently, there are various places in the REPL where extra colors are being used, e.g. stacktraces. I don't think more colors should necessarily be used just because they are available.

@KristofferC
Copy link
Sponsor Member

Currently, there are various places in the REPL where extra colors are being used, e.g. stacktraces.

Could you be a bit more specific about where those extra colors are being printed? AFAIU, we only use colors from the 16 ANSI colors in default Julia output.

@adigitoleo
Copy link
Contributor

Woops, you're right, it's all using the base 16. I got confused because setting Base.text_colors[:light_black] doesn't seem to change the light black (I think its ANSI 8) that is used for the line numbers in the stacktrace, but I see there are already other issues tracking stacktrace coloring.

@blackeneth
Copy link

Why do we need fanciful color names at all? I can think of at least 3 reasons:

  1. Easier to remember than hex codes (entering code)
  2. Give you an idea of the hue, saturation, and lightness of the color (viewing code)
  3. Could guide users towards good, or at least adequate, choices for color designs

Easier to Remember than Hex Codes

Julia has printstyled, which allows:

color may take any of the values :normal, :default, :bold, :black, :blink, :blue, :cyan, :green, :hidden, :light_black, :light_blue, :light_cyan, :light_green, :light_magenta, :light_red, :light_yellow, :magenta, :nothing, :red, :reverse, :underline, :white, or :yellow or an integer between 0 and 255 inclusive. Note that not all terminals support 256 colors. If the keyword bold is given as true, the result will be printed in bold.

These are mostly the ANSI color names. If you want something else, you can specify an 8-bit color from 0-255.

This is great--I can remember ":blue" and ":red" and make it easy to specify colors if I'm using printstyled.

Give you an idea of the hue, saturation, and lightness of the color

Consider these two mocked-up config files.

Using hex codes:

colors:
	stacktrace.error: #FF0000
	stacktrace.error.title: #000000
	stacktrace.error.text: #B22222
	stacktrace.linenumbers: #00FFFF
	stacktrace.methods.iterator: #FF4500, FFD700, #228B22, #6495ED, #9400D3 
	stacktrace.paths: #A9A9A9
	stacktrace.types: #00CED1
fontmodifiers:
	stacktrace.error: bold
	stacktrace.error.title: normal
	stacktrace.error.text: bold
	stacktrace.linenumbers: normal
	stacktrace.methods.iterator: bold, bold, bold, bold, bold
	stacktrace.paths: italic
	stacktrace.types: normal

In the above, I can figure out the first two are red and black; the rest I have little intuition for.

Using color names:

colors:
	stacktrace.error: Red 
	stacktrace.error.title: Black 
	stacktrace.error.text: FireBrick 
	stacktrace.linenumbers: Cyan 
	stacktrace.methods: OrangeRed, Gold, ForestGreen, CornflowerBlue, DarkViolet
	stacktrace.paths: DarkGrey
	stacktrace.types: DarkTurquoise
fontmodifiers:
	stacktrace.error: bold
	stacktrace.error.title: normal
	stacktrace.error.text: bold
	stacktrace.linenumbers: normal
	stacktrace.methods:bold
	stacktrace.paths: italic
	stacktrace.types: normal 

In the above, I am given better hints as to what the hue, saturation, and lightness is of each color used.
If I'm looking for a color to change for my own custom color config, I would expect to be easier to find the parameter I need to change.

Guide users towards good, or at least adequate, choices for color designs

Users who don't want to learn about color theory may prefer to just choose from the color names available. Given that, the color names given to them could be selected such that the colors have uniform hue, saturation, lightness, and consistent variations thereof. Use of those colors would result is an adequate color design with no egregious problems.

24-bit Color

24 bit color is common today and should be supported. This allows the creation of beautiful color designs. It also provides flexibility for designing color schemes for people with color blindness, contrast sensitivity, or other low vision issues.

I don't think more colors should necessarily be used just because they are available.

This is a graphic design issue. Bad color design is not uncommon. Excess use of colors can lead to an angry fruit salad color design.

@Arkoniak
Copy link
Contributor

Arkoniak commented Aug 5, 2021

If I may say, this issue has actually two different layers:

  1. User level layer, where users have some sort of API (*) to represent their preferred theme.
  2. Code level layer, where values provided through this API actually applied to produce colored output.

(*) I use word API here in a rather generic sense, it can be a configuration file, dispatch functions, dictionaries of colors or anything else.

By "applying colorscheme" i mean all those places, where instead of printstyled("something to print", color = :red) should be used something else and it would be good to understand, what exactly should be used.

I think that while user layer is important, it is in some sense less important than the second layer. And the reason is the following: if I have a way to influence colorschemes some way, I am as a user can create any sort of package with any functionality I desire. Users can make packages which accept hex codes instead of human readable names, or they can use JSON instead of TOML or even they can upload configs from network in some binary form, it doesn't really matter for as long as there is a way to change themes. Of course, some way to work with themes should be presented in Base, so all previous discussions are relevant.

One possible way to organize this second layer is proposed in #41791: you put all your colors in a Ref variable and use them correspondingly and configuration agnostic in a code base. On the other hand, other configuration functions should provide some way to update this variable (by reading TOML file, and providing mapping from TOML configuration to the fields of this variable). Surely, this approach seems too hacky and ad-hoc, so it would be good if something better can be invented.

I am bringing it here, because even if #41791 is considered a bad approach and closed, I am thinking that the idea of decoupling configuration and usage part is important and a proper way to solve this issue.

@goerz
Copy link
Contributor

goerz commented Aug 14, 2021

  1. What are the color keys? Presumably descriptions of how we use colors.

This seems like the most critical issue. Right now, there are lots of hardcoded colors in the standard library and core packages, most notably inTest and Pkg. These seem to be designed for dark background terminals, which is highly problematic, and leads to severe legibility issues on light background terminals. Apart from redefining Julia's internal color names (which is a dirty hack at best), this can partly be mitigated by redefining the ANSI colors in one's terminal emulator. However, e.g. the "Test Summary" is hard-coded as "white", and is always going to be illegible on a white background terminal (you can't reasonably redefine ANSI-White in the terminal emulator, as it would break the legitimate use case of white text on dark background e.g. in a vim, tmux, or mutt status bar).

Thus, all colors in core packages should have semantic keys in some kind of Julia colorscheme configuration. It would probably be a good idea to grep for printstyled in base/stdlib and any Julia Org repos like Pkg.jl to get an idea what semantic keys might be needed.

It would also be great if the default values would take into account that people run both dark and light background terminals. Ideally, Julia could try to detect this, e.g. by looking for the standard COLORFGBG environment variable and use different default colors. It might also be useful to try to detect if the terminal supports more than 16 colors. With 256-color support, it is possible to find red/gree/blue/yellow/orange/magenta shades that are at roughly 50% brightness such that they are legible both in dark and light terminals

@goerz
Copy link
Contributor

goerz commented Aug 14, 2021

Also, the color scheme should automatically transfer to the subprocess that Pkg.test creates — current workarounds of setting colors in startup.jl do not transfer unless the parent julia process is explicitly started with --startup-file=yes. Now, I do normally want Pkg.test to be as isolated as possible, so loading startup.jl is actually not a great idea in general, but in this case I want Pkg.test to have legible colors more than I want test isolation (I happen to not have anything nontrivial in startup.jl)

@goerz
Copy link
Contributor

goerz commented Aug 14, 2021

It would also be great if the default values would take into account that people run both dark and light background terminals. Ideally, Julia could try to detect this, e.g. by looking for the standard COLORFGBG environment variable and use different default colors.

See also #28690

@goerz
Copy link
Contributor

goerz commented Aug 14, 2021

Also, third-party packages should be able to register their own semantic color keys. Absent that, packages with color output would need to maintain their own config files. Having all colors defined in one central location arguably seems like a better solution.

@imciner2
Copy link
Contributor

Also, third-party packages should be able to register their own semantic color keys. Absent that, packages with color output would need to maintain their own config files. Having all colors defined in one central location arguably seems like a better solution.

I don't think merging third-party package colors into the main color scheme file is a good idea. My understanding is that the main file will be environment-independent, so allowing packages to store information there will pollute other environments. I think it is better to have those packages that want colors implement a color scheme in the preference framework for the package (and that could be helped by an additional package someone were to write).

@brett-mahar
Copy link

brett-mahar commented Aug 15, 2021

"Having all colors defined in one central location" can become a disaster. Look at gtk/gnome.

The "central location" has changed about 127 times since I started using gtk2 about 10 years ago. Takes days of trial and error on a new distro to figure out which of the ~/gtkrc*, ~/gtk*/, /usr/share/gtk/, , /usr/local/share/gtk/, /etc/gtk will actually change anything. And impossible to (eg) change the default theme on pacmanfm without also messing up firefox theme.

Please don't do this. Each Julia program that wants to use custom colours should do it itself, within its own source files.

@KristofferC
Copy link
Sponsor Member

KristofferC commented Aug 16, 2021

These seem to be designed for dark background terminals, which is highly problematic, and leads to severe legibility issues on light background terminals.

One big reason why we are using the 16 base colors is that they are already themable! Just go into your terminal option and set them to whatever you want.
So the assumption is that the 16 base colors (modulo black/white) should be visible no matter what theme you have. If you have a white background and yellow set such that you can't read it, then that is arguably a buggy theme and you should just tweak the yellow color to be slightly darker.

@goerz
Copy link
Contributor

goerz commented Aug 16, 2021

That's a very tricky assumption, though. In a true 16 color terminal, those are all the colors you have to work with for both foreground and background. If you make yellow readable on white, you might make it unreadable, e.g., on a dark background in a status bar. I'd say, if you make assumptions about ANSI colors, assume that they are the default RGB colors for the different terminal emulators. I've certainly tweaked the ANSI colors in the past to be more aesthetically pleasing (pure cyan is particularly jarring), but changing their brightness values too much will inevitably break something. No matter how you slice it: at least without looking at COLORFGBG, and without further configuration, it's almost impossible to use more than a couple of colors to good effect. Hence most software allows to configure colors (as does julia with it's environment variables, except that at least Pkg and Test still print colors outside of that configuration).

I do take your point though that if you have to hard code values, using base ANSI colors is better than hardcoding RGB values. Still, by far the best option is to have configuration. Alas, I don't think anyone is really arguing against that, and this issue is just to flesh out the details (and some of my comments are just to create a small sense of urgency based on my personal mild frustration with Julia's color output on my admittedly non-fashionable white background terminal 😉)

P.S.: I retract my comments about having third-party packages plug into the color configuration file.

@StefanKarpinski
Copy link
Sponsor Member Author

StefanKarpinski commented Aug 16, 2021

@brett-mahar, I don't think the fact that Gtk has totally bungled this is a particularly compelling argument for Julia not to have a single file that controls colors. In particular, Linux folks seem to have a fondness for pointless moving files around the file system that Julia has never been prone to. We're perfectly capable of picking a good design and a suitable location and sticking to it.

@blackeneth
Copy link

After a lot of research, I have several updates

Color Scheme File - TOML
Reading the TOML spec, it would be inconvenient to specify colors by name -- the names would have to be in quotes, as they would have to be read in as strings.

So I say abandon the fancy color names idea; just use hex codes. One is not going to be editing the file frequently.

The TOML format would be something like this:

# Julia default color scheme; 24 bit color hex values 

[base.colors]
black= 0x000000
red= 0x800000
green= 0x008000
yellow= 0x808000
blue= 0x008000
magenta= 0x800080
cyan= 0x008080
white= 0xC0C0C0
light_black= 0x808080
light_red= 0xFF0000
light_green= 0x00FF00
light_yellow= 0xFFFF00
light_blue= 0x0000FF
light_magenta= 0xFF00FF
light_cyan= 0x00FFFF
color_normal= 0x000000
default_color_answer= 0x000000
default_color_warn = 0x808000
default_color_error = 0xFF0000
default_color_info = 0x008080
default_color_input= 0x000000
default_color_debug = 0x008000

[base.styles]
normal= "\\033[0m"
bold= "\\033[1m"
underline= "\\033[4m"
blink= "\\033[5m"
reverse= "\\033[7m"
hidden= "\\033[8m"
nothing = ""
JULIA_STACKFRAME_LINEINFO_COLOR = "\\033[1m"
JULIA_STACKFRAME_FUNCTION_COLOR = "\\033[1m"

The above is valid TOML file. You can read it into Julia as shown below. What will be created is a dictionary of dictionaries.

julia> using TOML
julia> fn = "path to your filename" 

julia> myt = TOML.parsefile(fn)
Dict{String, Any} with 1 entry:
  "base" => Dict{String, Any}("styles"=>Dict{String, Any}("normal"=>"\\033[0m", "bold"=>"\\033[1m", "blink"=>"\\033[5m"…

julia> keys(myt)
KeySet for a Dict{String, Any} with 1 entry. Keys:
  "base"

julia> myt2=get(myt,"base","")
Dict{String, Any} with 2 entries:
  "styles" => Dict{String, Any}("normal"=>"\\033[0m", "bold"=>"\\033[1m", "blink"=>"\\033[5m", "reverse"=>"\\033[7m", "…
  "colors" => Dict{String, Any}("light_blue"=>255, "white"=>12632256, "light_red"=>16711680, "green"=>32768, "light_gre…

julia> get(myt2,"styles","")
Dict{String, Any} with 9 entries:
  "normal"                          => "\\033[0m"
  "bold"                            => "\\033[1m"
  "blink"                           => "\\033[5m"
  "reverse"                         => "\\033[7m"
  "hidden"                          => "\\033[8m"
  "underline"                       => "\\033[4m"
  "JULIA_STACKFRAME_LINEINFO_COLOR" => "\\033[1m"
  "JULIA_STACKFRAME_FUNCTION_COLOR" => "\\033[1m"
  "nothing"                         => ""

Note how the TOML reader converted the Hex values to integers!

Users could customize colors by putting whatever keys they want to change in a .julia/CONFIG/color_customization.toml file. Only the keys they want to change would need to be present, not all keys.

Note: While there is 1 Julia "default" color scheme, there would also be 3 more to support users with low vision:

  • Red-green colorblind
  • blue-yellow colorblind
  • high-contrast

About 10% of people have colorblindness, and MicroSoft reports that 4% of Windows users use a high-contrast theme.

Determining Terminal/Console Color Capability

This, alas, does not have a clean and tidy solution.

Base has a function ttyhascolor.jl that determines if a console has color capability as follows:

  • Windows --> true (has color)
  • Linux - check environment variable TERM -- if it starts with "xterm", then true
  • FreeBSD -- check a if a couple of tput statements are successful; if yes, then true

OK, that determines if the console has color. But what color depth?
Alas, not good solution.
Windows 10 has supported 24 bit color since build 14931 (2016).
Otherwise, the convention is to set the COLORTERM environment variable to "truecolor" or "24bit" -- anything else is 8 bit color.

As for what the foreground and background colors are, the Linux convention is again to set an environmental variable, COLORFGBG to:

  • 15;0 is dark background
  • 0;15 is light background

Where "15" is the 8-bit ANSI code for "white" and "0" is the 8-bit ANSI code for "black".

This has a natural extension to 24 bit color:

  • (r,b,g);(r,g,b)

For example, (238,238,238);(0,0,0)

If you want to see your terminal's 24 bit color ability, the Crayons.jl package has a test function.

julia> using Crayons
julia> Crayons.test_24bit_colors()

Modifying Default Colors For Light Backgrounds

@goerz

It would also be great if the default values would take into account that people run both dark and light background terminals. Ideally, Julia could try to detect this, e.g. by looking for the standard COLORFGBG environment variable and use different default colors.

With 24 bit color support, this would be possible. I would restrict this adjustment to the existing ANSI colors that Julia currently uses (all other colors can be adjusted via the ColorScheme.toml file).

The way it would work is to read the foreground and background colors from COLORFGBG. Then check the contrast of the colors :black, :blue, :cyan, :green, light_black, :light_blue, :light_cyan, :light_green, :light_magenta, :light_red, :light_yellow, :magenta, :red, :white, and :yellow against the background.

If the luminance contrast is less than 2.1, boost it up to 2.1. My testing has shown that any further boosting of the luminance will tend to change the chroma of the color severely.

Here's how it would look like against two different white backgrounds.

Text Luminance Change

Note that the ANSI white Julia uses is actually RBG(192,192,192).

The only thing a little bit sketchy with the above is that the :white color is no longer white! Maybe that's a feature?

Note that is contrast adjustment is not quite as nice when the console only has 8 bit color (this is on a white RBG(192,192,192) background):

Contrast adjustment 8-bit

Note you get some color shifts and :cyan is not fixed at all.

It can probably be tweaked further, though.

Packages Can Add Color Keys to Central ColorScheme File

@goerz

Also, third-party packages should be able to register their own semantic color keys. Absent that, packages with color output would need to maintain their own config files. Having all colors defined in one central location arguably seems like a better solution.

I like the idea, not sure exactly how to accomplish it.

I would think the Package manager would handle it. Packages would store a color scheme TOML file as an artifact. The package manager would download that TOML file, then append it to a Packages_colorschemes.toml file. I suppose you could append to the 4 TOML color files that ship with Julia (default, red-green colorblind, blue-yellow colorblind, high-contrast), but it might we wise to just leave those alone and put all packages in their own .toml file.

One thing to avoid is multiple copies of package color keys being written to the packages_colorschemes.toml file. Say, for example, I deleted a package, then I installed it again. I wouldn't want the colors cheme to be re-added (note I'm assuming it wouldn't be removed when I deleted the package). The package manager could probably just check if the Packages dictionary is in the color "dictionary of dictionaries" -- and if it is, don't append the package color scheme again.

@StefanKarpinski could provide some direction on a good approach here.

@brett-mahar
Copy link

@StefanKarpinski , fair enough! If the file stays in one place and is left in whatever format it was at the start, then most of my objection is gone.

@goerz
Copy link
Contributor

goerz commented Aug 16, 2021

Thanks for making this more concrete... because this is actually not what I was thinking of as a "color scheme". I don't really want to change what "yellow" is. Instead, I'd like to configure what color/style should be used in different places. What I had in mind was a ~/.julia/config/colorscheme.toml along these lines (incomplete, and untested/possibly not valid TOML):

[Core]
# equivalent to JULIA_ERROR_COLOR etc
answer = ["black", "normal", "bold"]
warn = "orange"
error = "red"
info = 0x008080

[Shell]
prompt = "red"  # color of the ;shell prompt

[Pkg]
prompt = "blue" # color of the ]Pkg prompt
action = ["green", "normal", "bold"]  # for strings "Updating", "Resolving", "No Changes", etc

[Test]
passed = "green"
failed = "red"
summary = "black"  # The "Test Summary" string that's currently "white"

I suppose each entry should support foreground color, background color, and style (bold/underline); with different "shorthands" as I tried to indicate above (just one string/hex value would be the foreground color).

Third party packages could have their own sections in this file. This file could be parsed into a global dict in Julia (similar to but separate from Base.text_colors)

I would then simply extend printstyled with a style argument so that

printstyled("text", style="Test.passed")

uses the style defined in the TOML file / internal dict.

If there is no ~/.julia/config/colorscheme.toml, I would consider setting the defaults depending on detected properties of the terminal (background color, number of available colors, etc). There could also be a julia command line flag (maybe overload --color=<pathtofile>) to use a different colorscheme.toml file, so people can switch color schemes easily.

If you really want to, you could still have a sections [base.colors] and [base.styles] in the same to toml file (as in the @blackeneth proposal) that basically get parsed into Base.text_colors and allows to change e.g. "yellow" from meaning "ANSI yellow" to an arbitrary hex code (or even to "ANSI green"). Personally, though, I would probably leave Base.text_colors alone. Changing it (which people currently do, as a workaround for missing color themes) seems like a very dirty hack, although there might still be legitimate use cases for that, e.g. to compensate for color blindness.

I would however extend Base.text_colors with more color names - these color names along with hex codes would be the accepted values for colors in colorscheme.toml.

@blackeneth
Copy link

@goerz

I don't really want to change what "yellow" is

Users who have a white background, or a pale yellow background, very well might want to redefine what "yellow" is. Users with yellow-blue color blindness will certainly want to redefine what "yellow" is (as well as "blue"). Users with red-green color blindness will want to redefine what "red" and "green" are.

The concept I'm following is: every color used is defined in the colorscheme.toml file. The only colors defined in code would be a small set of defaults used if the colorscheme.toml file could not be opened.

TOML File

Your example color scheme file is indeed not valid TOML.
You need to read up on the TOML spec first before proposing something, as many of the items in your example are not possible. It just confuses everyone on a way to a solution to make proposals that are not possible.

Your example:

printstyled("text", style="Test.passed")

Is not possible -- there will be no "Test.passed". There will be a dictionary "Test" with keys "colors" and "styles", each which have keys that specify either a color or a style.

Base.text_colors

This dictionary won't change. It defines 279 names, most of them by number 1 - 256. Plus the symbols normal, :default, :bold, :black, :blink, :blue, :cyan, :green, :hidden, :light_black, :light_blue, :light_cyan, :light_green, :light_magenta, :light_red, :light_yellow, :magenta, :nothing, :red, :reverse, :underline, :white, and :yellow.

This is for compatibility. Not every part of Julia will adopt the new color functionality at the same time.

Semantic Colors Only

The overarching concept is that code never specifies a color (e.g., :blue) or style (e.g,. :bold) directly -- instead, the colors and styles are read from a color scheme dictionary.

My prior TOML example didn't have semantic colors for other functionality or packages, as I hadn't researched them yet. It only had definitions for the default colors in Julia.

Here is the TOML file with some preliminary semantic color and style definitions for "stacktraces"

# Julia default color scheme; 24 bit color hex values 

[base.colors]
black= 0x000000
red= 0x800000
green= 0x008000
yellow= 0x808000
blue= 0x008000
magenta= 0x800080
cyan= 0x008080
white= 0xC0C0C0
light_black= 0x808080
light_red= 0xFF0000
light_green= 0x00FF00
light_yellow= 0xFFFF00
light_blue= 0x0000FF
light_magenta= 0xFF00FF
light_cyan= 0x00FFFF
color_normal= 0x000000
default_color_answer= 0x000000
default_color_warn = 0x808000
default_color_error = 0xFF0000
default_color_info = 0x008080
default_color_input= 0x000000
default_color_debug = 0x008000

[base.styles]
normal= "\\033[0m"
bold= "\\033[1m"
underline= "\\033[4m"
blink= "\\033[5m"
reverse= "\\033[7m"
hidden= "\\033[8m"
nothing = ""
JULIA_STACKFRAME_LINEINFO_COLOR = "\\033[1m"
JULIA_STACKFRAME_FUNCTION_COLOR = "\\033[1m"

[stacktrace.colors]
argname = 0x808080 # light_black
filepath = 0x808080 # light_black
function = 0x008000 # blue
lineinfo = 0x000000 # black
module = 0xFFA500 # orange
paren = 0x000000 # black
type = 0x008000 # green 

[stacktrace.styles]
argname_bold = false
filepath_bold = false
filepath_underline = true
function_bold = true
lineinfo_bold = true
module_bold = true
paren_bold = true
type_bold = false

Load it into Julia like this:

Julia> fn = "pathname to your file"

julia> mytn = TOML.parsefile(fn)
Dict{String, Any} with 2 entries:
  "stacktrace" => Dict{String, Any}("styles"=>Dict{String, Any}("module_bold"=>true, "type_bold"=>false, "filepath_bold…
  "base"       => Dict{String, Any}("styles"=>Dict{String, Any}("normal"=>"\\033[0m", "bold"=>"\\033[1m", "blink"=>"\\0…

Now, in addition to "base", we have another top level dictionary, "stacktrace".
Let's open up "stacktrace":

julia> stacktrace_dict = get(mytn,"stacktrace","")
Dict{String, Any} with 2 entries:
  "styles" => Dict{String, Any}("module_bold"=>true, "type_bold"=>false, "filepath_bold"=>false, "paren_bold"=>true, "fil…
  "colors" => Dict{String, Any}("lineinfo"=>0, "function"=>32768, "module"=>16753920, "filepath"=>8421504, "type"=>32768,…

Now let's get the colors and styles dictionaries underneath "stacktrace":

julia> stacktrace_colors = get(stacktrace_dict,"colors","")
Dict{String, Any} with 7 entries:
  "lineinfo" => 0
  "function" => 32768
  "module"   => 16753920
  "filepath" => 8421504
  "type"     => 32768
  "argname"  => 8421504
  "paren"    => 0

The colors also need to be post-processed to ANSI escape sequences. This is what stacktrace_colors looks like after post-processing:

julia> stacktrace_colors
Dict{String, Any} with 7 entries:
  "lineinfo" => "\e[38;2;0;0;0m"
  "function" => "\e[38;2;0;0;128m"
  "module"   => "\e[38;2;255;164;0m"
  "filepath" => "\e[38;2;128;128;128m"
  "type"     => "\e[38;2;0;128;0;m"
  "argname"  => "\e[38;2;128;128;128m"
  "paren"    => "\e[38;2;0;0;0m"

Now the stacktrace styles

julia> stacktrace_styles = get(stacktrace_dict,"styles","")
Dict{String, Any} with 8 entries:
  "module_bold"        => true
  "type_bold"          => false
  "filepath_bold"      => false
  "paren_bold"         => true
  "filepath_underline" => true
  "argname_bold"       => false
  "lineinfo_bold"      => true
  "function_bold"      => true

Note that the stacktrace styles are booleans -- in contrast to base, in which they were ANSI escape sequence strings. This is because stacktraces make use of the print_within_stacktrace() function, which has "bold" as a boolean argument. Here is how it is used:

\julia\base\show.jl (9 hits)
	Line 2189:             print_within_stacktrace(io, uw.name.module, '.', bold=true)
	Line 2192:         print_within_stacktrace(io, s, bold=true)
	Line 2198:         print_within_stacktrace(io, f, bold=true)
	Line 2204:             print_within_stacktrace(io, "($fargname::", ft, ")", bold=true)
	Line 2210: function print_within_stacktrace(io, s...; color=:normal, bold=false)
	Line 2234:     print_within_stacktrace(io, "(", bold=true)
	Line 2240:             print_within_stacktrace(io, argnames[i]; color=:light_black)
	Line 2251:             print_within_stacktrace(io, k; color=:light_black)
	Line 2256:     print_within_stacktrace(io, ")", bold=true)

This makes the point that how styles are defined can be module-specific. Some may want ANSI control sequences ("\033[1m" for bold), others may want booleans, or whatever.

Using the Semantic Specifications

Given the above, then, example calls to print_within_stacktrace would be changed as follows:

function print_within_stacktrace(io, s...; color=stacktrace_colors["function"], bold=stacktrace_styles["function"])

Fancy Color Names

@goerz

I would however extend Base.text_colors with more color names - these color names along with hex codes would be the accepted values for colors in colorscheme.toml.

Well, we could use fancy color names -- they would have to be defined in the colorschmes.toml file as well.

It would require more post-processing to convert those fancy names to RGB(r,g,b) values.

My proposal is to not define fancy color names -- just have users specify hex color values, 0xRRGGBB.

Generic Semantic Colors

Not every package developer would want to define their own color scheme for their package. For them, we could define a set of "generic" semantic colors.

For example, we could add something like this to the colorschemes.toml file:

[generic.colors]
normal = 0x000000 # black
emphasis = 0xFFA500 # orange
critical = 0x800000 # red 

[generic.styles]
normal = "\\033[0m"
emphasis = "\\033[1m" # bold
critical = "\\033[1m" # bold

@goerz
Copy link
Contributor

goerz commented Aug 17, 2021

Users who have a white background, or a pale yellow background, very well might want to redefine what "yellow" is.

That's fine... I don't mind if there's an option to change the definition of color names, in addition to which color names get used where. I'd just be primarily interested in the second part, personally.

Your example color scheme file is indeed not valid TOML.

Actually, it does happens to be valid TOML (not that that's what I was going for)!

julia> TOML.parsefile("./test.toml")
Dict{String, Any} with 4 entries:
  "Core"  => Dict{String, Any}("error"=>"red", "info"=>32896, "answer"=>["black", "normal", "bold"], "warn"=>"orange")
  "Pkg"   => Dict{String, Any}("action"=>["green", "normal", "bold"], "prompt"=>"blue")
  "Shell" => Dict{String, Any}("prompt"=>"red")
  "Test"  => Dict{String, Any}("summary"=>"black", "passed"=>"green", "failed"=>"red")

I don't think it would be very hard to flatten that into a dict "Core.error" => "red", "Core.info" => "32896" ... "Test.failed" => "red". The values would probably have to be processed too, e.g. into a consistent 3-tuple (foreground, background, textstyle). But this is just an idea; I'm not particularly invested in the exact implementation details.

Or, you could read the TOML it into a global Core.colorscheme dict, which would turn it into a nested dict, and then the above would be accessible as e.g. Core.colorscheme["Core"]["error"].

You need to read up on the TOML spec first before proposing something, as many of the items in your example are not possible. It just confuses everyone on a way to a solution to make proposals that are not possible.

Please don't go down this road. I don't "need" to do anything. I appreciate that you've thought deeper into the details of the implementation, but that doesn't mean I can't engage in a conceptual discussion. Especially when your admonishments are factually unfounded!

Your example:

printstyled("text", style="Test.passed")

Is not possible -- there will be no "Test.passed". There will be a dictionary "Test" with keys "colors" and "styles", each which have keys that specify either a color or a style.

Well, it's definitely possible, as I explain above.. You'd just have to transform the TOML into the internal dict a bit more than just reading it in. Or, use a slightly different argument for style (the nested dict).

[Base.text_colors] won't change. It defines 279 names, most of them by number 1 - 256. Plus the symbols normal, :default, :bold, :black, :blink, :blue, :cyan, :green, :hidden, :light_black, :light_blue, :light_cyan, :light_green, :light_magenta, :light_red, :light_yellow, :magenta, :nothing, :red, :reverse, :underline, :white, and :yellow.

This is for compatibility. Not every part of Julia will adopt the new color functionality at the same time.

Right, and in fact I was proposing not to change Base.text_colors. If you want to, you could extend it with more names, but that's entirely optional.

The overarching concept is that code never specifies a color (e.g., :blue) or style (e.g,. :bold) directly -- instead, the colors and styles are read from a color scheme dictionary.

My prior TOML example didn't have semantic colors for other functionality or packages, as I hadn't researched them yet. It only had definitions for the default colors in Julia.

Here is the TOML file with some preliminary semantic color and style definitions for "stacktraces"
[...]

I don't think our proposals are very far apart then. I'm not strongly invested in any particular details... as long as for every colored string that gets printed by the core packages, there's a setting somewhere where I can change the color/styling of that string (without redefining the definition of the color)

[...]
Given the above, then, example calls to print_within_stacktrace would be changed as follows:

function print_within_stacktrace(io, s...; color=stacktrace_colors["function"], bold=stacktrace_styles["function"])

I'm not sure I could follow all those details, but it still seems to me like it would be elegant to have the semantic colors/styles available in printstyled. It could be

printstyled("text", style="Test.passed")

if you process the original TOML, or

printstyled("text", style=Core.colorscheme["Test"]["passed"])

if you just load the TOML into a global Core.colorscheme dict. That's just a detail that has to be worked out in the implementation... and a again, I won't be upset if you choose to implement this some other way ;-)

Not every package developer would want to define their own color scheme for their package. For them, we could define a set of "generic" semantic colors.

For example, we could add something like this to the colorschemes.toml file:

[generic.colors]
normal = 0x000000 # black
emphasis = 0xFFA500 # orange
critical = 0x800000 # red 

[generic.styles]
normal = "\\033[0m"
emphasis = "\\033[1m" # bold
critical = "\\033[1m" # bold

I think that's an excellent idea!

@imciner2
Copy link
Contributor

imciner2 commented Aug 17, 2021

Users with yellow-blue color blindness will certainly want to redefine what "yellow" is (as well as "blue"). Users with red-green color blindness will want to redefine what "red" and "green" are.

Speaking as someone who is partially red-green color blind, I NEVER redefine what the actual colors are - I will just change the color associated with an object to be something else. Changing what "red" is will cause me more annoyance than actually just using the original color scheme with red in it. Simply redefining the color to a different can create a lot of problems when color mixing happens, because the resulting colors will also all change.

IMO the better (and more standard) way of handling color blind accessibility is to design a theme distinctly for those users, not by redefining what the base colors are but by reassigning the higher level mapping to use different base colors.

@StefanKarpinski
Copy link
Sponsor Member Author

StefanKarpinski commented Aug 17, 2021

Your example color scheme file is indeed not valid TOML.
You need to read up on the TOML spec first before proposing something, as many of the items in your example are not possible. It just confuses everyone on a way to a solution to make proposals that are not possible.

Woah, please cool it a bit here. First, the TOML example is valid. Second, I don't think anyone should have to become a TOML expert before they can express an opinion here.

Your example:

printstyled("text", style="Test.passed")

Is not possible -- there will be no "Test.passed". There will be a dictionary "Test" with keys "colors" and "styles", each which have keys that specify either a color or a style.

This is perfectly possible. We can interpret style = "Test.passed" to mean "look up the color for Test and passed, applying various defaults as appropriate and use that color".

@KristofferC
Copy link
Sponsor Member

Just as a note, putting raw ANSI codes in a TOML is unlikely to be a good idea. You want something higher level and then it is up to the system displaying the color to decide how that is done.

@StefanKarpinski
Copy link
Sponsor Member Author

Especially since we want these styles to be applicable not only in the terminal but also in HTML-based environments like Juypter and Pluto as well as elsewhere that we haven't thought of yet.

@mcabbott
Copy link
Contributor

@goerz
[stacktrace.colors]
argname = 0x808080 # light_black
filepath = 0x808080 # light_black
function = 0x008000 # blue
lineinfo = 0x000000 # black
module = 0xFFA500 # orange
paren = 0x000000 # black
type = 0x008000 # green

Maybe worth noting that this is both more and less general than current code.

More, in that the present code doesn't apply colour to half these things. Of course it could be generalised to break things up into many more separate print statements each referencing the appropriate global options (whose names are documented somewhere). But deciding to do that widely sounds like a step beyond just building a mechanism for adjusting existing colours.

And less, in that modules aren't one colour. They are drawn cyclically from a list Base.STACKTRACE_MODULECOLORS == [:magenta, :cyan, :green, :yellow], which might ideally be longer but is constrained by using only some of the 16 which work well on dark & light, and are sufficiently distinct in most themes. I think Pkg has a similar list for printing version conflicts.

@goerz
Copy link
Contributor

goerz commented Aug 17, 2021

I realized that printstyled, respectively the underlying with_output_color doesn't actually support background colors at the moment (apart from :reverse mode). An implementation of color schemes is probably easiest if the properties for the named styles in a color scheme correspond directly to the arguments for the printstyled routine. So, if the color scheme should support explicit background coloring, it would be good to add that option to printstyled/with_output_color first.

@StefanKarpinski
Copy link
Sponsor Member Author

We can always add capabilities to printstyled if necessary. That bit is not written in stone.

@goerz
Copy link
Contributor

goerz commented Aug 17, 2021

I would also point out that what I have in mind would be easy to implement progressively in a series of separate PR's. In particular, it would be easy to get started without much impact on the code base, and no impact on the user-facing parts of Julia

PR 1

  • In base/util.jl, define a new global dict

    const named_styles = Dict{String, Tuple{Union{Symbol,Int}, Dict{Symbol, Bool}}}()

    (I'm not particular about the name; in my previous comments I referred to this as colorscheme)

    In the initial PR, this dict would be empty, but the idea is that it gets filled with e.g.

    Dict("Test.passed" => (:green, Dict(:bold=>true, :underline=>false, :blink=>false, :reverse=>false, :hidden=>false)))

    This corresponds exactly to the arguments of with_output_color.

  • Add a keyword argument namedstyle (again, not particular about the name) to printstyled, in base/util.jl:

    function printstyled(io::IO, msg...; namedstyle::Union{Nothing, String}=nothing, bold::Bool=false, underline::Bool=false, blink::Bool=false, reverse::Bool=false, hidden::Bool=false, color::Union{Int,Symbol}=:normal)
        if namedstyle  nothing
            color, properties = named_styles[namedstyle]
            with_output_color(print, color, io, msg...; properties...)
        else
            with_output_color(print, color, io, msg...; bold=bold, underline=underline, blink=blink, reverse=reverse, hidden=hidden)
        end
    end

    This ignores all other keyword arguments if namedstyle is given. It would be even nicer if the other keyword args could get merged into properties, but I don't know how to do that (apart from initializing them all as nothing and setting the defaults inside the routine)

If with_output_color gained support for background color, in the form of

with_output_color(f, color, background, io, args...; kwargs...)

the values of named_styles would change correspondingly from a 2-tuple to a 3-tuple foreground, background, properties.

I think that's all there is to get started. I could make that PR pretty easily!

PR 2

Go through the code base, and for every instance where printstyled is called, create an appropriate named style in the named_styles dict and replace the printstyled call with one that uses namedstyle as the only keyword argument.

At that point, all changes would be completely internal, but that would already be sufficient to have a "hackable" theming support, in the same way that people currently modify Base.text_colors in their startup.jl (not that I would advertise/document that fact)

PR 3

Add support for a colorscheme.toml (or whatever other name) that overwrites/adds to Core.named_styles. The exact format of the TOML would be TBD. I still think my earlier proposal looks pretty clean, but @blackeneth's proposal might work too. It would probably not correspond 1-to-1 to the internal structure of named_styles, but need a translation of keys/value as outlined in my response to @blackeneth.

Also, if the same TOML should also have a section to redefine color names as @blackeneth proposes (presumably modifying Core.text_colors), I have to problem with that.

This is where the internal changes become user-facing and need documentation.

There's also lots of technical details that would need to be addressed here or in a subsequent PR, such as watching and reloading the file, modifications to the environment variables and/or command line flags Julia uses, etc.

Those would definitely be out of my league in terms of the required familiarity with Julia's internals.

PR 1 is pretty easy, though, and PR 2 is easy but a bit tedious.

There could also be a PR 4 that adds an interface for third-party packages to define their own keys into Core.named_styles, if necessary.

@StefanKarpinski let me know what you think and if I should submit "PR1". Someone else might have to help with "PR2" and definitely with "PR3".

@goerz
Copy link
Contributor

goerz commented Aug 17, 2021

It would be even nicer if the other keyword args could get merged into properties, but I don't know how to do that (apart from initializing them all as nothing and setting the defaults inside the routine)

The following would probably be pretty good (except for obscuring the keyword arguments in the printstyled signature... but then, the user-facing docstring is entirely separate anyway).

function printstyled(io::IO, msg...; namedstyle::Union{Nothing, String}=nothing, kwargs...)
    if namedstyle  nothing
        style = named_styles[namedstyle]
        properties = merge(style.properties, kwargs)
        color = pop!(properties, :color, style.color)
    else
        properties = copy(kwargs)
        color = pop!(properties, :color, :normal)
    end
    with_output_color(print, color, io, msg...; properties...)
end

@blackeneth
Copy link

blackeneth commented Aug 17, 2021

Wow, a lot of great input and ideas.

Fancy Color Names
I also took a look at Roger Luo's GarishPrint.jl package, figuring here was someone who had thought out generic elements to color in printouts. Looking at his code made me rethink the idea of having fancy names (e.g., "cornflowerblue") for colors. Namely, he often specifies "normal" for some colors -- well, it would be valuable to to be able to redefine "normal" for that package, and then have it be reflected everywhere that "normal" is used.

Then @StefanKarpinski mentioned:

Especially since we want these styles to be applicable not only in the terminal but also in HTML-based environments like Juypter and Pluto as well as elsewhere that we haven't thought of yet.

Well, that clinches it. It also means we need to use the W3C CSS3 or CCS4 color lists.

Might as well packages define their own custom color palettes as well (see below)

Color Cycling
@mcabbott critiqued my "Stacktrace" example, commenting it had more and less.

Well, let me say - it's an "example," not a "proposal". Someone who knows the stacktrace coloring much better than I would have to define to actual keys. So if I added "more," feel free to disregard it.

However, you comment on "less" brought forth another requirement -- color cycling. A tuple of colors to cycle through.

module = [":magenta", ":cyan", ":green", ":yellow"]

Accessibility

@imciner2 gave some perspective as someone who is red-green colorblind. I really glad you came by to give some input. Not being color blind myself, I've been following the published guidelines for colorblind support. Highly valuable to have the input of someone with actual experience.

Let me ask you a couple of questions if you don't mind:

  1. There is "red" and then there are ... shades of red. The base :red is a pure red, RGB(128,0,0). Some of the advice for red-green colorblind support, is to add some blue into the red, say RGB(128,0,50). Is that helpful? Useless?
  2. Is there currently any part of Julia that causes you particular trouble?

IMO the better (and more standard) way of handling color blind accessibility is to design a theme distinctly for those users, not by redefining what the base colors are but by reassigning the higher level mapping to use different base colors.

Yes, I think that is where we want to get to.

Until that is achieved, some people might want to redefine base colors. Either because they want higher contrast (move :red from (128,0,0) to (255,0,0)) or for their light background.

However, the I would say the reason the base colors are in the TOML file isn't because I would expect a lot of people to redefine them. The reason is to just completely separate the color definitions from the code. It always seems if something is hard coded, someone always wants to want to change it to be configurable. So just get all the color definitions out into a file.

TOML Format
@KristofferC made the valuable point that it unwise to put ANSI escape sequences into the file, and that "You want something higher level and then it is up to the system displaying the color to decide how that is done."

I think the only exception might be in [base.styles], as those definitions are just replacing Julia code. Otherwise, use "bold", "underline", etc.

... and since we're reading in color name strings ("cornflowerblue"), might was well allow HTML hex codes ("#RRGGBB")

... and since we'll the CSS3/4 color names, might as well let packages define their own custom palettes for their private use. Below you will see definitions for an imaginary package "weigart", which defines a private palette. That private palette can then be used in the [weigart.colors] section.

... and packages can refer to colors from [base.colors], but not from other packages private palettes.

Also below in [stacktrace.colors], see a tuple of colors defined for "module" for color cycling.

# Julia default color scheme; 24 bit color hex values
# Assumes W3C CSS3 color palette names defined elsewhere

[base.colors]
black = 0x000000 # becomes :black symbol
normal = 0x000000 # becomes :normal symbol
red= 0x800000 	# becomes :red symbol, etc. 
green= 0x008000
yellow= 0x808000
blue= 0x008000
magenta= 0x800080
cyan= 0x008080
white= 0xC0C0C0
light_black= 0x808080
light_red= 0xFF0000
light_green= 0x00FF00
light_yellow= 0xFFFF00
light_blue= 0x0000FF
light_magenta= 0xFF00FF
light_cyan= 0x00FFFF
color_normal= 0x000000 
default_color_answer= 0x000000
default_color_warn = 0x808000
default_color_error = 0xFF0000
default_color_info = 0x008080
default_color_input= 0x000000
default_color_debug = 0x008000

[base.styles]
bold= "\\033[1m"
underline= "\\033[4m"
blink= "\\033[5m"
reverse= "\\033[7m"
hidden= "\\033[8m"
nothing = ""
JULIA_STACKFRAME_LINEINFO_COLOR = "\\033[1m"
JULIA_STACKFRAME_FUNCTION_COLOR = "\\033[1m"

[generic.colors]
fg = ":black"		# from base 
emphais = "orange"  	# W3C #FFA500
critical = "orangered" 	# W3C #FF4500
fieldname = ":light_blue" 	# from base
operator = ":normal" 		# from base 
literal = ":yellow"
constant = ":yellow"
number = ":normal"
string = ":yellow"
comment = ":light_black"
undef = ":normal"
linenumber = ":light_black"

[generic.styles]
normal = "normal"
emphasis = "bold"
critical = "bold"

[stacktrace.colors]
argname = 0x808080 # light_black
filepath = 0x808080 # light_black
function = 0x008000 # blue
lineinfo = 0x000000 # black
module = [":magenta", ":cyan", ":green", ":yellow"] # color cycling; colors from base
paren = 0x000000 # black
type = 0x008000 # green 

[stacktrace.styles]
argname_bold = false
filepath_bold = false
filepath_underline = true
function_bold = true
lineinfo_bold = true
module_bold = true
paren_bold = true
type_bold = false

[weigart.palette]
plainblue = "#123369"  # hex code in #RRGGBB format
plaingrid  = "#692312"
vividblue = "#0014cc"
vividred = "#f20020"

[weigart.colors]
button = "plainblue"
grid = "plaingrid"
popout = "vividblue"
window = "vividred"
fgtext = "wheat" # W3C color 

[weigart.styles]
fgtext = "bold" 

Let me add that the above file is designed to have as "flat" a structure as possible. If people prefer more hierarchy, well ... that could work too.

@goerz
Copy link
Contributor

goerz commented Aug 17, 2021

That TOML file is 100% compatible with my implementation outline. It’s maybe a bit more verbose (due to being “flat”) than what I was considering, but I’d be perfectly happy with either format, or anything similar.

In any case, the exact TOML format wouldn’t have to be nailed down until “PR3” in my outline

@blackeneth
Copy link

@goerz Regarding PR1:

Ah, I see where you're going. You want more complex style you pass along, and it specifies everything. Just pass that one thing along, and it has it all.

My approach is much simpler: the ability to look up a color for an element. Think simple programming. Very simple.

Say I can look up in a dictionary, "pathcolor"

Then I use it as follows:

pname = "C:\\Data\\Mydata.csv"
print("Enter the following path: ")
printstyled(pname, color=pathcolor)

Ideally, the TOML format could support both.

Package developers have varying levels of programming ability. Some may want to define named styles, others may just want to grab some colors.

@goerz
Copy link
Contributor

goerz commented Aug 17, 2021

I understand... My motivation was more like "what is the minimum amount of code I'll have to change". Hence, I'm grouping collections of color and styles together with a single semantic name, and then I just have to give printstyled one new parameter to look up that name in a global dict.

For what you have in mind, you'll want to keep all of the information from your TOML file separately accessible, so that a developer could look up a semantic color separately from a semantic style (bold etc). That's obviously more flexible. Although strictly speaking, even with my proposed global

const named_styles = Dict{String, NamedTuple{(:color, :properties), Tuple{Union{Symbol,Int}, Dict{Symbol, Bool}}}}

someone could still do printstyled(pname, color=Core.named_styles['path'].color).

It'll be up to the maintainers (@StefanKarpinski) to decide what internal structure they'd prefer.

@goerz
Copy link
Contributor

goerz commented Aug 17, 2021

I didn't explicitly take into account color cycling for e.g. stacktraces, but for that, I would do

modulestyles = Iterators.cycle(["stacktrace.module1", "stacktrace.module2"]) # ...
for (style, modname) in zip(modulestyles, modulenames)
    printstyled(modname, namedstyle=style)
end

@blackeneth
Copy link

blackeneth commented Aug 22, 2021

Technical Notes

The ANSI escape sequence syntax for 24-bit color is:

ESC[38;2;⟨r⟩;⟨g⟩;⟨b⟩m Select RGB foreground color
ESC[48;2;⟨r⟩;⟨g⟩;⟨b⟩m Select RGB background color

For example,
cornflowerblue-example

The input formats we have discussed so far are:

  • Hex numbers: 0x6495ED
  • Hex strings: "#6495ED"
  • fancy color names: "cornflowerblue" (from W3C CSS3/4 color names)
  • References to base colors: ":blue"
  • tuple of colors: [0x6495ED, "6495ED", "cornflowerblue", ":red"]

Color Storage Formats

ANSI in-band escape sequences need color information as RGB(R,G,B) -- for example, (100,149,237) -- so the escape sequence ESC[38;2;⟨r⟩;⟨g⟩;⟨b⟩m can be constructed.

HTML users may prefer the fancy color name, "cornflowerblue", or the Hex code, "#6495ED" -- as these plug more directly into HTML/CSS code.

Data Structures

That leads to the question -- should we have parallel data structures for colors? Some that store color as (100,149,237), another that stores it as "#6495ED", and another as "cornflowerblue"?

Or do you pick 1 color format, and convert it on the fly as needed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
display and printing Aesthetics and correctness of printed representations of objects. REPL Julia's REPL (Read Eval Print Loop)
Projects
None yet
Development

Successfully merging a pull request may close this issue.