Source code for TermTk.TTkCore.color

# MIT License
#
# Copyright (c) 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

__all__ = ['TTkColor',
           'TTkColorGradient', 'TTkLinearGradient', 'TTkAlternateColor']

from TermTk.TTkCore.TTkTerm.colors import TTkTermColor
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.helper import TTkHelper

# Ansi Escape Codes:
# https://conemu.github.io/en/AnsiEscapeCodes.html

# From http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
# Code:         Client:   Meaning:
# [0m           --        reset; clears all colors and styles (to white on black)
# [1m           --        bold on (see below)
# [3m           --        italics on
# [4m           --        underline on
# [7m           2.50      inverse on; reverses foreground & background colors
# [9m           2.50      strikethrough on
# [22m          2.50      bold off (see below)
# [23m          2.50      italics off
# [24m          2.50      underline off
# [27m          2.50      inverse off
# [29m          2.50      strikethrough off
# [30m          --        set foreground color to black
# [31m          --        set foreground color to red
# [32m          --        set foreground color to green
# [33m          --        set foreground color to yellow
# [34m          --        set foreground color to blue
# [35m          --        set foreground color to magenta (purple)
# [36m          --        set foreground color to cyan
# [37m          --        set foreground color to white
# [39m          2.53      set foreground color to default (white)
# [40m          --        set background color to black
# [41m          --        set background color to red
# [42m          --        set background color to green
# [43m          --        set background color to yellow
# [44m          --        set background color to blue
# [45m          --        set background color to magenta (purple)
# [46m          --        set background color to cyan
# [47m          --        set background color to white
# [49m          2.53      set background color to default (black)

class _TTkColor:
    __slots__ = ('_fg','_bg','_mod', '_colorMod', '_link', '_buffer', '_clean')
    _fg: tuple[int]; _bg: tuple[int]; _mod: int
    def __init__(self,
                 fg:tuple[int]=None,
                 bg:tuple[int]=None,
                 mod:int=0,
                 colorMod=None,
                 link:str='',
                 clean=False) -> None:
        self._fg  = fg
        self._bg  = bg
        self._mod = mod
        self._link = link
        self._clean = clean or not (fg or bg or mod)
        self._colorMod = colorMod
        self._buffer = None

    def foreground(self):
        if self._fg:
            return _TTkColor(fg=self._fg)
        else:
            return None

    def background(self):
        if self._bg:
            return _TTkColor(bg=self._bg)
        else:
            return None

    def bold(self) -> bool:
        return  self._mod & TTkTermColor.BOLD

    def italic(self) -> bool:
        return  self._mod & TTkTermColor.ITALIC

    def underline(self) -> bool:
        return  self._mod & TTkTermColor.UNDERLINE

    def strikethrough(self) -> bool:
        return  self._mod & TTkTermColor.STRIKETROUGH

    def blinking(self) -> bool:
        return  self._mod & TTkTermColor.BLINKING

    def colorType(self):
        return \
            ( TTkK.Foreground if self._fg  else TTkK.NONE ) | \
            ( TTkK.Background if self._bg  else TTkK.NONE ) | \
            ( TTkK.Modifier   if self._mod else TTkK.NONE )

    @staticmethod
    def rgb2hsl(rgb):
        r = rgb[0]/255
        g = rgb[1]/255
        b = rgb[2]/255
        cmax = max(r,g,b)
        cmin = min(r,g,b)

        lum = (cmax+cmin)/2
        if cmax == cmin:
            return 0,0,lum

        delta = cmax-cmin
        if   cmax == r:
            hue = ((g-b)/delta)%6
        elif cmax == g:
            hue = (b-r)/delta+2
        else:
            hue = (r-g)/delta+4

        sat = delta / (1 - abs(delta-1))
        hue = int(hue*60) + ( 360 if hue < 0 else 0 )
        sat = int(sat*100)
        lum = int(lum*100)

        return hue,sat,lum

    @staticmethod
    def hsl2rgb(hsl):
        hue = hsl[0]
        sat = hsl[1] / 100
        lum = hsl[2] / 100

        c = (1-abs(2*lum-1))*sat
        x = c*(1-abs((hue/60)%2-1))
        m = lum-c/2

        if     0 <= hue < 60:
          r,g,b = c,x,0
        elif  60 <= hue < 120:
          r,g,b = x,c,0
        elif 120 <= hue < 180:
          r,g,b = 0,c,x
        elif 180 <= hue < 240:
          r,g,b = 0,x,c
        elif 240 <= hue < 300:
          r,g,b = x,0,c
        elif 300 <= hue < 360:
          r,g,b = c,0,x

        r = int((r + m) * 255)
        g = int((g + m) * 255)
        b = int((b + m) * 255)

        return r,g,b

    def getHex(self, ctype):
        if ctype == TTkK.Foreground:
            r,g,b = self.fgToRGB()
        else:
            r,g,b = self.bgToRGB()
        return f"#{r<<16|g<<8|b:06x}"

    def fgToRGB(self):
        return self._fg if self._fg else (0,0,0)

    def bgToRGB(self):
        return self._bg if self._bg else (0,0,0)

    def invertFgBg(self):
        ret = self.copy()
        ret._fg = self._bg
        ret._bg = self._fg
        return ret

    def __str__(self):
        if not self._buffer:
            self._buffer = TTkTermColor.rgb2ansi(
                                fg=self._fg, bg=self._bg, mod=self._mod,
                                link=self._link, clean=self._clean)
        return self._buffer

    def __eq__(self, other):
        if other is None: return False
        return (
            self._fg   == other._fg   and
            self._bg   == other._bg   and
            self._mod  == other._mod  and
            self._link == other._link )

    # self | other
    def __or__(self, other):
        # TTkLog.debug("__add__")
        if other._clean:
            return other.copy()
        clean = self._clean
        fg:  str = self._fg or other._fg
        bg:  str = self._bg or other._bg
        mod: str = self._mod + other._mod
        link:str = self._link or other._link
        colorMod = self._colorMod or other._colorMod
        return TTkColor(
                    fg=fg, bg=bg, mod=mod,
                    colorMod=colorMod, link=link,
                    clean=clean)

    # self + other
    def __add__(self, other):
        # TTkLog.debug("__add__")
        if other._clean:
            return other.copy()
        clean = self._clean
        fg:  str = other._fg or self._fg
        bg:  str = other._bg or self._bg
        mod: str = self._mod + other._mod
        link:str = self._link or other._link
        colorMod = other._colorMod or self._colorMod
        return TTkColor(
                    fg=fg, bg=bg, mod=mod,
                    colorMod=colorMod, link=link,
                    clean=clean)

    def __sub__(self, other):
        # TTkLog.debug("__sub__")
        # if other is None: return str(self)
        if ( None == self._bg   != other._bg   or
             None == self._fg   != other._fg   or
                     self._link != other._link or
                     self._mod  != other._mod ):
            ret = self.copy()
            ret._clean = True
            return ret
        return self

    def modParam(self, *args, **kwargs) -> None:
        if not self._colorMod: return self
        ret = self.copy()
        ret._colorMod.setParam(*args, **kwargs)
        return ret

    def mod(self, x , y):
        if not self._colorMod: return self
        return self._colorMod.exec(x,y,self)

    def copy(self, modifier=True):
        ret = _TTkColor()
        ret._fg   = self._fg
        ret._bg   = self._bg
        ret._mod  = self._mod
        ret._link = self._link
        ret._clean = self._clean
        if modifier and self._colorMod:
            ret._colorMod = self._colorMod.copy()
        return ret

class _TTkColorModifier():
    def __init__(self, *args, **kwargs) -> None: pass
    def setParam(self, *args, **kwargs) -> None: pass
    def copy(self): return self

[docs] class TTkColorGradient(_TTkColorModifier): '''TTkColorGradient''' __slots__ = ('_fgincrement', '_bgincrement', '_val', '_step', '_buffer', '_orientation') _increment: int; _val: int def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) if "increment" in kwargs: self._fgincrement = kwargs.get("increment") self._bgincrement = kwargs.get("increment") else: self._fgincrement = kwargs.get("fgincrement",0) self._bgincrement = kwargs.get("bgincrement",0) self._orientation = kwargs.get("orientation", TTkK.VERTICAL) self._val = 0 self._step = 1 self._buffer = {}
[docs] def setParam(self, *args, **kwargs) -> None: self._val = kwargs.get("val",0) self._step = kwargs.get("step",1)
[docs] def exec(self, x, y, color): vx = x if self._orientation == TTkK.HORIZONTAL else y step = self._step def _applyGradient(c,incr): if not c: return c multiplier = abs(self._val + vx) r = int(c[0])+ incr * multiplier // step g = int(c[1])+ incr * multiplier // step b = int(c[2])+ incr * multiplier // step r = max(min(255,r),0) g = max(min(255,g),0) b = max(min(255,b),0) return (r,g,b) bname = str(color) # I made a buffer to keep all the gradient values to speed up the paint process if bname not in self._buffer: self._buffer[bname] = [None]*(256*2) id = self._val + vx - 256 if self._buffer[bname][id] is not None: return self._buffer[bname][id] copy = color.copy(modifier=False) copy._fg = _applyGradient(color._fg, self._fgincrement) copy._bg = _applyGradient(color._bg, self._bgincrement) self._buffer[bname][id] = copy return self._buffer[bname][id]
[docs] def copy(self): return self
[docs] class TTkLinearGradient(_TTkColorModifier): '''TTkLinearGradient''' __slots__ = ( '_direction', '_direction_squaredlength', '_base_pos', '_target_color') default_target_color = _TTkColor(fg=(0,255,0), bg=(255,0,0)) def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._base_pos = (0, 0) self._direction = (30, 30) self._target_color = self.default_target_color self.setParam(*args, **kwargs)
[docs] def setParam(self, *args, **kwargs) -> None: self._base_pos = tuple(kwargs.get('base_pos', self._base_pos)) direct = tuple(kwargs.get('direction', self._direction)) self._direction = direct self._direction_squaredlength = direct[0]*direct[0] + direct[1]*direct[1] self._target_color = kwargs.get('target_color', self._target_color)
[docs] def exec(self, x, y, base_color): diffx, diffy = x - self._base_pos[0], y - self._base_pos[1] prod = diffx * self._direction[0] + diffy * self._direction[1] beta = prod/self._direction_squaredlength if beta <= 0: return base_color target_color = self._target_color if beta >= 1: return target_color alpha = 1.0 - beta copy = base_color.copy(modifier=False) if copy._fg is not None and target_color._fg is not None: copy._fg = ( int(alpha*base_color._fg[0] + beta*target_color._fg[0]), int(alpha*base_color._fg[1] + beta*target_color._fg[1]), int(alpha*base_color._fg[2] + beta*target_color._fg[2])) if copy._bg is not None and target_color._bg is not None: copy._bg = ( int(alpha*base_color._bg[0] + beta*target_color._bg[0]), int(alpha*base_color._bg[1] + beta*target_color._bg[1]), int(alpha*base_color._bg[2] + beta*target_color._bg[2])) return copy
[docs] class TTkColor(_TTkColor): ''' TermTk Color helper .. role:: strike :class: strike .. role:: underline :class: underline The TTkColor constructor creates the color based on HEX values. Example: .. code:: python # Foreground only colors: color_fg_red = TTkColor.fg('#FF0000') color_fg_green = TTkColor.fg('#00FF00') color_fg_blue = TTkColor.fg('#0000FF') # Background only colors: color_bg_red = TTkColor.bg('#FF0000') color_bg_green = TTkColor.bg('#00FF00') color_bg_blue = TTkColor.bg('#0000FF') # Combine color_1 = color_fg_red + color_bg_blue color_2 = color_fg_red + TTkColor.bg('#FFFF00') color_3 = color_2 + TTkColor.UNDERLINE + TTkColor.BOLD # Use presets color_4 = TTkColor.RED color_5 = TTkColor.BG_YELLOW + color_4 color_6 = color_5 + TTkColor.UNDERLINE + TTkColor.BOLD ''' RST = _TTkColor() '''Reset to the default terminal color and modifiers''' BLACK = _TTkColor(fg=( 0, 0, 0)) '''(fg) #000000 - Black''' WHITE = _TTkColor(fg=(255,255,255)) '''(fg) #FFFFFF - White''' RED = _TTkColor(fg=(255, 0, 0)) '''(fg) #FF0000 - Red''' GREEN = _TTkColor(fg=( 0,255, 0)) '''(fg) #00FF00 - Green''' BLUE = _TTkColor(fg=( 0, 0,255)) '''(fg) #0000FF - Blue''' CYAN = _TTkColor(fg=( 0,255,255)) '''(fg) #00FFFF - Cyan''' MAGENTA = _TTkColor(fg=(255, 0,255)) '''(fg) #FF00FF - Magenta''' YELLOW = _TTkColor(fg=(255,255, 0)) '''(fg) #FFFF00 - Yellow''' FG_BLACK = BLACK '''(fg) #000000 - Black''' FG_WHITE = WHITE '''(fg) #FFFFFF - White''' FG_RED = RED '''(fg) #FF0000 - Red''' FG_GREEN = GREEN '''(fg) #00FF00 - Green''' FG_BLUE = BLUE '''(fg) #0000FF - Blue''' FG_CYAN = CYAN '''(fg) #00FFFF - Cyan''' FG_MAGENTA = MAGENTA '''(fg) #FF00FF - Magenta''' FG_YELLOW = YELLOW '''(fg) #FFFF00 - Yellow''' BG_BLACK = BLACK.invertFgBg() '''(bg) #000000 - Black''' BG_WHITE = WHITE.invertFgBg() '''(bg) #FFFFFF - White''' BG_RED = RED.invertFgBg() '''(bg) #FF0000 - Red''' BG_GREEN = GREEN.invertFgBg() '''(bg) #00FF00 - Green''' BG_BLUE = BLUE.invertFgBg() '''(bg) #0000FF - Blue''' BG_CYAN = CYAN.invertFgBg() '''(bg) #00FFFF - Cyan''' BG_MAGENTA = MAGENTA.invertFgBg() '''(bg) #FF00FF - Magenta''' BG_YELLOW = YELLOW.invertFgBg() '''(bg) #FFFF00 - Yellow''' # Modifiers: BOLD = _TTkColor(mod=TTkTermColor.BOLD) '''**Bold** modifier''' ITALIC = _TTkColor(mod=TTkTermColor.ITALIC) '''*Italic* modifier''' UNDERLINE = _TTkColor(mod=TTkTermColor.UNDERLINE) ''':underline:`Underline` modifier''' STRIKETROUGH = _TTkColor(mod=TTkTermColor.STRIKETROUGH) ''':strike:`Striketrough` modifier''' BLINKING = _TTkColor(mod=TTkTermColor.BLINKING) '''"Blinking" modifier'''
[docs] @staticmethod def hexToRGB(val): r = int(val[1:3],base=16) g = int(val[3:5],base=16) b = int(val[5:7],base=16) return (r,g,b)
[docs] @staticmethod def ansi(ansi): fg,bg,mod,clean = TTkTermColor.ansi2rgb(ansi) return TTkColor(fg=fg, bg=bg, mod=mod, clean=clean)
[docs] @staticmethod def fg(*args, **kwargs) -> None: ''' Helper to generate a Foreground color Example: .. code:: python color_1 = TTkColor.fg('#FF0000') color_2 = TTkColor.fg(color='#00FF00') color_3 = TTkColor.fg('#0000FF', modifier=TTkColorGradient(increment=6)) :param str color: the color representation in (str)HEX :type color: str :param str modifier: (experimental) the color modifier to be used to improve the **kinkiness** :type modifier: TTkColorModifier, optional :return: :py:class:`TTkColor` ''' mod = kwargs.get('modifier', None ) link = kwargs.get('link', '' ) if len(args) > 0: color = args[0] else: color = kwargs.get('color', "" ) return TTkColor(fg=TTkColor.hexToRGB(color), colorMod=mod, link=link)
[docs] @staticmethod def bg(*args, **kwargs) -> None: ''' Helper to generate a Background color Example: .. code:: python color_1 = TTkColor.bg('#FF0000') color_2 = TTkColor.bg(color='#00FF00') color_3 = TTkColor.bg('#0000FF', modifier=TTkColorGradient(increment=6)) :param str color: the color representation in (str)HEX :type color: str :param str modifier: (experimental) the color modifier to be used to improve the **kinkiness** :type modifier: TTkColorModifier, optional :return: :py:class:`TTkColor` ''' mod = kwargs.get('modifier', None ) link = kwargs.get('link', '' ) if len(args) > 0: color = args[0] else: color = kwargs.get('color', "" ) return TTkColor(bg=TTkColor.hexToRGB(color), colorMod=mod, link=link)
[docs] @staticmethod def fgbg(fg:str='', bg:str='', link:str='', modifier:_TTkColorModifier=None): ''' Helper to generate a Background color Example: .. code:: python color_1 = TTkColor.fgbg('#FF0000','#0000FF') color_2 = TTkColor.fgbg(fg='#00FF00',bg='#0000FF') color_3 = TTkColor.fgbg('#0000FF','#0000FF', modifier=TTkColorGradient(increment=6)) :param str fg: the foreground color representation in (str)HEX :type fg: str :param str bg: the background color representation in (str)HEX :type bg: str :param str modifier: (experimental) the color modifier to be used to improve the **kinkiness** :type modifier: TTkColorModifier, optional :return: :py:class:`TTkColor` ''' return TTkColor(fg=TTkColor.hexToRGB(fg), bg=TTkColor.hexToRGB(bg), colorMod=modifier, link=link)
[docs] class TTkAlternateColor(_TTkColorModifier): '''TTkAlternateColor''' __slots__ = ('_alternateColor') def __init__(self, alternateColor:TTkColor=TTkColor.RST, **kwargs) -> None: super().__init__(**kwargs) self.setParam(alternateColor)
[docs] def setParam(self, alternateColor:TTkColor): self._alternateColor = alternateColor
[docs] def exec(self, x:int, y:int, base_color:TTkColor) -> TTkColor: if y%2: return self._alternateColor else: return base_color.copy(modifier=False)