You are not logged in.

#1 2012-10-21 17:09:30

Xyne
Administrator/PM
Registered: 2008-08-03
Posts: 6,963
Website

RGB values of current terminal color settings?

The colors displayed for different ANSI escape codes are configurable via .Xdefaults or directly on the command line via other ANSI escapes.

Is there any way to dump the current values from a terminal? For example, given the ANSI escape code "\033[31m", I would like to know the current RGB values used to display it.

Obviously this will depend on the terminal (e.g. urxvt can have different settings from xterm), but I'm hoping that there is some obscure tool to do this.


My Arch Linux StuffForum EtiquetteCommunity Ethos - Arch is not for everyone

Offline

#2 2012-10-21 17:46:41

Trilby
Inspector Parrot
Registered: 2011-11-29
Posts: 29,422
Website

Re: RGB values of current terminal color settings?

infocmp looks promising.  I'm reading the man page now myself.

It - apparently - dumps information from the active terminfo database.

Edit: er ... maybe not.  There is a lot of information available from this tool - but I'm not seeing anything promising for the color settings.

Last edited by Trilby (2012-10-21 17:57:41)


"UNIX is simple and coherent..." - Dennis Ritchie, "GNU's Not UNIX" -  Richard Stallman

Offline

#3 2012-10-21 18:19:31

Xyne
Administrator/PM
Registered: 2008-08-03
Posts: 6,963
Website

Re: RGB values of current terminal color settings?

I didn't see any RGB info in there either, but it did give me the idea to check different curses utilities. I'll post if I find anything.


My Arch Linux StuffForum EtiquetteCommunity Ethos - Arch is not for everyone

Offline

#4 2012-10-21 18:23:09

Procyon
Member
Registered: 2008-05-07
Posts: 1,819

Re: RGB values of current terminal color settings?

Does it have to be the current terminal or are the current xrdb settings okay?

Offline

#5 2012-10-21 18:29:32

Xyne
Administrator/PM
Registered: 2008-08-03
Posts: 6,963
Website

Re: RGB values of current terminal color settings?

I would prefer the current terminal to be sure that the colors correspond to what is being printed, but both are interesting.


My Arch Linux StuffForum EtiquetteCommunity Ethos - Arch is not for everyone

Offline

#6 2012-10-21 19:06:35

Doomcide
Member
Registered: 2011-08-22
Posts: 221

Re: RGB values of current terminal color settings?

Do you mean something like this? Or am I completly wrong?

#!/bin/bash
colors=($(xrdb -query | sed -n 's/.*color\([0-9]\)/\1/p' | sort -nu | cut -f2))

echo -e "\e[1;37m 
 Black    Red      Green    Yellow   Blue     Magenta   Cyan    White   
 ──────────────────────────────────────────────────────────────────────\e[0m"
 for i in {0..7}; do echo -en "\e[$((30+$i))m ${colors[i]} \e[0m"; done
 echo
 for i in {8..15}; do echo -en "\e[1;$((22+$i))m ${colors[i]} \e[0m"; done
 echo -e "\n"

Example output:
tZnl2Yw

Offline

#7 2012-10-21 19:23:47

Xyne
Administrator/PM
Registered: 2008-08-03
Posts: 6,963
Website

Re: RGB values of current terminal color settings?

Doomcide wrote:

The hex codes are exactly what I want. Unfortunately, "xrdb -query" gives me no output (urxvt, xterm).

Sorry if the answer is obvious, but what am I missing?

edit: Nice theme btw.

Last edited by Xyne (2012-10-21 19:24:38)


My Arch Linux StuffForum EtiquetteCommunity Ethos - Arch is not for everyone

Offline

#8 2012-10-21 19:27:31

karol
Archivist
Registered: 2009-05-06
Posts: 25,440

Re: RGB values of current terminal color settings?

Do you have xorg-xrdb installed?
Did you do 'xrdb -merge' to update your settings?

Edit: I've run the script and I get different output

 
 Black    Red      Green    Yellow   Blue     Magenta   Cyan    White   
 ──────────────────────────────────────────────────────────────────────
                
                

When I highlight the table with my mouse, I can see a color palette on the left. Maybe that's what Xyne war referring to as 'no output'.

Last edited by karol (2012-10-21 19:33:31)

Offline

#9 2012-10-21 19:41:54

WonderWoofy
Member
From: Los Gatos, CA
Registered: 2012-05-19
Posts: 8,414

Re: RGB values of current terminal color settings?

It works for me... its pretty neat.

Offline

#10 2012-10-21 19:44:03

Šaran
Member
From: Bosnia
Registered: 2011-09-03
Posts: 407

Re: RGB values of current terminal color settings?

If I remove "xrdb -load $HOME/.Xresources &" from .xinitrc, "xrdb -query" gives me nothing. Otherwise It outputs content of .Xresources. This doesn't work in tty where I'm setting colors with following in .bashrc.

if [ "$TERM" = "linux" ]; then
    echo -en "\e]P0000000"
    echo -en "\e]P85D5D5D"
    echo -en "\e]P1C75646"
    echo -en "\e]P9E09690"
    echo -en "\e]P28EB338"
    echo -en "\e]PACDEE69"
    echo -en "\e]P3D0B03C"
    echo -en "\e]PBFFE377"
    echo -en "\e]P472B3CC"
    echo -en "\e]PC9CD9F0"
    echo -en "\e]P5C9A0D1"
    echo -en "\e]PDFBB1F9"
    echo -en "\e]P6218693"
    echo -en "\e]PE77DFD8"
    echo -en "\e]P7B0B0B0"
    echo -en "\e]PFF7F7F7"
    clear # bring us back to default input colours
fi

I hope this helps.

Offline

#11 2012-10-21 19:45:36

karol
Archivist
Registered: 2009-05-06
Posts: 25,440

Re: RGB values of current terminal color settings?

@Šaran
Mystery solved :-)
That's exactly my setup. Thanks.

Offline

#12 2012-10-21 19:45:59

Xyne
Administrator/PM
Registered: 2008-08-03
Posts: 6,963
Website

Re: RGB values of current terminal color settings?

karol wrote:

Do you have xorg-xrdb installed?
Did you do 'xrdb -merge' to update your settings?

Edit: I've run the script and I get different output

 
 Black    Red      Green    Yellow   Blue     Magenta   Cyan    White   
 ──────────────────────────────────────────────────────────────────────
                
                

When I highlight the table with my mouse, I can see a color palette on the left. Maybe that's what Xyne war referring to as 'no output'.

I get the same output for the script (no visible output unless highlighted). I said that I get no output for the "xrdb -query" command itself, which is where those hex values are coming from.

Even if this doesn't work out, I have a new theme (same as above except for modified black and white values):
terminal_theme_small.png


edit: Ok, it's working here now thanks to Šaran's post. I'll play around with this now to see if I can extract all of the info that I need.

edit2: Looking at the output of "xrdb -query" and the sed command in that script, terminal-specific colors will be lost. For example, "URxvt*" prefixes are just stripped off and will be confused with colors for other terminals. Is there some way to determine the correct prefix for the current terminal? I did not see anything that I could use in the output of "set".

Last edited by Xyne (2012-10-21 20:04:25)


My Arch Linux StuffForum EtiquetteCommunity Ethos - Arch is not for everyone

Offline

#13 2012-10-21 20:06:18

Doomcide
Member
Registered: 2011-08-22
Posts: 221

Re: RGB values of current terminal color settings?

@ Xyne: Here's a similar one, that reads the colors from .Xdefaults. I suppose the regex could be improved:

#!/bin/bash

XFILE="$HOME/.Xdefaults"
TERM="urxvt"

COLS="$(cat $XFILE | grep -Ei $TERM[\.\*]color[01-9] | sort -n -tr -k2 | cut -d: -f2 | tr -d [:blank:])"

for i in {0..7}; do echo -en "\e[0;3${i}m ▉▉ $(echo "$COLS" | sed -n $(($i+1))'p')\e[0m"; done; echo
for i in {0..7}; do echo -en "\e[0;9${i}m ▉▉ $(echo "$COLS" | sed -n $(($i+9))'p')\e[0m"; done; echo -e "\n"

Offline

#14 2012-10-21 20:14:40

Šaran
Member
From: Bosnia
Registered: 2011-09-03
Posts: 407

Re: RGB values of current terminal color settings?

I think prefix is not important because "*color0: #000000" in .Xresources sets black color for any terminal.

Offline

#15 2012-10-21 20:53:26

Xyne
Administrator/PM
Registered: 2008-08-03
Posts: 6,963
Website

Re: RGB values of current terminal color settings?

@Doomcide
Thanks, but that uses a hard-coded prefix. Ideally I want something that can detect it automatically.

Even then, this approach will not suffice because it can only detect colors that have been set in the Xdefaults/Xresources files. Different terminals use different default RGB values and those can be changed dynamically using other ANSI escape codes (see tiv's adaptive color mode for an example).

I appreciate the help nevertheless.

@Šaran
It does matter. Here's an example:
xterm_vs_urxvt_small.png

As you can see, the prefix changes the color values for each terminal.


My Arch Linux StuffForum EtiquetteCommunity Ethos - Arch is not for everyone

Offline

#16 2012-10-21 20:58:35

Trilby
Inspector Parrot
Registered: 2011-11-29
Posts: 29,422
Website

Re: RGB values of current terminal color settings?

xrdb --query parrots back exactly what is in the .Xresources (or .Xdefaults) file.  In my case only a couple of the colors are set with the RGB codes, many of them are set by the 256 color names like blue2 and yellow3.

A more elaborate script could have 256 sed-type replacements that would replace these color names with RGB codes, but this doesn't seem to be a great approach.  Also it will give nothing if the colors are not set in .Xresource.

Last edited by Trilby (2012-10-21 20:59:28)


"UNIX is simple and coherent..." - Dennis Ritchie, "GNU's Not UNIX" -  Richard Stallman

Offline

#17 2012-10-21 21:02:30

Šaran
Member
From: Bosnia
Registered: 2011-09-03
Posts: 407

Re: RGB values of current terminal color settings?

This is what I get with color scheme sourced in .Xresources

#include "/home/fuchs/.colors/ambiance"

tZnl4eA

*background:        #282828
*foreground:        #F2F1F0

!black
*color0: #222222
*color8: #666666
!red
*color1: #E84F4F
*color9: #D23D3D
!green
*color2: #B7CE42
*color10: #BDE077
!yellow
*color3: #F07746
*color11: #F07746
!blue
*color4: #66AABB
*color12: #AACCBB
!magenta
*color5: #B7416E
*color13: #E16A98
!cyan
*color6: #6D878D
*color14: #42717B
!white
*color7: #F2F1F0
*color15: #F2F1F0

Offline

#18 2012-10-21 21:28:59

Trilby
Inspector Parrot
Registered: 2011-11-29
Posts: 29,422
Website

Re: RGB values of current terminal color settings?

Appres is another interesting tool, but that also gives the settings as they are in the resources file which leads me to conclude that the X resrouce database does not necessarily include RGB codes - rather it includes the key-value pairings as they are provided by the Xresources file(s).  I suspect it is up to the individual application (xterm, urxvt, etc) to translate those key-value pairings into actual colors.  Different apps may do this differently, and certainly retreiving the info from them would have to be done differently.

A roundabout kludge would be to have an X program launch such a terminal, send output to it with the necessary escape sequences to show colors, then copy an image of the relevant region of the display from the X server and get the actual color codes from there.


"UNIX is simple and coherent..." - Dennis Ritchie, "GNU's Not UNIX" -  Richard Stallman

Offline

#19 2012-10-21 22:42:56

cfr
Member
From: Cymru
Registered: 2011-11-27
Posts: 7,130

Re: RGB values of current terminal color settings?

setvtrgb is interesting and (relatedly)

cat /sys/module/vt/parameters/default_{red,grn,blu}

CLI Paste | How To Ask Questions

Arch Linux | x86_64 | GPT | EFI boot | refind | stub loader | systemd | LVM2 on LUKS
Lenovo x270 | Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz | Intel Wireless 8265/8275 | US keyboard w/ Euro | 512G NVMe INTEL SSDPEKKF512G7L

Offline

#20 2012-10-21 22:46:45

Xyne
Administrator/PM
Registered: 2008-08-03
Posts: 6,963
Website

Re: RGB values of current terminal color settings?

Trilby wrote:

A roundabout kludge would be to have an X program launch such a terminal, send output to it with the necessary escape sequences to show colors, then copy an image of the relevant region of the display from the X server and get the actual color codes from there.

That's the unappealing partial solution that I've had in mind since the first post. Apart from the overall indirectness of the approach, it is also a problem if it it has to spam the terminal or launch another instance just for that. Getting the data in a reliable way will require knowledge or calculation of character and line widths, detection of the window, and possibly a screenshot and more 3rd-party code to extract the data.

Part of that could be avoided by creating a separate script to do the above and then create a color configuration file that can be used in my code (which is a small Python module with color conversion functions complementary to the standard colorsys module).

Even then it remains kludgy.


There must be some better way. I wouldn't mind if it was terminal specific as long as it's complete. By complete, I mean a way to detect the total number of colors that can be displayed by the terminal along with a way to map each of those to RGB values.

I'll check the rxvt source code for relevant functionality.


My Arch Linux StuffForum EtiquetteCommunity Ethos - Arch is not for everyone

Offline

#21 2012-10-21 23:14:26

Trilby
Inspector Parrot
Registered: 2011-11-29
Posts: 29,422
Website

Re: RGB values of current terminal color settings?

EDIT 2: this post is irrelevant in light of the far better solution below.  But it was fun to brainstorm anyhow.

I agree it would be a miserable solution, but it shouldn't require any screenshot tools or 3rd party code (aside from Xlib).  Xlib's XCopyArea or XGetBitmap would do the trick.  If you calculated the proper and precise coordinates you could even go a lot simpler and just git specific pixel values which would then go through an Xlib color mapping to RGB values.

Actually, if you launch a terminal window, set the override_redirect flag, and move it off screen, the window manager shouldn't touch it, and the user might not even know it was ever created.  Then, as you would have moved it to your chosen off screen coordinates, and you could have specified a font for it, it would be (relatively) easy to know which pixels to read.

Edit: this is assuming you'd be using C + Xlib, or another tool that has sufficient Xlib bindings.

Last edited by Trilby (2012-10-21 23:31:00)


"UNIX is simple and coherent..." - Dennis Ritchie, "GNU's Not UNIX" -  Richard Stallman

Offline

#22 2012-10-21 23:19:20

Lux Perpetua
Member
From: The Local Group
Registered: 2009-02-22
Posts: 69

Re: RGB values of current terminal color settings?

Coincidentally, I recently wrote a script to do exactly this: query all the color values from a terminal emulator and show them in a nice table. I was planning to make a thread for this, but this thread is perfect.  In my opinion, the strategy of querying colors from the terminal itself is superior (when it is supported by the terminal) to mucking around with .Xresources, .Xdefaults, or "xrdb --query", which only work in pretty limited situations. The drawbacks of my approach are: (1) it can be challenging to make the same code work on different terminal emulators, and (2) you have to use it from the terminal you want to query, and not from within a screen or tmux session. (I think there may be partial workarounds for (2), but I haven't implemented them.) I've tested my code on a number of terminals, and the upshot is that xterm and urxvt are supported flawlessly, and VTE-based terminals like gnome-terminal and XFCE's "Terminal" are almost fully supported. (I wasn't able to query the default foreground and background colors, but everything else seems to work.) Most other X terminals don't seem to support this method of querying color values.

Here's an example of what the script produces:
tZnl5eg
Obviously, that's a heavily formatted table, but I think the code has enough comments that you can take what you need from it. It works under python 3 or python 2.7.

Edit: This version is superseded by the one in post #27, which is structured better. I'm leaving this here since some of the future replies refer to it.

#!/usr/bin/python

'''
This is a Python script to show off your terminal ANSI colors (or more
colors, if your terminal has them).  It works on both Python 2.7 and
Python 3.

This script must be run from the terminal whose colors you want to
showcase.  Not all terminal types are supported (see below).  At the
very minimum, 16-color support is required. 

Fully supported terminals:
    
    xterm
    urxvt

For these terminals, this script can show a color table with correct RGB
values for each color.  It queries the RGB values from the terminal
itself, so it works even if you change the colors after starting the
terminal.

Mostly supported terminals: pretty much all VTE-based terminals. This
includes:

    vte
    Terminal (XFCE)
    gnome-terminal
    terminator
    tilda

and many more.  These are on "mostly" status because I don't know how to
query their foreground and background colors.  Everything else works,
though, albeit with noticeable slowness (which may be beyond this
script's control).

Somewhat supported terminals: pretty much all other X-client terminals
I've tried.  These include:

    konsole (KDE)
    terminology (Enlightenment)
    Eterm (Enlightenment)
    (etc.)

For these terminals, the script can output a color table just fine, but
without RGB values.

Unsupported terminals:

    ajaxterm
    Linux virtual console (i.e., basic TTY without X-windows)

Warning: do not run this script on the Linux virtual console unless you
want a garbled TTY!  That said, you can still type `tput reset<Enter>'
afterward to get back to a usable console. :-)  The situation with
ajaxterm is similar, but not as bad.

If a terminal isn't mentioned here, I probably haven't tried it.  Attempt
at your own risk!

Note regarding screen/tmux: this script can theoretically be run from a
screen or tmux session, but you will not get any RGB values in the
output (indeed, a screen session can be opened on multiple terminals
simultaneously, so there typically isn't a well defined color value for
a given index).  However, it's interesting to observe that screen and
tmux emulate a 256 color terminal independently of the terminal(s)
to which they are attached, which is very apparent if you run the script
with 256-color output on a screen session attached to a terminal with 8-
or 16-color terminfo (or with $TERM set to such).

This code is licensed under the terms of the GNU General Public License:
    http://www.gnu.org/licenses/gpl-3.0.html

and with absolutely no warranty.  All use is strictly at your own risk.

'''

from sys import stdin, stdout, stderr
import re
import select
import termios
from collections import defaultdict
from argparse import (ArgumentParser, ArgumentError)


# Operating system command
osc = "\033]"

# String terminator
#  ("\033\\" is another option, but "\007" seems to be understood by
#  more terminals.  Terminology, for example, doesn't seem to like
#  "\033\\".)
st = "\007"

# Control sequence introducer
csi = "\033["

# ANSI SGR0
reset = csi + 'm'


#######################################################################
# Errors that may be raised by rgb_query

num_errors = 0


class InvalidResponseError(Exception):

    '''
    The terminal's response couldn't be parsed.

    '''

    def __init__(self, q, r):
        global num_errors
        num_errors += 1
        Exception.__init__(self, "Couldn't parse response " + repr(r) +
                           " to query " + repr(q))


class NoResponseError(Exception):

    '''
    The terminal didn't respond, or we were too impatient.

    '''

    def __init__(self, q):
        global num_errors
        num_errors += 1
        Exception.__init__(self, "Timeout on query " + repr(q))


########################################################################
# Wrappers for xterm & urxvt operating system controls.
#
# These codes are all common to xterm and urxvt. Their responses aren't
# always in the same format (xterm generally being more consistent), but
# the regular expression used to parse the responses is general enough
# to work for both.
#
# Note: none of these functions is remotely thread-safe.


def get_fg(timeout):
    '''
    Get the terminal's foreground (text) color as a 6-digit
    hexadecimal string.

    '''
    return rgb_query([10], timeout)


def get_bg(timeout):
    '''
    Get the terminal's background color as a 6-digit hexadecimal
    string.

    '''
    return rgb_query([11], timeout)


def get_color(a, timeout):
    '''
    Get color a as a 6-digit hexadecimal string.

    '''
    return rgb_query([4, a], timeout)


def test_fg(timeout):
    '''
    Return True if the terminal responds to the "get foreground" query
    within the time limit and False otherwise.

    '''
    return test_rgb_query([10], timeout)


def test_bg(timeout):
    '''
    Return True if the terminal responds to the "get background" query
    within the time limit and False otherwise.

    '''
    return test_rgb_query([11], timeout)


def test_color(timeout):
    '''
    Return True if the terminal responds to the "get color 0" query
    within the time limit and False otherwise.

    '''
    return test_rgb_query([4, 0], timeout)


def test_rgb_query(q, timeout):
    '''
    Determine if the terminal supports query q.

    Arguments: `q' and `timeout' have the same interpretation as in
        rgb_query().

    Return: True if the terminal gives a valid response within the
        time limit and False otherwise.

    This function will not raise InvalidResponseError or
    NoResponseError, but any other errors raised by rgb_query will
    be propagated. 

    '''
    try:
        rgb_query(q, timeout)
        return True
    except (InvalidResponseError, NoResponseError):
        return False


# String to use for color values that couldn't be determined
rgb_placeholder = '??????'

# This is what we expect the terminal's response to a query for a color
# to look like.  If we didn't care about urxvt, we could get away with a
# simpler implementation here, since xterm and vte seem to give pretty 
# consistent and systematic responses.  But I actually use urxvt most of
# the time, so....
ndec = "[0-9]+"
nhex = "[0-9a-fA-F]+"
crgb = ("\033\\]({ndec};)+rgba?:" +
        "({nhex})/({nhex})/({nhex})(/({nhex}))?").format(**vars())

re_response = re.compile(crgb)

# The problem I'm attempting to work around with this complicated
# implementation is that if you supply a terminal with a query that it
# does not recognize or does not have a good response to, it will simply
# not respond *at all* rather than signaling the error in any way.
# Moreover, there is a large variation in how long terminals take to
# respond to valid queries, so it's difficult to know whether the
# terminal has decided not to respond at all or it needs more time.
# This is why rgb_query has a user-settable timeout.

P = select.poll()
P.register(stdin.fileno(), select.POLLIN)


def flush_input():
    '''
    Discard any input that can be read at this moment.

    '''
    repeat = True
    while repeat:
        evs = P.poll(0)
        if len(evs) > 0:
            stdin.read()
            repeat = True
        else:
            repeat = False


def rgb_query(q, timeout=-1):
    '''
    Query a color-valued terminal parameter. 

    Arguments:
        q: The query code as a sequence of nonnegative integers, i.e.,
            [q0, q1, ...] if the escape sequence in pseudo-Python is

                "\033]{q0};{q1};...;?\007"
    
        timeout: how long to wait for a response.  (negative means
            wait indefinitely if necessary)

    Return: the color value as a 6-digit hexadecimal string.

    Errors:
        NoResponseError will be raised if the query times out.

        InvalidResponseError will be raised if the terminal's
        response can't be parsed.

    See 
        http://invisible-island.net/xterm/ctlseqs/ctlseqs.html

    ("Operating System Controls") to see the various queries
    supported by xterm.  Urxvt supports some, but not all, of them,
    and has a number of its own (see man -s7 urxvt). 

    Warning: before calling this function, make sure the terminal is
    in noncanonical, non-blocking mode.

    '''
    query = osc + ';'.join([str(k) for k in q]) + ';?' + st

    flush_input()
    stdout.write(query)
    stdout.flush()

    # This is addmittedly flawed, since it assumes the entire response
    # will appear in one shot.  It seems to work in practice, though.

    evs = P.poll(timeout)
    if len(evs) == 0:
        raise NoResponseError(query)

    r = stdin.read()

    m = re_response.search(r)

    if not m:
        raise InvalidResponseError(query, r)

    # (possibly overkill, since I've never seen anything but 4-digit RGB
    # components in responses from terminals, in which case `nd' is 4
    # and `u' is 0xffff, and the following can be simplified as well
    # (and parse_component can be eliminated))
    nd = len(m.group(2))
    u = int('f'*nd, 16)

    # An "rgba"-type reply (for urxvt) is apparently actually
    #
    #    rgba:{alpha}/{alpha * red}/{alpha * green}/{alpha * blue}
    #
    # I opt to extract the actual RGB values by eliminating alpha.  (In
    # other words, the alpha value is discarded completely in the
    # reported color value, which is a compromise I make in order to get
    # an intuitive and compact output.)

    if m.group(5):
        # There is an alpha component
        alpha = float(int(m.group(2), 16))/u
        idx = [3, 4, 6]
    else:
        # There is no alpha component
        alpha = 1.0
        idx = [2, 3, 4]

    c_fmt = '%0' + ('%d' % nd) + 'x'

    components = [int(m.group(i), 16) for i in idx]
    t = tuple(parse_component(c_fmt % (c/alpha)) for c in components)

    return "%02X%02X%02X" % t


def parse_component(s):
    '''
    Take a string representation of a hexadecimal integer and transorm
    the two most significant digits into an actual integer (or double
    the string if it has only one character).

    '''
    n = len(s)

    if n == 1:
        s += s
    elif n > 2:
        s = s[:2]

    return int(s, 16)


########################################################################

def test_num_colors(timeout):
    '''
    Attempt to determine the number of colors we are able to query from
    the terminal.  timeout is measured in milliseconds and has the same
    interpretation as in rgb_query.  A larger timeout is safer but will
    cause this function to take proportionally more time.

    '''
    if not test_color(timeout):
        return 0
    
    a = 0
    b = 1
    while test_rgb_query([4, b], timeout):
        a = b
        b += b

    while b - a > 1:
        c = (a + b)>>1
        if test_rgb_query([4, c], timeout):
            a = c
        else:
            b = c

    return b


########################################################################

class ColorDisplay(object):

    '''
    Class for producing a colored display of terminal RGB values.

    '''

    def __init__(self, timeout=100, color_level=3, do_query=True):
        '''
        timeout: same interpretation as in rgb_query. A larger timeout
            will be used a small number of times to test the
            capabilities of the terminal.

        color_level: how much color should be in the output. Use 0 to
            suppress all color and 3 or greater for maximum coloredness.

        do_query: whether to attempt to query RGB values from the
            terminal or just use placeholders everywhere

        '''
        self.timeout=timeout
        self.color_level=color_level

        # try getting the rgb value for color 0 to decide whether to
        # bother trying to query any more colors.
        self.do_query = do_query and test_color(self.timeout*5)

        def none_factory():
            return None

        # colors for highlighting
        self.hi = defaultdict(none_factory)

        self.hi['['] = 10
        self.hi[']'] = 10
        self.hi['+'] = 9
        self.hi['/'] = 9

        for c in '0123456789ABCDEF':
            self.hi[c] = 12


    def show_ansi(self):
        '''
        Show the 16 ANSI colors (colors 0-15).

        '''
        color_order = [0, 1, 3, 2, 6, 4, 5, 7]

        names = ['   Black ', '    Red  ', '   Green ', '  Yellow ',
                 '   Blue  ', '  Magenta', '   Cyan  ', '   White ']

        stdout.write(self.fgcolor('15', 3))

        for k in range(8):
            a = color_order[k]
            stdout.write(names[a])

        stdout.write('\n')
        stdout.write(self.fgcolor(None, 3))

        c = None
        for k in range(8):
            a = color_order[k]
            c = self.hiprint('   [%X/%X] ' % (a, 8 + a), c)
        stdout.write('\n')

        self.show_color_table([0,8], color_order)


    def show_color_cube(self, n):
        '''
        Show the "RGB cube" (xterm colors 16-231 (256-color) or 16-79
        (88-color)).  The cube has sides of length 6 or 4 (for 256-color
        or 88-color, respectively).

        '''
        base = {256:6, 88:4}[n]

        c = None
        c = self.hiprint('[ + ]   ', c)
        for w in range(base):
            c = self.hiprint('[%X]      ' % w, c)
        stdout.write('\n\n' + self.fgcolor(None, 3))

        for u in range(base):
            for v in range(base):
                stdout.write(' '*v)
                x = (u*base + v)*base
                self.hiprint('  [%02X]  ' % (16 + x))
                for w in range(base):
                    self.show_color(x + w + 16)
                stdout.write('\n')
            stdout.write('\n\n')


    def show_grayscale_ramp(self, end):
        '''
        Show the "grayscale ramp" (xterm colors 232-255 (256-color) or
        80-87 (88-color)).

        '''
        start = {256:232, 88:80}[end]
        n = end - start

        vals = [self.get_color(a) for a in range(start, end)]

        #stdout.write(reset)
        c = None

        c = self.hiprint('[ ', c)
        for v in range(n):
            c = self.hiprint('%02X ' % (start + v), c)
        c = self.hiprint(']\n', c)

        stdout.write('\n ' + self.fgcolor(None, 3))

        for v in range(n):
            stdout.write(' ' + self.block(start + v, 2))
        stdout.write('\n ')

        for u in range(3):
            for v in range(n):
                stdout.write(' ')
                stdout.write(self.fgcolor(start + v, 2))
                stdout.write(vals[v][2*u : 2*(u + 1)])
                stdout.write(self.fgcolor(None, 2))
            stdout.write('\n ')
        stdout.write('\n')


    def show_colors(self, n):
        '''
        Make a table showing colors 0 through n-1.

        '''
        self.show_color_table(range(0,n,8), range(8), n, True)


    def show_color_table(self, rows, cols, stop=-1, label=False):
        '''
        Make a color table with all possible color indices of the form
        rows[k] + cols[j] that are less than `stop' (if `stop' is not
        negative). If label is True, then print row and column labels.

        '''
        if label:
            self.hiprint('[ + ]')
            stdout.write(self.fgcolor(None, 3))

            for a in cols:
                stdout.write('   ' + self.octal(a) + '  ')
            stdout.write('\n')

        if label:
            stdout.write('     ')

        stdout.write('\n')

        for b in rows:
            if label:
                stdout.write(self.octal(b) + ' ')

            for a in cols:
                c = a + b
                if stop < 0 or c < stop:
                    self.show_color(b + a)
                else:
                    stdout.write('         ')
            stdout.write('\n')
        stdout.write('\n')


    def show_color(self, a):
        '''
        Make a pretty display of color number `a', showing a block of
        that color followed by the 6-character hexadecimal code for the
        color.

        '''
        stdout.write(' ' + self.block(a) + ' ')
        stdout.write(self.fgcolor(a, 2) + (self.get_color(a)))
        stdout.write(self.fgcolor(None, 2))


    def hiprint(self, s, last_color=-1):
        '''
        Print s to stdout, highlighting digits, brackets, etc. if the
        color level allows it.

        Arguments:
            s: the string to print.

            last_color: the current terminal foreground color.  This
                should be `None' if no color is set, or the current
                color index, or something else (like a negative integer)
                if the color isn't known.  (The last option is always
                safe and will force this function to do the right
                thing.)

        Return: the current foreground color, which can be passed as
            last_color to the next call if the color isn't changed in
            between.

        '''
        for c in s:
            if c == ' ':
                color = last_color
            else:
                color = self.hi[c]

            if color != last_color:
                stdout.write(self.fgcolor(color, 3))

            stdout.write(c)
            last_color = color

        return last_color


    def octal(self, x):
        '''
        Return a base-8 string for the integer x, highlighted if the
        color level allows it.

        '''
        return self.fgcolor(self.hi['+'], 3) + '0' + \
               self.fgcolor(self.hi['0'], 3) + ('%03o' % x)


    def block(self, c, n=1):
        '''
        Return a string that prints as a block of color `c' and size `n'.

        '''
        return self.bgcolor(c, 1) + ' '*n + self.bgcolor(None, 1)


    # Changing the foreground and background colors.
    #
    # While the 38;5 and 48;5 SGR codes are less portable than the usual
    # 30-37 and 40-47, these codes seem to be fairly widely implemented (on
    # X-windows terminals, screen, and tmux) and support the whole color
    # range, as opposed to just colors 0-8.  They also make it very easy to
    # set the background to a given color without needing to mess around
    # with bold or reverse video (which are hardly portable themselves).
    # This is useful even for the 16 ANSI colors.


    def fgcolor(self, a=None, level=-1):
        '''
        Return a string designed to set the foreground color to `a' when 
        printed to the terminal. None means default.

        '''
        if self.color_level >= level:
            if a is None:
                return csi + '39m'
            else:
                return csi + '38;5;' + str(a) + 'm'
        else:
            return ''


    def bgcolor(self, a=None, level=-1):
        '''
        Return a string designed to set the background color to `a' when 
        printed to the terminal. None means default.

        '''
        if self.color_level >= level:
            if a is None:
                return csi + '49m'
            else:
                return csi + '48;5;' + str(a) + 'm'
        else:
            return ''


    def get_color(self, a):
        if self.do_query:
            try:
                return get_color(a, timeout=self.timeout)
            except (InvalidResponseError, NoResponseError):
                return rgb_placeholder
        else:
            return rgb_placeholder


########################################################################
# Command-line arguments

timeout_dft = 200

parser = ArgumentParser(
        description="Python script to show off terminal colors.",
        epilog="Run this script from the terminal whose colors " +
               "you want to showcase.  " +
               "For a brief synopsis of which terminal types are " +
               "supported, see the top of the source code.")

mode_group = parser.add_mutually_exclusive_group()

p_choices = [16, 88, 256]

arg_p = mode_group.add_argument(
        '-p', '--pretty',
        action='store_true', default=False,
        help="show colors 0 through N-1 in a pretty format.  " +
             ("N must belong to %r.  " % p_choices) +
             "If N > 16, it should be the actual number of colors " +
             "supported by the terminal, or the output will almost " +
             "certainly not be pretty.")

mode_group.add_argument(
        '-f', '--flat',
        action='store_true', default=False,
        help="show a simple table with colors 0 through N-1.  ")

parser.add_argument(
        'n', nargs='?', metavar='N',
        type=int, default=16,
        help="number of colors to show.  " +
             "Unless you explicitly supply -p/--pretty or -f/--flat, " +
             "--pretty is used if possible and --flat is used " +
             "otherwise.  " +
             "N defaults to 16, showing the ANSI colors 0-15.  " +
             "If N is 0, the script will attempt to determine the " +
             "maximum number of colors automatically " +
             "(which may be slow).")

parser.add_argument(
        '--no-fgbg',
        action='store_false', dest='fgbg', default=True,
        help="suppress display of foreground/background colors.")

parser.add_argument(
        '--no-query',
        action='store_false', dest='do_query', default=True,
        help="don't try to query any RGB values from the terminal " +
             "and just use placeholders.")

parser.add_argument(
        '-t', '--timeout', metavar='T',
        type=int, default=timeout_dft,
        help="how long to wait for the terminal to "
             "respond to a query, in milliseconds  " +
             "[default: {0}].  ".format(timeout_dft) +
             "If your output has '?' characters " +
             "instead of RGB values " +
             "or junk printed after the script runs, " +
             "increasing this value may or may not " +
             "help, depending on the terminal.  " +
             "A negative T will behave like infinity.")

parser.add_argument(
        '-l', '--level', metavar='L',
        type=int, default=3,
        help="choose how much color to use in the output.  " +
             "(0 = no color; 3 = most color [default])")


########################################################################

if __name__ == '__main__':
    args = parser.parse_args()

    assert not (args.pretty and args.flat)

    if args.pretty:
        if args.n not in p_choices:
            raise ArgumentError(
                    arg_p,
                    "N must belong to %r" % p_choices)

    tc_save = None

    try:
        tc_save = termios.tcgetattr(stdout.fileno())

        tc = termios.tcgetattr(stdout.fileno())

        # Don't echo the terminal's responses
        tc[3] &= ~termios.ECHO

        # Noncanonical mode (i.e., disable buffering on the terminal
        # level)
        tc[3] &= ~termios.ICANON

        # Make input non-blocking
        tc[6][termios.VMIN] = 0
        tc[6][termios.VTIME] = 0

        termios.tcsetattr(stdout.fileno(), termios.TCSANOW, tc)

        if args.n == 0:
            args.n = test_num_colors(args.timeout)
            
            # We are guaranteed to have failed some queries now, but
            # that isn't meaningful.
            num_errors = 0

        if not (args.pretty or args.flat):
            if args.n in p_choices:
                args.pretty = True
            else:
                args.flat = True

        if args.level >= 1:
            stdout.write(reset)

        if args.fgbg:
            if args.do_query:
                try:
                    bg = get_bg(timeout=args.timeout)
                except (InvalidResponseError, NoResponseError):
                    bg = rgb_placeholder

                try:
                    fg = get_fg(timeout=args.timeout)
                except (InvalidResponseError, NoResponseError):
                    fg = rgb_placeholder
            else:
                bg = rgb_placeholder
                fg = rgb_placeholder

            stdout.write("\n    Background: %s\n" % bg)
            stdout.write("    Foreground: %s\n\n" % fg)

        C = ColorDisplay(args.timeout, args.level, args.do_query)

        if args.pretty:
            assert args.n in p_choices

            stdout.write('\n    ANSI colors:\n\n')
            C.show_ansi()

            if args.n > 16:
                stdout.write('\n    RGB cube:\n\n')
                C.show_color_cube(args.n)

                stdout.write('    Grayscale ramp:\n\n')
                C.show_grayscale_ramp(args.n)
        else:
            C.show_colors(args.n)

        if num_errors > 0:
            stderr.write("Warning: not all queries succeeded\n" +
                         "Warning:     (output contains " + 
                         "placeholders and may be inaccurate)\n")

    finally:
        if args.level >= 1:
            stdout.write(reset)

        flush_input()

        if tc_save != None:
            termios.tcsetattr(stdout.fileno(), 
                              termios.TCSANOW, tc_save)

Run it as

python showcolors.py -h

to get an overview of the functionality.

TL;DR: you can echo a string like "\033]4;13;?\007" to an xterm, and it will output the RGB value of color #13 to itself in the form "\033]4;13;rgb:rrrr/gggg/bbbb\007", which you can then read and parse. To try it out:

$ echo -en "\033]4;13;?\007"

There are some subtleties if you want this to work on other terminals like urxvt; for that you can experiment yourself or read the code. 

Further reading: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html (heading "Operating System Controls").

Xyne, regarding determining the number of colors: I don't know how to do that completely reliably. You can try simply

tput colors

but this relies on the value of $TERM and its terminfo and thus may be unreliable. A kludgy but workable way to do this using the "rgb_query" function in my script is to query color values using binary search to find the highest color value the terminal will allow you to query. The main problem with this is that if you query an invalid color index, then the terminal just won't respond at all, and if you query a valid index, it will respond but after a somewhat unpredictable delay, so it's hard to know for sure which one is the case. Like I said, it's kludgy but workable.

Edit: I went ahead and added that functionality to my script. Try running it as

python showcolors.py 0

or look at the function "test_num_colors".

Last edited by Lux Perpetua (2012-10-22 11:18:21)

Offline

#23 2012-10-21 23:25:28

Trilby
Inspector Parrot
Registered: 2011-11-29
Posts: 29,422
Website

Re: RGB values of current terminal color settings?

Lux Perpetua wrote:
$ echo -en "\033]4;132;?\007"

Dear lord.  That's much easier!  I'll have to remember that one.

Last edited by Trilby (2012-10-21 23:25:37)


"UNIX is simple and coherent..." - Dennis Ritchie, "GNU's Not UNIX" -  Richard Stallman

Offline

#24 2012-10-22 04:00:13

Xyne
Administrator/PM
Registered: 2008-08-03
Posts: 6,963
Website

Re: RGB values of current terminal color settings?

@Lux Perpetua
Perfect!

I need to read through it to fully understand it but from the output and a quick look it seems to do exactly what I need.

I see that all of the terminal initialization code is in the main routine for now. Would it make sense to encapsulate that in a terminal class and then use that for queries?

*some time later*

Ok, I've been playing around with the code and I've managed to reduce/butcher it down to what I need:

#!/usr/bin/env python3

from sys import stdout, stdin
import re
import select
import termios

def ansi_to_rgb(ansi, poll, timeout=-1, retries=5):
  query = "\033]" + ';'.join([str(a) for a in ansi]) + ';?' + "\007"

  # Discard just in case.
  while poll.poll(0):
    stdin.read()
  stdout.write(query)
  stdout.flush()

  regex = re.compile(
    "\033\\]([0-9]+;)+rgba?:([0-9a-fA-F]+)/([0-9a-fA-F]+)/([0-9a-fA-F]+)(/([0-9a-fA-F]+))?"
  )
  m = None
  output = ''

  # Attempt to read expected output from stdin.
  # Give up after a timeout or a limited number of retries
  while not m:
    if retries < 1 or not poll.poll(timeout):
      return None
    retries -= 1
    output += stdin.read()
    m = regex.search(output)

  width = len(m.group(3))
  base = float(0x10**width-1)

  # Group 5 is only present if there is an alpha channel. The value apparently
  # precedes the RGB values.
  if m.group(5):
    rgb_indices = [3, 4, 6]
  else:
    rgb_indices = [2, 3, 4]

  return tuple(int(m.group(i), 16)/base for i in rgb_indices)



def term_reset(tc_save):
  termios.tcsetattr(
    stdout.fileno(),
    termios.TCSANOW,
    tc_save
  )



def term_init():
  tc_save = None
  poll = select.poll()
  poll.register(stdin.fileno(), select.POLLIN)
  try:
    tc_save = termios.tcgetattr(stdout.fileno())

    tc = termios.tcgetattr(stdout.fileno())

    # Don't echo the terminal's responses
    tc[3] &= ~termios.ECHO

    # Noncanonical mode (i.e., disable buffering on the terminal
    # level)
    tc[3] &= ~termios.ICANON

    # Make input non-blocking
    tc[6][termios.VMIN] = 0
    tc[6][termios.VTIME] = 0

    termios.tcsetattr(stdout.fileno(), termios.TCSANOW, tc)

  except:
    if tc_save != None:
      term_reset(tc_save)
  return tc_save, poll


if __name__ == '__main__':
  try:
    tc_save, poll = term_init()

    i = 0
    while True:
      rgb = ansi_to_rgb([4, i], poll, timeout=1000)
      if rgb:
        print(i,rgb)
        i += 1
      else:
        break

  except KeyboardInterrupt:
    pass

  finally:
    term_reset(tc_save)

My main interest is converting to and from RGB values. With the above code I can get all available term colors and their matching rgb values. I thought I could use

\033[38;2;<r>;<g>;<b>m

to go the other way, but it doesn't seem to work. I may well be doing something wrong. It's not a show-stopper as I can use vector projections to go the other way. I already have code for that.

I don't fully understand what's going on with the polling. I noticed the comments in rgb_query function and I've restructured the code to try to address the issue in ansi_to_rbg above. I am may well be messing with something that I shouldn't.



Incidentally, I smiled when I saw this rgb_query because I had to deal with exactly the same code earlier today for my own module (which is what I need this for in the first place):

nd = len(m.group(2))
u = int('f'*nd, 16)

My alternative solution avoids string conversions, if you're interested:

  width = len(m.group(3))
  base = float(0x10**width-1)

I use 3 instead of 2 because the rest of the code indicates that 2 may be an alpha channel. While it will probably be the same length as the rgb fields, I'd rather use 3 just in case.


My Arch Linux StuffForum EtiquetteCommunity Ethos - Arch is not for everyone

Offline

#25 2012-10-22 07:55:16

Xyne
Administrator/PM
Registered: 2008-08-03
Posts: 6,963
Website

Re: RGB values of current terminal color settings?

Just bumping to announce the release of the module for which this was intended (colorsysplus):


My Arch Linux StuffForum EtiquetteCommunity Ethos - Arch is not for everyone

Offline

Board footer

Powered by FluxBB