├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── docs ├── gauge.png ├── progress.png └── spinner.gif ├── py_clui ├── __init__.py └── colorize.py ├── requirements_dev.txt ├── setup.py └── tests ├── __init__.py ├── test_colorize.py ├── test_gauge.py └── test_progress.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.6" 5 | install: "pip install -r requirements_dev.txt" 6 | script: make test 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Henrique Leal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "Makefile for py_clui " 3 | @echo " " 4 | @echo "Usage: " 5 | @echo " test Run project tests" 6 | 7 | upload: 8 | @python setup.py sdist upload -r pypi 9 | 10 | test: 11 | @py.test tests --cov=py_clui --pep8 --flakes 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Py-clui 2 | > This is a Python toolkit for quickly building nice looking command line interfaces. 3 | 4 | [![Build Status](https://travis-ci.org/hmleal/py-clui.svg?branch=master)](https://travis-ci.org/hmleal/py-clui) 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 6 | 7 | It also includes the following easy to use components: 8 | 9 | * Spinners 10 | * Gauge 11 | * Progress 12 | 13 | ### Spinner(message, style=None) 14 | 15 | ![Picture of a spinner](https://raw.githubusercontent.com/hmleal/py-clui/master/docs/spinner.gif) 16 | 17 | __Parameters__ 18 | 19 | * `message` - The default status text to display while the spinner is spinning. 20 | * `style` - Array of graphical characters used to draw the spinner. By default, 21 | on Windows: ['|', '/', '-', '\'], on other platforms: ['◜','◠','◝','◞','◡','◟'] 22 | 23 | __Methods__ 24 | 25 | * `run()` - Show the spinner on the screen. 26 | * `update_msg(message)` - Update the status message that follows the spinner. 27 | 28 | __Example__ 29 | 30 | ```python 31 | from py_clui import Spinner 32 | 33 | spinner = Spinner('Processing documents...') 34 | spinner.run() 35 | 36 | for x in range(100): 37 | spinner.update_msg('{0} Processed documents'.format(x)) 38 | spinner.run() 39 | ``` 40 | 41 | ### Gauge(value, max_value, width, danger_zone, suffix=None) 42 | 43 | ![Picture of a gauge](https://raw.githubusercontent.com/hmleal/py-clui/master/docs/gauge.png) 44 | 45 | Draw a basic horizontal gauge to the screen. 46 | 47 | Parameters 48 | 49 | * `value` 50 | * `max_value` 51 | * `width` 52 | * `danger_zone` 53 | * `suffix` 54 | 55 | Example 56 | 57 | ```python 58 | from py_clui import gauge 59 | 60 | total = 100 61 | free = 30 62 | 63 | used = total - free 64 | 65 | print(gauge(used, total, 20, total * 0.8, 'Used memory')) 66 | ``` 67 | 68 | ### Progress(width=20) 69 | 70 | ![Picture of a progress](https://raw.githubusercontent.com/hmleal/py-clui/master/docs/progress.png) 71 | 72 | __Parameters__ 73 | 74 | * `width` - The width in characters of progress_bar 75 | 76 | __Methods__ 77 | 78 | * `update(percent)` - Return a progress bar width a this percente filled. 79 | 80 | __Example__ 81 | 82 | ```python 83 | from py_clui import Progress 84 | 85 | progress = Progress(20) 86 | 87 | print(progress.update(.5)) 88 | ``` 89 | 90 | ## Motivation 91 | 1. [clui](https://github.com/nathanpeck/clui) makes NodeJS even more sexy. Python needed something like it. 92 | -------------------------------------------------------------------------------- /docs/gauge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmleal/py-clui/0b408c96bd9e521ba89466757bb9e3101c37b2ca/docs/gauge.png -------------------------------------------------------------------------------- /docs/progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmleal/py-clui/0b408c96bd9e521ba89466757bb9e3101c37b2ca/docs/progress.png -------------------------------------------------------------------------------- /docs/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmleal/py-clui/0b408c96bd9e521ba89466757bb9e3101c37b2ca/docs/spinner.gif -------------------------------------------------------------------------------- /py_clui/__init__.py: -------------------------------------------------------------------------------- 1 | import math 2 | import sys 3 | 4 | from .colorize import Colorize 5 | 6 | 7 | __author__ = "Henrique Leal" 8 | __author_email__ = "hm.leal@hotmail.com" 9 | __version__ = "0.0.3" 10 | 11 | 12 | c = Colorize() 13 | 14 | 15 | def gauge(value, max_value, width, danger_zone, suffix=None): 16 | if max_value == 0: 17 | return "[]" 18 | 19 | length = math.ceil(value / max_value * width) 20 | 21 | if length > width: 22 | length = width 23 | 24 | bars = "|" * length 25 | if value > danger_zone: 26 | bars = c.red(bars) 27 | bars = c.green(bars) 28 | bars += "-" * (width + 1 - length) 29 | 30 | return "[{0}] {1}".format(bars, suffix) 31 | 32 | 33 | class Progress: 34 | 35 | def __init__(self, width=100): 36 | self.width = width 37 | self.current_value = 0 38 | 39 | def update(self, current_value): 40 | self.current_value = current_value 41 | 42 | return "[{0}{1}] {2}".format( 43 | c.green("|" * self.bar_length), 44 | "-" * (self.width - self.bar_length), 45 | c.grey("{0}%".format(self.percent)) 46 | ) 47 | 48 | @property 49 | def percent(self): 50 | return int(self.current_value * 100) 51 | 52 | @property 53 | def bar_length(self): 54 | return math.ceil(self.width * self.percent / 100) 55 | 56 | 57 | class Spinner: 58 | 59 | def __init__(self, message, style=None): 60 | self.message = message 61 | self._number = 0 62 | self.style = self._style(style) 63 | 64 | def run(self): 65 | self._draw() 66 | 67 | def update_msg(self, message): 68 | self.message = message 69 | 70 | def _draw(self): 71 | self._number += 1 72 | 73 | frames = [" {0} ".format(c.light_blue(f)) for f in self.style] 74 | 75 | print( 76 | "{0}{1}".format( 77 | frames[self._number % len(self.style)], c.grey(self.message) 78 | ), 79 | end="\r", 80 | file=sys.stdout, 81 | flush=True, 82 | ) 83 | 84 | def _style(self, style): 85 | if style is not None: 86 | return style 87 | 88 | if sys.platform == "win32": 89 | return ["|", "/", "-", "\\"] 90 | 91 | return ["◜", "◠", "◝", "◞", "◡", "◟"] 92 | 93 | 94 | # return ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'] 95 | -------------------------------------------------------------------------------- /py_clui/colorize.py: -------------------------------------------------------------------------------- 1 | class Colorize: 2 | 3 | def _colorize(self, color_code, text): 4 | return "\033[{0}m{1}\33[0m".format(color_code, text) 5 | 6 | def grey(self, text): 7 | return self._colorize("30;1", text) 8 | 9 | def red(self, text): 10 | return self._colorize("31", text) 11 | 12 | def green(self, text): 13 | return self._colorize("32", text) 14 | 15 | def yellow(self, text): 16 | return self._colorize("33", text) 17 | 18 | def blue(self, text): 19 | return self._colorize("34", text) 20 | 21 | def pink(self, text): 22 | return self._colorize("35", text) 23 | 24 | def light_blue(self, text): 25 | return self._colorize("36", text) 26 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-flakes 4 | pytest-pep8 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | 3 | from py_clui import (__author__, __author_email__, __version__) 4 | 5 | 6 | README = codecs.open("README.md", encoding="UTF-8").read() 7 | 8 | 9 | try: 10 | from setuptools import setup 11 | except ImportError: 12 | from distutils.core import setup 13 | 14 | 15 | setup( 16 | name="py_clui", 17 | version=__version__, 18 | description="Toolkit for quickly building nice looking command line interfaces", 19 | long_description=README, 20 | long_description_content_type="text/markdown", 21 | url="https://github.com/hmleal/py-clui", 22 | author=__author__, 23 | author_email=__author_email__, 24 | packages=["py_clui"], 25 | classifiers=["Programming Language :: Python :: 3 :: Only"], 26 | ) 27 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmleal/py-clui/0b408c96bd9e521ba89466757bb9e3101c37b2ca/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_colorize.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from py_clui.colorize import Colorize 4 | 5 | 6 | class TestColorize(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.c = Colorize() 10 | 11 | def test_grey(self): 12 | self.assertEqual(self.c.grey("Colorize"), "\x1b[30;1mColorize\x1b[0m") 13 | 14 | def test_red(self): 15 | self.assertEqual(self.c.red("Colorize"), "\x1b[31mColorize\x1b[0m") 16 | 17 | def test_green(self): 18 | self.assertEqual(self.c.green("Colorize"), "\x1b[32mColorize\x1b[0m") 19 | 20 | def test_yellow(self): 21 | self.assertEqual(self.c.yellow("Colorize"), "\x1b[33mColorize\x1b[0m") 22 | 23 | def test_blue(self): 24 | self.assertEqual(self.c.blue("Colorize"), "\x1b[34mColorize\x1b[0m") 25 | 26 | def test_pink(self): 27 | self.assertEqual(self.c.pink("Colorize"), "\x1b[35mColorize\x1b[0m") 28 | 29 | def test_light_blue(self): 30 | self.assertEqual(self.c.light_blue("Colorize"), "\x1b[36mColorize\x1b[0m") 31 | -------------------------------------------------------------------------------- /tests/test_gauge.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from py_clui import gauge 4 | 5 | 6 | class TestGauge(unittest.TestCase): 7 | 8 | def test_empty_max_value_gauge(self): 9 | self.assertEqual("[]", gauge(10, 0, 100, 80)) 10 | -------------------------------------------------------------------------------- /tests/test_progress.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from py_clui import Progress 4 | 5 | 6 | class TestProgress(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.progress = Progress(width=100) 10 | 11 | def test_percent_progress_bar(self): 12 | self.progress.update(.8) 13 | 14 | self.assertEqual(self.progress.percent, 80) 15 | 16 | def test_bar_length(self): 17 | self.progress.update(.8) 18 | 19 | self.assertEqual(self.progress.bar_length, 80) 20 | 21 | def test_empty_progress_bar(self): 22 | self.assertEqual(self.progress.percent, 0) 23 | self.assertEqual(self.progress.bar_length, 0) 24 | --------------------------------------------------------------------------------