Skip to content

Commit

Permalink
Merge pull request #43 from frostming/feature/typehint
Browse files Browse the repository at this point in the history
Add type hints
  • Loading branch information
frostming authored Jun 24, 2021
2 parents 869b9c5 + d350988 commit b728d53
Show file tree
Hide file tree
Showing 19 changed files with 464 additions and 419 deletions.
15 changes: 1 addition & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,6 @@ on:
- master

jobs:
Linting:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Linting
run: |
pip install flake8
flake8 --ignore=W,E203,F405 --max-line-length=88 cfonts tests
Testing:
runs-on: ${{ matrix.os }}
strategy:
Expand All @@ -34,7 +21,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pdm install -d
run: pdm install
- name: Run Tests
run: |
pdm run pytest tests
31 changes: 31 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Release

on:
push:
tags:
- "*"

jobs:
release-pypi:
name: release-pypi
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.8
architecture: "x64"
- name: Install build tool
run: |
pip install -U build
- name: Build artifacts
run: |
python -m build
- name: Test Build
run: |
pip install twine
twine check dist/*
- name: Upload to Pypi
run: |
twine upload --username __token__ --password ${{ secrets.PYPI_TOKEN }} dist/*
25 changes: 25 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
ci:
autoupdate_schedule: monthly
repos:
- repo: https://github.com/psf/black
rev: 21.6b0
hooks:
- id: black
exclude: ^pdm/_vendor

- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
hooks:
- id: flake8

- repo: https://github.com/pycqa/isort
rev: 5.8.0
hooks:
- id: isort

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.812
hooks:
- id: mypy
args: [cfonts]
pass_filenames: false
2 changes: 1 addition & 1 deletion cfonts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
"""
__all__ = ["say", "render"]

from .core import say, render
from .core import render, say
31 changes: 19 additions & 12 deletions cfonts/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
"""
import argparse
import sys
from typing import List, Optional

from .consts import * # noqa
from .core import say, render
from . import consts
from .__version__ import __version__
from .core import render, say


class CFontsArgumentParser(argparse.ArgumentParser):
Expand All @@ -26,7 +27,9 @@ def format_help(self) -> str:
formatter.add_text(self.description)

# usage
formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups)
formatter.add_usage(
self.usage or "", self._actions, self._mutually_exclusive_groups
)

# positionals, optionals and user-defined groups
for action_group in self._action_groups:
Expand All @@ -42,7 +45,7 @@ def format_help(self) -> str:
return formatter.format_help()


def parse_args():
def parse_args() -> argparse.Namespace:
parser = CFontsArgumentParser(
"cfonts",
description="This is a tool for sexy fonts in the console. "
Expand All @@ -60,24 +63,28 @@ def parse_args():
parser.add_argument(
"-f",
"--font",
default=FONTFACES.block,
choices=FONTFACES.all(),
default=consts.FontFaces.block,
choices=consts.FontFaces,
type=consts.FontFaces,
help="Use to define the font face",
)
parser.add_argument(
"-c", "--colors", default=COLORS.system, help="Use to define the font color"
"-c",
"--colors",
default=consts.Colors.system.value,
help="Use to define the font color",
)
parser.add_argument(
"-b",
"--background",
default=BGCOLORS.transparent,
default=consts.BgColors.transparent.value,
help="Use to define the background color",
)
parser.add_argument(
"-a",
"--align",
default="left",
choices=ALIGNMENT,
choices=consts.ALIGNMENT,
help="Use to align the text output",
)
parser.add_argument(
Expand Down Expand Up @@ -130,16 +137,16 @@ def parse_args():
return args


def main():
def main() -> None:
args = parse_args()

colors = [c.strip() for c in args.colors.split(",")]
if args.gradient:
gradient = [g.strip() for g in args.gradient.split(",")]
gradient: Optional[List[str]] = [g.strip() for g in args.gradient.split(",")]
else:
gradient = None
options = {
"font": args.font,
"font": args.font.value,
"colors": colors,
"background": args.background,
"align": args.align,
Expand Down
116 changes: 44 additions & 72 deletions cfonts/colors.py
Original file line number Diff line number Diff line change
@@ -1,105 +1,75 @@
"""
Utility functions for handling terminal colors
"""
from __future__ import division
import colorsys
import os
from collections import namedtuple
from typing import Iterable, List, Mapping, NamedTuple, Tuple

from .consts import ANSI_COLORS, ANSI_RGB

Style = namedtuple("Style", "open,close")

class Style(NamedTuple):
open: str
close: str

def hex_to_rgb(hex_string):

_Rgb = Tuple[int, int, int]
_Hsv = Tuple[float, float, float]


def hex_to_rgb(hex_string: str) -> _Rgb:
"""Return a tuple of red, green and blue components for the color
given as #rrggbb.
"""
assert len(hex_string) in (4, 7), "Hex color format is not correct."
if len(hex_string) == 4:
return tuple(int(c * 2, 16) for c in hex_string[1:])
return tuple(int(hex_string[i : i + 2], 16) for i in range(1, len(hex_string), 2))
return tuple(int(c * 2, 16) for c in hex_string[1:]) # type: ignore
return tuple(
int(hex_string[i : i + 2], 16) for i in range(1, len(hex_string), 2)
) # type: ignore


def rgb_to_hex(rgb):
def rgb_to_hex(rgb: _Rgb) -> str:
return "#" + "".join("%02x" % c for c in rgb)


def rgb_to_hsv(rgb):
r, g, b = rgb
r /= 255
g /= 255
b /= 255

max_value = max(r, g, b)
min_value = min(r, g, b)
diff = max_value - min_value

h, s, v = 0, diff / max_value if max_value > 0 else 0, max_value

if max_value == min_value:
h = 0
elif max_value == r:
h = 60 * (g - b) / diff
if g < b:
h += 360
elif max_value == g:
h = 60 * (b - r) / diff + 120
else:
h = 60 * (r - g) / diff + 240

return h, (s * 100), (v * 100)


def hsv_to_rgb(hsv):
h, s, v = hsv
h /= 60
s /= 100
v /= 100
hi = int(h) % 6
def rgb_to_hsv(rgb: _Rgb) -> _Hsv:
return colorsys.rgb_to_hsv(*rgb)

f = h - int(h)
p = 255 * v * (1 - s)
q = 255 * v * (1 - (s * f))
t = 255 * v * (1 - (s * (1 - f)))
v *= 255

result = {
0: (v, t, p),
1: (q, v, p),
2: (p, v, t),
3: (p, q, v),
4: (t, p, v),
5: (v, p, q),
}[hi]
return tuple(int(c) for c in result)
def hsv_to_rgb(hsv: _Hsv) -> _Rgb:
return tuple(int(c) for c in colorsys.hsv_to_rgb(*hsv)) # type: ignore


def _color_distance(left, right):
def _color_distance(left: _Hsv, right: _Hsv) -> float:
return sum((i - j) ** 2 for i, j in zip(left, right))


def _ensure_rgb(color):
def _ensure_rgb(color: str) -> _Rgb:
if color in ANSI_RGB:
return ANSI_RGB[color]
return hex_to_rgb(color)


def get_closest(rgb, rgb_map=ANSI_RGB):
def get_closest(rgb: _Rgb, rgb_map: Mapping[str, _Rgb] = ANSI_RGB) -> str:
"""Return the closest ANSI color name from the given RGB."""
color = min(rgb_map.items(), key=lambda x: _color_distance(rgb, x[1]))
return color[0]
return min(rgb_map, key=lambda name: _color_distance(rgb, rgb_map[name]))


def get_linear(start, end, steps):
def get_linear(start: float, end: float, steps: int) -> List[float]:
"""Get a list of numbers interpolated from start to end inclusively."""
step = (end - start) / (steps - 1)
return [start + i * step for i in range(steps)]


def get_interpolated_hsv(start_hsv, end_hsv, steps, transition=False):
def get_interpolated_hsv(
start_hsv: _Hsv, end_hsv: _Hsv, steps: int, transition: bool = False
) -> Iterable[_Hsv]:
"""Get a sequence of HSV colors interpolated from start to end"""
if transition:
return zip(*[get_linear(s, e, steps) for s, e in zip(start_hsv, end_hsv)])
return zip(
*[get_linear(s, e, steps) for s, e in zip(start_hsv, end_hsv)]
) # type: ignore
s_sequence = get_linear(start_hsv[1], end_hsv[1], steps)
v_sequence = get_linear(start_hsv[2], end_hsv[2], steps)
start_h, end_h = start_hsv[0], end_hsv[0]
Expand All @@ -119,7 +89,7 @@ class AnsiPen:
CLOSE_BIT = "\x1b[39m"
BG_CLOSE_BIT = "\x1b[49m"

def style(self, color, background=False):
def style(self, color: str, background: bool = False) -> Style:
if color == "system":
return Style("", "")
if color in ANSI_COLORS:
Expand All @@ -128,31 +98,33 @@ def style(self, color, background=False):
return self.hex_style(color, background)
raise ValueError("Unsupported color: {}".format(color))

def ansi_style(self, color, background):
def ansi_style(self, color: str, background: bool) -> Style:
offset = 10 if background else 0
close = self.BG_CLOSE_BIT if background else self.CLOSE_BIT
code = ANSI_COLORS[color]
return Style("\x1b[{}m".format(offset + code), close)

def hex_style(self, color, background):
def hex_style(self, color: str, background: bool) -> Style:
return self.rgb_style(hex_to_rgb(color), background)

def rgb_style(self, color, background):
def rgb_style(self, color: _Rgb, background: bool) -> Style:
ansi_color = get_closest(color)
return self.ansi_style(ansi_color, background)

def get_gradient(self, colors, steps, transition=False):
def get_gradient(
self, colors: List[str], steps: int, transition: bool = False
) -> List[Style]:
if transition and len(colors) < 2:
raise ValueError("Transition gradient needs at least two colors")
elif not transition and len(colors) != 2:
raise ValueError("Gradient needs exactly two colors")
colors = [_ensure_rgb(color) for color in colors]
color_steps = [(steps - 1) // (len(colors) - 1)] * (len(colors) - 1)
rgb_colors = [_ensure_rgb(color) for color in colors]
color_steps = [(steps - 1) // (len(rgb_colors) - 1)] * (len(rgb_colors) - 1)
if sum(color_steps) < (steps - 1):
color_steps[-1] += 1
assert sum(color_steps) == steps - 1
result = []
for start, end, st in zip(colors, colors[1:], color_steps):
result: List[Style] = []
for start, end, st in zip(rgb_colors, rgb_colors[1:], color_steps):
start_hsv, end_hsv = rgb_to_hsv(start), rgb_to_hsv(end)
styles = [
hsv_to_rgb(hsv)
Expand All @@ -168,11 +140,11 @@ def get_gradient(self, colors, steps, transition=False):


class TrueColorPen(AnsiPen):
def rgb_style(self, color, background):
def rgb_style(self, color: _Rgb, background: bool) -> Style:
open_bit = 48 if background else 38
close = self.BG_CLOSE_BIT if background else self.CLOSE_BIT
r, g, b = color
return Style("\x01\x1b[{};2;{};{};{}m".format(open_bit, r, g, b), close)
return Style("\x1b[{};2;{};{};{}m".format(open_bit, r, g, b), close)


if (os.getenv("DISABLE_TRUECOLOR") or os.name == "nt") and not os.getenv(
Expand Down
Loading

0 comments on commit b728d53

Please sign in to comment.