Advertisement
Guest User

Color remapping

a guest
Dec 21st, 2013
587
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 20.17 KB | None | 0 0
  1. #   Copyright 2013 Adam Ehlers Nyholm Thomsen
  2. #   Copyright 2012 Darsh Ranjan
  3. #
  4. #   This file is based on code from Darsh Ranjan's termcolors project.
  5. #
  6. #   termcolors is free software: you can redistribute it and/or modify
  7. #   it under the terms of the GNU General Public License as published by
  8. #   the Free Software Foundation, either version 3 of the License, or
  9. #   (at your option) any later version.
  10. #
  11. #   termcolors is distributed in the hope that it will be useful, but
  12. #   WITHOUT ANY WARRANTY; without even the implied warranty of
  13. #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14. #   General Public License for more details.
  15. #
  16. #   You should have received a copy of the GNU General Public License
  17. #   along with termcolors.  If not, see
  18. #   <http://www.gnu.org/licenses/>.
  19.  
  20. import os
  21. import re
  22. import select
  23. import termios
  24. from sys import version_info
  25.  
  26. #######################################################################
  27. # Query-related error conditions
  28.  
  29. class TerminalQueryError(Exception):
  30.    
  31.     '''
  32.    Base class for the other exceptions.
  33.  
  34.    '''
  35.  
  36.     def __init__(self, message):
  37.         Exception.__init__(self, message)
  38.  
  39.  
  40. class TerminalSetupError(TerminalQueryError):
  41.  
  42.     '''
  43.    We couldn't set up the terminal properly.
  44.  
  45.    '''
  46.  
  47.     def __init__(self, fd):
  48.         TerminalQueryError.__init__(
  49.             self,
  50.             ("Couldn't set up terminal on file " +
  51.              ("descriptor %d" % fd)))
  52.  
  53.  
  54. class NoResponseError(TerminalQueryError):
  55.  
  56.     '''
  57.    The terminal didn't respond, or we were too impatient.
  58.  
  59.    '''
  60.  
  61.     def __init__(self, q):
  62.         TerminalQueryError.__init__(
  63.             self,
  64.             "Timeout on query " + repr(q))
  65.  
  66.  
  67. class TerminalUninitializedError(TerminalQueryError):
  68.  
  69.     '''
  70.    Someone tried to do something without setting up the terminal
  71.    properly (by calling TerminalQueryContext.__enter__).
  72.  
  73.    '''
  74.    
  75.     def __init__(self, fd):
  76.         TerminalQueryError.__init__(
  77.             self,
  78.             (("Terminal on file descriptor %d " % fd) +
  79.              "not set up"))
  80.  
  81.  
  82. ########################################################################
  83.  
  84. class TerminalQueryContext(object):
  85.  
  86.     '''
  87.    Context manager for terminal RGB queries.
  88.  
  89.    '''
  90.  
  91.     # Operating system command
  92.     osc = "\033]"
  93.  
  94.     # String terminator
  95.     #  ("\033\\" is another option, but "\007" seems to be understood by
  96.     #  more terminals.  Terminology, for example, doesn't seem to like
  97.     #  "\033\\".)
  98.     st = "\007"
  99.  
  100.     # Control sequence introducer
  101.     csi = "\033["
  102.  
  103.     # ANSI SGR0
  104.     reset = csi + 'm'
  105.  
  106.  
  107.     def __init__(self, fd=0, timeout = -1):
  108.         '''
  109.        fd: open file descriptor referring to the terminal we care
  110.        about.  The default (0) is almost always correct.
  111.  
  112.        timeout: how many milliseconds to wait for a response, a negative
  113.            number meaning "infinite". A larger timeout is safer but will cause
  114.            this function to take proportionally more time.
  115.        '''
  116.         self.tc_save = None
  117.         self.fd = fd
  118.  
  119.         self.num_errors = 0
  120.         self.timeout = timeout
  121.  
  122.  
  123.     def __enter__(self):
  124.         '''
  125.        Set up the terminal for queries.
  126.  
  127.        '''
  128.         self.tc_save = termios.tcgetattr(self.fd)
  129.  
  130.         tc = termios.tcgetattr(self.fd)
  131.  
  132.         # Don't echo the terminal's responses
  133.         tc[3] &= ~termios.ECHO
  134.  
  135.         # Noncanonical mode (i.e., disable buffering on the terminal
  136.         # level)
  137.         tc[3] &= ~termios.ICANON
  138.  
  139.         # Make input non-blocking
  140.         tc[6][termios.VMIN] = 0
  141.         tc[6][termios.VTIME] = 0
  142.  
  143.         termios.tcsetattr(self.fd, termios.TCSANOW, tc)
  144.  
  145.         # Check if it succeeded
  146.         if termios.tcgetattr(self.fd) != tc:
  147.             termios.tcsetattr(self.fd, termios.TCSANOW, self.tc_save)
  148.             raise TerminalSetupError(self.fd)
  149.  
  150.         self.P = select.poll()
  151.         self.P.register(self.fd, select.POLLIN)
  152.  
  153.         return self
  154.  
  155.  
  156.     def __exit__(self, exc_type, exc_value, traceback):
  157.         '''
  158.        Reset the terminal to its original state.
  159.  
  160.        '''
  161.         self.flush_input()
  162.  
  163.         if self.tc_save is not None:
  164.             termios.tcsetattr(self.fd, termios.TCSANOW, self.tc_save)
  165.  
  166.         del self.P
  167.  
  168.  
  169.     def get_num_colors(self):
  170.         '''
  171.        Attempt to determine the number of colors we are able to query
  172.        from the terminal.  
  173.  
  174.        '''
  175.         # We won't count failed queries in this function, since we're
  176.         # guaranteed to fail a few.
  177.         num_errors = self.num_errors
  178.  
  179.         if not self.get_indexed_color(0):
  180.             return 0
  181.  
  182.         a = 0
  183.         b = 1
  184.         while self.get_indexed_color(b):
  185.             a = b
  186.             b += b
  187.  
  188.         while b - a > 1:
  189.             c = (a + b)>>1
  190.             if self.get_indexed_color(c):
  191.                 a = c
  192.             else:
  193.                 b = c
  194.  
  195.         self.num_errors = num_errors
  196.         return b
  197.  
  198.  
  199.     def get_all_indexed_colors(self, limit=-1):
  200.         '''
  201.        Query as many indexed colors as possible up to `limit'
  202.        and return them all in a list.
  203.        '''
  204.         num_colors = self.get_num_colors()
  205.         if limit >= 0:
  206.             num_colors = min(num_colors, limit)
  207.         if num_colors == 0:
  208.             return []
  209.  
  210.         return self.color_query(range(num_colors))
  211.  
  212.     # Wrappers for xterm & urxvt operating system controls.
  213.     #
  214.     # These codes are all common to xterm and urxvt. Their responses
  215.     # aren't always in the same format (xterm generally being more
  216.     # consistent), but the regular expression used to parse the
  217.     # responses is general enough to work for both.
  218.     #
  219.     # Note: none of these functions is remotely thread-safe.
  220.  
  221.  
  222.     def get_indexed_color(self, a):
  223.         '''
  224.        Get color number `a'.
  225.  
  226.        '''
  227.         return self.color_query([a,])
  228.  
  229.  
  230.     def flush_input(self):
  231.         '''
  232.        Discard any input that can be read at this moment.
  233.  
  234.        '''
  235.         while self.P.poll(0):
  236.             os.read(self.fd, 4096)
  237.  
  238.     # Patterns matching unsigned decimal and hexadecimal integer
  239.     # literals
  240.     ndec = "[0-9]+"
  241.     nhex = "[0-9a-fA-F]+"
  242.  
  243.     # The "guard" query and its response pattern.
  244.     q_guard = csi + "6n"
  245.  
  246.     str_guard = "(.*)\033\\[{ndec};{ndec}R".format(**vars())
  247.     re_guard = re.compile(str_guard)
  248.  
  249.     # This is what we expect the terminal's response to a query for a
  250.     # color to look like.  If we didn't care about urxvt, we could get
  251.     # away with a simpler implementation here, since xterm and vte seem
  252.     # to give pretty consistent and systematic responses.  But I
  253.     # actually use urxvt most of the time, so....
  254.     str_color = "\033\\]4;(?:(?P<idx>{ndec});)?(?P<color>[^;\007]+)\007".format(**vars())
  255.     re_color = re.compile(str_color)
  256.  
  257.     # This is what the resp
  258.     str_default_color = "\033\\](?P<key_id>{ndec});(?P<color>[^;\007]+)\007".format(**vars())
  259.     re_default_color = re.compile(str_default_color)
  260.  
  261.     def color_query(self, q):
  262.         '''
  263.        Query color-valued terminal parameter.
  264.  
  265.        Arguments:
  266.            q: The query code as a sequence of nonnegative integers,
  267.                i.e., [q0, q1, ...] if the escape sequence in
  268.                pseudo-Python is
  269.  
  270.                    "\033]4;{q0};?;{q1};?;...\007"
  271.  
  272.        Return:
  273.            A list of tuples (idx, color) where color is a string representing
  274.            the color value of the color with index idx.  I.e. the response
  275.            should look like [(q0, color0), (q1, color1), ...].  If the
  276.            terminal provides an unparseable (or no) response, then None will
  277.            be returned.  
  278.  
  279.        See
  280.            http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
  281.  
  282.        ("Operating System Controls") to see the various queries
  283.        supported by xterm.  Urxvt supports some, but not all, of them,
  284.        and has a number of its own (see man -s7 urxvt).
  285.  
  286.        self.__enter__ must be called prior to calling this function, or
  287.        TerminalUninitializedError will be raised.
  288.  
  289.        '''
  290.         if not q:
  291.             return None
  292.  
  293.         query = (self.osc + '4;' +
  294.                  ';'.join(('%d;?' % k for k in q)) +
  295.                  self.st)
  296.  
  297.         try:
  298.             response = self.guarded_query(query)
  299.         except NoResponseError:
  300.             return None
  301.  
  302.         res = []
  303.         for i, m in zip(q, self.re_color.finditer(response)):
  304.             idx = m.group('idx')
  305.             if idx != None:
  306.                 assert int(idx) == i  # Make sure we get the responses in the correct order
  307.             res.append((i, m.group('color')))
  308.  
  309.         # Since we do multiple queries at once, try to calculate the number of
  310.         # errors.
  311.         num_errors = len(q) - len(res)
  312.         assert num_errors >= 0
  313.         self.num_errors += num_errors
  314.  
  315.         return res
  316.  
  317.     def color_setup(self, q):
  318.         """Setup color values of the terminal.
  319.  
  320.        Arguments:
  321.            q:  A list of tuples (idx, color) such that color is a string
  322.                representing the color value that idx should take.  Which
  323.                strings are permittable depends on the terminal, but most
  324.                terminals accept anything you can throw at XParseColor.
  325.                Specifically "#rrggbb" works.
  326.  
  327.                If q = [(i0, col0), (i1, col1), ...] then the escape sequence
  328.                used is:
  329.                    "\033]4;{i0};{col0};{i1};{col1};...\007"
  330.  
  331.        Returns:  None
  332.  
  333.        See
  334.            http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
  335.  
  336.        ("Operating System Controls") to see the various queries
  337.        supported by xterm.  Urxvt supports some, but not all, of them,
  338.        and has a number of its own (see man -s7 urxvt).
  339.  
  340.        self.__enter__ must be called prior to calling this function, or
  341.        TerminalUninitializedError will be raised.
  342.        """
  343.         if not q:
  344.             return None
  345.  
  346.         query = (self.osc + '4;' +
  347.                  ';'.join(('%d;%s' % k for k in q)) +
  348.                  self.st)
  349.  
  350.         try:
  351.             response = self.guarded_query(query)
  352.         except NoResponseError:
  353.             return None
  354.  
  355.     def get_default_color(self, key_id):
  356.         """Gets the values of the default color with id key_id
  357.  
  358.        The following key_id's of interest(it has more) are specified in the
  359.        xterm ctlseqs.txt file:
  360.            10     Foreground color
  361.            11     Background color
  362.            12     Text cursor color
  363.            13     Mouse foreground color
  364.            14     Mouse background color
  365.        
  366.        Note this is NOT supported on VTE, and I have only found it to work on
  367.        xterm and urxvt"""
  368.        
  369.         query = (self.osc + ('%d;?' % key_id) + self.st)
  370.  
  371.         try:
  372.             response = self.guarded_query(query)
  373.         except NoResponseError:
  374.             return None
  375.  
  376.         m = self.re_default_color.match(response)
  377.         assert int(m.group('key_id')) == key_id
  378.         return m.group('color')
  379.  
  380.     def set_default_color(self, key_id, color):
  381.         """Sets the values of the default color with key_id
  382.        
  383.        Note this is NOT supported on VTE, and I have only found it to work on
  384.        xterm and urxvt"""
  385.         query = (self.osc + ('%d;%s' % (key_id,color)) + self.st)
  386.         try:
  387.             response = self.guarded_query(query)
  388.         except NoResponseError:
  389.             return None
  390.  
  391.     # If a terminal sees an escape sequence it doesn't like, it will
  392.     # simply ignore it.  Also, it's hard to predict how long a terminal
  393.     # will take to respond to a query it does like.  However, some
  394.     # escape sequences, like "\033[6n", will produce a predictable
  395.     # response on *most* (but not all) terminals, and this fact can be
  396.     # used to test for the absence of a response to a particular query
  397.     # on such terminals.
  398.  
  399.     def guarded_query(self, q, flush=True):
  400.         '''
  401.        Send the terminal query string `q' and return the terminal's
  402.        response.
  403.  
  404.        Arguments:
  405.            q: the query string to send to the terminal.
  406.  
  407.            flush: whether to discard waiting input before sending the
  408.                query.  It usually makes sense to do this, but note that
  409.                the terminal may still send seemingly unsolicited data
  410.                (possibly in response to an earlier query) after the
  411.                input is flushed, so flushing the input does not
  412.                provide any guarantees.
  413.  
  414.        Return: The terminal's response to the query as a string.
  415.  
  416.        Errors:
  417.            NoResponseError will be raised if the query times out.
  418.  
  419.            TerminalUninitializedError will be raised if this instance's
  420.            context has not been __enter__-ed.
  421.  
  422.        '''
  423.         if not hasattr(self, "P"):
  424.             raise TerminalUninitializedError(self.fd)
  425.  
  426.         query = q + self.q_guard
  427.  
  428.         if flush:
  429.             self.flush_input()
  430.  
  431.         os.write(self.fd, query.encode())
  432.  
  433.         response = ""
  434.  
  435.         while self.P.poll(self.timeout):
  436.             s = os.read(self.fd, 4096)
  437.             if version_info.major >= 3:
  438.                 s = s.decode()
  439.             response += s
  440.  
  441.             m = self.re_guard.match(response)
  442.  
  443.             if m:
  444.                 return m.group(1)
  445.         else:
  446.             self.num_errors += 1
  447.             raise NoResponseError(query)
  448.  
  449. def get_random_rgb():
  450.     import random
  451.     return '#' + ''.join(('%x' % random.randint(0,15) for i in range(6)))
  452.  
  453. def get_light_name(name):
  454.     """Get the name of the corresponding color in the light palette"""
  455.     if name.startswith('base'):
  456.         # The base colors are opposite in the light palette
  457.         if len(name) == 6:
  458.             return 'base' + name[5]
  459.         elif len(name) == 5:
  460.             return 'base0' + name[4]
  461.         else:
  462.             assert False
  463.     else:
  464.         return name
  465.  
  466. def get_light(name):
  467.     """Get the information for the corresponding light color"""
  468.     light_val = sol[get_light_name(name)] # The light color information, for the hex and the 256 index
  469.     dark_val = sol[name] # The dark color information, for the 16 and 8 indices
  470.     # Return the light hex, dark 16 index, dark 8 indices, and light 256 color
  471.     # We return the light 256 color, since the 256 color map is supposed to
  472.     # approximate rgb colors and hence switching between light and dark
  473.     # mode should be done in the application color scheme not in the
  474.     # terminal color map.
  475.     return (light_val[0], dark_val[1], dark_val[2], light_val[3])
  476.  
  477. def get_colors(mode):
  478.     """Get the index to hex mapping for the given color mode.
  479.    
  480.    Arguments:
  481.        mode:
  482.            Get the index to hex mapping for mode, currently supported modes are:
  483.                16-dark     The dark version of the 16-color palette, for legacy applications like irssi
  484.                16-light    The light version of the 16-color palette, for legacy applications like irssi
  485.                256         The approximate 256 color entries for xterm and the exact solarized hex values
  486.  
  487.    Returns:
  488.        A list of tuples (idx, color) where idx is an integer and color is a
  489.        hex RGB representation of the color.
  490.    """
  491.  
  492.     return modes[mode]
  493.  
  494. def get_default_colors_aux(light = False):
  495.     get_hex_dark = lambda name: sol[name][0]
  496.     get_hex_light = lambda name: get_light(name)[0]
  497.     get_color = get_hex_light if light else get_hex_dark
  498.  
  499.     return [(i, get_color(name)) for (i, name) in default_colors.items()]
  500.  
  501. def get_default_colors(mode):
  502.     if not mode:
  503.         return []
  504.     return get_default_colors_aux(mode == 'light')
  505.  
  506. def get_default_ids():
  507.     return list(default_colors.keys())
  508.  
  509. #SOLARIZED HEX     16/8 TERMCOL  XTERM/HEX   L*A*B      RGB         HSB
  510. #--------- ------- ---- -------  ----------- ---------- ----------- -----------
  511. #base03    #002b36  8/4 brblack  234 #1c1c1c 15 -12 -12   0  43  54 193 100  21
  512. #base02    #073642  0/4 black    235 #262626 20 -12 -12   7  54  66 192  90  26
  513. #base01    #586e75 10/7 brgreen  240 #585858 45 -07 -07  88 110 117 194  25  46
  514. #base00    #657b83 11/7 bryellow 241 #626262 50 -07 -07 101 123 131 195  23  51
  515. #base0     #839496 12/6 brblue   244 #808080 60 -06 -03 131 148 150 186  13  59
  516. #base1     #93a1a1 14/4 brcyan   245 #8a8a8a 65 -05 -02 147 161 161 180   9  63
  517. #base2     #eee8d5  7/7 white    254 #e4e4e4 92 -00  10 238 232 213  44  11  93
  518. #base3     #fdf6e3 15/7 brwhite  230 #ffffd7 97  00  10 253 246 227  44  10  99
  519. #yellow    #b58900  3/3 yellow   136 #af8700 60  10  65 181 137   0  45 100  71
  520. #orange    #cb4b16  9/3 brred    166 #d75f00 50  50  55 203  75  22  18  89  80
  521. #red       #dc322f  1/1 red      160 #d70000 50  65  45 220  50  47   1  79  86
  522. #magenta   #d33682  5/5 magenta  125 #af005f 50  65 -05 211  54 130 331  74  83
  523. #violet    #6c71c4 13/5 brmagenta 61 #5f5faf 50  15 -45 108 113 196 237  45  77
  524. #blue      #268bd2  4/4 blue      33 #0087ff 55 -10 -45  38 139 210 205  82  82
  525. #cyan      #2aa198  6/6 cyan      37 #00afaf 60 -35 -05  42 161 152 175  74  63
  526. #green     #859900  2/2 green     64 #5f8700 60 -20  65 133 153   0  68 100  60
  527.  
  528. sol = {
  529.     # Name    : (HEX,      16, 8, 256)
  530.     'base03'  : ('#002b36',  8, 4, 234),
  531.     'base02'  : ('#073642',  0, 4, 235),
  532.     'base01'  : ('#586e75', 10, 7, 240),
  533.     'base00'  : ('#657b83', 11, 7, 241),
  534.     'base0'   : ('#839496', 12, 6, 244),
  535.     'base1'   : ('#93a1a1', 14, 4, 245),
  536.     'base2'   : ('#eee8d5',  7, 7, 254),
  537.     'base3'   : ('#fdf6e3', 15, 7, 230),
  538.     'yellow'  : ('#b58900',  3, 3, 136),
  539.     'orange'  : ('#cb4b16',  9, 3, 166),
  540.     'red'     : ('#dc322f',  1, 1, 160),
  541.     'magenta' : ('#d33682',  5, 5, 125),
  542.     'violet'  : ('#6c71c4', 13, 5,  61),
  543.     'blue'    : ('#268bd2',  4, 4,  33),
  544.     'cyan'    : ('#2aa198',  6, 6,  37),
  545.     'green'   : ('#859900',  2, 2,  64),
  546. }
  547.  
  548. default_colors = {
  549.     10 : 'base0',  #Foreground
  550.     11 : 'base03', #Background
  551.     12 : 'base1',  #Text cursor color
  552. #    13 : 'base1',  #Mouse foreground color
  553. #    14 : 'base01', #Mouse background color
  554. }
  555.  
  556. modes = {
  557.     '16-dark'  : [(i16,  h) for (h, i16, i8, i256) in sol.values()],
  558.     '16-light' : [(i16,  h) for (h, i16, i8, i256) in map(get_light, sol)],
  559.     '256'      : [(i256, h) for (h, i16, i8, i256) in sol.values()],
  560. }
  561.  
  562. def construct_argparser():
  563.     import argparse
  564.     parser = argparse.ArgumentParser(description = 'Color remapping for xterm-like terminals.')
  565.     parser.add_argument('-n', '--no-reset', dest = 'reset', action = 'store_const', const = False, default = True, help = 'Don\'t reset the colors afterwards(keep solarized colors)')
  566.     parser.add_argument('-m', '--mode', dest = 'mode', action = 'store', default = '256', help = 'The color palette to map', choices = ['16-dark', '16-light', '256'])
  567.     parser.add_argument('-d', '--set-defaults', dest = 'default_cols', action = 'store', default = False, help = 'Set the default colors(foreground, background, cursor, etc.)', choices = ['dark', 'light'])
  568.     parser.add_argument('CMD', nargs=argparse.REMAINDER, help = 'An (optional) command line application with arguments to run in the remapped color environment, for when you don\'t pass --no-reset')
  569.     return parser
  570.  
  571. if __name__ == '__main__':
  572.     import subprocess, sys
  573.     parser = construct_argparser()
  574.     args = parser.parse_args()
  575.     with TerminalQueryContext() as tq:
  576.         def set_defaults(cols):
  577.             for i, col in cols:
  578.                 tq.set_default_color(i, col)
  579.  
  580.         orig = [] # The original color mappings
  581.         orig_def = []
  582.  
  583.         new_def = get_default_colors(args.default_cols)
  584.         m = get_colors(args.mode)
  585.         if args.reset:
  586.             orig = tq.color_query([idx for (idx, col) in m])
  587.             orig_def = [(i, tq.get_default_color(i)) for i, col in new_def]
  588.  
  589.         tq.color_setup(m)
  590.         set_defaults(new_def)
  591.  
  592.         ret = 0
  593.         try:
  594.             if args.CMD:
  595.                 ret = subprocess.call(args.CMD)
  596.         finally:
  597.             tq.color_setup(orig)
  598.             set_defaults(orig_def)
  599.         if False: #Debug output
  600.             print('Errors: ', tq.num_errors)
  601.         sys.exit(ret)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement