diff --git a/click_tools/__init__.py b/click_tools/__init__.py index e5da332..3d5ac9d 100644 --- a/click_tools/__init__.py +++ b/click_tools/__init__.py @@ -2,5 +2,6 @@ from . import eng from . import pipes from . import resources from . import utils +from . import text import crayons \ No newline at end of file diff --git a/click_tools/cols.py b/click_tools/cols.py new file mode 100644 index 0000000..a92cd84 --- /dev/null +++ b/click_tools/cols.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +""" +clint.textui.columns +~~~~~~~~~~~~~~~~~~~~ + +Core TextUI functionality for column formatting. + +""" + +from __future__ import absolute_import + +from .formatters import max_width, min_width +from .utils import tsplit + +import sys + + +NEWLINES = ('\n', '\r', '\r\n') + + + +def _find_unix_console_width(): + import termios, fcntl, struct, sys + + # fcntl.ioctl will fail if stdout is not a tty + if not sys.stdout.isatty(): + return None + + s = struct.pack("HHHH", 0, 0, 0, 0) + fd_stdout = sys.stdout.fileno() + size = fcntl.ioctl(fd_stdout, termios.TIOCGWINSZ, s) + height, width = struct.unpack("HHHH", size)[:2] + return width + + +def _find_windows_console_width(): + # http://code.activestate.com/recipes/440694/ + from ctypes import windll, create_string_buffer + STDIN, STDOUT, STDERR = -10, -11, -12 + + h = windll.kernel32.GetStdHandle(STDERR) + csbi = create_string_buffer(22) + res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) + + if res: + import struct + (bufx, bufy, curx, cury, wattr, + left, top, right, bottom, + maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) + sizex = right - left + 1 + sizey = bottom - top + 1 + return sizex + + +def console_width(kwargs): + """"Determine console_width.""" + + if sys.platform.startswith('win'): + console_width = _find_windows_console_width() + else: + console_width = _find_unix_console_width() + + _width = kwargs.get('width', None) + if _width: + console_width = _width + else: + if not console_width: + console_width = 80 + + return console_width + + + +def columns(*cols, **kwargs): + + columns = list(cols) + + cwidth = console_width(kwargs) + + _big_col = None + _total_cols = 0 + + cols = [list(c) for c in cols] + + for i, (string, width) in enumerate(cols): + + if width is not None: + _total_cols += (width + 1) + cols[i][0] = max_width(string, width).split('\n') + else: + _big_col = i + + if _big_col: + cols[_big_col][1] = (cwidth - _total_cols) - len(cols) + cols[_big_col][0] = max_width(cols[_big_col][0], cols[_big_col][1]).split('\n') + + height = len(max([c[0] for c in cols], key=len)) + + for i, (strings, width) in enumerate(cols): + + for _ in range(height - len(strings)): + cols[i][0].append('') + + for j, string in enumerate(strings): + cols[i][0][j] = min_width(string, width) + + stack = [c[0] for c in cols] + _out = [] + + for i in range(height): + _row = '' + + for col in stack: + _row += col[i] + _row += ' ' + + _out.append(_row) + + + + return '\n'.join(_out) + + + +########################### +if __name__ == '__main__': + a = 'this is text that goes into a small column\n cool?' + b = 'this is other text\nothertext\nothertext' + + print(columns((a, 10), (b, 20), (b, None))) + diff --git a/click_tools/formatters.py b/click_tools/formatters.py new file mode 100644 index 0000000..7b4fd38 --- /dev/null +++ b/click_tools/formatters.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +""" +clint.textui.formatters +~~~~~~~~~~~~~~~~~~~~~~~ + +Core TextUI functionality for text formatting. + +""" + +from __future__ import absolute_import + +from crayons import ColoredString, clean +from .utils import tsplit, schunk + + +NEWLINES = ('\n', '\r', '\r\n') + + +def min_width(string, cols, padding=' '): + """Returns given string with right padding.""" + + is_color = isinstance(string, ColoredString) + + stack = tsplit(str(string), NEWLINES) + + for i, substring in enumerate(stack): + _sub = clean(substring).ljust((cols + 0), padding) + if is_color: + _sub = (_sub.replace(clean(substring), substring)) + stack[i] = _sub + + return '\n'.join(stack) + + +def max_width(string, cols, separator='\n'): + """Returns a freshly formatted + :param string: string to be formatted + :type string: basestring or clint.textui.colored.ColoredString + :param cols: max width the text to be formatted + :type cols: int + :param separator: separator to break rows + :type separator: basestring + + >>> formatters.max_width('123 5678', 8) + '123 5678' + >>> formatters.max_width('123 5678', 7) + '123 \n5678' + + """ + + is_color = isinstance(string, ColoredString) + + if is_color: + string_copy = string._new('') + string = string.s + + stack = tsplit(string, NEWLINES) + + for i, substring in enumerate(stack): + stack[i] = substring.split() + + _stack = [] + + for row in stack: + _row = ['', ] + _row_i = 0 + + for word in row: + if (len(_row[_row_i]) + len(word)) <= cols: + _row[_row_i] += word + _row[_row_i] += ' ' + + elif len(word) > cols: + + # ensure empty row + if len(_row[_row_i]): + _row[_row_i] = _row[_row_i].rstrip() + _row.append('') + _row_i += 1 + + chunks = schunk(word, cols) + for i, chunk in enumerate(chunks): + if not (i + 1) == len(chunks): + _row[_row_i] += chunk + _row[_row_i] = _row[_row_i].rstrip() + _row.append('') + _row_i += 1 + else: + _row[_row_i] += chunk + _row[_row_i] += ' ' + else: + _row[_row_i] = _row[_row_i].rstrip() + _row.append('') + _row_i += 1 + _row[_row_i] += word + _row[_row_i] += ' ' + else: + _row[_row_i] = _row[_row_i].rstrip() + + _row = map(str, _row) + _stack.append(separator.join(_row)) + + _s = '\n'.join(_stack) + if is_color: + _s = string_copy._new(_s) + return _s diff --git a/click_tools/text.py b/click_tools/text.py index 8a15f75..efa3878 100644 --- a/click_tools/text.py +++ b/click_tools/text.py @@ -6,7 +6,7 @@ from contextlib import contextmanager from .formatters import max_width, min_width from .cols import columns -from ..utils import tsplit +from .utils import tsplit __all__ = ( diff --git a/setup.py b/setup.py index 966f20c..ec39754 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ AUTHOR = 'Kenneth Reitz' # What packages are required for this module to be executed? REQUIRED = [ - 'appdirs' + 'appdirs', 'crayons' ] # The rest you shouldn't have to touch too much :)