Skip to content

Commit

Permalink
add color terminal
Browse files Browse the repository at this point in the history
  • Loading branch information
lidefang committed Mar 25, 2022
1 parent 5ebf14a commit e9b7ed3
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 1 deletion.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
14. 比较两个字符串差异,高亮差异部分
15. 根据项目名和当前脚本路径检测项目路径并添加至系统路径
16. 读取yaml文件
17. 终端彩色输出

## 使用

Expand Down Expand Up @@ -390,6 +391,14 @@ test: 3
{'test': 3}
```

### 17.终端彩色输出

```python
from lightutils import color

print(color.green("green"))
```

## 参考
1. [lightless233/colorlog: Python彩色log模块封装](https://github.com/lightless233/colorlog)
2. [Getting a random free tcp port in python using sockets](https://gist.github.com/gabrielfalcao/20e567e188f588b65ba2)
Expand All @@ -402,6 +411,7 @@ test: 3
9. [python如何将变量名转化为同名字符串? - 知乎](https://www.zhihu.com/question/42768955)
10. [bert4keras/snippets.py at master · bojone/bert4keras](https://github.com/bojone/bert4keras/blob/master/bert4keras/snippets.py)
11. [bojone/text_compare: 用python比较两个字符串差异,高亮差异部分](https://github.com/bojone/text_compare)
12. [reorx/python-terminal-color: Drop-in single file library for printing color in terminal](https://github.com/reorx/python-terminal-color)

## 打赏

Expand Down
3 changes: 3 additions & 0 deletions examples/color_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from lightutils import color

print(color.green("green"))
1 change: 1 addition & 0 deletions lightutils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .common import *
from .sys import *
from .io import *
from .common import color
331 changes: 331 additions & 0 deletions lightutils/common/color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
"""
color.py
========
Usage
-----
>>> import color
>>>
>>> # 8-bit color
>>> print(red('red') + green('green') + blue('blue'))
>>> print(bold(yellow('bold yellow')) + underline(cyan('underline cyan')))
>>> print(magenta_hl('magenta highlight'))
>>>
>>> # xterm 256 color
>>> print(bg256('A9D5DE', fg256('276F86', 'Info!')))
>>> print(bg256('E0B4B4', fg256('912D2B', 'Warning!')))
>>> print(hl256('10a3a3', 'Teal'))
Note:
1. Every color function receives and returns string, so that the result
could be used with any other strings, in any string formatting situation.
2. If you pass a str type string, the color function will return a str.
If you pass a bytes type string, the color function will return a bytes string.
3. Color functions could be composed together, like put ``red`` into ``bold``,
or put ``bg256`` into ``fg256``. ``xxx_hl`` and ``hl256`` are mostly used
independently.
API
---
8-bit colors:
======== ============ ===========
Colors Background Highlight
======== ============ ===========
black black_bg black_hl
red red_bg red_hl
green green_bg green_hl
yellow yellow_bg yellow_hl
blue blue_bg blue_hl
magenta magenta_bg magenta_hl
cyan cyan_bg cyan_hl
white white_bg white_hl
======== ============ ===========
Styles:
- bold
- italic
- underline
- strike
- blink
.. py:function:: <color_function>(s)
Decorate string with specified color or style.
A color function with ``_bg`` suffix means it will set color as background.
A color function with ``_hl`` suffix means it will set color as background,
and change the foreground as well to make the word standout.
:param str s: The input string
:return: The decorated string
:rtype: string
:raises ValueError: if the message_body exceeds 160 characters
256 colors:
- fg256
- bg256
- hl256
.. py:function:: <256_color_function>(hexrgb, s)
Decorate string with specified hex rgb color
``fg256`` will set color as foreground.
``bg256`` will set color as background.
``hg256`` will highlight input with the color.
:param str hexrgb: The hex rgb color string, accept length 3 and 6. eg: ``555``, ``912D2B``
:param str s: The input string
:return: The decorated string
:rtype: string
:raises ValueError: If the input string's length not equal to 3 or 6.
"""

from typing import Union, Any, Callable, Optional, Tuple, List, Dict
import sys


_use_color_no_tty = True


def use_color_no_tty(flag):
global _use_color_no_tty
_use_color_no_tty = flag


def use_color():
if sys.stdout.isatty():
return True
if _use_color_no_tty:
return True
return False


def esc(*codes: Union[int, str]) -> str:
"""Produces an ANSI escape code from a list of integers
:rtype: text_type
"""
return t_('\x1b[{}m').format(t_(';').join(t_(str(c)) for c in codes))


def t_(b: Union[bytes, Any]) -> str:
"""ensure text type"""
if isinstance(b, bytes):
return b.decode()
return b


def b_(t: Union[str, Any]) -> bytes:
"""ensure binary type"""
if isinstance(t, str):
return t.encode()
return t


###############################################################################
# 8 bit Color
###############################################################################

def make_color(start, end: str) -> Callable[[str], str]:
def color_func(s: str) -> str:
if not use_color():
return s

# render
return start + t_(s) + end

return color_func


# According to https://en.wikipedia.org/wiki/ANSI_escape_code#graphics ,
# 39 is reset for foreground, 49 is reset for background, 0 is reset for all
# we can use 0 for convenience, but it will make color combination behaves weird.
END = esc(0)

FG_END = esc(39)
black = make_color(esc(30), FG_END)
red = make_color(esc(31), FG_END)
green = make_color(esc(32), FG_END)
yellow = make_color(esc(33), FG_END)
blue = make_color(esc(34), FG_END)
magenta = make_color(esc(35), FG_END)
cyan = make_color(esc(36), FG_END)
white = make_color(esc(37), FG_END)

BG_END = esc(49)
black_bg = make_color(esc(40), BG_END)
red_bg = make_color(esc(41), BG_END)
green_bg = make_color(esc(42), BG_END)
yellow_bg = make_color(esc(43), BG_END)
blue_bg = make_color(esc(44), BG_END)
magenta_bg = make_color(esc(45), BG_END)
cyan_bg = make_color(esc(46), BG_END)
white_bg = make_color(esc(47), BG_END)

HL_END = esc(22, 27, 39)
#HL_END = esc(22, 27, 0)

black_hl = make_color(esc(1, 30, 7), HL_END)
red_hl = make_color(esc(1, 31, 7), HL_END)
green_hl = make_color(esc(1, 32, 7), HL_END)
yellow_hl = make_color(esc(1, 33, 7), HL_END)
blue_hl = make_color(esc(1, 34, 7), HL_END)
magenta_hl = make_color(esc(1, 35, 7), HL_END)
cyan_hl = make_color(esc(1, 36, 7), HL_END)
white_hl = make_color(esc(1, 37, 7), HL_END)

bold = make_color(esc(1), esc(22))
italic = make_color(esc(3), esc(23))
underline = make_color(esc(4), esc(24))
strike = make_color(esc(9), esc(29))
blink = make_color(esc(5), esc(25))


###############################################################################
# Xterm 256 Color (delete if you don't need)
###############################################################################
#
# Rewrite from: https://gist.github.com/MicahElliott/719710

import re # NOQA

# Default color levels for the color cube
CUBELEVELS: List[int] = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]

# Generate a list of midpoints of the above list
SNAPS: List[int] = [(x + y) // 2 for x, y in list(zip(CUBELEVELS, [0] + CUBELEVELS))[1:]]

# Gray-scale range.
_GRAYSCALE = [
(0x08, 232), # 0x08 means 080808 in HEX color
(0x12, 233),
(0x1c, 234),
(0x26, 235),
(0x30, 236),
(0x3a, 237),
(0x44, 238),
(0x4e, 239),
(0x58, 240),
(0x62, 241),
(0x6c, 242),
(0x76, 243),
(0x80, 244),
(0x8a, 245),
(0x94, 246),
(0x9e, 247),
(0xa8, 248),
(0xb2, 249),
(0xbc, 250),
(0xc6, 251),
(0xd0, 252),
(0xda, 253),
(0xe4, 254),
(0xee, 255),
]
GRAYSCALE: Dict[int, int] = dict(_GRAYSCALE)

GRAYSCALE_POINTS: List[int] = [i for i, _ in _GRAYSCALE]


def get_closest(v: int, l: list):
return min(l, key=lambda x: abs(x - v))


class Memorize(dict):
def __init__(self, func):
self.func = func
self.__doc__ = func.__doc__

def __call__(self, *args):
return self[args]

def __missing__(self, key):
result = self[key] = self.func(*key)
return result


def memorize(func) -> Callable:
func._cache = {}

def wrapper(*args, **kwargs):
if kwargs:
return func(*args, **kwargs)
if args not in func._cache:
func._cache[args] = func(*args, **kwargs)
return func._cache[args]

for i in ('__module__', '__name__', '__doc__'):
setattr(wrapper, i, getattr(func, i))
wrapper.__dict__.update(getattr(func, '__dict__', {})) # type: ignore
wrapper._origin = func # type: ignore
return wrapper


@memorize
def rgb_to_xterm(r: int, g: int, b: int) -> int:
""" Converts RGB values to the nearest equivalent xterm-256 color.
"""
if r == g == b:
# use gray scale
gs = get_closest(r, GRAYSCALE_POINTS)
return GRAYSCALE[gs]
# Using list of snap points, convert RGB value to cube indexes
r, g, b = map(lambda x: len(tuple(s for s in SNAPS if s < x)), (r, g, b))
# Simple colorcube transform
return r * 36 + g * 6 + b + 16


@memorize
def hex_to_rgb(hx: str) -> Tuple[int, int, int]:
hxlen = len(hx)
if hxlen != 3 and hxlen != 6:
raise ValueError('hx color must be of length 3 or 6')
if hxlen == 3:
hx = t_('').join(i * 2 for i in hx)
parts = [int(h, 16) for h in re.split(t_(r'(..)(..)(..)'), hx)[1:4]]
return tuple(parts) # type: ignore


def make_256(start: str, end: str) -> Callable[[Union[tuple, str], str, Optional[Tuple[int, int, int]]], str]:
def rgb_func(rgb: Union[tuple, str], s: str, x: Optional[Tuple[int, int, int]] = None) -> str:
"""
:param rgb: (R, G, B) tuple, or RRGGBB hex string
"""
if not use_color():
return s

t = t_(s)

# render
if not isinstance(rgb, tuple):
rgb = hex_to_rgb(t_(rgb))
if x is not None:
xcolor = x
else:
xcolor = rgb_to_xterm(*rgb)

tpl = start + t_('{s}') + end
f = tpl.format(
x=xcolor,
s=t)

return f

return rgb_func


fg256 = make_256(esc(38, 5, t_('{x}')), esc(39))
bg256 = make_256(esc(48, 5, t_('{x}')), esc(49))
hl256 = make_256(esc(1, 38, 5, t_('{x}'), 7), esc(27, 39, 22))

_grayscale_xterm_codes = [i for _, i in _GRAYSCALE]
grayscale = {(i - _grayscale_xterm_codes[0]): make_color(esc(38, 5, i), esc(39)) for i in _grayscale_xterm_codes}
grayscale_bg = {(i - _grayscale_xterm_codes[0]): make_color(esc(48, 5, i), esc(49)) for i in _grayscale_xterm_codes}
grayscale_hl = {(i - _grayscale_xterm_codes[0]): make_color(esc(1, 38, 5, i, 7), esc(27, 39, 22)) for i in _grayscale_xterm_codes}
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.16
0.1.17

0 comments on commit e9b7ed3

Please sign in to comment.