Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Custom colorz #4

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- A `utils` submodule with `hex_to_rgb` function. Yes, custom colors coming!
- Added regexp based optimization of PDA stack.
- UserString instance, so the Huestr behaves more like a hue string.

### Changed
- Using `.format` strings.

### Fixed
- Fixed old reference name in a test, which was causing builds to fail.
- Many many readme fixes.
Expand Down
2 changes: 1 addition & 1 deletion example.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def destroy_planet(planet):
if __name__ == '__main__':
hues.info('Destroying the planets. Please wait.')

for planet in ('Murcury', 'Venus', 'Earth', 'Mars', 'Uranus',):
for planet in ('Mercury', 'Venus', 'Earth', 'Mars', 'Uranus',):
try:
success = destroy_planet(planet)
except ThisPlanetIsProtected:
Expand Down
17 changes: 12 additions & 5 deletions hues/colortable.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
'''
from collections import namedtuple

ANSIColors = namedtuple('ANSIColors', [
ANSIColors = namedtuple('ANSIColors', (
'black', 'red', 'green', 'yellow',
'blue', 'magenta', 'cyan', 'white',
])
ANSIStyles = namedtuple('ANSIStyles', [
))
ANSIStyles = namedtuple('ANSIStyles', (
'reset', 'bold', 'italic', 'underline', 'defaultfg', 'defaultbg',
])
))

# Style Codes
STYLE = ANSIStyles(0, 1, 3, 4, 39, 49)
Expand All @@ -24,7 +24,11 @@
HI_BG = ANSIColors(*range(100, 108))

# Terminal sequence format
SEQ = '\033[%sm'
SEQ = '\033[{0}m'

# Extended ANSI Foreground and Background Sequence format
XFG_SEQ = '38;2;{0};{1};{2}'
XBG_SEQ = '48;2;{0};{1};{2}'


def __gen_keywords__(*args, **kwargs):
Expand All @@ -42,3 +46,6 @@ def __gen_keywords__(*args, **kwargs):
return namedtuple('ANSISequences', fields)(*values)

KEYWORDS = __gen_keywords__(STYLE, FG, bg=BG, bright=HI_FG, bg_bright=HI_BG)


__all__ = ('FG', 'BG', 'HI_FG', 'HI_BG', 'KEYWORDS', 'XFG_SEQ', 'XBG_SEQ')
13 changes: 13 additions & 0 deletions hues/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import sys

if sys.version_info.major == 2:
string = unicode # noqa
else:
string = str

try:
from collections import UserString
except ImportError:
class UserString(string):
def __new__(cls, seq, *args, **kwargs):
return super(UserString, cls).__new__(cls, seq)
12 changes: 5 additions & 7 deletions hues/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@

from .huestr import HueString
from .colortable import KEYWORDS, FG

if sys.version_info.major == 2:
str = unicode # noqa
from .compat import string


CONFIG_FNAME = '.hues.yml'
Expand Down Expand Up @@ -96,7 +94,7 @@ def _base_log(self, contents):
def build_component(content, color=None):
fg = KEYWORDS.defaultfg if color is None else color
return (
HueString(u'{}'.format(content), hue_stack=(fg,)),
HueString(u'{}'.format(content), fg),
HueString(u' - '),
)

Expand Down Expand Up @@ -124,7 +122,7 @@ def log(self, *args, **kwargs):
label = getattr(self.conf.labels, k)
color = getattr(self.conf.hues, k)
nargs.append((label, color))
content = u' '.join([str(x) for x in args])
content = u' '.join([string(x) for x in args])
nargs.append((content, self.conf.hues.default))
return self._base_log(nargs)

Expand Down Expand Up @@ -161,8 +159,8 @@ def build_component(content, color=None, next_fg=None):
next_bg = KEYWORDS.defaultbg if next_fg is None else (next_fg + 10)

return (
HueString(u' {} '.format(content), hue_stack=(text_bg, text_fg)),
HueString(u'', hue_stack=(fg, next_bg)),
HueString(u' {} '.format(content), text_bg, text_fg),
HueString(u'', fg, next_bg),
)

nargs = ()
Expand Down
18 changes: 16 additions & 2 deletions hues/dpda.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
This module implements helper functions to allow producing deterministic
representation of arbitrarily chained props.
'''
import re
from functools import reduce, partial


Expand All @@ -15,7 +16,7 @@ def zero_break(stack):
return reduce(reducer, stack, tuple())


def annihilate(predicate, stack):
def _annihilate(predicate, stack):
'''Squash and reduce the input stack.
Removes the elements of input that match predicate and only keeps the last
match at the end of the stack.
Expand All @@ -25,9 +26,22 @@ def annihilate(predicate, stack):
return extra + (head,) if head else extra


def _annitilate_regex(pregex, stack):
'''Squash and reduce input stack with given regex predicate.
'''
extra = tuple(filter(lambda x: re.match(pregex, str(x)) is None, stack))
head = reduce(lambda x, y: y if re.match(pregex, str(y)) is not None else x, stack, None)
return extra + (head,) if head else extra


def annihilator(predicate):
'''Build a partial annihilator for given predicate.'''
return partial(annihilate, predicate)
return partial(_annihilate, predicate)


def regxannihilator(pregex):
'''Build a partial annihilator for given predicate.'''
return partial(_annitilate_regex, pregex)


def dedup(stack):
Expand Down
126 changes: 100 additions & 26 deletions hues/huestr.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,126 @@
# Unicorns
import sys
from functools import partial

from .colortable import FG, BG, HI_FG, HI_BG, SEQ, STYLE, KEYWORDS
from .dpda import zero_break, annihilator, dedup, apply

if sys.version_info.major == 2:
str = unicode # noqa
from .compat import string, UserString
from .colortable import FG, BG, HI_FG, HI_BG, SEQ, STYLE, KEYWORDS, XFG_SEQ, XBG_SEQ
from .dpda import zero_break, annihilator, regxannihilator, dedup, apply
from .utils import hex_to_rgb

XFG_REX = r'38;2;\d{1,3};\d{1,3};\d{1,3}'
XBG_REX = r'48;2;\d{1,3};\d{1,3};\d{1,3}'

OPTIMIZATION_STEPS = (
zero_break, # Handle Resets using `reset`.
annihilator(FG + HI_FG), # Squash foreground colors to the last value.
annihilator(BG + HI_BG), # Squash background colors to the last value.
dedup, # Remove duplicates in (remaining) style values.
zero_break, # Handle Resets using `reset`.
annihilator(FG + HI_FG), # Squash foreground colors to the last value.
annihilator(BG + HI_BG), # Squash background colors to the last value.
regxannihilator(XFG_REX), # Squash extended foreground
regxannihilator(XBG_REX), # Squash extended background
dedup, # Remove duplicates in (remaining) style values.
)
optimize = partial(apply, OPTIMIZATION_STEPS)


def colorize(string, stack):
def colorize(seq, stack):
'''Apply optimal ANSI escape sequences to the string.'''
codes = optimize(stack)
if len(codes):
prefix = SEQ % ';'.join(map(str, codes))
suffix = SEQ % STYLE.reset
return prefix + string + suffix
prefix = SEQ.format(';'.join(map(string, codes)))
suffix = SEQ.format(STYLE.reset)
return prefix + seq + suffix
else:
return string
return seq


class HueString(str):
class HueString(UserString):
'''Extend the string class to support hues.'''
def __new__(cls, string, hue_stack=None):
'''Return a new instance of the class.'''
return super(HueString, cls).__new__(cls, string)

def __init__(self, string, hue_stack=tuple()):
self.__string = string
self.__hue_stack = hue_stack
def __init__(self, seq, *hues, **kwhues):
if 'fg' in kwhues:
hues += (XFG_SEQ.format(*hex_to_rgb(kwhues['fg'])),)
if 'bg' in kwhues:
hues += (XBG_SEQ.format(*hex_to_rgb(kwhues['bg'])),)
self.hues = hues
self.data = seq

def __getattr__(self, attr):
try:
code = getattr(KEYWORDS, attr)
hues = self.__hue_stack + (code,)
return HueString(self.__string, hue_stack=hues)
hues = self.hues + (code,)
return HueString(self.data, *hues)
except AttributeError as e:
raise e

@property
def colorized(self):
return colorize(self.__string, self.__hue_stack)
return colorize(self.data, self.hues)

def __get_wrapped__(self, dat):
return self.__class__(dat, *self.hues)

def capitalize(self):
return self.__get_wrapped__(self.data.capitalize())

def casefold(self):
return self.__get_wrapped__(self.data.casefold())

def center(self, width, *args):
return self.__get_wrapped__(self.data.center(width, *args))

def expandtabs(self, tabsize=8):
return self.__get_wrapped__(self.data.expandtabs(tabsize))

def format(self, *args, **kwds):
return self.__get_wrapped__(self.data.format(*args, **kwds))

def format_map(self, mapping):
return self.__get_wrapped__(self.data.format_map(mapping))

def ljust(self, width, *args):
return self.__get_wrapped__(self.data.ljust(width, *args))

def lower(self):
return self.__get_wrapped__(self.data.lower())

def lstrip(self, chars=None):
return self.__get_wrapped__(self.data.lstrip(chars))

def partition(self, sep):
return tuple(map(self.__get_wrapped__, self.data.partition(sep)))

def replace(self, old, new, maxsplit=-1):
return self.__get_wrapped__(self.data.replace(old, new, maxsplit))

def rjust(self, width, *args):
return self.__get_wrapped__(self.data.rjust(width, *args))

def rpartition(self, sep):
return tuple(map(self.__get_wrapped__, self.data.rpartition(sep)))

def rstrip(self, chars=None):
return self.__get_wrapped__(self.data.rstrip(chars))

def split(self, sep=None, maxsplit=-1):
return tuple(map(self.__get_wrapped__, self.data.split(sep, maxsplit)))

def rsplit(self, sep=None, maxsplit=-1):
return tuple(map(self.__get_wrapped__, self.data.rsplit(sep, maxsplit)))

def splitlines(self, keepends=False):
return tuple(map(self.__get_wrapped__, self.data.splitlines(keepends)))

def strip(self, chars=None):
return self.__get_wrapped__(self.data.strip(chars))

def swapcase(self):
return self.__get_wrapped__(self.data.swapcase())

def title(self):
return self.__get_wrapped__(self.data.title())

def translate(self, *args):
return self.__get_wrapped__(self.data.translate(*args))

def upper(self):
return self.__get_wrapped__(self.data.upper())

def zfill(self, width):
return self.__get_wrapped__(self.data.zfill(width))
21 changes: 21 additions & 0 deletions hues/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'''Implements helper utilities.'''
from collections import namedtuple

RGB = namedtuple('RGB', ('red', 'green', 'blue'))


def hex_to_rgb(color):
'''Convert hex color string to a RGB tuple

>>> hex_to_rgb('#FFF')
RGB(red=255, green=255, blue=255)
'''
if not color.startswith('#') or len(color) not in (4, 7):
raise ValueError('Expected a hex coded color value, got `{0}`'.format(color))

hexcode = color[1:]
if len(hexcode) == 3:
hexcode = ''.join([x * 2 for x in hexcode])

cvals = (int(hexcode[i * 2:(i + 1) * 2], 16) for i in range(0, 3))
return RGB(*cvals)
2 changes: 1 addition & 1 deletion tests/test_colortable.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def test_ansi_styles():
assert STYLE.defaultbg == 49

def test_sequence():
assert SEQ % BG.black == '\033[40m'
assert SEQ.format(BG.black) == '\033[40m'

def test_keywords():
assert KEYWORDS.bg_black == BG.black
Expand Down
18 changes: 18 additions & 0 deletions tests/test_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import sys
import collections

from hues.compat import string, UserString


def test_string_compat():
if sys.version_info.major == 2:
assert string is unicode # noqa: F821
else:
assert string is str


def test_user_string():
if hasattr(collections, 'UserString'):
assert UserString is collections.UserString
else:
UserString('This is a test')
Loading