Wide gamut colors

Added in version 0.50.0.

Terminal colors are sRGB. Most displays made in the last few years can show colors outside that gamut, the deeper reds and greens of Display P3 and the still larger Rec. 2020 gamut, but a program has no way to ask for them. This extension lets a program say which gamut an RGB color is in, using a slot the existing escape code already has, so that nothing changes for programs that do not use it.

Choosing a gamut

The colon form of the RGB color SGR has an optional color space id between the 2 and the red component, defined in ITU-T T.416 but never given a meaning by terminals, which skip it. This extension gives it one:

<ESC>[38:2:<id>:<r>:<g>:<b>m      foreground
<ESC>[48:2:<id>:<r>:<g>:<b>m      background

id is 0 for sRGB, which is the same as leaving it out, 1 for Display P3 and 2 for Rec. 2020. Any other id is treated as 0. The components are 0 to 255, encoded with the sRGB transfer curve in every gamut, so only the primaries change with the id and a terminal needs a single curve. The brightest red of Display P3 is:

<ESC>[38:2:1:255:0:0m

The gamut belongs to the RGB color it was given with. Selecting one of the sixteen basic colors, a color from the 256 color palette or the default color also selects sRGB, as does an RGB color without an id. Under reverse video the gamut follows the color, so a reversed cell with a Display P3 foreground shows a Display P3 background. Decorations, underline and strikethrough, use the foreground and its gamut unless a decoration color is set with 58, which is always sRGB.

Terminals that do not know the id skip it, so a program can emit the id unconditionally and get the same color interpreted as sRGB elsewhere, which is a little less saturated and never wrong. The id is only recognized in the colon form. The semicolon form <ESC>[38;2;<r>;<g>;<b>m has no id, since the parameters it is followed by are unrelated attributes.

Images

The pixels of an image sent with the Terminal graphics protocol are sRGB as well. The G key of a command that transmits them says otherwise, with the ids used for text:

<ESC>_Gf=24,s=10,v=20,G=1;<payload><ESC>\

G=1 is Display P3 and G=2 is Rec. 2020. G=0, any other value and no key at all are sRGB. As for text, the components are encoded with the sRGB transfer curve in every gamut. The key applies to RGB and RGBA data, f=24 and f=32, and belongs in the first chunk of a chunked transmission. It applies in the same way to the pixels of a GPU surface, t=g. It is ignored for the two formats that have no gamut to choose: an HDR image, f=64, is linear extended sRGB by definition, see the HDR brightness protocol, and the colors in the records of a vector image, f=200, are sRGB.

A PNG image, f=100, says what its colors are by itself, and the key is ignored for it. A PNG is in Display P3 or Rec. 2020 when its embedded profile has the primaries of that gamut, whatever the curve of the profile, or when its cICP chunk names those primaries together with the sRGB transfer function. Any other PNG is sRGB, converted to it if it carries a profile, as it always was.

All the frames of an animated image are in the gamut of the image, which is that of the data it was created with. A frame transmitted in another gamut is converted to that of the image when it is loaded, so a program may send every frame as it has it.

Unlike the id of a text color, the key is not skipped by a terminal that does not know it: such a terminal may reject the whole command, without an answer. A program therefore asks first, with the query action of the graphics protocol and a single pixel:

<ESC>_Gi=31,a=q,f=24,s=1,v=1,G=1;AAAA<ESC>\

and sends the key only to a terminal that answers OK.

What the terminal does with it

The terminal converts a color tagged with a gamut to the gamut it draws in, through CIE XYZ, and clips what the display cannot show. Colors without an id are what they always were, the terminal’s own colorspace, and are not touched, so programs that do not use the id are rendered exactly as they were before this extension existed. When a terminal draws in sRGB, a Display P3 or Rec. 2020 color is shown as the nearest sRGB color, which is close to, though not the same as, reading its components as sRGB. Only when the terminal draws in a wider gamut do the extra colors appear. An image in a gamut is converted in the same way, and an image without one is drawn exactly as before.

A program that wants to know whether asking for wide gamut colors is worthwhile asks for the gamut of the display with the display query of the display information protocol, where the terminal has one, and otherwise simply emits the id. Brightness above SDR white is a separate attribute, the 56 and 57 of the HDR brightness protocol, and the two combine: a Display P3 red at twice SDR white is <ESC>[38:2:1:255:0:0m<ESC>[56:200m.

In Silkie

In Silkie the gamut is stored with the color of the cell and survives everything the color survives: scrollback, copying with formatting, and DECRQSS reports of the current SGR state, which include the id when it is not 0. On macOS the window draws in the colorspace selected by macos_colorspace, and tagged colors are converted to it: with srgb Display P3 and Rec. 2020 colors are clipped to sRGB, with displayp3 Display P3 colors are shown as they are and Rec. 2020 colors are clipped to Display P3, while untagged colors keep being interpreted as Display P3, as that option has always done. With default the window is in the native colorspace of the display and every color is passed through unconverted.

On Wayland the window draws in sRGB unless wayland_colorspace asks for displayp3, rec2020 or auto and the compositor implements color management, the color-management-v1 protocol. The compositor is then told that the window is in that gamut, and colors and images tagged with a gamut are converted to it: in a displayp3 window Display P3 colors are shown as they are and Rec. 2020 colors are clipped to Display P3. Unlike on macOS, untagged colors stay sRGB: they are converted to the gamut of the window as well, so everything that does not ask for a wider gamut looks as it does in an sRGB window. auto picks the widest of the three gamuts whose primaries the display reaches, going by what the compositor reports for it. The option takes effect on existing windows when the configuration is reloaded.

The window is declared to the compositor with the primaries of its gamut and a gamma of 2.2, the display the colors of a terminal are made for and what a compositor takes a window that declares nothing to be. Grays therefore have the same values in a wide window as in an sRGB one. The conversion between primaries is done in the linear light of the sRGB curve, as all blending in Silkie is, which can move a saturated untagged color by one step in 255 from what an sRGB window shows. A compositor that offers no gamma of 2.2 is given the sRGB curve instead. On a compositor without the protocol, and on X11, the window draws in sRGB.

A window that also shows HDR, see hdr and the HDR brightness protocol, has a single description like any other, and it carries both: the primaries of the gamut chosen here, linear values in place of the gamma, and the luminances the contents reach. Colors are converted to the gamut of the window as above, and what is brighter than SDR white stays so in that gamut. When the compositor does not take linear values in the wide primaries, the window keeps its HDR and is drawn in sRGB; when the window cannot show HDR at all, it is a wide window as described above.

The profile of a PNG is taken to be Display P3 or Rec. 2020 when each of its three primaries is within 0.005 in x and in y of the primary of that gamut, both adapted to the D50 white point profiles are made for. The pixels are then converted from the curve of the profile to the sRGB curve and nothing else. Other uses of PNG in Silkie, such as background images and window logos, are sRGB as before.