├── terminal_layout ├── extensions │ ├── __init__.py │ ├── input │ │ ├── __init__.py │ │ ├── README.md │ │ ├── _old.py │ │ └── input_ex.py │ ├── choice │ │ ├── __init__.py │ │ ├── README.md │ │ └── choice.py │ ├── scroll │ │ ├── __init__.py │ │ ├── README.zh.md │ │ ├── README.md │ │ └── scroll.py │ └── progress │ │ ├── __init__.py │ │ ├── README.md │ │ ├── loading.py │ │ └── progress.py ├── helper │ ├── __init__.py │ ├── helper.py │ └── class_helper.py ├── __init__.py ├── readkey │ ├── __init__.py │ ├── event.py │ ├── windows.py │ ├── key.py │ ├── linux.py │ └── listener.py ├── view │ ├── util.py │ ├── __init__.py │ ├── params.py │ ├── base.py │ ├── text_view.py │ └── layout.py ├── ansi │ ├── __init__.py │ ├── font.py │ ├── style.py │ ├── back.py │ └── fore.py ├── log.py ├── types.py └── ctl.py ├── demo ├── __init__.py ├── textview_properties_demo │ ├── __init__.py │ ├── width.py │ ├── style.py │ ├── gravity.py │ ├── ex_style.py │ ├── visibility.py │ ├── weight.py │ └── color.py ├── input_ex.py ├── choice.py ├── key_listener.py ├── progress.py ├── scroll.py ├── demo6(get_width).py ├── demo_v2_1.py └── record_demo.py ├── tests ├── __init__.py └── test_1.py ├── requirements.txt ├── docs ├── requirements.txt ├── _static │ ├── demo.gif │ ├── py2.png │ ├── choice.gif │ ├── color.jpeg │ ├── hello.png │ ├── input.gif │ ├── scroll.gif │ ├── style.jpeg │ ├── table.png │ ├── width.jpeg │ ├── demo_v2_1.gif │ ├── ex_color.jpeg │ ├── ex_style.jpeg │ ├── gravity.jpeg │ ├── progress.gif │ ├── weight.jpeg │ └── visibility.jpeg ├── installation.rst ├── Makefile ├── FAQ.rst ├── extensions │ └── index.rst ├── make.bat ├── getStarted.rst ├── index.rst ├── changelog.rst ├── conf.py ├── keyListener.rst ├── Properties.rst ├── View.rst └── draw.rst ├── pic ├── py2.png ├── .DS_Store ├── demo.gif ├── hello.png ├── input.gif ├── choice.gif ├── color.jpeg ├── gravity.jpeg ├── loading.jpg ├── progress.gif ├── progress.jpg ├── scroll.gif ├── style.jpeg ├── weight.jpeg ├── width.jpeg ├── cal_scroll.png ├── demo_v2_1.gif ├── ex_color.jpeg ├── ex_style.jpeg └── visibility.jpeg ├── setup.py ├── pyproject.toml ├── .gitignore ├── README.ZH.md ├── README.md └── README.rst /terminal_layout/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /terminal_layout/helper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama==0.4.6 2 | colored==1.4.4 3 | -------------------------------------------------------------------------------- /demo/textview_properties_demo/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==3.4.3 2 | sphinx-rtd-theme==0.5.1 3 | Pillow==9.3.0 -------------------------------------------------------------------------------- /pic/py2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/py2.png -------------------------------------------------------------------------------- /pic/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/.DS_Store -------------------------------------------------------------------------------- /pic/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/demo.gif -------------------------------------------------------------------------------- /pic/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/hello.png -------------------------------------------------------------------------------- /pic/input.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/input.gif -------------------------------------------------------------------------------- /pic/choice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/choice.gif -------------------------------------------------------------------------------- /pic/color.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/color.jpeg -------------------------------------------------------------------------------- /pic/gravity.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/gravity.jpeg -------------------------------------------------------------------------------- /pic/loading.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/loading.jpg -------------------------------------------------------------------------------- /pic/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/progress.gif -------------------------------------------------------------------------------- /pic/progress.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/progress.jpg -------------------------------------------------------------------------------- /pic/scroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/scroll.gif -------------------------------------------------------------------------------- /pic/style.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/style.jpeg -------------------------------------------------------------------------------- /pic/weight.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/weight.jpeg -------------------------------------------------------------------------------- /pic/width.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/width.jpeg -------------------------------------------------------------------------------- /pic/cal_scroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/cal_scroll.png -------------------------------------------------------------------------------- /pic/demo_v2_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/demo_v2_1.gif -------------------------------------------------------------------------------- /pic/ex_color.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/ex_color.jpeg -------------------------------------------------------------------------------- /pic/ex_style.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/ex_style.jpeg -------------------------------------------------------------------------------- /docs/_static/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/demo.gif -------------------------------------------------------------------------------- /docs/_static/py2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/py2.png -------------------------------------------------------------------------------- /pic/visibility.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/pic/visibility.jpeg -------------------------------------------------------------------------------- /docs/_static/choice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/choice.gif -------------------------------------------------------------------------------- /docs/_static/color.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/color.jpeg -------------------------------------------------------------------------------- /docs/_static/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/hello.png -------------------------------------------------------------------------------- /docs/_static/input.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/input.gif -------------------------------------------------------------------------------- /docs/_static/scroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/scroll.gif -------------------------------------------------------------------------------- /docs/_static/style.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/style.jpeg -------------------------------------------------------------------------------- /docs/_static/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/table.png -------------------------------------------------------------------------------- /docs/_static/width.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/width.jpeg -------------------------------------------------------------------------------- /docs/_static/demo_v2_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/demo_v2_1.gif -------------------------------------------------------------------------------- /docs/_static/ex_color.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/ex_color.jpeg -------------------------------------------------------------------------------- /docs/_static/ex_style.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/ex_style.jpeg -------------------------------------------------------------------------------- /docs/_static/gravity.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/gravity.jpeg -------------------------------------------------------------------------------- /docs/_static/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/progress.gif -------------------------------------------------------------------------------- /docs/_static/weight.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/weight.jpeg -------------------------------------------------------------------------------- /terminal_layout/extensions/input/__init__.py: -------------------------------------------------------------------------------- 1 | from terminal_layout.extensions.input.input_ex import InputEx 2 | -------------------------------------------------------------------------------- /docs/_static/visibility.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojuukaze/terminal_layout/HEAD/docs/_static/visibility.jpeg -------------------------------------------------------------------------------- /terminal_layout/extensions/choice/__init__.py: -------------------------------------------------------------------------------- 1 | from terminal_layout.extensions.choice.choice import Choice,StringStyle 2 | -------------------------------------------------------------------------------- /terminal_layout/extensions/scroll/__init__.py: -------------------------------------------------------------------------------- 1 | from terminal_layout.extensions.scroll.scroll import Scroll,ScrollEvent 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = 'gojuukaze' 4 | 5 | from setuptools import setup 6 | 7 | # pyproject.toml 8 | setup() 9 | -------------------------------------------------------------------------------- /terminal_layout/__init__.py: -------------------------------------------------------------------------------- 1 | from terminal_layout.ansi import * 2 | from terminal_layout.view import * 3 | from terminal_layout.ctl import LayoutCtl 4 | from terminal_layout.readkey import Key, KeyListener 5 | 6 | -------------------------------------------------------------------------------- /terminal_layout/extensions/progress/__init__.py: -------------------------------------------------------------------------------- 1 | from terminal_layout.extensions.progress.progress import Progress, SuffixStyle, ProgressWidth 2 | from terminal_layout.extensions.progress.loading import Loading, InfixChoices 3 | -------------------------------------------------------------------------------- /terminal_layout/readkey/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | based on https://github.com/magmax/python-readchar 3 | 4 | 5 | """ 6 | 7 | from terminal_layout.readkey.key import Key 8 | from terminal_layout.readkey.listener import KeyListener 9 | -------------------------------------------------------------------------------- /terminal_layout/view/util.py: -------------------------------------------------------------------------------- 1 | from terminal_layout.view.text_view import TextView 2 | 3 | from terminal_layout.view.layout import TableLayout 4 | 5 | 6 | def is_layout(view): 7 | return not isinstance(view, TextView) 8 | -------------------------------------------------------------------------------- /terminal_layout/view/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from terminal_layout.view.params import Visibility, Gravity, Width, Overflow, OverflowVertical 4 | from terminal_layout.view.layout import TableLayout, TableRow 5 | from terminal_layout.view.text_view import TextView 6 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | installation 2 | ============ 3 | 4 | .. code-block:: bash 5 | 6 | pip install terminal-layout 7 | 8 | 9 | Python Support 10 | ---------------------- 11 | 12 | ====== =============== 13 | Python terminal_layout 14 | ====== =============== 15 | 2.7 2.1.x 16 | 3.5+ 3.x 17 | ====== =============== -------------------------------------------------------------------------------- /terminal_layout/readkey/event.py: -------------------------------------------------------------------------------- 1 | class KeyPressEvent(object): 2 | def __init__(self, k, t): 3 | """ 4 | 5 | :param k: 6 | :type k: KeyInfo 7 | :param t: 8 | :type t: 9 | """ 10 | self.key = k 11 | self.time = t 12 | 13 | def __str__(self): 14 | return 'KeyPressEvent(key=<%s>, time=%s)' % (self.key.name, self.time) 15 | -------------------------------------------------------------------------------- /terminal_layout/helper/helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import subprocess 4 | from shutil import get_terminal_size 5 | 6 | def get_terminal_size2(): 7 | """ 8 | 之前写的,暂时不用了 9 | """ 10 | r = subprocess.check_output("stty size", shell=True) 11 | size = str(r, encoding="utf8").strip().split(' ') 12 | return int(size[0]), int(size[1]) 13 | 14 | 15 | def is_ascii(c): 16 | return 255 >= ord(c) >= 0 17 | -------------------------------------------------------------------------------- /demo/input_ex.py: -------------------------------------------------------------------------------- 1 | from terminal_layout import * 2 | from terminal_layout.extensions.input import * 3 | 4 | ctl = LayoutCtl.quick(TableRow, 5 | [TextView('', 'Input Something: ', fore=Fore.magenta), 6 | TextView('input', '', width=11, fore=Fore.blue)]) 7 | ctl.draw() 8 | ok, s = InputEx(ctl).get_input('input') 9 | if ok: 10 | print('-------') 11 | print('Your input:', Fore.blue, s, Fore.reset) -------------------------------------------------------------------------------- /demo/choice.py: -------------------------------------------------------------------------------- 1 | from terminal_layout.extensions.choice import * 2 | from terminal_layout import * 3 | 4 | c = Choice('Which is the Best Programming Language?', 5 | ['Python', 'C/C++', 'Java', 'PHP', 'Go', 'JS', '...'], 6 | icon_style=StringStyle(fore=Fore.blue), 7 | selected_style=StringStyle(fore=Fore.blue),default_index=2) 8 | 9 | choice = c.get_choice() 10 | if choice: 11 | index, value = choice 12 | print(value, 'is the Best Programming Language') 13 | -------------------------------------------------------------------------------- /demo/textview_properties_demo/width.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from terminal_layout import * 3 | 4 | ctl = LayoutCtl.quick(TableLayout, 5 | [ 6 | [TextView('', 'width = 12', width=12, back=Back.green)], 7 | [TextView('', 'width = warp', width=Width.wrap, back=Back.green)], 8 | [TextView('', 'width = fill', width=Width.fill, back=Back.green)], 9 | ]) 10 | 11 | ctl.get_layout().set_width(50) 12 | 13 | ctl.draw(auto_re_draw=False) 14 | -------------------------------------------------------------------------------- /demo/textview_properties_demo/style.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # -*- coding: utf-8 -*- 3 | from terminal_layout import * 4 | 5 | print('\nstyle support window, linux, osx\n') 6 | 7 | ctl = LayoutCtl.quick(TableLayout, 8 | [ 9 | [TextView('', 'style = bright', style=Style.bright)], 10 | [TextView('', 'style = dim', style=Style.dim)], 11 | [TextView('', 'style = normal', style=Style.normal)] 12 | ]) 13 | 14 | ctl.draw(auto_re_draw=False) 15 | 16 | -------------------------------------------------------------------------------- /terminal_layout/ansi/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from terminal_layout.ansi.fore import Fore as _Fore 3 | from terminal_layout.ansi.back import Back as _Back 4 | from terminal_layout.ansi.style import Style as _Style 5 | from colorama import Cursor as _Cursor 6 | from colorama import init as _init 7 | from colorama.ansi import clear_line as _clear_line 8 | from colorama.ansi import clear_screen as _clear_screen 9 | 10 | Fore = _Fore 11 | Back = _Back 12 | Style = _Style 13 | Cursor = _Cursor 14 | 15 | term_init = _init 16 | clear_line = _clear_line 17 | clear_screen = _clear_screen 18 | -------------------------------------------------------------------------------- /demo/textview_properties_demo/gravity.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from terminal_layout import * 4 | 5 | ctl = LayoutCtl.quick(TableLayout, 6 | [ 7 | [TextView('', 'gravity = left', width=25, gravity=Gravity.left, back=Back.cyan)], 8 | [TextView('', 'gravity = center', width=25, gravity=Gravity.center, back=Back.green)], 9 | [TextView('', 'gravity = right', width=25, gravity=Gravity.right, back=Back.magenta)], 10 | ]) 11 | 12 | ctl.get_layout().set_width(50) 13 | 14 | ctl.draw(auto_re_draw=False) 15 | -------------------------------------------------------------------------------- /tests/test_1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from terminal_layout.types import String 4 | import six 5 | import sys 6 | 7 | if six.PY2: 8 | reload(sys) 9 | sys.setdefaultencoding('utf-8') 10 | 11 | 12 | def test_view_string(): 13 | s = String('aaa') 14 | assert len(s) == 3 15 | assert s[:2] == 'aa' 16 | if six.PY2: 17 | s = String(u'a啊啊') 18 | assert len(s) == 5 19 | assert s[:2] == 'a' 20 | 21 | assert s[:3] == u'a啊啊'[:2] 22 | else: 23 | s = String('a啊啊') 24 | assert len(s) == 5 25 | assert s[:2] == 'a' 26 | assert s[:3] == 'a啊' 27 | -------------------------------------------------------------------------------- /terminal_layout/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | logger.setLevel(level=logging.DEBUG) 5 | 6 | 7 | def disable_logger(): 8 | handler = logging.NullHandler() 9 | logger.handlers = [handler] 10 | 11 | 12 | def enable_logger(): 13 | handler = logging.FileHandler("terminal_layout.log") 14 | handler.setLevel(logging.DEBUG) 15 | # formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 16 | formatter = logging.Formatter('%(levelname)s : %(message)s') 17 | 18 | handler.setFormatter(formatter) 19 | logger.addHandler(handler) 20 | 21 | 22 | disable_logger() 23 | -------------------------------------------------------------------------------- /demo/key_listener.py: -------------------------------------------------------------------------------- 1 | from terminal_layout import * 2 | 3 | key_listener = KeyListener() 4 | 5 | 6 | @key_listener.bind_key(Key.UP) 7 | def _(kl, e): 8 | print(e) 9 | 10 | 11 | @key_listener.bind_key(Key.DOWN, 'a', '[0-9]') 12 | def _(kl, e): 13 | if e.key == 'a': 14 | print('Press a') 15 | elif e.key == Key.DOWN: 16 | print('Press DOWN') 17 | elif e.key == '0': 18 | print('Press 0') 19 | else: 20 | print('Press 1-9') 21 | 22 | 23 | def stop(kl, e): 24 | print('Press', e.key, 'stop!') 25 | kl.stop() 26 | 27 | 28 | key_listener.bind_key(Key.ENTER, stop, decorator=False) 29 | 30 | key_listener.listen(stop_key=[Key.F1, Key.CTRL_A]) 31 | -------------------------------------------------------------------------------- /demo/textview_properties_demo/ex_style.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # -*- coding: utf-8 -*- 3 | from terminal_layout import * 4 | 5 | print('\n[ex]style support linux, osx\n') 6 | 7 | ctl = LayoutCtl.quick(TableLayout, 8 | [[TextView('', 'style = ex_bold', style=Style.ex_bold)], 9 | [TextView('', 'style = ex_dim', style=Style.ex_dim)], 10 | [TextView('', 'style = ex_underlined', style=Style.ex_underlined)], 11 | [TextView('', 'style = ex_blink', style=Style.ex_blink)], 12 | [TextView('', 'style = ex_reverse', style=Style.ex_reverse)], 13 | ]) 14 | 15 | ctl.draw(auto_re_draw=False) 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/FAQ.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | ===== 3 | 4 | 如何获取 ``View`` 的宽度 5 | -------------------------------- 6 | 7 | View的宽度有两种,``width``, ``real_width`` 8 | 9 | * ``width`` : 初始化时设置的宽度值 10 | * ``real_width`` : 真正绘制的宽度。注意!在绘制之前这个值都不是有效的值 11 | 12 | 如果需要在绘制之前获取 ``real_width`` ,可以调用 ``LayoutCtl.update_width()`` 更新宽度后再获取 13 | ``real_width`` 不是固定的,终端宽度发生变化这个值就会改变。 14 | 因此你可能会需要每次获取 ``real_width`` 之前都调用 ``update_width()`` 15 | 16 | 具体参照 https://github.com/gojuukaze/terminal_layout/blob/master/demo/demo6(get_width).py 17 | 18 | 19 | 屏幕闪烁 20 | -------------------------------- 21 | 22 | 输出的文本太大会出现界面闪烁的情况,这时要调大sys.stdout的缓冲区。具体情况见: https://github.com/gojuukaze/terminal_layout/issues/3 23 | 24 | 可通过 ``ctl.set_buffer_size()`` 函数调大缓冲区。(建议在draw之前调用) 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /terminal_layout/view/params.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Width(object): 5 | wrap = 'wrap' 6 | fill = 'fill' 7 | 8 | 9 | class Visibility(object): 10 | visible = 'visible' 11 | invisible = 'invisible' 12 | gone = 'gone' 13 | 14 | 15 | class Gravity(object): 16 | left = 'left' 17 | center = 'center' 18 | right = 'right' 19 | 20 | 21 | class Overflow(object): 22 | hidden_left = 'hidden_left' 23 | hidden_right = 'hidden_right' 24 | 25 | 26 | class OverflowVertical(object): 27 | hidden_top = 'hidden_top' 28 | hidden_btm = 'hidden_btm' 29 | none = 'none' 30 | 31 | 32 | # linearlayout ###############3 33 | 34 | class Orientation(object): 35 | # 水平 36 | horizon = 'horizon' 37 | # 垂直 38 | vertical = 'vertical' 39 | -------------------------------------------------------------------------------- /docs/extensions/index.rst: -------------------------------------------------------------------------------- 1 | 扩展 2 | ======= 3 | 4 | progress 5 | ----------------- 6 | 7 | 文档查看:https://github.com/gojuukaze/terminal_layout/tree/master/terminal_layout/extensions/progress 8 | 9 | .. image:: ../_static/progress.gif 10 | 11 | 12 | choice 13 | ----------------- 14 | 15 | 文档查看:https://github.com/gojuukaze/terminal_layout/tree/master/terminal_layout/extensions/choice 16 | 17 | 18 | .. image:: ../_static/choice.gif 19 | 20 | 21 | input 22 | ----------------- 23 | 24 | 文档查看:https://github.com/gojuukaze/terminal_layout/tree/master/terminal_layout/extensions/input 25 | 26 | 27 | .. image:: ../_static/input.gif 28 | 29 | scroll 30 | ----------------- 31 | 32 | 文档查看:https://github.com/gojuukaze/terminal_layout/tree/master/terminal_layout/extensions/scroll 33 | 34 | 35 | .. image:: ../_static/scroll.gif 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /demo/progress.py: -------------------------------------------------------------------------------- 1 | import time 2 | from terminal_layout.extensions.progress import * 3 | 4 | with Progress('Downloading', 10) as p: 5 | for i in range(10): 6 | if p.is_finished(): 7 | break 8 | time.sleep(0.3) 9 | p.add_progress(i) 10 | 11 | with Progress('Downloading', 10, reached='▓', unreached='░', suffix_style=SuffixStyle.fraction) as p: 12 | for i in range(10): 13 | if p.is_finished(): 14 | break 15 | time.sleep(0.3) 16 | p.add_progress(i) 17 | print('') 18 | with Loading('Loading', 10) as l: 19 | for i in range(10): 20 | if l.is_finished(): 21 | break 22 | time.sleep(0.3) 23 | l.add_progress(i) 24 | 25 | with Loading('Loading', 10, infix=InfixChoices.style10, delimiter=[' ', ' | '], suffix_style=SuffixStyle.fraction) as l: 26 | for i in range(10): 27 | if l.is_finished(): 28 | break 29 | time.sleep(0.3) 30 | l.add_progress(i) 31 | -------------------------------------------------------------------------------- /terminal_layout/ansi/font.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from colorama.ansi import code_to_chars 3 | from colored import fg, bg, attr 4 | 5 | 6 | class Font(object): 7 | ex_function = { 8 | 'fore': fg, 9 | 'back': bg, 10 | 'style': attr 11 | } 12 | 13 | def __init__(self, name, type, code=None): 14 | """ 15 | 16 | :param name: color name or style name 17 | :param type: fore ,back , style 18 | :param code: colorama color,style num 19 | """ 20 | self.name = name 21 | self.type = type 22 | self.code = code 23 | 24 | def __str__(self): 25 | if not self.name: 26 | return '' 27 | if self.name.startswith('ex_'): 28 | func = self.ex_function[self.type] 29 | return func(self.name[3:]) 30 | else: 31 | return code_to_chars(self.code) 32 | 33 | def __add__(self, other): 34 | print('add') 35 | return str(self) + str(other) 36 | -------------------------------------------------------------------------------- /terminal_layout/ansi/style.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from terminal_layout.ansi.font import Font 3 | 4 | 5 | class Style(object): 6 | 7 | bright = Font('bright', 'style', 1) 8 | dim = Font('dim', 'style', 2) 9 | normal = Font('normal', 'style', 22) 10 | reset_all = Font('reset_all', 'style', 0) 11 | 12 | # ex_xx only support linux, mac 13 | 14 | ex_bold = Font('ex_bold', 'style') 15 | ex_dim = Font('ex_dim', 'style') 16 | ex_underlined = Font('ex_underlined', 'style') 17 | ex_blink = Font('ex_blink', 'style') 18 | ex_reverse = Font('ex_reverse', 'style') 19 | ex_hidden = Font('ex_hidden', 'style') 20 | ex_reset = Font('ex_reset', 'style') 21 | ex_res_bold = Font('ex_res_bold', 'style') 22 | ex_res_dim = Font('ex_res_dim', 'style') 23 | ex_res_underlined = Font('ex_res_underlined', 'style') 24 | ex_res_blink = Font('ex_res_blink', 'style') 25 | ex_res_reverse = Font('ex_res_reverse', 'style') 26 | ex_res_hidden = Font('ex_res_hidden', 'style') 27 | -------------------------------------------------------------------------------- /terminal_layout/extensions/input/README.md: -------------------------------------------------------------------------------- 1 | # input ex 2 | 3 | Read input string. 4 | 注意:必须和 Textview 配合使用 5 | 6 | **Unsupport Windows** 7 | 8 | ![choice.gif](../../../pic/input.gif) 9 | 10 | 11 | ### usage 12 | 13 | ```python 14 | from terminal_layout import * 15 | from terminal_layout.extensions.input import * 16 | 17 | ctl = LayoutCtl.quick(TableRow, 18 | [TextView('', 'Input Something: ', fore=Fore.magenta), 19 | TextView('input', '', width=11, fore=Fore.blue)]) 20 | ctl.draw() 21 | ok, s = InputEx(ctl).get_input('input') 22 | if ok: 23 | print('-------') 24 | print('Your input:', Fore.blue, s, Fore.reset) 25 | ``` 26 | 27 | There are several parameter you can set: 28 | 29 | | name | default | desc | 30 | |-----------------|---------------------------------|-------------------| 31 | | input_buffer | 30 | io read buffer | 32 | | max_length | None | max input char | 33 | -------------------------------------------------------------------------------- /demo/textview_properties_demo/visibility.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # -*- coding: utf-8 -*- 3 | # -*- coding: utf-8 -*- 4 | from terminal_layout import * 5 | 6 | ctl = LayoutCtl.quick(TableLayout, 7 | [ 8 | [TextView('', 'visibility = visible ', width=25, back=Back.green), 9 | TextView('', ' hhhh ', visibility=Visibility.visible, back=Back.lightblack), 10 | TextView('', ' end ', back=Back.red)], 11 | [TextView('', 'visibility = invisible ', width=25, back=Back.green), 12 | TextView('', ' hhhh ', visibility=Visibility.invisible, back=Back.lightblack), 13 | TextView('', ' end ', back=Back.red)], 14 | [TextView('', 'visibility = gone ', width=25, back=Back.green), 15 | TextView('', ' hhhh ', visibility=Visibility.gone, back=Back.lightblack), 16 | TextView('', ' end ', back=Back.red)], 17 | 18 | ]) 19 | 20 | ctl.draw(auto_re_draw=False) 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "terminal_layout" 7 | version = "3.0.0" 8 | authors = [ 9 | { name="gojuukaze", email="ikaze_email@163.com" }, 10 | ] 11 | description = "The project help you to quickly build layouts in terminal (命令行ui布局工具)" 12 | readme = "README.rst" 13 | requires-python = ">=3.5" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "Operating System :: OS Independent", 17 | "Environment :: Console", 18 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 19 | "Development Status :: 5 - Production/Stable", 20 | "Topic :: Terminals", 21 | ] 22 | 23 | dependencies = [ 24 | "colorama==0.4.6", 25 | 'colored==1.4.4', 26 | ] 27 | 28 | [project.urls] 29 | "Homepage" = "https://github.com/gojuukaze/terminal_layout" 30 | "Bug Tracker" = "https://github.com/gojuukaze/terminal_layout/issues" 31 | "Documentation"="https://doc.ikaze.cn/terminal_layout" 32 | 33 | [tool.setuptools.packages.find] 34 | include = ["terminal_layout*"] 35 | exclude = ["demo*", "tests*"] 36 | -------------------------------------------------------------------------------- /demo/textview_properties_demo/weight.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # -*- coding: utf-8 -*- 3 | from terminal_layout import * 4 | 5 | ctl = LayoutCtl.quick(TableLayout, 6 | [ 7 | [TextView('', 'weight = 1 ', weight=1, back=Back.cyan), 8 | TextView('', 'weight = 1 ', weight=1, back=Back.green)], 9 | [TextView('', 'weight = 3 ', weight=3, back=Back.cyan), 10 | TextView('', 'weight = 1 ', weight=1, back=Back.green)], 11 | [TextView('', 'weight = 1 ', weight=1, back=Back.cyan), 12 | TextView('', 'width = 15 ', width=15, back=Back.green)], 13 | [TextView('', 'weight = 2 ', weight=1, back=Back.cyan), 14 | TextView('', 'width = 15 ', width=15, back=Back.green)], 15 | [TextView('', 'weight = 1 ', weight=1, back=Back.cyan), 16 | TextView('', 'width = wrap ', width=Width.wrap, back=Back.green)], 17 | 18 | ]) 19 | 20 | t = ctl.get_layout() 21 | t.set_width(40) 22 | 23 | ctl.draw(auto_re_draw=False) 24 | -------------------------------------------------------------------------------- /docs/getStarted.rst: -------------------------------------------------------------------------------- 1 | 快速开始 2 | ============ 3 | 4 | 欢迎使用terminal_layout,这个项目可以帮你告别单调的命令行输出,使你的输出富有色彩、结构。 5 | 你可以通过下面例子快速的了解这个项目 6 | 7 | .. code-block:: python 8 | 9 | from terminal_layout import * 10 | 11 | ctl = LayoutCtl.quick(TableLayout, 12 | [ 13 | [TextView('title', 'Student', fore=Fore.black, back=Back.blue, width=17, 14 | gravity=Gravity.center)], 15 | 16 | [TextView('', 'No.', width=5, back=Back.blue), 17 | TextView('', 'Name', width=12, back=Back.blue)], 18 | 19 | [TextView('st1_no', '1', width=5, back=Back.blue), 20 | TextView('st1_name', 'Bob', width=12, back=Back.blue)], 21 | 22 | [TextView('st2_no', '2', width=5, back=Back.blue), 23 | TextView('st2_name', 'Tom', width=12, back=Back.blue)], 24 | ] 25 | 26 | ) 27 | 28 | ctl.draw() 29 | ctl.stop() 30 | 31 | ``LayoutCtl`` , ``TableLayout`` , ``TextView`` 是该项目重要的元素,接下来阅读 :doc:`/draw` 熟悉如何使用它们。 32 | 33 | 如需监听键盘事件,则阅读: :doc:`/keyListener` -------------------------------------------------------------------------------- /terminal_layout/readkey/windows.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Initially taken from: 3 | # http://code.activestate.com/recipes/134892/#c9 4 | # Thanks to Stephen Chappell 5 | import msvcrt 6 | 7 | from terminal_layout.readkey.key import Key 8 | 9 | xlate_dict = { 10 | 8: Key.BACKSPACE.code, 11 | 13: Key.ENTER.code, 12 | 27: Key.ESC.code, 13 | 15104: Key.F1.code, 14 | 15360: Key.F2.code, 15 | 15616: Key.F3.code, 16 | 15872: Key.F4.code, 17 | 16128: Key.F5.code, 18 | 16384: Key.F6.code, 19 | 16640: Key.F7.code, 20 | 16896: Key.F8.code, 21 | # cmd 22 | 18656: Key.UP.code, 23 | 20704: Key.DOWN.code, 24 | 19424: Key.LEFT.code, 25 | 19936: Key.RIGHT.code, 26 | # powershell 27 | 18432: Key.UP.code, 28 | 20480: Key.DOWN.code, 29 | 19200: Key.LEFT.code, 30 | 19712: Key.RIGHT.code, 31 | } 32 | 33 | 34 | def readkey(buffer=0): 35 | while True: 36 | if msvcrt.kbhit(): 37 | ch = msvcrt.getch() 38 | a = ord(ch) 39 | if a == 0 or a == 224: 40 | b = ord(msvcrt.getch()) 41 | x = a + (b * 256) 42 | return xlate_dict.get(x, None) 43 | else: 44 | return xlate_dict.get(a, ch.decode()) 45 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. terminal_layout documentation master file, created by 2 | sphinx-quickstart on Sun Feb 10 14:41:08 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to terminal_layout's documentation! 7 | =========================================== 8 | 9 | | The project help you to quickly build layouts in terminal 10 | | (这个一个命令行ui布局工具) 11 | 12 | .. image:: _static/demo_v2_1.gif 13 | 14 | | 15 | 16 | .. image:: _static/demo.gif 17 | 18 | | 19 | 20 | .. image:: https://asciinema.org/a/226120.svg 21 | :target: https://asciinema.org/a/226120 22 | 23 | | 24 | 25 | 你可以从 :doc:`getStarted` 开始学习如何使用terminal_layout 26 | 27 | 28 | Link 29 | =============== 30 | 31 | - `All 32 | Demo `__ 33 | - `Github `__ 34 | - `Docs `__ 35 | - `https://asciinema.org/a/226120 `__ 36 | 37 | ------------ 38 | 39 | .. toctree:: 40 | :maxdepth: 1 41 | 42 | getStarted 43 | installation 44 | draw 45 | keyListener 46 | Properties 47 | View 48 | extensions/index 49 | changelog 50 | FAQ 51 | 52 | -------------------------------------------------------------------------------- /terminal_layout/helper/class_helper.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import inspect 3 | 4 | """ 5 | instance_variables 6 | 7 | ----------------------------- ------------------------------ 8 | |def __init__(self, a, b): | | @instance_variables | 9 | | self.a=a | ==> | def __init__(self, a, b): | 10 | | self.b=b | | pass | 11 | ----------------------------- ------------------------------ 12 | 13 | """ 14 | 15 | def instance_variables(f): 16 | sig = inspect.signature(f) 17 | 18 | @wraps(f) 19 | def wrapper(self, *args, **kwargs): 20 | values = sig.bind(self, *args, **kwargs) 21 | for k, p in sig.parameters.items(): 22 | if k != 'self': 23 | if k in values.arguments: 24 | val = values.arguments[k] 25 | if p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY): 26 | setattr(self, k, val) 27 | elif p.kind == inspect.Parameter.VAR_KEYWORD: 28 | for k, v in values.arguments[k].items(): 29 | setattr(self, k, v) 30 | else: 31 | setattr(self, k, p.default) 32 | 33 | f(self, *args, **kwargs) 34 | return wrapper 35 | -------------------------------------------------------------------------------- /terminal_layout/readkey/key.py: -------------------------------------------------------------------------------- 1 | class KeyInfo: 2 | def __init__(self, name, code): 3 | self.name = name 4 | self.code = code 5 | 6 | def __eq__(self, other): 7 | if isinstance(other, KeyInfo): 8 | return self.code == other.code 9 | return self.code == other 10 | 11 | def __str__(self): 12 | return '<%s>' % self.name 13 | def __repr__(self): 14 | return self.__str__() 15 | 16 | class Key: 17 | ENTER = KeyInfo('enter', '\r') 18 | TAB = KeyInfo('tab', '\x09') 19 | BACKSPACE = KeyInfo('backspace', '\x7f') 20 | ESC = KeyInfo('esc', '\x1b') 21 | 22 | UP = KeyInfo('up', '\x1b[A') 23 | DOWN = KeyInfo('down', '\x1b[B') 24 | LEFT = KeyInfo('left', '\x1b[D') 25 | RIGHT = KeyInfo('right', '\x1b[C') 26 | 27 | CTRL_A = KeyInfo('ctrl_a', '\x01') 28 | CTRL_B = KeyInfo('ctrl_b', '\x02') 29 | CTRL_C = KeyInfo('ctrl_c', '\x03') 30 | CTRL_D = KeyInfo('ctrl_d', '\x04') 31 | CTRL_E = KeyInfo('ctrl_e', '\x05') 32 | CTRL_F = KeyInfo('ctrl_f', '\x06') 33 | CTRL_X = KeyInfo('ctrl_x', '\x18') 34 | CTRL_Z = KeyInfo('ctrl_z', '\x1a') 35 | 36 | F1 = KeyInfo('f1', '\x1bOP') 37 | F2 = KeyInfo('f2', '\x1bOQ') 38 | F3 = KeyInfo('f3', '\x1bOR') 39 | F4 = KeyInfo('f4', '\x1bOS') 40 | F5 = KeyInfo('f5', '\x1b[15') 41 | F6 = KeyInfo('f6', '\x1b[17') 42 | F7 = KeyInfo('f7', '\x1b[18') 43 | F8 = KeyInfo('f8', '\x1b[19') 44 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | changelog 2 | ============= 3 | 4 | 2.1.4 5 | --------- 6 | * ``TableLayout`` 添加 ``overflow_vertical`` 参数,用于terminal高度不够时隐藏row(默认不隐藏) 7 | * 运行环境检测,非Terminal下抛出错误 ( `#25 `__ ) 8 | * 添加 `scroll `__ 扩展,让 ``TableLayout`` 支持滚动 ( `#24 `__ ) 9 | * 添加 ``remove``, ``remove_view_by_id`` 函数 10 | * choice扩展改用scroll实现滚动 11 | * 添加 ``is_show`` 用于 使用 ``scroll`` 或 ``overflow_vertical`` 为 ``hidden_top`` 、 ``hidden_btm`` 时判断 ``TableRow`` 是否隐藏。( **只能判断TableRow** ) 12 | * 修改一些小bug 13 | 14 | 2.1.3 15 | --------- 16 | * 解决 ``readkey()`` 函数在 Win PowerShell 下无法识别方向键 bug ( `#22 `__ ) 17 | * choice扩展适配高度不够情况,当高度不够时隐藏部分选项 ( `#21 `__ ) 18 | * choice扩展支持设置stop_key,默认为 ``['q']`` 19 | 20 | 2.1.2 21 | --------- 22 | * 增加input扩展,可以获取文字输入了(不支持windows) 23 | * TextView 增加 ``overflow`` 属性,用户文本过长时隐藏左边还是右边 24 | * view 增加 parent 属性 25 | * ctl自动重绘可通过设置 ``refresh_thread_stop`` 停止重绘 26 | 27 | 28 | 2.0.0 29 | --------- 30 | * auto refresh 增加自动刷新功能 31 | * add ``delay_set_text()`` 增加渐进显示字符的函数 ``delay_set_text()`` 32 | * ``find_view_by_id()`` 返回 ``ViewProxy`` ,不再直接返回view 33 | * 增加扩展 extensions 34 | * 增加按钮监听功能 35 | * 修复python2 bug 36 | 37 | 1.0.0 38 | -------- 39 | -------------------------------------------------------------------------------- /demo/scroll.py: -------------------------------------------------------------------------------- 1 | import platform 2 | from terminal_layout import * 3 | from terminal_layout.extensions.scroll import * 4 | 5 | s = '''python2 - print "Hello, World!" 6 | python3 - print("Hello, World!") 7 | c - printf("Hello, World!") 8 | c++ - cout << "Hello, World!" << endl 9 | java - System.out.println("Hello, World!") 10 | JS - console.log('Hello, World!'); 11 | php - echo "Hello, World!" 12 | c# - Console.WriteLine("Hello, World!") 13 | shell - echo "Hello, World!" 14 | go - fmt.Println("Hello, World!") 15 | rust - println("Hello, World!")''' 16 | 17 | title_style = { 18 | 'back': Back.magenta if platform.system() == 'Windows' else Back.ex_plum_2, 19 | 'width': Width.fill, 20 | 'gravity': Gravity.center 21 | 22 | } 23 | rows = [[], 24 | [TextView('title', 'Hello, World!', **title_style)], 25 | [TextView('', ' ', **title_style)], 26 | ] 27 | 28 | scroll_style = { 29 | 'back': Back.blue if platform.system() == 'Windows' else Back.ex_sky_blue_2, 30 | 'fore': Fore.black 31 | } 32 | for i, ss in enumerate(s.split('\n')): 33 | lan, code = ss.split(' - ') 34 | rows.append([ 35 | TextView('', ' ' + str(i) + '.' + lan.title(), width=10, **scroll_style), 36 | TextView('', '| ' + code, width=Width.fill, **scroll_style) 37 | ]) 38 | 39 | ctl = LayoutCtl.quick(TableLayout, rows) 40 | ctl.set_buffer_size(200) 41 | ctl.enable_debug(height=12) 42 | 43 | scroll = Scroll(ctl, stop_key='q', loop=True, more=True, scroll_box_start=3) 44 | scroll.scroll() 45 | -------------------------------------------------------------------------------- /terminal_layout/extensions/choice/README.md: -------------------------------------------------------------------------------- 1 | # choice 2 | Shows a list of choices, and allows the selection of one of them. 3 | 4 | 5 | ![choice.gif](../../../pic/choice.gif) 6 | 7 | 8 | ### usage 9 | 10 | ```python 11 | from terminal_layout.extensions.choice import * 12 | from terminal_layout import * 13 | 14 | c = Choice('Which is the Best Programming Language? (press to exit) ', 15 | ['Python', 'C/C++', 'Java', 'PHP', 'Go', 'JS', '...'], 16 | icon_style=StringStyle(fore=Fore.blue), 17 | selected_style=StringStyle(fore=Fore.blue)) 18 | 19 | choice = c.get_choice() 20 | if choice: 21 | index, value = choice 22 | print(value, 'is the Best Programming Language') 23 | ``` 24 | 25 | There are several parameter you can set: 26 | 27 | | name | default | desc | 28 | |-----------------|---------------------------------|--------------------| 29 | | title | | title | 30 | | choices | | a list of choices | 31 | | icon | '> ' | delimiter list | 32 | | icon\_style | StringStyle\(fore=Fore\.green\) | icon style | 33 | | choices\_style | StringStyle\(\) | choices style | 34 | | selected\_style | StringStyle\(\) | selected style | 35 | | loop | True | loop | 36 | | default_index | 0 | default icon index | 37 | | stop_key | ['q'] | stop key | -------------------------------------------------------------------------------- /demo/demo6(get_width).py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from terminal_layout import * 3 | import time 4 | 5 | 6 | def show_width(w1, w2, w3): 7 | for w in [w1, w2, w3]: 8 | print('%s: width=%s ,real_width=%s' % (w.get_id(), str(w.get_width()), str(w.get_real_width()))) 9 | 10 | 11 | ctl = LayoutCtl.quick(TableLayout, 12 | [ 13 | [TextView('w1', 'w1: width=15', back=Back.cyan, width=20)], 14 | [TextView('w2', 'w2: width=fill', back=Back.cyan, width=Width.fill)], 15 | [TextView('w3', 'w3: width=warp', back=Back.cyan, width=Width.wrap)], 16 | ] 17 | 18 | ) 19 | 20 | w1 = ctl.find_view_by_id('w1') 21 | w2 = ctl.find_view_by_id('w2') 22 | w3 = ctl.find_view_by_id('w3') 23 | 24 | print(str(Fore.green) + "get width before draw()" + str(Style.reset_all)) 25 | show_width(w1, w2, w3) 26 | 27 | print('==================') 28 | print(str(Fore.green) + "get width after draw()" + str(Style.reset_all)) 29 | 30 | ctl.draw(auto_re_draw=False) 31 | 32 | show_width(w1, w2, w3) 33 | print('==================') 34 | 35 | # 36 | ctl = LayoutCtl.quick(TableLayout, 37 | [ 38 | [TextView('w1', 'w1: width=15', back=Back.cyan, width=20)], 39 | [TextView('w2', 'w2: width=fill', back=Back.cyan, width=Width.fill)], 40 | [TextView('w3', 'w3: width=warp', back=Back.cyan, width=Width.wrap)], 41 | ] 42 | 43 | ) 44 | print(str(Fore.green) + "get width after update_width()" + str(Style.reset_all)) 45 | 46 | ctl.update_width() 47 | 48 | show_width(w1, w2, w3) 49 | -------------------------------------------------------------------------------- /terminal_layout/readkey/linux.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Initially taken from: 3 | # http://code.activestate.com/recipes/134892/ 4 | # Thanks to Danny Yoo 5 | import os 6 | import sys 7 | import tty 8 | import termios 9 | 10 | from terminal_layout.readkey.key import Key 11 | 12 | 13 | def readkey(buffer=10): 14 | fd = sys.stdin.fileno() 15 | old_settings = termios.tcgetattr(fd) 16 | try: 17 | tty.setraw(fd) 18 | c = os.read(fd, buffer) 19 | if isinstance(c, str): 20 | # py2 21 | c = c.decode('utf-8') 22 | else: 23 | # py3 24 | try: 25 | c = str(c, encoding='utf-8') 26 | except: 27 | c = '' 28 | finally: 29 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 30 | if c == Key.CTRL_C: 31 | raise KeyboardInterrupt 32 | return c 33 | 34 | 35 | def readkey2(): 36 | fd = sys.stdin.fileno() 37 | old_settings = termios.tcgetattr(fd) 38 | # 配置终端 39 | new_settings = old_settings[:] 40 | 41 | # 使用非规范模式(索引3是c_lflag 也就是本地模式) 42 | new_settings[3] &= ~termios.ICANON 43 | # # 关闭回显(输入不会被显示) 44 | new_settings[3] &= ~termios.ECHO 45 | try: 46 | termios.tcsetattr(fd, termios.TCSANOW, new_settings) 47 | c = os.read(fd, 4) 48 | if isinstance(c, str): 49 | # py2 50 | c = c.decode('utf-8') 51 | else: 52 | # py3 53 | try: 54 | c = str(c, encoding='utf-8') 55 | except: 56 | c = '' 57 | finally: 58 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 59 | if c == Key.CTRL_C: 60 | raise KeyboardInterrupt 61 | return c 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit tests / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .idea 107 | 108 | test.py 109 | run.py 110 | 111 | .DS_Store -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'terminal_layout' 21 | copyright = '2021, gojuukaze' 22 | author = 'gojuukaze' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '2.0.1' 26 | 27 | import sphinx_rtd_theme 28 | 29 | extensions = [ 30 | 'sphinx_rtd_theme' 31 | ] 32 | 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The language for content autogenerated by Sphinx. Refer to documentation 38 | # for a list of supported languages. 39 | # 40 | # This is also used if you do content translation via gettext catalogs. 41 | # Usually you set "language" from the command line for these cases. 42 | language = 'zh_CN' 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 48 | 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | 52 | # The theme to use for HTML and HTML Help pages. See the documentation for 53 | # a list of builtin themes. 54 | # 55 | html_theme = 'sphinx_rtd_theme' 56 | 57 | # Add any paths that contain custom static files (such as style sheets) here, 58 | # relative to this directory. They are copied after the builtin static files, 59 | # so a file named "default.css" will overwrite the builtin "default.css". 60 | html_static_path = ['_static'] 61 | -------------------------------------------------------------------------------- /docs/keyListener.rst: -------------------------------------------------------------------------------- 1 | 监听键盘按键 2 | ======================= 3 | 4 | 绑定键盘事件 5 | ------------------------------- 6 | 实例化一个 :class:`KeyListener` 对象,通过 ``bind_key`` 绑定key 7 | 8 | 9 | .. code-block:: python 10 | 11 | from terminal_layout import * 12 | 13 | key_listener = KeyListener() 14 | 15 | @key_listener.bind_key(Key.UP) 16 | def _(kl, e): 17 | print(e) 18 | 19 | @key_listener.bind_key(Key.DOWN, 'a', '[0-9]') 20 | def _(kl, e): 21 | if e.key == 'a': 22 | print('Press a') 23 | elif e.key == Key.DOWN: 24 | print('Press DOWN') 25 | elif e.key == '0': 26 | print('Press 0') 27 | else: 28 | print('Press 1-9') 29 | 30 | def stop(kl, e): 31 | print('Press', e.key, 'stop!') 32 | kl.stop() 33 | 34 | key_listener.bind_key(Key.ENTER,Key.F1, stop, decorator=False) 35 | 36 | key_listener.listen(stop_key=[Key.CTRL_A]) 37 | 38 | bind_key 39 | -------------- 40 | 41 | ``bind_key`` 绑定的key有三种类型: 42 | 43 | - Key的成员变量 44 | - 正则表达式 45 | - "any"(任意按键) 46 | 47 | 其有两种用法,装饰器模式和非装饰器模; 48 | 49 | 非装饰器模式下,倒数第二个参数为回调的函数,最后一个参数必须是 ``decorator=False`` 50 | 51 | 52 | 允许的Key 53 | --------------- 54 | 55 | ======== ============================================================== 56 | name keys 57 | ======== ============================================================== 58 | Arrows UP, DOWN, LEFT, RIGHT 59 | Control+ CTRL_A, CTRL_B, CTRL_D, CTRL_E, CTRL_F, CTRL_X, CTRL_Z 60 | F F1, F2, F3, F4, F5, F6, F7, F8 61 | Other ENTER, TAB, BACKSPACE, ESC 62 | ======== ============================================================== 63 | 64 | .. note:: 65 | 66 | 不支持绑定 CTRL_C 67 | 68 | 如果需要绑定 ESC ,记得修改stop_key。 69 | 70 | 停止监听 71 | -------------- 72 | 73 | 有两种方法可以停止监听 74 | 75 | 1. 回调函数中调用 ``stop()`` 76 | 77 | 2. 开启监听时设置 ``stop_key`` ,如果不设置默认为 [Key.ESC] 78 | 79 | 绑定不在列表中的key 80 | ---------------------- 81 | 82 | bind_key的参数是非常宽松的,因此你可以绑定不在支持列表中的key 83 | 84 | .. code-block:: python 85 | 86 | from terminal_layout import * 87 | from terminal_layout.readkey.key import KeyInfo 88 | 89 | kl = KeyListener() 90 | 91 | ctrl_g = KeyInfo('ctrl_g', '\x07') 92 | 93 | @kl.bind_key(ctrl_g) 94 | def _(kl, e): 95 | print('按下 ctrl_g', e) 96 | 97 | 98 | # or 99 | ctrl_h_code = '\x08' 100 | 101 | @kl.bind_key(ctrl_h_code) 102 | def _(kl, e): 103 | print('按下 ctrl_h', e) 104 | 105 | 106 | kl.listen() 107 | 108 | -------------------------------------------------------------------------------- /demo/textview_properties_demo/color.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from terminal_layout import * 3 | 4 | print('\ncolor support window, linux, osx\n') 5 | 6 | ctl = LayoutCtl.quick(TableLayout, 7 | [ 8 | [TextView('', 'fore = black', width=30, fore=Fore.black), 9 | TextView('', 'back = black', back=Back.black)], 10 | [TextView('', 'fore = red', width=30, fore=Fore.red), TextView('', 'back = red', back=Back.red)], 11 | [TextView('', 'fore = green', width=30, fore=Fore.green), 12 | TextView('', 'back = green', back=Back.green)], 13 | [TextView('', 'fore = yellow', width=30, fore=Fore.yellow), 14 | TextView('', 'back = yellow', back=Back.yellow)], 15 | [TextView('', 'fore = blue', width=30, fore=Fore.blue), TextView('', 'back = blue', back=Back.blue)], 16 | [TextView('', 'fore = magenta', width=30, fore=Fore.magenta), 17 | TextView('', 'back = magenta', back=Back.magenta)], 18 | [TextView('', 'fore = cyan', width=30, fore=Fore.cyan), TextView('', 'back = cyan', back=Back.cyan)], 19 | [TextView('', 'fore = white', width=30, fore=Fore.white), 20 | TextView('', 'back = white', back=Back.white)], 21 | [TextView('', 'fore = lightblack', width=30, fore=Fore.lightblack), 22 | TextView('', 'back = lightblack', back=Back.lightblack)], 23 | [TextView('', 'fore = lightred', width=30, fore=Fore.lightred), 24 | TextView('', 'back = lightred', back=Back.lightred)], 25 | [TextView('', 'fore = lightgreen', width=30, fore=Fore.lightgreen), 26 | TextView('', 'back = lightgreen', back=Back.lightgreen)], 27 | [TextView('', 'fore = lightyellow', width=30, fore=Fore.lightyellow), 28 | TextView('', 'back = lightyellow', back=Back.lightyellow)], 29 | [TextView('', 'fore = lightblue', width=30, fore=Fore.lightblue), 30 | TextView('', 'back = lightblue', back=Back.lightblue)], 31 | [TextView('', 'fore = lightmagenta', width=30, fore=Fore.lightmagenta), 32 | TextView('', 'back = lightmagenta', back=Back.lightmagenta)], 33 | [TextView('', 'fore = lightcyan', width=30, fore=Fore.lightcyan), 34 | TextView('', 'back = lightcyan', back=Back.lightcyan)], 35 | [TextView('', 'fore = lightwhite', width=30, fore=Fore.lightwhite), 36 | TextView('', 'back = lightwhite', back=Back.lightwhite)] 37 | ]) 38 | 39 | ctl.draw(auto_re_draw=False) 40 | 41 | 42 | -------------------------------------------------------------------------------- /terminal_layout/extensions/input/_old.py: -------------------------------------------------------------------------------- 1 | """ 2 | input初期的测试代码,放到这只做备忘 3 | """ 4 | 5 | import sys 6 | from terminal_layout import KeyListener, Key 7 | import logging 8 | 9 | from terminal_layout.types import String 10 | 11 | logger = logging.getLogger(__name__) 12 | logger.setLevel(level=logging.DEBUG) 13 | handler = logging.FileHandler("terminal_layout.log") 14 | logger.addHandler(handler) 15 | 16 | from terminal_layout.ansi import Cursor 17 | 18 | 19 | k = KeyListener() 20 | 21 | s = String('') 22 | s_char_list_index = 0 23 | cursor_index = 0 24 | 25 | 26 | @k.bind_key('any') 27 | def _(_, e): 28 | global s, s_char_list_index, cursor_index 29 | 30 | c = e.key.code 31 | logger.debug(f's={s}, c={c}') 32 | 33 | if repr(c).startswith("'\\x") or repr(c).startswith("u'\\x"): 34 | 35 | if e.key == Key.LEFT and s_char_list_index > 0: 36 | back = len(s.char_list[s_char_list_index - 1]) 37 | cursor_index -= back 38 | s_char_list_index -= 1 39 | sys.stdout.write(Cursor.BACK(back)) 40 | 41 | if e.key == Key.RIGHT and s_char_list_index < len(s.char_list): 42 | forward = len(s.char_list[s_char_list_index]) 43 | cursor_index += forward 44 | s_char_list_index += 1 45 | sys.stdout.write(Cursor.FORWARD(forward)) 46 | 47 | if e.key == Key.BACKSPACE and s_char_list_index > 0: 48 | back = len(s.char_list[s_char_list_index - 1]) 49 | cursor_index -= back 50 | s_char_list_index -= 1 51 | 52 | len_right_s = 0 53 | show_s = '' 54 | for c in s.get_char_list_item(s_char_list_index + 1): 55 | len_right_s += len(c) 56 | show_s += str(c) 57 | 58 | s.pop(s_char_list_index) 59 | 60 | logger.info(f'{back} - {len_right_s} - {show_s} - {repr(Cursor.BACK(len_right_s) if len_right_s else "")}') 61 | 62 | sys.stdout.write( 63 | Cursor.BACK(back) + ' ' * (len_right_s + back) + Cursor.BACK(len_right_s + back) + show_s) 64 | 65 | if len_right_s > 0: 66 | sys.stdout.write(Cursor.BACK(len_right_s)) 67 | 68 | 69 | else: 70 | show_s = c 71 | c_str = String(c) 72 | after_show_s = '' 73 | len_right_s = 0 74 | for c in s.get_char_list_item(s_char_list_index): 75 | len_right_s += len(c) 76 | show_s += str(c) 77 | 78 | if len_right_s > 0: 79 | sys.stdout.write(' ' * len_right_s + Cursor.BACK(len_right_s)) 80 | after_show_s = Cursor.BACK(len_right_s) 81 | 82 | sys.stdout.write(show_s + after_show_s) 83 | 84 | s.insert_into_char_list(s_char_list_index, c_str) 85 | s_char_list_index += len(c_str.char_list) 86 | cursor_index += len(c_str) 87 | logger.debug(f's_char_list_index={s_char_list_index}, cursor_index={cursor_index}, s={s}') 88 | 89 | sys.stdout.flush() 90 | 91 | 92 | s1 = 'bbb, input:' 93 | sys.stdout.write(s1) 94 | sys.stdout.write('\n') 95 | sys.stdout.write('asd222\n' + Cursor.UP(2) + Cursor.FORWARD(len(s1))) 96 | sys.stdout.flush() 97 | k.listen(stop_key=[Key.ENTER]) 98 | 99 | print('\nasd222') 100 | -------------------------------------------------------------------------------- /docs/Properties.rst: -------------------------------------------------------------------------------- 1 | 属性效果说明 2 | =============== 3 | 4 | 属性效果展示 5 | 6 | fore & back 7 | ------------- 8 | 9 | 颜色,背景色 10 | 所有值参考 ``Fore`` ,``Back`` 类 11 | 12 | .. code:: python 13 | 14 | TextView('','fore',fore=Fore.red) 15 | TextView('','back',back=Back.red) 16 | 17 | |image5| 18 | 19 | style 20 | ------- 21 | 22 | 样式 23 | 所有值参考 ``Style`` 类 24 | 25 | 26 | .. code:: python 27 | 28 | TextView('','style',style=Style.dim) 29 | 30 | |image6| 31 | 32 | width 33 | -------- 34 | 宽度 35 | 36 | * 正整数 :表示所占的字符位数,ascii码占1位,非ascii码占2位 37 | * Width.wrap :根据内容决定宽度 38 | * Width.fill :填满父布局 39 | 40 | .. code:: python 41 | 42 | TextView('','width',width=10) 43 | 44 | |image7| 45 | 46 | weight 47 | -------- 48 | 比重 49 | 50 | 值是正整数,表示所占比重。 51 | 52 | .. code:: python 53 | 54 | TextView('','weight',weight=1) 55 | 56 | weight=2 表示宽度所占比重为2。 57 | 58 | 比如: 59 | 60 | .. code-block:: python 61 | 62 | # t1 real_width=2 , t2 real_width=4 63 | TableRow('',[TextView('t1','',weight=1), TextView('t2','',weight=2),] , width=6) 64 | 65 | 当同时设置weight与width时,使用weight确定宽度 66 | 67 | .. code-block:: python 68 | 69 | # t1 real_width=3 70 | TableRow('',[TextView('t1','',weight=1, width=10), ] , width=3) 71 | 72 | 73 | 当其他view设置了width时,设置有weight的view按比重分配剩下的宽度 74 | 75 | .. code-block:: python 76 | 77 | # t2 real_width=2 ,t3 real_width=4 78 | TableRow('', 79 | [ 80 | TextView('t1','',width=10), 81 | TextView('t2','',weight=1), 82 | TextView('t3','',weight=2), 83 | ] , 84 | 85 | width=16) 86 | 87 | 88 | |image8| 89 | 90 | gravity 91 | ---------- 92 | 93 | 对齐方式 94 | 95 | * Gravity.left : 居左 96 | * Gravity.center : 居中 97 | * Gravity.right : 居右 98 | 99 | .. code:: python 100 | 101 | TextView('','gravity',gravity=Gravity.left) 102 | 103 | |image9| 104 | 105 | visibility 106 | ----------- 107 | 是否显示 108 | 109 | * Visibility.visible :显示 110 | * Visibility.invisible :不显示,当占宽度 111 | * Visibility.gone :不显示,不占宽度 112 | 113 | 114 | .. code:: python 115 | 116 | TextView('','',visibility=Visibility.visible) 117 | 118 | |image10| 119 | 120 | ex_style 121 | ---------- 122 | 123 | ``ex_`` 开头的字体样式,不支持windows 124 | 125 | .. code:: python 126 | 127 | from terminal_layout import * 128 | TextView('','ex_style',style=Style.ex_blink) 129 | 130 | |image11| 131 | 132 | ex_fore & ex_back 133 | ------------------- 134 | 135 | ``ex_`` 开头颜色,背景色,不支持windows 136 | 137 | .. code:: python 138 | 139 | from terminal_layout import * 140 | TextView('','ex_fore',fore=Fore.ex_red_1) 141 | TextView('','ex_back',back=Back.ex_red_1) 142 | 143 | |image12| 144 | 145 | .. |image5| image:: _static/color.jpeg 146 | :scale: 50% 147 | .. |image6| image:: _static/style.jpeg 148 | :scale: 50% 149 | .. |image7| image:: _static/width.jpeg 150 | :scale: 50% 151 | .. |image8| image:: _static/weight.jpeg 152 | :scale: 50% 153 | .. |image9| image:: _static/gravity.jpeg 154 | :scale: 50% 155 | .. |image10| image:: _static/visibility.jpeg 156 | :scale: 50% 157 | .. |image11| image:: _static/ex_style.jpeg 158 | :scale: 50% 159 | .. |image12| image:: _static/ex_color.jpeg 160 | :scale: 50% 161 | -------------------------------------------------------------------------------- /terminal_layout/readkey/listener.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from datetime import datetime 4 | 5 | from terminal_layout.readkey.key import Key, KeyInfo 6 | from terminal_layout.readkey.event import KeyPressEvent 7 | 8 | if sys.platform in ('win32', 'cygwin'): 9 | from terminal_layout.readkey.windows import readkey as _readkey 10 | else: 11 | from terminal_layout.readkey.linux import readkey as _readkey 12 | 13 | 14 | def get_code_to_name(): 15 | """ 16 | :return: 17 | :rtype: dict 18 | """ 19 | d = {} 20 | for k, info in Key.__dict__.items(): 21 | if not k.startswith("_"): 22 | d[info.code] = info.name 23 | return d 24 | 25 | 26 | code_to_name = get_code_to_name() 27 | 28 | 29 | def readkey(buffer=10): 30 | c = _readkey(buffer) 31 | n = datetime.now() 32 | name = code_to_name.get(c, None) 33 | if name: 34 | return KeyPressEvent(KeyInfo(name, c), n) 35 | if repr(c).startswith("'\\x") or repr(c).startswith("u'\\x"): 36 | return KeyPressEvent(KeyInfo('unknown', c), n) 37 | else: 38 | return KeyPressEvent(KeyInfo(c, c), n) 39 | 40 | 41 | class KeyListener(object): 42 | 43 | def __init__(self, input_buffer=10): 44 | self.key_func = {} 45 | self.re_func = {} 46 | self.stop_flag = False 47 | self.input_buffer = input_buffer 48 | 49 | def bind_key(self, *args, **kwargs): 50 | """ 51 | args: key or regular expression; 52 | :param args: 53 | :param kwargs: 54 | """ 55 | decorator = kwargs.get('decorator', True) 56 | if decorator: 57 | keys = args 58 | else: 59 | keys = args[:-1] 60 | 61 | def inner(func): 62 | for k in keys: 63 | if isinstance(k, KeyInfo): 64 | self.key_func[k.code] = func 65 | elif isinstance(k, str): 66 | self.re_func[k] = func 67 | else: 68 | raise TypeError('item in *args must be a KeyInfo or a regular expression string') 69 | return func 70 | 71 | if decorator: 72 | return inner 73 | else: 74 | inner(args[-1]) 75 | 76 | def stop(self): 77 | self.stop_flag = True 78 | 79 | def listen(self, stop_key=None): 80 | """ 81 | 82 | :param stop_key: key or regular expression; default [Key.ESC] 83 | :type stop_key: list 84 | :return: 85 | :rtype: 86 | """ 87 | if stop_key is None: 88 | stop_key = [Key.ESC] 89 | 90 | while True: 91 | if self.stop_flag: 92 | break 93 | c = readkey(self.input_buffer) 94 | code = c.key.code 95 | for s in stop_key: 96 | if isinstance(s, KeyInfo): 97 | if s.code == code: 98 | return 99 | else: 100 | if re.match(s, code): 101 | return 102 | 103 | func = self.key_func.get(c.key.code, None) 104 | if func: 105 | func(self, c) 106 | 107 | for s, func in self.re_func.items(): 108 | if s == 'any': 109 | func(self, c) 110 | continue 111 | if re.match(s, code): 112 | func(self, c) 113 | continue 114 | -------------------------------------------------------------------------------- /terminal_layout/view/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from terminal_layout.view.params import Visibility, Gravity 3 | 4 | 5 | class View(object): 6 | __slots__ = ('id', 'width', 'height', 'visibility', 'gravity', 7 | 'real_width', 'real_height', 'data', 'parent', '_is_show') 8 | 9 | def __init__(self, id, width, height=1, visibility=Visibility.visible, gravity=Gravity.left): 10 | """ 11 | 12 | :param id: 13 | :param width: 14 | :param height: no used 15 | 16 | :type id:str 17 | :type width: 18 | :type height:int 19 | :type visibility:str 20 | :type gravity:str 21 | """ 22 | 23 | self.data = [] # type: list[View] 24 | self.id = id 25 | self.width = width 26 | self.height = height 27 | self.visibility = visibility 28 | self.gravity = gravity 29 | 30 | self.real_width = None 31 | self.real_height = None 32 | self.parent = None 33 | 34 | # 是否显示在屏幕上。这个值用于: 当终端高度不够触发隐藏时,标识view是否显示在屏幕上。 35 | # 注意:你不应当主动修改这个值这是没意义的;另外is_show不会因visibility改变而改变。 36 | # 它只有下面两种情况才会被修改: 37 | # 1. tableLayout的overflow_vertical设为hidden_btm或hidden_top 38 | # 2. 使用scroll 39 | self._is_show = True 40 | 41 | def draw(self): 42 | pass 43 | 44 | def clear(self): 45 | pass 46 | 47 | def update_width(self, parent_width): 48 | pass 49 | 50 | def find_view_by_id(self, id): 51 | """ 52 | 53 | :param id: 54 | :return: 55 | :rtype: View 56 | """ 57 | if self.id == id: 58 | return self 59 | temp = None 60 | for v in self.data: 61 | # 这里不用判断v类型,textView的data是空list 62 | temp = v.find_view_by_id(id) 63 | if temp: 64 | break 65 | return temp 66 | 67 | def remove(self): 68 | """ 69 | :rtype: bool 70 | """ 71 | # 不能移除最外层view 72 | if not self.parent: 73 | return False 74 | return self.parent.remove_view_by_id(self.id) 75 | 76 | def remove_view_by_id(self, id): 77 | """ 78 | :rtype: bool 79 | """ 80 | if self.id == id: 81 | return self.remove() 82 | index = -1 83 | for i, v in enumerate(self.data): 84 | if v.id == id: 85 | index = i 86 | break 87 | # 这里不用判断v类型,textView的data是空list 88 | ok = v.remove_view_by_id(id) 89 | if ok: 90 | return ok 91 | if index == -1: 92 | return False 93 | self.data = self.data[:index]+self.data[index+1:] 94 | return True 95 | 96 | def get_width(self): 97 | """ 98 | the width you set in __init__() 99 | 初始化时设置的宽度 100 | 101 | :return: 102 | """ 103 | return self.width 104 | 105 | def get_real_width(self): 106 | """ 107 | 108 | the real width of the display 109 | 实际显示的宽度 110 | 111 | :return: 112 | """ 113 | return self.real_width 114 | 115 | def __getitem__(self, item): 116 | """ 117 | 118 | :param item: 119 | :return: 120 | :rtype:View 121 | """ 122 | return self.data[item] 123 | 124 | def insert(self, index, view): 125 | self.data.insert(index, view) 126 | 127 | def is_show(self): 128 | return self._is_show and self.visibility != Visibility.gone 129 | 130 | def _set_is_show(self, is_show): 131 | self._is_show = is_show 132 | 133 | terminal_w = 0 134 | terminal_h = 0 135 | 136 | def set_terminal_size(self, w, h): 137 | self.terminal_w = w 138 | self.terminal_h = h 139 | -------------------------------------------------------------------------------- /docs/View.rst: -------------------------------------------------------------------------------- 1 | View 2 | ============= 3 | 4 | ``View`` 的概念继承自安卓,一共有两种类型的View,分别是 ``Layout`` 与 ``Widget`` 。 5 | 6 | 此项目 ``Widget`` 只有 ``TextView`` ; ``Layout`` 有 ``TableLayout`` , ``TableRow`` 7 | 8 | 9 | 10 | .. py:class:: View 11 | 12 | View方法说明 13 | 14 | .. py:method:: __init__(id, width, height=1, visibility=Visibility.visible, gravity=Gravity.left) 15 | 16 | 初始化 17 | 18 | .. py:attribute:: id 19 | 20 | view的唯一id 21 | 22 | .. py:attribute:: width 23 | 24 | view的宽度,可以是正整数,或者 Width.wrap,Width.fill 25 | 26 | .. py:attribute:: height 27 | 28 | view的高度,暂时没用,始终为1 29 | 30 | 31 | .. py:attribute:: visibility 32 | 33 | 是否显示,可选值 :Visibility.visible,Visibility.invisible,Visibility.gone 34 | 35 | .. py:attribute:: gravity 36 | 37 | view内部对其方式,可选值:Gravity.left,Gravity.center,Gravity.right 38 | 39 | 40 | .. py:method:: get_width() 41 | 42 | 获取初始化时设置的width 43 | 44 | .. py:method:: get_real_width() 45 | 46 | 最终显示的宽度 47 | 48 | .. py:method:: add_view(view) 49 | 50 | 向view中添加一个view 51 | 52 | .. py:method:: insert(index, view) 53 | 54 | 向view中每个位置插入一个view 55 | 56 | .. py:method:: remove() 57 | 58 | 从父view中移除自身 59 | 60 | .. py:method:: remove_view_by_id(id) 61 | 62 | 删除view 63 | 64 | Layout 65 | ------- 66 | 67 | ``Layout`` 是布局控制器,他控制每个 ``Widget`` 显示的位置。 68 | 69 | TableLayout 70 | +++++++++++++ 71 | 72 | 表格布局 73 | 74 | .. py:class:: TableLayout 75 | 76 | .. py:method:: __init__(id, width=Width.fill, height=1, visibility=Visibility.visible, overflow_vertical=OverflowVertical.none) 77 | 78 | init,**需要注意TableLayout的gravity总是为left** 79 | 80 | .. py:classmethod:: quick_init(id, data, width=Width.fill, height=1, visibility=Visibility.visible) 81 | 82 | 快速初始化, 83 | 84 | .. py:attribute:: data 85 | 86 | 初始的view list 87 | 88 | 89 | .. py:method:: add_view(view) 90 | 91 | 向view中添加一个view 92 | 93 | .. py:method:: add_view_list(view_list) 94 | 95 | 向view中添加多个view, 96 | 97 | 98 | TableRow 99 | +++++++++++++ 100 | 101 | 行布局 102 | 103 | .. py:class:: TableLayout 104 | 105 | .. py:method:: __init__(id, width=Width.fill, height=1, back=None, visibility=Visibility.visible, gravity=Gravity.left) 106 | 107 | init 108 | 109 | .. py:attribute:: back 110 | 111 | 背景色 112 | 113 | .. py:classmethod:: quick_init(id, data, width=Width.fill, height=1, back=None, visibility=Visibility.visible, gravity=Gravity.left) 114 | 115 | 快速初始化 116 | 117 | .. py:attribute:: data 118 | 119 | 初始的view list 120 | 121 | 122 | .. py:method:: add_view(view) 123 | 124 | 向view中添加一个view,只支持添加TextView 125 | 126 | .. py:method:: add_view_list(view_list) 127 | 128 | 向view中添加多个view,只支持添加TextView 129 | 130 | .. py:method:: is_show() 131 | 132 | view是否显示出来。 133 | 134 | 在 v2.1.4 之后, 如果 terminal 高度时会隐藏不能显示的部分,此时可通过is_show判断view是否显示。 135 | 136 | 注意只有使用 ``scroll`` 或 ``overflow_vertical`` 为 ``hidden_top`` 、 ``hidden_btm`` 时这个函数返回值才是有意义的, 137 | 且只对 ``TableRow`` 有效,对于 ``TextView`` 这个返回值一样是无意义的。 138 | 139 | 140 | TextView 141 | ---------- 142 | 143 | 用于显示文本 144 | 145 | .. py:class:: TextView 146 | 147 | .. py:method:: __init__(id, text, fore=None, back=None, style=None, width=Width.wrap, height=1, weight=None, visibility=Visibility.visible, gravity=Gravity.left) 148 | 149 | 初始化 150 | 151 | .. py:attribute:: text 152 | 153 | 文本 154 | 155 | .. py:attribute:: fore 156 | 157 | 颜色 158 | 159 | 160 | .. py:attribute:: back 161 | 162 | 背景色 163 | 164 | 165 | .. py:attribute:: style 166 | 167 | 字体样式 168 | 169 | .. py:attribute:: weight 170 | 171 | 宽度比重 172 | 173 | -------------------------------------------------------------------------------- /terminal_layout/view/text_view.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import threading 4 | 5 | from terminal_layout.ansi import * 6 | from terminal_layout.types import String 7 | from terminal_layout.view.base import View 8 | from terminal_layout.view.params import Visibility, Width, Gravity, Overflow 9 | 10 | 11 | class TextView(View): 12 | __slots__ = ('back', 'style', 'fore', 'text', 'text_string', 'weight', 'overflow') 13 | 14 | def __init__(self, id, text, fore=None, back=None, style=None, width=Width.wrap, 15 | height=1, weight=None, visibility=Visibility.visible, gravity=Gravity.left, 16 | overflow=Overflow.hidden_right): 17 | """ 18 | 19 | :param id: 20 | :param width: 21 | :param height: no used 22 | 23 | :type id:str 24 | :type width: Union[int, str] 25 | :type height:int 26 | :type visibility:str 27 | :type gravity:str 28 | :type overflow:str 29 | 30 | """ 31 | 32 | super(TextView, self).__init__(id, width, height, visibility, gravity) 33 | self.text_string = None # type:String 34 | 35 | self.text = text 36 | self.fore = fore or '' 37 | self.back = back or '' 38 | self.style = style or '' 39 | self.weight = weight 40 | self.overflow = overflow 41 | 42 | self.real_height = 1 43 | 44 | def update_width(self, parent_width): 45 | if parent_width <= 0: 46 | self.real_width = 0 47 | return self.real_width 48 | if self.weight: 49 | return None 50 | if isinstance(self.width, int) and self.width >= 0: 51 | self.real_width = int(self.width) 52 | temp = parent_width - self.real_width 53 | if temp <= 0: 54 | self.real_width = parent_width 55 | 56 | elif self.width == Width.wrap: 57 | self.real_width = len(self.text_string) 58 | temp = parent_width - self.real_width 59 | if temp <= 0: 60 | self.real_width = parent_width 61 | 62 | elif self.width == Width.fill: 63 | self.real_width = parent_width 64 | 65 | return self.real_width 66 | 67 | def draw(self): 68 | sys.stdout.write(self.get_final_text()) 69 | 70 | def clear(self): 71 | sys.stdout.write(Cursor.UP(self.real_height) + clear_line()) 72 | 73 | def get_final_text(self): 74 | 75 | if self.visibility == Visibility.visible: 76 | self.real_height = 1 77 | show_text = self.text_string[:self.real_width] if self.overflow == Overflow.hidden_right \ 78 | else self.text_string[-self.real_width:] 79 | elif self.visibility == Visibility.invisible: 80 | self.real_height = 1 81 | return ' ' * self.real_width 82 | elif self.visibility == Visibility.gone: 83 | return '' 84 | 85 | # show_text是str 要转为String 86 | show_string = String(show_text) 87 | if self.real_width > len(show_string): 88 | if self.gravity == Gravity.left: 89 | show_text = show_text + ' ' * (self.real_width - len(show_string)) 90 | elif self.gravity == Gravity.right: 91 | show_text = ' ' * (self.real_width - len(show_string)) + show_text 92 | elif self.gravity == Gravity.center: 93 | p = (self.real_width - len(show_string)) / 2.0 94 | show_text = ' ' * int(p) + show_text + ' ' * (int(p) if int(p) == p else int(p) + 1) 95 | 96 | return str(self.fore) + str(self.back) + str(self.style) + show_text + str(Style.reset_all) 97 | 98 | def __str__(self): 99 | return str(self.fore) + str(self.back) + str(self.style) + self.text + str(Style.reset_all) 100 | 101 | def __setattr__(self, key, value): 102 | super(TextView, self).__setattr__(key, value) 103 | if key == 'text': 104 | self.text_string = String(value) 105 | 106 | -------------------------------------------------------------------------------- /terminal_layout/extensions/progress/README.md: -------------------------------------------------------------------------------- 1 | # progress 2 | loading and progress bar 3 | 4 | ![progress.gif](../../../pic/progress.gif) 5 | 6 | * [Progress](#Progress) 7 | * [Loading](#Loading) 8 | 9 | ## Progress 10 | 11 | ### usage 12 | 13 | ```python 14 | import time 15 | from terminal_layout.extensions.progress import * 16 | 17 | p = Progress('Downloading', 20) 18 | p.start() 19 | p.set_progress(2) 20 | time.sleep(0.3) 21 | for i in range(10): 22 | if p.is_finished(): 23 | break 24 | time.sleep(0.3) 25 | p.add_progress(i - 1) 26 | p.stop() 27 | ``` 28 | 29 | or use context manager 30 | ```python 31 | import time 32 | from terminal_layout.extensions.progress import * 33 | 34 | with Progress('Downloading', 20) as p: 35 | p.set_progress(2) 36 | time.sleep(0.3) 37 | for i in range(10): 38 | if p.is_finished(): 39 | break 40 | time.sleep(0.3) 41 | p.add_progress(i) 42 | ``` 43 | 44 | The result will be a bar like the following: 45 | 46 | ``` 47 | Downloading |███████████████████████████████ | 60% 48 | ``` 49 | 50 | ## parameter 51 | There are several parameter you can set: 52 | 53 | ![](../../../pic/progress.jpg) 54 | 55 | | name | default | desc | 56 | |---------------|----------------------|----------------------------------------------| 57 | | prefix | | prefix string | 58 | | max | | maximum value | 59 | | delimiter | `[" \|","\| "]` | delimiter list | 60 | | reached | '█'(on linux); '='(on windows) | | 61 | | unreached | '' | | 62 | | suffix\_style | SuffixStyle\.percent | SuffixStyle class variable | 63 | | width | ProgressWidth\.half | a int number or ProgressWidth class variable | 64 | 65 | recommend for the collocation of reached and unreached 66 | 推荐的reached, unreached组合 67 | 68 | | reached | unreached | 69 | |---------|-----------| 70 | | █ | | 71 | | = | | 72 | | █ | ∙ | 73 | | = | . | 74 | | ▓ | ░ | 75 | 76 | > some special characters (█, ░) won't display correctly on Windows. 77 | > 一些特殊字符(如:█, ░)在 Windows可能无法正确显示。 78 | > 具体表现为在多行显示progress 79 | 80 | ## Loading 81 | 82 | ### usage 83 | 84 | ```python 85 | import time 86 | from terminal_layout.extensions.progress import * 87 | 88 | l = Loading('loading', 20) 89 | l.start() 90 | l.set_progress(1) 91 | for i in range(10): 92 | if l.is_finished(): 93 | break 94 | time.sleep(0.3) 95 | l.add_progress(i) 96 | l.stop() 97 | ``` 98 | 99 | or use context manager 100 | ```python 101 | import time 102 | from terminal_layout.extensions.progress import * 103 | 104 | with Loading('loading', 20) as l: 105 | for i in range(10): 106 | if l.is_finished(): 107 | break 108 | time.sleep(0.3) 109 | l.add_progress(i) 110 | ``` 111 | 112 | The result will be a bar like the following: 113 | 114 | ``` 115 | loading ⣟ 70% 116 | ``` 117 | 118 | ## parameter 119 | There are several parameter you can set: 120 | 121 | ![](../../../pic/loading.jpg) 122 | 123 | | name | default | desc | 124 | |---------------|----------------------|-------------------------------------------------| 125 | | prefix | | prefix string | 126 | | max | | maximum value | 127 | | refresh\_time | 0\.2 | refresh time | 128 | | delimiter | `[" "," "]` | delimiter list | 129 | | infix | InfixChoices\.style7 | a list of string or InfixChoices class variable | 130 | | suffix\_style | SuffixStyle\.percent | SuffixStyle class variable | 131 | -------------------------------------------------------------------------------- /terminal_layout/extensions/progress/loading.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | from terminal_layout import * 5 | from terminal_layout.extensions.progress.progress import Progress, SuffixStyle 6 | from terminal_layout.helper.class_helper import instance_variables 7 | 8 | 9 | class InfixChoices: 10 | style1 = ['-', '\\', '|', '/'] 11 | style2 = ["◰", "◳", "◲", "◱"] 12 | style3 = ["◐", "◓", "◑", "◒"] 13 | style4 = [".", "o", "O", "°", "O", "o", "."] 14 | style5 = ['▁', '▂', '▃', '▅', '▆', '▇', ] 15 | style6 = ["ဝ", "၀"] 16 | style7 = ['⣾', '⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽'] 17 | style8 = ['◷', '◶', '◵', '◴'] 18 | style9 = ['( ◐ ω ◐ )', '( ◓ ω ◓ )', '( ◑ ω ◑ )', '( ◒ ω ◒ )', ] 19 | style10 = ["( ^ . ^ )", "( ^ o ^ )", "( ^ O ^ )", "( ^ o ^ )"] 20 | 21 | 22 | class Loading(Progress): 23 | ctl = None 24 | 25 | @instance_variables 26 | def __init__(self, prefix, max, refresh_time=0.2, delimiter=None, infix=None, 27 | suffix_style=SuffixStyle.percent): 28 | """ 29 | 30 | 31 | :param prefix: 32 | :type prefix: str 33 | :param max: 34 | :type max: int 35 | :param suffix_style: 36 | :type suffix_style: SuffixStyle 37 | """ 38 | if infix is None: 39 | self.infix = InfixChoices.style7 40 | if delimiter is None: 41 | self.delimiter = [' ', ' '] 42 | self.infix_index = 0 43 | self.current_progress = 0 44 | self.lock = threading.Lock() 45 | self.update_infix_thread = threading.Thread( 46 | target=self.update_infix, 47 | args=(self,) 48 | ) 49 | self.update_infix_thread.daemon = True 50 | self.stop_flag = False 51 | 52 | @staticmethod 53 | def update_infix(loading): 54 | def run(): 55 | progress_view = loading.ctl.find_view_by_id('progress') 56 | loading.infix_index += 1 57 | loading.infix_index %= len(loading.infix) 58 | progress_view.set_text(loading.infix[loading.infix_index]) 59 | 60 | while True: 61 | if loading.stop_flag: 62 | break 63 | loading.run_with_lock(run) 64 | time.sleep(loading.refresh_time) 65 | 66 | def run_with_lock(self, func, args=None): 67 | if args == None: 68 | args = [] 69 | self.lock.acquire() 70 | func(*args) 71 | self.lock.release() 72 | 73 | def add_progress(self, num): 74 | self.run_with_lock(super().add_progress, [num]) 75 | 76 | def set_progress(self, num): 77 | self.run_with_lock(super().set_progress, [num]) 78 | 79 | def update(self): 80 | 81 | self.ctl.find_view_by_id('suffix').set_text(self.get_suffix()) 82 | 83 | def start(self): 84 | prefix_width = len(self.prefix) 85 | if self.suffix_style == SuffixStyle.percent: 86 | suffix_width = 4 87 | else: 88 | suffix_width = len(str(self.max)) * 2 + 1 89 | 90 | self.ctl = LayoutCtl.quick(TableLayout, 91 | [[TextView('prefix', self.prefix, width=prefix_width), 92 | TextView('', self.delimiter[0]), 93 | TextView('progress', ''), 94 | TextView('', self.delimiter[1]), 95 | TextView('suffix', self.get_suffix(), width=suffix_width, gravity=Gravity.right)]]) 96 | 97 | self.update() 98 | self.stop_flag = False 99 | self.update_infix_thread.start() 100 | self.ctl.draw() 101 | 102 | def stop(self): 103 | self.stop_flag = True 104 | if self.ctl: 105 | self.ctl.stop() 106 | self.ctl = None 107 | 108 | def is_finished(self): 109 | return self.current_progress >= self.max 110 | 111 | def __enter__(self): 112 | self.start() 113 | return self 114 | 115 | def __exit__(self, exc_type, exc_val, exc_tb): 116 | self.stop() 117 | -------------------------------------------------------------------------------- /demo/demo_v2_1.py: -------------------------------------------------------------------------------- 1 | from terminal_layout import * 2 | from terminal_layout.extensions.input import * 3 | 4 | title_style = { 5 | 'width': 20, 6 | 'gravity': Gravity.center, 7 | 'fore': Fore.ex_dark_slate_gray_3, 8 | 'back': Back.ex_dark_gray 9 | } 10 | table_color = { 11 | # 'fore': Fore.ex_dark_slate_gray_3, 12 | 'back': Back.ex_deep_sky_blue_3a 13 | } 14 | ctl = LayoutCtl.quick(TableLayout, 15 | [ 16 | # row_0:title 17 | [TextView('step1', 'Step 1', **title_style), 18 | TextView('step2', 'Step 2', **title_style), 19 | TextView('step3', 'Step 3', **title_style) 20 | ], 21 | # row_1:tip 22 | [TextView('tip1', '(your name)', **title_style), 23 | TextView('tip2', '(your age)', **title_style), 24 | TextView('tip3', '(confirm)', **title_style) 25 | ], 26 | # row_2:line 27 | [ 28 | TextView('', '-' * 60) 29 | ], 30 | # row_3:input 31 | [TextView('', ' input: ', ), 32 | TextView('input', '', ) 33 | ], 34 | # row_4:empty 35 | [ 36 | TextView('', ' ', ), 37 | ], 38 | # row_5:table-name 39 | [TextView('', ' '), 40 | TextView('', 'name: ', width=7, gravity=Gravity.right, **table_color), 41 | TextView('name', '', width=10, gravity=Gravity.left, **table_color), 42 | ], 43 | # row_6:table-age 44 | [TextView('', ' '), 45 | TextView('', 'age: ', width=7, gravity=Gravity.right, **table_color), 46 | TextView('age', '', width=10, gravity=Gravity.left, **table_color), 47 | ], 48 | # row_7:line 49 | [ 50 | TextView('', '-' * 60) 51 | ], 52 | # row_8:help 53 | [ 54 | TextView('', '(press enter to confirm)') 55 | ], 56 | 57 | ]) 58 | 59 | 60 | def change_step(last_i, new_i): 61 | for i, back in [[last_i, title_style['back']], [new_i, Back.ex_slate_blue_3b], ]: 62 | v = ctl.find_view_by_id('step' + str(i)) 63 | if not v: 64 | continue 65 | v.set_back(back) 66 | v = ctl.find_view_by_id('tip' + str(i)) 67 | v.set_back(back) 68 | 69 | 70 | def clear_input(): 71 | v = ctl.find_view_by_id('input') 72 | v.set_text('') 73 | 74 | 75 | def get_user(): 76 | step = 1 77 | user = {} 78 | for i, key in enumerate(['name', 'age', ]): 79 | step += i 80 | clear_input() 81 | change_step(step - 1, step) 82 | ctl.re_draw() 83 | ok, v = InputEx(ctl).get_input('input') 84 | user[key] = v 85 | return user 86 | 87 | 88 | def update_table(show_table, user): 89 | for id in ['root_row_3', 'root_row_4']: 90 | v = ctl.find_view_by_id(id) 91 | v.set_visibility(Visibility.gone if show_table else Visibility.visible) 92 | for id in ['root_row_5', 'root_row_6']: 93 | v = ctl.find_view_by_id(id) 94 | v.set_visibility(Visibility.visible if show_table else Visibility.gone) 95 | if id == 'root_row_5': 96 | v = ctl.find_view_by_id('name') 97 | v.set_text(user.get('name', '')) 98 | if id == 'root_row_6': 99 | v = ctl.find_view_by_id('age') 100 | v.set_text(user.get('age', '')) 101 | ctl.re_draw() 102 | 103 | 104 | ctl.draw(auto_re_draw=False) 105 | update_table(False, {}) 106 | 107 | user = get_user() 108 | change_step(2, 3) 109 | update_table(True, user) 110 | 111 | kl = KeyListener() 112 | 113 | 114 | @kl.bind_key(Key.ENTER) 115 | def _(kl, e): 116 | print('\n', Fore.blue, '- Hello,', user['name'], Fore.reset, '\n') 117 | kl.stop() 118 | 119 | 120 | kl.listen() 121 | -------------------------------------------------------------------------------- /terminal_layout/extensions/progress/progress.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from terminal_layout import * 4 | from terminal_layout.helper.class_helper import instance_variables 5 | 6 | 7 | class SuffixStyle: 8 | fraction = '{fraction}' # "1/10" 9 | percent = '{percent}' # "10%" 10 | none = '' 11 | 12 | 13 | class ProgressWidth: 14 | fill = 'fill' 15 | half = 'half' # half of terminal width 16 | 17 | 18 | class Progress(object): 19 | ctl = None 20 | 21 | @instance_variables 22 | def __init__(self, prefix, max, reached=None, unreached='', delimiter=None, suffix_style=SuffixStyle.percent, 23 | width=ProgressWidth.half): 24 | """ 25 | 26 | :param prefix: 27 | :type prefix: str 28 | :param max: 29 | :type max: int 30 | :param reached: 31 | :type reached: str 32 | :param unreached: 33 | :type unreached: str 34 | :param suffix_style: 35 | :type suffix_style: SuffixStyle 36 | :param width: ProgressWidth or width num. width=len(prefix)+len(progress)+len(delimiter)+len(suffix) 37 | """ 38 | if delimiter is None: 39 | self.delimiter = [' |', '| '] 40 | assert self.max > 0 41 | self.current_progress = 0 42 | if self.reached is None: 43 | if sys.platform in ('win32', 'cygwin'): 44 | self.reached = '=' 45 | else: 46 | self.reached = '█' 47 | 48 | def get_suffix(self): 49 | fraction = '%d/%d' % (self.current_progress, self.max) 50 | percent = '{:.0%}'.format(float(self.current_progress) / self.max) 51 | 52 | return self.suffix_style.format(fraction=fraction, percent=percent) 53 | 54 | def set_progress(self, num): 55 | if num > self.max: 56 | num = self.max 57 | elif num < 0: 58 | num = 0 59 | self.current_progress = num 60 | self.update() 61 | 62 | def add_progress(self, num): 63 | if num < 0: 64 | num = 0 65 | self.current_progress += num 66 | if self.current_progress > self.max: 67 | self.current_progress = self.max 68 | self.update() 69 | 70 | def update(self): 71 | progress_view = self.ctl.find_view_by_id('progress') 72 | w = progress_view.get_real_width() 73 | reached_num = int(float(self.current_progress) / self.max * w) 74 | s = self.reached * reached_num + self.unreached * (w - reached_num) 75 | progress_view.set_text(s) 76 | 77 | self.ctl.find_view_by_id('suffix').set_text(self.get_suffix()) 78 | 79 | def start(self): 80 | prefix_width = len(self.prefix) 81 | delimiter_width = len(self.delimiter[0]) + len(self.delimiter[1]) 82 | if self.suffix_style == SuffixStyle.percent: 83 | suffix_width = 4 84 | else: 85 | suffix_width = len(str(self.max)) * 2 + 1 86 | 87 | self.ctl = LayoutCtl.quick(TableLayout, 88 | [[TextView('prefix', self.prefix, width=prefix_width), 89 | TextView('', self.delimiter[0]), 90 | TextView('progress', ''), 91 | TextView('', self.delimiter[1]), 92 | TextView('suffix', self.get_suffix(), width=suffix_width, gravity=Gravity.right)]]) 93 | 94 | if self.width == ProgressWidth.fill: 95 | self.ctl.find_view_by_id('progress').set_weight(1) 96 | else: 97 | terminal_width, _ = self.ctl.get_terminal_size() 98 | if self.width == ProgressWidth.half: 99 | progress_width = int((terminal_width - prefix_width - suffix_width - delimiter_width) / 2) 100 | else: 101 | progress_width = self.width - prefix_width - suffix_width - delimiter_width 102 | self.ctl.find_view_by_id('progress').set_width(progress_width) 103 | self.ctl.update_width() 104 | self.update() 105 | self.ctl.draw() 106 | 107 | def stop(self): 108 | if self.ctl: 109 | self.ctl.stop() 110 | self.ctl = None 111 | 112 | def is_finished(self): 113 | return self.current_progress >= self.max 114 | 115 | def __enter__(self): 116 | self.start() 117 | return self 118 | 119 | def __exit__(self, exc_type, exc_val, exc_tb): 120 | self.stop() 121 | -------------------------------------------------------------------------------- /terminal_layout/extensions/scroll/README.zh.md: -------------------------------------------------------------------------------- 1 | # scroll 2 | 3 | 让 `TableLayout` 支持滚动。 4 | 5 | 注意:**必须配合`TableLayout`和`TableRow`使用!!** 6 | 7 | ![scroll.gif](../../../pic/scroll.gif) 8 | 9 | ## usage 10 | 11 | ```python 12 | from terminal_layout import * 13 | from terminal_layout.extensions.scroll import * 14 | 15 | rows = [[TextView(str(i), str(i))] for i in range(50)] 16 | 17 | ctl = LayoutCtl.quick(TableLayout, rows) 18 | # ctl.enable_debug(height=13) 19 | scroll = Scroll(ctl, stop_key='q', loop=True, more=True, scroll_box_start=3) 20 | scroll.scroll() 21 | 22 | ``` 23 | 24 | There are several parameter you can set: 25 | 26 | | **name** | **default** | **desc** | 27 | |----------------------|-------------|------------------------------------------------------------------| 28 | | ctl | | LayoutCtl | 29 | | stop_key | Key.ESC | 停止滚动的key | 30 | | up_key | Key.UP | | 31 | | down_key | Key.DOWN | | 32 | | scroll_box_start | 0 | 从哪行开始可以滚动。若第一行要显示标题,可设置scroll_box_start=1 (详细说明见最后的cal_scroll部分) | 33 | | default_scroll_start | 0 | 初始化时, scroll_box中哪行显示在第一的位置 | 34 | | loop | False | | 35 | | btm_text | '' | 底部的文本,为空则不显示 | 36 | | more | False | 类似于man的效果。为Ture会自动添加 btm_text | 37 | | callback | None | 滚动后的回调 | 38 | | re_draw_after_scroll | True | 滚动后是否执行重绘。为false时你需要自己调用re_draw | 39 | | re_draw_after_stop | False | 停止滚动后是否重绘 | 40 | 41 | 42 | > default_scroll_start 说明 43 | > 若terminal高度为4, table有6行(即高度为6),default_scroll_start=6。 44 | > 绘制时,显示在terminal顶部的是row_2。 45 | > ``` 46 | > row_0 47 | > row_1 48 | > |------------| 49 | > | row_2 | 50 | > | row_3 | <=== terminal, h=4 51 | > | row_4 | 52 | > | row_5 | 53 | > |------------| 54 | > ``` 55 | 56 | 57 | ## 修改滚动事件行为 58 | 59 | 有多种方法可以修改滚动后的行为 60 | 61 | ### 1. 通过callback 62 | 63 | ```python 64 | from terminal_layout.extensions.scroll import * 65 | 66 | ctl = ... 67 | scroll = ... 68 | 69 | 70 | def my_callback(event): 71 | if event == ScrollEvent.up: 72 | ... 73 | # or ctl.re_draw() 74 | scroll.draw() 75 | # ... 76 | 77 | ``` 78 | 79 | `up`, `down` 事件默认会在调用callback之前进行`re_draw`。如果你callback中对view进行了修改,则需要调用函数重绘。 80 | 81 | 如果你callback中改变了table的行数,你应该使用 `scroll.draw()` 这个函数会重新计算scroll位置 82 | 83 | ### 2. 重写滚动事件 84 | 85 | 如果你想在计算scroll位置之前进行一些操作(比如:禁止向上循环,但允许向下循环), 86 | 就需要重写滚动事件。 87 | 88 | 有多种方法可以重写滚动事件 89 | 90 | * 通过 `stop_func`, `up_func`, `down_func` 91 | 92 | ```python 93 | from terminal_layout.extensions.scroll import * 94 | 95 | ctl = LayoutCtl(...) 96 | scroll = Scroll(ctl, loop=True) 97 | 98 | def up(kl, event): 99 | if scroll.current_scroll_start - 1 < scroll.scroll_box_start: 100 | return 101 | scroll.up() 102 | ctl.re_draw() 103 | 104 | scroll.scroll(up_func=up) 105 | ``` 106 | 107 | * 添加`key_listener`事件 108 | 109 | ```python 110 | from terminal_layout.extensions.scroll import * 111 | from terminal_layout.readkey import Key 112 | 113 | ctl = LayoutCtl(...) 114 | scroll = Scroll(ctl, up_key=None, loop=True) 115 | 116 | key_listener = scroll.init_kl() 117 | 118 | @key_listener.bind_key(Key.UP) 119 | def up(kl, event): 120 | if scroll.current_scroll_start - 1 < scroll.scroll_box_start: 121 | return 122 | scroll.up() 123 | ctl.re_draw() 124 | 125 | scroll.draw() 126 | key_listener.listen() 127 | ``` 128 | 或者不通过`init_kl`自己初始化`KeyListener` 129 | 130 | ```python 131 | from terminal_layout.extensions.scroll import * 132 | from terminal_layout.readkey import Key, KeyListener 133 | 134 | ctl = LayoutCtl(...) 135 | scroll = Scroll(ctl, loop=True) 136 | 137 | key_listener = KeyListener() 138 | 139 | @key_listener.bind_key(Key.UP) 140 | def up(kl, event): 141 | ... 142 | 143 | key_listener.bind_key(Key.DOWN, down_func , decorator=False) 144 | key_listener.bind_key('q', stop_func , decorator=False) 145 | 146 | scroll.draw() 147 | key_listener.listen() 148 | 149 | ``` 150 | 151 | ## cal_scroll 返回值说明 152 | 153 | * `scroll_box_start` : 可滚动区域开始位置。一般用于要一直显示标题的情况 154 | * `scroll_box_end` : 可滚动区域结束位置 155 | * `scroll_start` : 滚动区域实际显示的开始位置 156 | * `scroll_end` : 滚动区域实际显示的结束位置 157 | 158 | ![cal_scroll.png](../../../pic/cal_scroll.png) 159 | -------------------------------------------------------------------------------- /demo/record_demo.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from terminal_layout import * 4 | 5 | ctl = LayoutCtl.quick(TableLayout, 6 | [ 7 | [TextView('title', '** Terminal Layout **', fore=Fore.black, width=Width.fill, 8 | gravity=Gravity.center, style=Style.ex_bold, )], 9 | 10 | [TextView('', 'What can terminal_layout do for you?')], 11 | [TextView('', '')], 12 | # 1. set fore back style in terminal 13 | [TextView('s1-1', ''), 14 | TextView('s1-2', '', fore=Fore.ex_dark_slate_gray_3), 15 | TextView('s1-3', '', back=Back.ex_dark_slate_gray_3), 16 | TextView('', ' '), 17 | TextView('s1-4', '', style=Style.ex_underlined), 18 | TextView('', ' '), 19 | TextView('s1-5', ''), 20 | ], 21 | # 2. print string char one by one 22 | [TextView('s2', '')], 23 | # 3. listen keyboard events 24 | [TextView('s3', '')], 25 | # 4. draw a table 26 | [TextView('s4', '')], 27 | # 4-1. add row 28 | [TextView('s4-1', '')], 29 | # 4-2. change text 30 | [TextView('s4-2', '')], 31 | # 4-3. set gravity and more 32 | [TextView('s4-3', '')], 33 | [TextView('', '')], 34 | # s3 table 35 | [TextView('s4-table', 'Table', width=20, gravity=Gravity.center, back=Back.ex_slate_blue_3b, 36 | visibility=Visibility.invisible)], 37 | [TextView('s4-table-r1-1', 'No1', width=10, back=Back.ex_sky_blue_2, 38 | visibility=Visibility.invisible), 39 | TextView('s4-table-r1-2', 'Bob', width=10, back=Back.ex_sky_blue_2, 40 | visibility=Visibility.invisible)], 41 | [TextView('s4-table-r2-1', 'No2', width=10, back=Back.ex_sky_blue_2, 42 | visibility=Visibility.invisible), 43 | TextView('s4-table-r2-2', 'Tom', width=10, back=Back.ex_sky_blue_2, 44 | visibility=Visibility.invisible)], 45 | 46 | ] 47 | 48 | ) 49 | 50 | 51 | def s1(): 52 | """ 53 | 1. set fore back style in terminal 54 | :return: 55 | :rtype: 56 | """ 57 | for i, s in enumerate(["1. Set", " Fore ", " Back ", "Style", " in terminal"]): 58 | ctl.find_view_by_id('s1-%d' % (i + 1)).delay_set_text(s, delay=0.1) 59 | 60 | 61 | def s2(): 62 | """ 63 | 2. print string char one by one 64 | :return: 65 | :rtype: 66 | """ 67 | ctl.find_view_by_id('s2').delay_set_text('2. Print string char one by one', delay=0.1) 68 | 69 | 70 | def s3(): 71 | """ 72 | 3. listen keyboard events 73 | :return: 74 | :rtype: 75 | """ 76 | ctl.find_view_by_id('s3').delay_set_text('3. Listen keyboard events', delay=0.1) 77 | 78 | 79 | def s4(): 80 | """ 81 | 4. draw a table 82 | :return: 83 | :rtype: 84 | """ 85 | 86 | ctl.find_view_by_id('s4').delay_set_text('4. Draw a table', delay=0.1) 87 | 88 | table_ids = ['s4-table', 's4-table-r1-1', 's4-table-r1-2', 's4-table-r2-1', 's4-table-r2-2'] 89 | for v_id in table_ids: 90 | ctl.find_view_by_id(v_id).set_visibility(Visibility.visible) 91 | time.sleep(0.2) 92 | # 4-1. add row 93 | ctl.find_view_by_id('s4-1').delay_set_text(' 4-1. add row', delay=0.1) 94 | ctl.get_layout().add_view( 95 | TableRow.quick_init('', [TextView('s4-table-r4-1', 'No3', width=10, back=Back.ex_sky_blue_2, ), 96 | TextView('s4-table-r4-2', '小王', width=10, back=Back.ex_sky_blue_2, )])) 97 | 98 | time.sleep(0.2) 99 | # 4-2. change text 100 | ctl.find_view_by_id('s4-2').delay_set_text(' 4-2. change text', delay=0.1) 101 | ctl.find_view_by_id('s4-table-r4-2').set_text('Wong') 102 | ctl.find_view_by_id('s4-table-r4-2').set_fore(Fore.black) 103 | 104 | time.sleep(0.2) 105 | 106 | # 4-3. set gravity and more 107 | ctl.find_view_by_id('s4-3').delay_set_text(' 4-3. set gravity and more', delay=0.1) 108 | 109 | for v_id in ['s4-table-r1-1', 's4-table-r1-2', 's4-table-r2-1', 's4-table-r2-2', 's4-table-r4-1', 's4-table-r4-2']: 110 | ctl.find_view_by_id(v_id).set_gravity(Gravity.center) 111 | 112 | 113 | print('') 114 | ctl.find_view_by_id('root_row_0').set_width(40) 115 | ctl.draw() 116 | 117 | time.sleep(0.5) 118 | s1() 119 | time.sleep(0.2) 120 | s2() 121 | time.sleep(0.2) 122 | s3() 123 | time.sleep(0.2) 124 | s4() 125 | 126 | ctl.stop() 127 | print('') 128 | -------------------------------------------------------------------------------- /README.ZH.md: -------------------------------------------------------------------------------- 1 | # terminal_layout 2 | 3 | The project help you to quickly build layouts in terminal 4 | (这个一个命令行ui布局工具) 5 | 6 | ![demo_v2_1.gif](pic/demo_v2_1.gif) 7 | 8 | demo.gif 9 | 10 | ---------------- 11 | 12 | **基于terminal_layout的扩展** 13 | 14 | * [progress](terminal_layout/extensions/progress/README.md) 15 | 16 | ![progress.gif](pic/progress.gif) 17 | 18 | * [choice](terminal_layout/extensions/choice/README.md) 19 | 20 | ![choice.gif](pic/choice.gif) 21 | 22 | 23 | ------------------- 24 | 25 | ** video demo ** 26 | 27 | 28 | asciicast 29 | 30 | 31 | # link 32 | 33 | * [Github](https://github.com/gojuukaze/terminal_layout) 34 | * [文档](https://doc.ikaze.cn/terminal_layout/) 35 | * [https://asciinema.org/a/226120](https://asciinema.org/a/226120) 36 | 37 | # 安装 38 | ```bash 39 | pip install terminal-layout 40 | ``` 41 | 42 | # 依赖 43 | * Python 2.7, 3.5+ (maybe 3.4) 44 | * Linux, OS X, and Windows systems. 45 | 46 | # Usage 47 | 48 | * easy demo: 49 | 50 | ```python 51 | import time 52 | from terminal_layout import * 53 | 54 | ctl = LayoutCtl.quick(TableLayout, 55 | # table id: root 56 | [ 57 | [TextView('t1', 'Hello World!', width=Width.fill, back=Back.blue)], # <- row id: root_row_00, 58 | [TextView('t2', '', fore=Fore.magenta)], # <- row id: root_row_1, 59 | ], 60 | ) 61 | 62 | # or layout=ctl.get_layout() 63 | layout = ctl.find_view_by_id('root') 64 | layout.set_width(20) 65 | 66 | # default: auto_re_draw=True 67 | ctl.draw() 68 | 69 | # 如果使用delay_set_text(), 必须把auto_re_draw设为True,否则你需要自己在线程中执行re_draw() 70 | ctl.find_view_by_id('t2').delay_set_text('你好,世界!', delay=0.2) 71 | 72 | time.sleep(0.5) 73 | row3 = TableRow.quick_init('', [TextView('t3', 'こんにちは、世界!')]) 74 | layout.add_view(row3) 75 | 76 | # 如果执行draw()时auto_re_draw=True,你必须执行stop() 77 | ctl.stop() 78 | 79 | ``` 80 | ![](pic/hello.png) 81 | 82 | * 禁用 auto_re_draw 83 | 84 | ```python 85 | import time 86 | from terminal_layout import * 87 | 88 | ctl = LayoutCtl.quick(TableLayout, 89 | [ 90 | [TextView('t1', 'Hello World!', width=Width.fill, back=Back.blue)], 91 | [TextView('t2', '', fore=Fore.magenta)], 92 | ], 93 | ) 94 | 95 | 96 | layout = ctl.find_view_by_id('root') 97 | layout.set_width(20) 98 | 99 | ctl.draw(auto_re_draw=False) 100 | 101 | ctl.find_view_by_id('t2').set_text('你好,世界!') 102 | ctl.re_draw() 103 | 104 | time.sleep(0.5) 105 | row3 = TableRow.quick_init('', [TextView('t3', 'こんにちは、世界!')]) 106 | layout.add_view(row3) 107 | ctl.re_draw() 108 | 109 | # 不需执行stop() 110 | # ctl.stop() 111 | ``` 112 | 113 | * use python2 unicode 114 | 115 | ```python 116 | # -*- coding: utf-8 -*- 117 | from terminal_layout import * 118 | import sys 119 | reload(sys) 120 | sys.setdefaultencoding('utf-8') 121 | 122 | ctl = LayoutCtl.quick(TableLayout, 123 | [ 124 | [TextView('', u'中文,你好', back=Back.cyan, width=Width.wrap)], 125 | [TextView('', u'中文,你好', back=Back.cyan, width=6)], 126 | [TextView('', u'日本語,こんにちは', back=Back.cyan, width=Width.wrap)], 127 | ] 128 | 129 | ) 130 | 131 | ctl.draw() 132 | 133 | ``` 134 | 135 | ![](pic/py2.png) 136 | 137 | 138 | ## View的属性 139 | 140 | * fore & back 141 | 142 | ```python 143 | TextView('','fore',fore=Fore.red) 144 | TextView('','back',back=Back.red) 145 | ``` 146 | 147 | 148 | 149 | * style 150 | 151 | ```python 152 | TextView('','style',style=Style.dim) 153 | ``` 154 | 155 | 156 | 157 | * width 158 | 159 | ```python 160 | TextView('','width',width=10) 161 | ``` 162 | 163 | 164 | 165 | * weight 166 | 167 | ```python 168 | TextView('','weight',weight=1) 169 | ``` 170 | 171 | 172 | 173 | * gravity 174 | 175 | ```python 176 | TextView('','gravity',gravity=Gravity.left) 177 | ``` 178 | 179 | 180 | 181 | * visibility 182 | 183 | ```python 184 | TextView('','',visibility=Visibility.visible) 185 | ``` 186 | 187 | 188 | 189 | 190 | * ex_style (not support windows) 191 | 192 | 193 | ```python 194 | TextView('','ex_style',style=Style.ex_blink) 195 | ``` 196 | 197 | 198 | 199 | * ex_fore & ex_back (not support windows) 200 | 201 | 202 | ```python 203 | TextView('','ex_fore',fore=Fore.ex_red_1) 204 | TextView('','ex_back',back=Back.ex_red_1) 205 | 206 | ``` 207 | 208 | 209 | 210 | # LICENSE 211 | 212 | [GPLv3](https://github.com/gojuukaze/terminal_layout/blob/master/LICENSE) 213 | 214 | # Thanks 215 | 216 | * [colorama](https://github.com/tartley/colorama) : Simple cross-platform colored terminal text in Python 217 | * [colored](https://gitlab.com/dslackw/colored) : Very simple Python library for color and formatting in terminal 218 | -------------------------------------------------------------------------------- /terminal_layout/extensions/choice/choice.py: -------------------------------------------------------------------------------- 1 | from terminal_layout import * 2 | from terminal_layout.helper.class_helper import instance_variables 3 | from terminal_layout.extensions.scroll import * 4 | 5 | 6 | class StringStyle(object): 7 | 8 | def __init__(self, fore=None, back=None, style=None): 9 | self.fore = fore or '' 10 | self.back = back or '' 11 | self.style = style or '' 12 | 13 | def to_dict(self): 14 | return { 15 | 'fore': self.fore, 16 | 'back': self.back, 17 | 'style': self.style 18 | } 19 | 20 | 21 | class Choice(object): 22 | @instance_variables 23 | def __init__(self, title, choices, icon='> ', icon_style=StringStyle(fore=Fore.green), choices_style=StringStyle(), 24 | selected_style=StringStyle(), loop=True, default_index=0, stop_key=None): 25 | self.current = self.get_default_index(default_index) 26 | self.result = None 27 | if not stop_key: 28 | self.stop_key = ['q'] 29 | 30 | def get_default_index(self, default_index): 31 | if default_index < 0 or default_index > len(self.choices): 32 | return 0 33 | return default_index 34 | 35 | h_cache = None 36 | 37 | def hidden_choices(self): 38 | """ 39 | 适配高度,当高度不够时隐藏部分choices 40 | """ 41 | if self.h_cache is None: 42 | _, self.h_cache = self.ctl.get_terminal_size() 43 | # 最后一行要留着显示光标,不能输出因此-1 44 | # title不能隐藏,因此再-1 45 | self.h_cache -= 2 46 | 47 | if self.h_cache >= len(self.choices) + 1: 48 | return 49 | 50 | # 优先显示current下面部分,还有剩余高度时从current上面补 51 | show_start = self.current 52 | show_end = len(self.choices) 53 | if show_end - show_start > self.h_cache: 54 | show_end = show_start + self.h_cache 55 | left = self.h_cache - show_end + show_start 56 | if left > 0: 57 | show_start -= left 58 | row_id_p = 'root_row_' 59 | for i in range(len(self.choices)): 60 | if i >= show_start and i < show_end: 61 | self.ctl.find_view_by_id( 62 | row_id_p + str(i + 1)).set_visibility(Visibility.visible) 63 | else: 64 | self.ctl.find_view_by_id( 65 | row_id_p + str(i + 1)).set_visibility(Visibility.gone) 66 | 67 | def get_choice(self): 68 | views = [[TextView('', self.title)]] 69 | for i, c in enumerate(self.choices): 70 | views.append( 71 | [TextView('icon%d' % i, self.icon, visibility=Visibility.invisible, **self.icon_style.to_dict()), 72 | TextView('value%d' % i, c, **self.choices_style.to_dict())]) 73 | views[self.current + 1][0].visibility = Visibility.visible 74 | views[self.current + 1][1] = TextView('value%d' % self.current, self.choices[self.current], 75 | **self.selected_style.to_dict()) 76 | self.ctl = LayoutCtl.quick(TableLayout, views) 77 | # self.hidden_choices() 78 | # self.ctl.draw(auto_re_draw=False) 79 | 80 | self.scroll = Scroll(self.ctl, scroll_box_start=1, default_scroll_start=self.default_index, 81 | up_key=None, down_key=None, stop_key=None, btm_text='') 82 | 83 | kl = self.scroll.init_kl() 84 | self.scroll.draw() 85 | kl.bind_key(Key.UP, Key.DOWN, self.change_current, decorator=False) 86 | kl.bind_key(Key.ENTER, self.select, decorator=False) 87 | 88 | kl.listen(self.stop_key) 89 | return self.result 90 | 91 | def update_style(self, view, style): 92 | view.set_back(style.back) 93 | view.set_style(style.style) 94 | view.set_fore(style.fore) 95 | 96 | def change_current(self, kl, event): 97 | temp = self.current 98 | loop_tigger = False 99 | if event.key == Key.UP: 100 | temp -= 1 101 | elif event.key == Key.DOWN: 102 | temp += 1 103 | if temp < 0: 104 | if self.loop: 105 | loop_tigger = True 106 | temp = len(self.choices) - 1 107 | else: 108 | temp = 0 109 | if temp >= len(self.choices): 110 | if self.loop: 111 | loop_tigger = True 112 | temp = 0 113 | else: 114 | temp = len(self.choices) - 1 115 | self.ctl.find_view_by_id( 116 | 'icon%d' % self.current).set_visibility(Visibility.invisible) 117 | self.update_style(self.ctl.find_view_by_id('value%d' % 118 | self.current), self.choices_style) 119 | 120 | self.current = temp 121 | self.ctl.find_view_by_id( 122 | 'icon%d' % self.current).set_visibility(Visibility.visible) 123 | self.update_style(self.ctl.find_view_by_id('value%d' % 124 | self.current), self.selected_style) 125 | # self.hidden_choices() 126 | self.scroll.loop = loop_tigger 127 | if event.key == Key.UP: 128 | self.scroll.up() 129 | else: 130 | self.scroll.down() 131 | self.ctl.re_draw() 132 | 133 | def select(self, kl, event): 134 | self.result = (self.current, self.choices[self.current]) 135 | self.scroll.stop(kl) 136 | -------------------------------------------------------------------------------- /terminal_layout/types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import copy 3 | 4 | char_to_length = { 5 | '\t': 4, 6 | } 7 | char_to_str = { 8 | '\t': ' ' * 4, 9 | '\n': ' ', 10 | } 11 | 12 | 13 | def add_char_length_map(d): 14 | """ 15 | ex: 16 | add_char_length_map({'a':1}) 17 | :param d: 18 | :type d: dict 19 | """ 20 | char_to_length.update(d) 21 | 22 | 23 | class Char(object): 24 | length = None 25 | 26 | def __init__(self, c): 27 | if len(c) != 1: 28 | raise TypeError( 29 | 'expected a character, but string of length %d found' % len(c)) 30 | self.c = c 31 | 32 | def __len__(self): 33 | if self.length is not None: 34 | return self.length 35 | 36 | temp = char_to_length.get(self.c, None) 37 | if temp: 38 | self.length = temp 39 | return self.length 40 | 41 | if ord(self.c) < 11904: 42 | self.length = 1 43 | else: 44 | self.length = 2 45 | return self.length 46 | 47 | def __str__(self): 48 | return char_to_str.get(self.c, self.c) 49 | 50 | 51 | class String(object): 52 | """ 53 | 54 | ex: 55 | 56 | >>> s=String('a啊啊') 57 | >>> len(s) 58 | 5 59 | >>> s[:2] 60 | 'a' 61 | >>> s[:3] 62 | 'a啊' 63 | 64 | """ 65 | length = None 66 | char_list = None 67 | 68 | def __init__(self, text): 69 | """ 70 | 71 | :param text: 72 | :type text:str 73 | """ 74 | self.char_list = [] # type: list[Char] 75 | self.origin_text = text 76 | for c in text: 77 | self.char_list.append(Char(c)) 78 | 79 | def __len__(self): 80 | if self.length is not None: 81 | return self.length 82 | 83 | t = 0 84 | for c in self.char_list: 85 | t += len(c) 86 | self.length = t 87 | 88 | return self.length 89 | 90 | def __str__(self): 91 | return ''.join(str(c) for c in self.char_list) 92 | 93 | def __add__(self, other): 94 | new_str = copy.deepcopy(self) 95 | new_str.length = None 96 | if isinstance(other, str): 97 | for c in other: 98 | new_str.char_list.append(Char(c)) 99 | new_str.origin_text += other 100 | if isinstance(other, String): 101 | new_str.char_list += other.char_list 102 | new_str.origin_text += other.origin_text 103 | 104 | if isinstance(other, Char): 105 | new_str.char_list.append(other) 106 | new_str.origin_text += str(other) 107 | return new_str 108 | 109 | def __radd__(self, other): 110 | new_str = copy.deepcopy(self) 111 | new_str.length = None 112 | if isinstance(other, str): 113 | for c in other: 114 | new_str.char_list.insert(0, Char(c)) 115 | new_str.origin_text = other + new_str.origin_text 116 | 117 | if isinstance(other, Char): 118 | new_str.char_list.insert(0, other) 119 | new_str.origin_text = str(other) + new_str.origin_text 120 | 121 | return new_str 122 | 123 | def __getitem__(self, item): 124 | """ 125 | 只支持 s[:i] , s[-i:] (i>0) 两种形式 126 | 127 | >>> s=String('a啊啊') 128 | >>> s[:2] 129 | 'a' 130 | >>> s[:3] 131 | 'a啊' 132 | >>> s[-1:] 133 | '' 134 | >>> s[-2:] 135 | '啊' 136 | 137 | :rtype: str 138 | """ 139 | start = item.start or 0 140 | if start > 0: 141 | raise TypeError('Slice start must be less than or equal to 0') 142 | stop = item.stop or 0 143 | if stop is None or stop < 0: 144 | raise TypeError('Slice start must be greater than or equal to 0') 145 | 146 | if start<0: 147 | cl=reversed(self.char_list) 148 | length=-start 149 | else: 150 | cl=self.char_list 151 | length=stop 152 | s=[] 153 | for c in cl: 154 | tmp = len(c) 155 | length-=tmp 156 | if length>=0: 157 | s.append(str(c)) 158 | else: 159 | break 160 | 161 | return ''.join(s if stop>0 else reversed(s)) 162 | 163 | def insert_into_char_list(self, i, s): 164 | 165 | if isinstance(s, str): 166 | s = String(s).char_list 167 | if isinstance(s, Char): 168 | s = [s] 169 | if isinstance(s, String): 170 | s = s.char_list 171 | self.char_list = self.char_list[:i] + s + self.char_list[i:] 172 | self.origin_text = str(self) 173 | self.length = None 174 | 175 | def get_char_list_item(self, start=None, stop=None): 176 | """ 177 | >>> s=String('123') 178 | >>> s.get_char_list_item() # s.char_list[:] 179 | ['1','2','3'] 180 | >>> s.get_char_list_item(start=1) # s.char_list[1:] 181 | ['2','3'] 182 | 183 | """ 184 | return self.char_list[start:stop] 185 | 186 | def pop(self, i=-1): 187 | self.char_list.pop(i) 188 | self.origin_text = str(self) 189 | self.length = None 190 | 191 | def char_list_slice(self, start=None, stop=None): 192 | self.char_list = self.get_char_list_item(start, stop) 193 | self.origin_text = str(self) 194 | self.length = None 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terminal_layout 2 | 3 | The project help you to quickly build layouts in terminal 4 | (这个一个命令行ui布局工具) 5 | 6 | ![demo_v2_1.gif](pic/demo_v2_1.gif) 7 | 8 | demo.gif 9 | 10 | ---------------- 11 | 12 | **Some extensions base on terminal_layout** 13 | 14 | * [progress](terminal_layout/extensions/progress/README.md) 15 | 16 | ![progress.gif](pic/progress.gif) 17 | 18 | * [choice](terminal_layout/extensions/choice/README.md) 19 | 20 | ![choice.gif](pic/choice.gif) 21 | 22 | ------------------- 23 | 24 | ** video demo ** 25 | 26 | 27 | asciicast 28 | 29 | 30 | 31 | # link 32 | 33 | * [Github](https://github.com/gojuukaze/terminal_layout) 34 | * [中文README](README.ZH.md) 35 | * [Documentation 中文文档](https://doc.ikaze.cn/terminal_layout/) 36 | 37 | 38 | # install 39 | ```bash 40 | pip install terminal-layout 41 | ``` 42 | 43 | # Dependencies 44 | * Python 3.5+ (maybe 3.4) 45 | * Linux, OS X, and Windows systems. 46 | 47 | ## Python Support 48 | 49 | | Python | terminal_layout | 50 | | ------ | --------------- | 51 | | 2.7 | 2.1.x | 52 | | 3.5+ | 3.x | 53 | 54 | # Usage 55 | 56 | * easy demo: 57 | 58 | ```python 59 | import time 60 | from terminal_layout import * 61 | 62 | ctl = LayoutCtl.quick(TableLayout, 63 | # table id: root 64 | [ 65 | [TextView('t1', 'Hello World!', width=Width.fill, back=Back.blue)], # <- row id: root_row_0, 66 | [TextView('t2', '', fore=Fore.magenta)], # <- row id: root_row_1, 67 | ], 68 | ) 69 | 70 | # or layout=ctl.get_layout() 71 | layout = ctl.find_view_by_id('root') 72 | layout.set_width(20) 73 | 74 | # default: auto_re_draw=True 75 | ctl.draw() 76 | 77 | # call delay_set_text() must be set auto_re_draw=True, 78 | # otherwise you must start a thread to call re_draw() by yourself 79 | ctl.find_view_by_id('t2').delay_set_text('你好,世界!', delay=0.2) 80 | 81 | time.sleep(0.5) 82 | row3 = TableRow.quick_init('', [TextView('t3', 'こんにちは、世界!')]) 83 | layout.add_view(row3) 84 | 85 | # If you call draw() with auto_re_draw=True, you must stop() 86 | ctl.stop() 87 | 88 | ``` 89 | ![](pic/hello.png) 90 | 91 | * disable auto_re_draw 92 | 93 | ```python 94 | import time 95 | from terminal_layout import * 96 | 97 | ctl = LayoutCtl.quick(TableLayout, 98 | # table id: root 99 | [ 100 | [TextView('t1', 'Hello World!', width=Width.fill, back=Back.blue)], # <- row id: root_row_1, 101 | [TextView('t2', '', fore=Fore.magenta)], # <- row id: root_row_2, 102 | ], 103 | ) 104 | 105 | 106 | layout = ctl.find_view_by_id('root') 107 | layout.set_width(20) 108 | 109 | ctl.draw(auto_re_draw=False) 110 | 111 | ctl.find_view_by_id('t2').set_text('你好,世界!') 112 | ctl.re_draw() 113 | 114 | time.sleep(0.5) 115 | row3 = TableRow.quick_init('', [TextView('t3', 'こんにちは、世界!')]) 116 | layout.add_view(row3) 117 | ctl.re_draw() 118 | 119 | # don't need call stop() 120 | # ctl.stop() 121 | ``` 122 | 123 | * use python2 unicode 124 | 125 | ```python 126 | # -*- coding: utf-8 -*- 127 | from terminal_layout import * 128 | import sys 129 | reload(sys) 130 | sys.setdefaultencoding('utf-8') 131 | 132 | ctl = LayoutCtl.quick(TableLayout, 133 | [ 134 | [TextView('', u'中文,你好', back=Back.cyan, width=Width.wrap)], 135 | [TextView('', u'中文,你好', back=Back.cyan, width=6)], 136 | [TextView('', u'日本語,こんにちは', back=Back.cyan, width=Width.wrap)], 137 | ] 138 | 139 | ) 140 | 141 | ctl.draw() 142 | 143 | ``` 144 | 145 | ![](pic/py2.png) 146 | 147 | 148 | ## Properties 149 | 150 | * fore & back 151 | 152 | ```python 153 | TextView('','fore',fore=Fore.red) 154 | TextView('','back',back=Back.red) 155 | ``` 156 | 157 | 158 | 159 | * style 160 | 161 | ```python 162 | TextView('','style',style=Style.dim) 163 | ``` 164 | 165 | 166 | 167 | * width 168 | 169 | ```python 170 | TextView('','width',width=10) 171 | ``` 172 | 173 | 174 | 175 | * weight 176 | 177 | ```python 178 | TextView('','weight',weight=1) 179 | ``` 180 | 181 | 182 | 183 | * gravity 184 | 185 | ```python 186 | TextView('','gravity',gravity=Gravity.left) 187 | ``` 188 | 189 | 190 | 191 | * visibility 192 | 193 | ```python 194 | TextView('','',visibility=Visibility.visible) 195 | ``` 196 | 197 | 198 | 199 | 200 | * ex_style (not support windows) 201 | 202 | 203 | ```python 204 | TextView('','ex_style',style=Style.ex_blink) 205 | ``` 206 | 207 | 208 | 209 | * ex_fore & ex_back (not support windows) 210 | 211 | 212 | ```python 213 | TextView('','ex_fore',fore=Fore.ex_red_1) 214 | TextView('','ex_back',back=Back.ex_red_1) 215 | 216 | ``` 217 | 218 | 219 | 220 | # LICENSE 221 | 222 | [GPLv3](https://github.com/gojuukaze/terminal_layout/blob/master/LICENSE) 223 | 224 | # Thanks 225 | 226 | * [colorama](https://github.com/tartley/colorama) : Simple cross-platform colored terminal text in Python 227 | * [colored](https://gitlab.com/dslackw/colored) : Very simple Python library for color and formatting in terminal 228 | 229 | 230 | # 捐赠 / Sponsor 231 | 232 | 开源不易,如果你觉得对你有帮助,求打赏个一块两块的 233 | 234 | ![](https://gitee.com/gojuukaze/liteAuth/raw/master/shang.jpg) 235 | 236 | -------------------------------------------------------------------------------- /terminal_layout/extensions/scroll/README.md: -------------------------------------------------------------------------------- 1 | # scroll 2 | 3 | Let `TableLayout` support scrolling. 4 | 5 | Note: **Must be used with `TableLayout` and `TableRow`!!** 6 | 7 | ![scroll.gif](../../../pic/scroll.gif) 8 | 9 | ## usage 10 | 11 | ```python 12 | from terminal_layout import * 13 | from terminal_layout.extensions.scroll import * 14 | 15 | rows = [[TextView(str(i), str(i))] for i in range(50)] 16 | 17 | ctl = LayoutCtl.quick(TableLayout, rows) 18 | # ctl.enable_debug(height=13) 19 | scroll = Scroll(ctl, stop_key='q', loop=True, more=True, scroll_box_start=3) 20 | scroll.scroll() 21 | 22 | ``` 23 | 24 | There are several parameter you can set: 25 | 26 | | **name** | **default** | **desc** | 27 | |----------------------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| 28 | | ctl | | LayoutCtl | 29 | | stop_key | Key.ESC | key to stop scrolling | 30 | | up_key | Key.UP | | 31 | | down_key | Key.DOWN | | 32 | | scroll_box_start | 0 | The row to start scrolling from. If the title is to be displayed on the first row, set scroll_box_start=1 (see the final cal_scroll section for details) | 33 | | default_scroll_start | 0 | When initializing, which row in scroll_box is displayed at the first position. | 34 | | loop | False | | 35 | | btm_text | '' | The text at the bottom, if it is empty, it will not be displayed | 36 | | more | False | Similar to the behavior of man. Its value is True will automatically add btm_text | 37 | | callback | None | Callback after scrolling | 38 | | re_draw_after_scroll | True | Whether to perform a redraw after scrolling. If set to false you need to call re_draw yourself | 39 | | re_draw_after_stop | False | Whether to redraw after stopping scrolling | 40 | 41 | 42 | > default_scroll_start Description 43 | > If the terminal height is 4, the table has 6 rows (height is 6), default_scroll_start=6. 44 | > When drawing, what is shown at the top of the terminal is row_2. 45 | > ``` 46 | > row_0 47 | > row_1 48 | > |------------| 49 | > | row_2 | 50 | > | row_3 | <=== terminal, h=4 51 | > | row_4 | 52 | > | row_5 | 53 | > |------------| 54 | > ``` 55 | 56 | 57 | ## Modify scroll event behavior 58 | 59 | There are various ways to modify the behavior after scrolling 60 | 61 | ### 1. Modify by callback 62 | 63 | ```python 64 | from terminal_layout.extensions.scroll import * 65 | 66 | ctl = ... 67 | scroll = ... 68 | 69 | 70 | def my_callback(event): 71 | if event == ScrollEvent.up: 72 | ... 73 | # or ctl.re_draw() 74 | scroll.draw() 75 | # ... 76 | 77 | ``` 78 | 79 | `up`, `down` events will perform `re_draw` by default before calling callback. If you modify the view in the callback, you need to call the function to redraw. 80 | 81 | If you change the row of the table in your callback, you should use `scroll.draw()` This function will recalculate the scroll position. 82 | 83 | ### 2. override scroll event 84 | 85 | If you want to do something before calculating the scroll position (for example: disallow scroll-up cycles, but allow down-scroll cycles), 86 | You need to override the scroll event. 87 | 88 | There are various ways to override the scroll event. 89 | 90 | * Override by `stop_func`, `up_func`, `down_func` 91 | 92 | ```python 93 | from terminal_layout.extensions.scroll import * 94 | 95 | ctl = LayoutCtl(...) 96 | scroll = Scroll(ctl, loop=True) 97 | 98 | def up(kl, event): 99 | if scroll.current_scroll_start - 1 < scroll.scroll_box_start: 100 | return 101 | scroll.up() 102 | ctl.re_draw() 103 | 104 | scroll.scroll(up_func=up) 105 | ``` 106 | 107 | * Add `key_listener` event 108 | 109 | ```python 110 | from terminal_layout.extensions.scroll import * 111 | from terminal_layout.readkey import Key 112 | 113 | ctl = LayoutCtl(...) 114 | scroll = Scroll(ctl, up_key=None, loop=True) 115 | 116 | key_listener = scroll.init_kl() 117 | 118 | @key_listener.bind_key(Key.UP) 119 | def up(kl, event): 120 | if scroll.current_scroll_start - 1 < scroll.scroll_box_start: 121 | return 122 | scroll.up() 123 | ctl.re_draw() 124 | 125 | scroll.draw() 126 | key_listener.listen() 127 | ``` 128 | Or initialize `KeyListener` by yourself instead of `init_kl` 129 | 130 | ```python 131 | from terminal_layout.extensions.scroll import * 132 | from terminal_layout.readkey import Key, KeyListener 133 | 134 | ctl = LayoutCtl(...) 135 | scroll = Scroll(ctl, loop=True) 136 | 137 | key_listener = KeyListener() 138 | 139 | @key_listener.bind_key(Key.UP) 140 | def up(kl, event): 141 | ... 142 | 143 | key_listener.bind_key(Key.DOWN, down_func , decorator=False) 144 | key_listener.bind_key('q', stop_func , decorator=False) 145 | 146 | scroll.draw() 147 | key_listener.listen() 148 | 149 | ``` 150 | 151 | ## cal_scroll return value description 152 | 153 | * `scroll_box_start` : The start position of the scrollable area. Can be used when the title needs to be always displayed 154 | * `scroll_box_end` : Scrollable area end position 155 | * `scroll_start` : The actual display start position of the scrolling area 156 | * `scroll_end` : The actual display end position of the scrolling area 157 | 158 | ![cal_scroll.png](../../../pic/cal_scroll.png) 159 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | terminal_layout 2 | =============== 3 | 4 | | The project help you to quickly build layouts in terminal 5 | | (这个一个命令行ui布局工具) 6 | 7 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/demo_v2_1.gif 8 | 9 | 10 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/demo.gif 11 | :width: 450 12 | 13 | -------------- 14 | 15 | **Some extensions base on terminal_layout** 16 | 17 | - `progress `__ 18 | 19 | |progress.gif| 20 | 21 | - `choice `__ 22 | 23 | |choice.gif| 24 | 25 | -------------- 26 | 27 | \*\* video demo \*\* 28 | 29 | .. image:: https://asciinema.org/a/226120.svg 30 | :width: 550 31 | :target: https://asciinema.org/a/226120 32 | 33 | 34 | link 35 | ==== 36 | 37 | - `All 38 | Demo `__ 39 | - `Github `__ 40 | - `Docs `__ 41 | - `https://asciinema.org/a/226120 `__ 42 | 43 | install 44 | ======= 45 | 46 | .. code:: bash 47 | 48 | pip install terminal-layout 49 | 50 | Dependencies 51 | ============ 52 | 53 | - Python 3.5+ (maybe 3.4) 54 | - Linux, OS X, and Windows systems. 55 | 56 | 57 | Python Support 58 | -------------- 59 | 60 | ====== =============== 61 | Python terminal_layout 62 | ====== =============== 63 | 2.7 2.1.x 64 | 3.5+ 3.x 65 | ====== =============== 66 | 67 | Usage 68 | ===== 69 | 70 | - easy demo: 71 | 72 | .. code:: python 73 | 74 | import time 75 | from terminal_layout import * 76 | 77 | ctl = LayoutCtl.quick(TableLayout, 78 | # table id: root 79 | [ 80 | [TextView('t1', 'Hello World!', width=Width.fill, back=Back.blue)], # <- row id: root_row_0, 81 | [TextView('t2', '', fore=Fore.magenta)], # <- row id: root_row_1, 82 | ], 83 | ) 84 | 85 | # or layout=ctl.get_layout() 86 | layout = ctl.find_view_by_id('root') 87 | layout.set_width(20) 88 | 89 | # default: auto_re_draw=True 90 | ctl.draw() 91 | 92 | # call delay_set_text() must be set auto_re_draw=True, 93 | # otherwise you must start a thread to call re_draw() by yourself 94 | ctl.find_view_by_id('t2').delay_set_text('你好,世界!', delay=0.2) 95 | 96 | time.sleep(0.5) 97 | row3 = TableRow.quick_init('', [TextView('t3', 'こんにちは、世界!')]) 98 | layout.add_view(row3) 99 | 100 | # If you call draw() with auto_re_draw=True, you must stop() 101 | ctl.stop() 102 | 103 | |image2| 104 | 105 | - disable auto_re_draw 106 | 107 | .. code:: python 108 | 109 | import time 110 | from terminal_layout import * 111 | 112 | ctl = LayoutCtl.quick(TableLayout, 113 | # table id: root 114 | [ 115 | [TextView('t1', 'Hello World!', width=Width.fill, back=Back.blue)], # <- row id: root_row_1, 116 | [TextView('t2', '', fore=Fore.magenta)], # <- row id: root_row_2, 117 | ], 118 | ) 119 | 120 | 121 | layout = ctl.find_view_by_id('root') 122 | layout.set_width(20) 123 | 124 | ctl.draw(auto_re_draw=False) 125 | 126 | ctl.find_view_by_id('t2').set_text('你好,世界!') 127 | ctl.re_draw() 128 | 129 | time.sleep(0.5) 130 | row3 = TableRow.quick_init('', [TextView('t3', 'こんにちは、世界!')]) 131 | layout.add_view(row3) 132 | ctl.re_draw() 133 | 134 | # don't need call stop() 135 | # ctl.stop() 136 | 137 | - use python2 unicode 138 | 139 | .. code:: python 140 | 141 | # -*- coding: utf-8 -*- 142 | from terminal_layout import * 143 | import sys 144 | reload(sys) 145 | sys.setdefaultencoding('utf-8') 146 | 147 | ctl = LayoutCtl.quick(TableLayout, 148 | [ 149 | [TextView('', u'中文,你好', back=Back.cyan, width=Width.wrap)], 150 | [TextView('', u'中文,你好', back=Back.cyan, width=6)], 151 | [TextView('', u'日本語,こんにちは', back=Back.cyan, width=Width.wrap)], 152 | ] 153 | 154 | ) 155 | 156 | ctl.draw() 157 | 158 | |image3| 159 | 160 | Properties 161 | ---------- 162 | 163 | - fore & back 164 | 165 | .. code:: python 166 | 167 | TextView('','fore',fore=Fore.red) 168 | TextView('','back',back=Back.red) 169 | 170 | 171 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/color.jpeg 172 | :width: 560 173 | 174 | - style 175 | 176 | .. code:: python 177 | 178 | TextView('','style',style=Style.dim) 179 | 180 | 181 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/style.jpeg 182 | :width: 560 183 | 184 | - width 185 | 186 | .. code:: python 187 | 188 | TextView('','width',width=10) 189 | 190 | 191 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/width.jpeg 192 | :width: 560 193 | 194 | - weight 195 | 196 | .. code:: python 197 | 198 | TextView('','weight',weight=1) 199 | 200 | 201 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/weight.jpeg 202 | :width: 560 203 | 204 | - gravity 205 | 206 | .. code:: python 207 | 208 | TextView('','gravity',gravity=Gravity.left) 209 | 210 | 211 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/gravity.jpeg 212 | :width: 560 213 | 214 | 215 | - visibility 216 | 217 | .. code:: python 218 | 219 | TextView('','',visibility=Visibility.visible) 220 | 221 | 222 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/visibility.jpeg 223 | :width: 560 224 | 225 | - ex_style (not support windows) 226 | 227 | .. code:: python 228 | 229 | TextView('','ex_style',style=Style.ex_blink) 230 | 231 | 232 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/ex_style.jpeg 233 | :width: 560 234 | 235 | - ex_fore & ex_back (not support windows) 236 | 237 | .. code:: python 238 | 239 | TextView('','ex_fore',fore=Fore.ex_red_1) 240 | TextView('','ex_back',back=Back.ex_red_1) 241 | 242 | 243 | .. image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/ex_color.jpeg 244 | :width: 560 245 | 246 | LICENSE 247 | ======= 248 | 249 | `GPLv3 `__ 250 | 251 | Thanks 252 | ====== 253 | 254 | - `colorama `__ : Simple 255 | cross-platform colored terminal text in Python 256 | - `colored `__ : Very simple Python 257 | library for color and formatting in terminal 258 | 259 | .. |progress.gif| image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/progress.gif 260 | .. |choice.gif| image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/choice.gif 261 | .. |image2| image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/hello.png 262 | .. |image3| image:: https://github.com/gojuukaze/terminal_layout/raw/master/pic/py2.png 263 | -------------------------------------------------------------------------------- /docs/draw.rst: -------------------------------------------------------------------------------- 1 | 输出文字 2 | ======================= 3 | 4 | 基本概念 5 | -------------- 6 | - **LayoutCtl** :layout管理器,负责绘制所有元素 7 | - **View** : 基础控件,Layout 与 TextView 其实都属于 View 8 | 9 | - Layout : 布局控制器,控制 TextView 显示的位置。 目前支持的Layout有两种 TableLayout(表格布局), TableRow(行布局) 10 | 11 | - TextView : 用于显示文字的view。 12 | 13 | 14 | 显示view 15 | ---------- 16 | 17 | 执行下列代码绘制layout 18 | 19 | .. code-block:: python 20 | 21 | from terminal_layout import * 22 | 23 | table = TableLayout('id1', width=Width.fill) 24 | 25 | row1 = TableRow('row1') 26 | row1.add_view(TextView('text1', 'hello',fore=Fore.red)) 27 | 28 | table.add_view(row1) 29 | table.add_view_list([TableRow('row2'), TableRow('row3')]) 30 | 31 | ctl = LayoutCtl() 32 | ctl.set_layout(table) 33 | ctl.draw() 34 | ctl.stop() 35 | 36 | .. note:: 37 | TableLayout,TableRow,TextView的第一个参数是view的id 38 | 39 | 40 | 41 | 使用quick init 42 | --------------- 43 | 44 | 每次手动创建layout,text_view会很麻烦,这里为 TableRow,TableLayout,LayoutCtl 提供了quick_init()函数帮助快速创建 45 | 46 | TableRow 47 | ~~~~~~~~~~~~~~~~ 48 | 49 | .. code-block:: python 50 | 51 | from terminal_layout import * 52 | 53 | row = TableRow.quick_init('row1', [TextView('title', 'Title', width=Width.wrap)], width=20, 54 | gravity=Gravity.center) 55 | 56 | ctl = LayoutCtl(row) 57 | ctl.draw() 58 | ctl.stop() 59 | 60 | .. note:: 61 | LayoutCtl()接受的参数是View,因此直接把TableRow放到ctl中。 62 | 63 | 你也可以把TextView直接放入LayoutCtl(),如:: 64 | 65 | ctl = LayoutCtl(TextView('title', 'Title', width=10, back=Back.blue)) 66 | 67 | 68 | TableLayout 69 | ~~~~~~~~~~~~~~~~~ 70 | 71 | .. code-block:: python 72 | 73 | from terminal_layout import * 74 | 75 | row1 = TableRow.quick_init('row1', [TextView('title', 'Title', width=Width.wrap)], width=10, 76 | gravity=Gravity.center) 77 | 78 | data1_view = TextView('data1', '1.', width=3) 79 | data2_view = TextView('data2', 'foo', width=5) 80 | row2 = TableRow.quick_init('row2', [data1_view, data2_view]) 81 | 82 | table_layout = TableLayout.quick_init('', [row1, row2], width=10) 83 | 84 | ctl = LayoutCtl(table_layout) 85 | ctl.draw() 86 | ctl.stop() 87 | 88 | v3.0.0+ data支持 ``[[TextView]]`` 这样的形式,且可以通过 ``row_id_formatter`` 修改row的默认id。( ``row_id_formatter`` 说明见下方 **LayoutCtl.quick** 部分) 89 | 90 | .. code-block:: python 91 | 92 | from terminal_layout import * 93 | 94 | table = TableLayout.quick_init('root', 95 | [ # table id: root 96 | [TextView('', '1')], # row id: root_row_0 97 | [TextView('', '2')] # row id: root_row_1 98 | ] , 99 | row_id_formatter='{table_id}_row_{index}' 100 | ) 101 | 102 | 103 | LayoutCtl 104 | ~~~~~~~~~~~~~~~~ 105 | .. code-block:: python 106 | 107 | from terminal_layout import * 108 | ctl = LayoutCtl.quick(TableLayout, 109 | # table id: root 110 | [ 111 | [TextView('title', 'Title', width=Width.wrap)], # row id: root_row_0 112 | [TextView('data1', '1.', width=3), TextView('data2', 'foo', width=5)], # row id: root_row_1 113 | ], 114 | id="root", 115 | row_id_formatter='{table_id}_row_{index}' 116 | ) 117 | ctl.draw() 118 | ctl.stop() 119 | 120 | .. note:: 121 | 122 | v3.0.0开始,可以通过 ``id`` 配置最外层的layout id。 123 | 124 | 创建 ``TableLayout`` 时可通过 ``row_id_formatter`` 配置 row id。其支持的展位符如下: 125 | 126 | - table_id :即 id 设置的值 127 | - index 128 | 129 | 修改view的属性 130 | ---------------- 131 | 132 | - 使用find_view_by_id获取view并修改(对于重复的id只能获取第一个view) 133 | 134 | .. code-block:: python 135 | 136 | import time 137 | from terminal_layout import * 138 | 139 | ctl = LayoutCtl.quick(TableLayout, 140 | [ 141 | [TextView('title', 'Title')], # row id: root_row_0 142 | [TextView('data1', '1.',width=3), TextView('data2', 'foo',width=5)], # row id: root_row_1 143 | ] 144 | ) 145 | ctl.draw() 146 | 147 | row = ctl.find_view_by_id('root_row_0') 148 | row.set_width(10) 149 | row.set_gravity(gravity=Gravity.center) 150 | 151 | time.sleep(0.3) 152 | ctl.find_view_by_id('data1').set_text('2.') 153 | 154 | time.sleep(0.3) 155 | ctl.find_view_by_id('data2').delay_set_text('FOO') 156 | 157 | ctl.stop() 158 | 159 | * 给layout添加view 160 | 161 | .. code-block:: python 162 | 163 | from terminal_layout import * 164 | 165 | from terminal_layout import * 166 | 167 | ctl = LayoutCtl.quick(TableLayout, []) 168 | 169 | table = ctl.find_view_by_id('root') 170 | # append 171 | table.add_view(TableRow('')) 172 | table.add_view_list([TableRow(''), TableRow('')]) 173 | 174 | # insert 用法和list相同 175 | table.insert(3, TableRow('')) 176 | 177 | .. note:: 178 | 179 | 因为 ``TextView`` 也属于 ``View`` ,因此你可以把 ``TextView`` 加入 ``TableLayout`` 中而不报错。 180 | 如: 181 | 182 | .. code-block:: python 183 | 184 | table = TableLayout('id1') 185 | table.add_view(TextView('', 'text')) 186 | 187 | 这样某些情况下做相当于 188 | 189 | .. code-block:: python 190 | 191 | table = TableLayout('id1') 192 | row = TableRow.quick_init('', [TextView('', 'text') ] ) 193 | table.add_view(row) 194 | 195 | 但第一种方式将不能正确处理某些 ``TextView`` 的自有属性(非基础 ``View`` 的属性)。 196 | 除非你知道你在做什么,否则建议使用第二种方式。 197 | 198 | 移除view 199 | --------------------- 200 | 201 | - 你可以使用remove或remove_view_by_id移除view 202 | 203 | 204 | .. code-block:: python 205 | 206 | from terminal_layout import * 207 | ctl = LayoutCtl.quick(TableLayout, 208 | # table id: root 209 | [ 210 | [TextView('title', 'Title', width=Width.wrap)], # row id: root_row_0 211 | [TextView('data1', '1.', width=3), TextView('data2', 'foo', width=5)], # row id: root_row_1 212 | ] 213 | ) 214 | # remove title 215 | ctl.remove_view_by_id('title') 216 | 217 | 218 | 自动刷新 219 | ------------- 220 | 221 | v2开始会启动线程自动刷新,因此结束程序时必须手动调用stop()。 222 | 223 | 如果你不需要,则设置 auto_re_draw为False 禁用,此时你需要手动调用re_draw() 224 | 225 | .. code-block:: python 226 | 227 | from terminal_layout import * 228 | 229 | ctl = LayoutCtl(TextView('title', 'Title', width=10)) 230 | ctl.draw(auto_re_draw=False) 231 | time.sleep(0.5) 232 | ctl.find_view_by_id('title').set_fore(Fore.red) 233 | 234 | ctl.re_draw() 235 | 236 | .. note:: 237 | 如果禁用了自动刷新,delay_set_text()函数就无效了 238 | 239 | View的属性 240 | ------------ 241 | View的属性包括: ``width`` , ``visibility`` , ``gravity`` 242 | 243 | TextView在上述基础上增加了:``text`` , ``back`` , ``style`` , ``fore`` , ``weight`` , ``weight`` , 244 | 245 | 关于属性的说明参照::doc:`/Properties` 246 | 247 | -------------------------------------------------------------------------------- /terminal_layout/extensions/scroll/scroll.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | from terminal_layout.helper.class_helper import instance_variables 5 | from terminal_layout.view.params import OverflowVertical, Visibility 6 | from terminal_layout.view.layout import TableRow 7 | from terminal_layout.view.text_view import TextView 8 | from terminal_layout.readkey import Key, KeyListener 9 | from terminal_layout.log import logger 10 | from terminal_layout.helper.helper import get_terminal_size 11 | 12 | 13 | class ScrollEvent: 14 | up = 'up' 15 | down = 'down' 16 | stop = 'stop' 17 | 18 | 19 | scroll_btm_row = '_scroll_btm_row_' 20 | scroll_btm_text = '_scroll_btm_text_' 21 | 22 | 23 | class Scroll(object): 24 | """ 25 | 注意:只支持TableLayout !!! 26 | """ 27 | __slots__ = ( 28 | 'ctl', 'stop_key', 'up_key', 'down_key', 'scroll_box_start', 'default_scroll_start', 'loop', 'btm_text', 'more', 29 | 'callback', 're_draw_after_scroll', 're_draw_after_stop', 'old_layout_overflow', 'current_scroll_start', 30 | 'key_listener') 31 | 32 | @instance_variables 33 | def __init__(self, ctl, 34 | stop_key=Key.ESC, up_key=Key.UP, down_key=Key.DOWN, 35 | scroll_box_start=0, default_scroll_start=0, loop=False, 36 | btm_text='', more=False, 37 | callback=None, re_draw_after_scroll=True, re_draw_after_stop=False): 38 | """ 39 | :param ctl: ctl 40 | :param overflow: hidden or scroll 41 | 42 | :param stop_key: 停止scroll的key 43 | :param up_key: 44 | :param down_key: 45 | :param scroll_box_start: 从哪行开始可以滚动。若第一行要显示标题,则scroll_start=1 46 | :param default_scroll_start: 初始化时, scroll_box中哪行显示在第一的位置。 47 | :param loop: 循环 48 | :param btm_text: 底部的文本,为空则不显示 49 | :param more: 类似于man的效果。Ture会自动添加 btm_text 50 | :param callback: 滚动后的回调 51 | :param re_draw_after_scroll: 滚动后执行重绘。为false时你需要自己调用re_draw 52 | :param re_draw_after_stop: 53 | 54 | 55 | default_scroll_start 说明: 56 | 57 | 若terminal高度为4, table有6行(即高度为6),default_scroll_start=6。 58 | 绘制时,显示在terminal顶部的是row_2。 59 | 60 | row_0 61 | row_1 62 | |------------| 63 | | row_2 | 64 | | row_3 | <=== terminal, h=4 65 | | row_4 | 66 | | row_5 | 67 | |------------| 68 | 69 | 70 | """ 71 | self.old_layout_overflow = None 72 | self.current_scroll_start = default_scroll_start 73 | if self.current_scroll_start < self.scroll_box_start: 74 | self.current_scroll_start = self.scroll_box_start 75 | 76 | """ 77 | current_scroll_start : 当前实际显示的滚动区域的第一行, 详细说明如下 78 | 79 | 80 | row_0 81 | |------------------------| 82 | | row_1 | <= terminal, height=4 83 | | row_2 | 84 | | row_3 | 85 | | row_4 | 86 | |------------------------| 87 | 88 | 外框表示terminal,其高度为4。table高度为5 row 1-4 显示在terminal中。 89 | 此时current_scroll_start = 1 90 | 若 loop=False ,触发down时 current_scroll_start不变 ( 注意不会+1 !!) 91 | 若 loop=True ,触发down时 current_scroll_start=0 92 | """ 93 | if more: 94 | self.btm_text = '-- more --' 95 | 96 | def init_kl(self, stop_func=None, up_func=None, down_func=None): 97 | """ 98 | :rtype: KeyListener 99 | """ 100 | self.key_listener = KeyListener() 101 | if self.stop_key: 102 | self.key_listener.bind_key(self.stop_key, stop_func or self._stop, decorator=False) 103 | if self.up_key: 104 | self.key_listener.bind_key(self.up_key, up_func or self._up, decorator=False) 105 | if self.down_key: 106 | self.key_listener.bind_key(self.down_key, down_func or self._down, decorator=False) 107 | return self.key_listener 108 | 109 | def draw(self): 110 | # 修改table的overflow 111 | table = self.ctl.get_layout() 112 | self.old_layout_overflow = table.get_overflow_vertical() 113 | table.set_overflow_vertical(OverflowVertical.none) 114 | # 添加btm 115 | if self.btm_text: 116 | r = TableRow.quick_init(scroll_btm_row, 117 | TextView(scroll_btm_text, self.btm_text) 118 | ) 119 | table.add_view(r) 120 | 121 | self.cal_and_update(self.current_scroll_start, is_first=True) 122 | 123 | self.ctl.draw(auto_re_draw=False) 124 | 125 | def scroll(self, stop_func=None, up_func=None, down_func=None): 126 | self.init_kl(stop_func, up_func, down_func) 127 | self.draw() 128 | self.key_listener.listen() 129 | 130 | def _callback(self, event): 131 | if not self.callback: 132 | return 133 | self.callback(event) 134 | 135 | def _up(self, kl, event): 136 | self.up() 137 | if self.re_draw_after_scroll: 138 | self.ctl.re_draw() 139 | self._callback(ScrollEvent.up) 140 | 141 | def up(self): 142 | self.cal_and_update(self.current_scroll_start - 1) 143 | 144 | def _down(self, kl, event): 145 | self.down() 146 | if self.re_draw_after_scroll: 147 | self.ctl.re_draw() 148 | self._callback(ScrollEvent.down) 149 | 150 | def down(self): 151 | self.cal_and_update(self.current_scroll_start + 1) 152 | 153 | def _stop(self, kl, event): 154 | self.stop(kl) 155 | if self.re_draw_after_stop: 156 | self.ctl.draw() 157 | self._callback(ScrollEvent.stop) 158 | 159 | def stop(self, kl): 160 | kl.stop() 161 | table = self.ctl.get_layout() 162 | table.set_overflow_vertical(self.old_layout_overflow) 163 | if self.btm_text: 164 | table.remove_view_by_id('_scroll_btm_row_') 165 | table.view.old_row_visibility.pop(-1) 166 | 167 | def cal_and_update(self, new_current_scroll_start, is_first=False): 168 | """ 169 | cal_scroll 和 update_view最开始是在一起的, 170 | 之所以拆成两个函数是为了满足需要cal_scroll的计算结果的需求 171 | """ 172 | r = self.cal_scroll(new_current_scroll_start, is_first) 173 | # 等于0说明高度足够,不用修改view的visibility 174 | if r[4] != 0: 175 | self.update_view(*r) 176 | 177 | def cal_scroll(self, new_current_scroll_start, is_first=False): 178 | _, h = self.ctl.get_terminal_size() 179 | # 最后一行需要显示光标,因此-1 180 | h -= 1 + self.scroll_box_start 181 | 182 | table = self.ctl.get_layout().view 183 | 184 | scroll_box_end = len(table.data) 185 | 186 | if self.btm_text: 187 | h -= 1 188 | scroll_box_end -= 1 189 | 190 | scroll_box_h = scroll_box_end - self.scroll_box_start 191 | 192 | for r in table.data: 193 | if r.visibility == Visibility.gone: 194 | scroll_box_h -= 1 195 | if h >= scroll_box_h: 196 | return 0, 0, 0, 0, 0 197 | 198 | scroll_start = new_current_scroll_start 199 | if scroll_start < self.scroll_box_start: 200 | # 首次绘制时,不会出现 scroll_start scroll_box_end: 207 | # 进这里说明是按down键,或者首次绘制时 208 | if self.loop and not is_first: 209 | scroll_start = self.scroll_box_start 210 | 211 | # 计算scroll_start, scroll_end 212 | # 优先显示scroll_start下面部分,高度还有剩余就从上面补 213 | 214 | left_h = h 215 | scroll_end = scroll_start 216 | 217 | # 因为可能有view隐藏的情况,这里直接循环到scroll_box_end,而不是 scroll_start+h 218 | for i, r in enumerate(table.data[scroll_start:scroll_box_end]): 219 | if not left_h: 220 | break 221 | 222 | scroll_end = scroll_start + i + 1 223 | if r.visibility != Visibility.gone: 224 | left_h -= 1 225 | 226 | while left_h and scroll_start - 1 >= self.scroll_box_start: 227 | scroll_start -= 1 228 | if table.data[scroll_start].visibility != Visibility.gone: 229 | left_h -= 1 230 | 231 | return self.scroll_box_start, scroll_box_end, scroll_start, scroll_end, h 232 | 233 | def update_view(self, scroll_box_start, scroll_box_end, scroll_start, scroll_end, h): 234 | self.current_scroll_start = scroll_start 235 | 236 | table = self.ctl.get_layout().view 237 | 238 | # 修改 visibility 239 | table.old_row_visibility = [] 240 | for r in table.data[:scroll_box_start]: 241 | table.old_row_visibility.append(r.visibility) 242 | 243 | for i, r in enumerate(table.data[scroll_box_start:scroll_box_end]): 244 | i += scroll_box_start 245 | table.old_row_visibility.append(r.visibility) 246 | if i >= scroll_start and i < scroll_end: 247 | r._set_is_show(True) 248 | else: 249 | r.visibility = Visibility.gone 250 | r._set_is_show(False) 251 | 252 | if self.btm_text: 253 | btm = table.data[-1] 254 | table.old_row_visibility.append(btm.visibility) 255 | if self.more and scroll_end >= scroll_box_end: 256 | btm.data[0].text = '-- end --' 257 | else: 258 | btm.data[0].text = '-- more --' 259 | -------------------------------------------------------------------------------- /terminal_layout/extensions/input/input_ex.py: -------------------------------------------------------------------------------- 1 | """ 2 | inputView两种实现方法: 3 | 1. 外部监听key事件,然后通过 setText 修改已有的textView值。这样做需要自己移动光标,频繁的redraw 4 | 2. 基于textView写个view,这样做需要在view内自己处理 宽度,以及父view 相关的逻辑问题 5 | 6 | InputEx使用第一种 7 | """ 8 | import sys 9 | 10 | from terminal_layout.log import logger 11 | from terminal_layout.ctl import TextViewProxy, LayoutProxy 12 | 13 | from typing import Union 14 | 15 | from terminal_layout import * 16 | 17 | from terminal_layout.types import String 18 | 19 | 20 | class InputEx(object): 21 | view = None # type:Union[TextViewProxy, LayoutProxy, None] 22 | x = -1 23 | y = -1 24 | layout_height = 0 25 | input_s = None # type: String 26 | input_char_list_index = 0 27 | input_char_list_start = 0 28 | input_char_list_end = 0 29 | cursor_index = 0 30 | max_length = None 31 | 32 | def __init__(self, _ctl, input_buffer=30): 33 | """ 34 | 35 | :param input_buffer: 读取字符的缓存;输入法一次输入多个字符时,若buffer过小会导致只读取到一部分 36 | 37 | :type _ctl:LayoutCtl 38 | """ 39 | self.ctl = _ctl 40 | self.input_buffer = input_buffer 41 | 42 | def get_view_y(self, id, row): 43 | y = 0 44 | for v in row.data: 45 | if v.id == id: 46 | if v.visibility != Visibility.visible: 47 | return True, -1 48 | return True, y 49 | y += v.real_width or 0 50 | return False, -1 51 | 52 | def get_view_x_y(self, id): 53 | """ 54 | 55 | (x, y)-------------- 56 | | 1 2 3 | 57 | | 4 5 6 | 58 | | 7 8 9 | 59 | --------------(x, y) 60 | 61 | (1,2) = 6 62 | """ 63 | x = 0 64 | y = 0 65 | layout = self.ctl.layout 66 | if isinstance(layout, TextView): 67 | return -1, -1 if layout.id != id else x, y 68 | if isinstance(layout, TableRow): 69 | ok, y = self.get_view_y(id, layout) 70 | return x, y 71 | for row in self.ctl.layout.data: 72 | if row.visibility == Visibility.gone: 73 | continue 74 | if row.visibility == Visibility.visible: 75 | ok, y = self.get_view_y(id, row) 76 | if ok: 77 | return x, y 78 | x += 1 79 | return -1, -1 80 | 81 | def get_parent_max_width(self, view): 82 | p = view.get_parent() 83 | if p is None: 84 | return self.ctl.get_terminal_size()[0] 85 | else: 86 | parent_max_width = self.get_parent_max_width(p) 87 | width = p.get_width() 88 | if isinstance(width, int) and width < parent_max_width: 89 | return width 90 | return parent_max_width 91 | 92 | def get_view_max_width(self): 93 | parent_max_width = self.get_parent_max_width(self.view) 94 | 95 | parent = self.view.get_parent() 96 | if parent is not None: 97 | for v in parent.data: 98 | if v.id == self.view.get_id(): 99 | continue 100 | if v.weight is None: 101 | parent_max_width -= v.real_width 102 | 103 | width = self.view.get_width() 104 | 105 | if isinstance(width, int) and width < parent_max_width: 106 | return width 107 | return parent_max_width 108 | 109 | def get_input(self, id, max_length=None): 110 | """ 111 | :param max_length: 输入字符数;注意区分max_width,max_width表示显示宽度,和输入无关 112 | """ 113 | self.view = self.ctl.find_view_by_id(id) 114 | self.max_length = max_length 115 | 116 | if not self.view or not isinstance(self.view, TextViewProxy): 117 | return False, '-1' 118 | 119 | self.x, self.y = self.get_view_x_y(id) 120 | if self.x < 0 or self.y < 0: 121 | return False, '-2' 122 | 123 | # 停止绘制线程 124 | if self.ctl.auto_re_draw: 125 | self.ctl.refresh_thread_stop = True 126 | self.ctl.refresh_thread.join() 127 | 128 | self.max_width = self.get_view_max_width() 129 | 130 | if self.max_width <= 0: 131 | return False, '-3' 132 | 133 | self.layout_height = self.ctl.layout.real_height 134 | 135 | self.input_s = String(self.view.get_text()) 136 | if self.max_length and len(self.input_s.char_list) > self.max_length: 137 | self.input_s.char_list_slice(stop=self.max_length) 138 | 139 | self.input_char_list_index = len(self.input_s.char_list) 140 | self.input_char_list_start = 0 141 | self.input_char_list_end = len(self.input_s.char_list) 142 | self.move_cursor_to_view() 143 | self.update_index_and_show_s() 144 | 145 | kl = KeyListener() 146 | 147 | kl.bind_key('any', self.key_event, decorator=False) 148 | 149 | kl.listen() 150 | self.move_cursor_to_btm() 151 | 152 | if self.ctl.auto_re_draw: 153 | self.ctl.init_refresh_thread() 154 | self.ctl.refresh_thread.start() 155 | 156 | return True, str(self.input_s) 157 | 158 | def move_cursor_to_view(self): 159 | s = '' 160 | if self.layout_height - self.x > 0: 161 | s += Cursor.UP(self.layout_height - self.x) 162 | if self.y + self.cursor_index > 0: 163 | s += Cursor.FORWARD(self.y + self.cursor_index) 164 | if s: 165 | sys.stdout.write(s) 166 | sys.stdout.flush() 167 | 168 | def char_list_iter(self, i, reverse): 169 | for i in range(i - 1, -1, -1) if reverse else range(i, len(self.input_s.char_list), 1): 170 | yield i, self.input_s.char_list[i] 171 | 172 | def update_index_and_show_s(self, right=True): 173 | """ 174 | right: 光标从左向右移动 175 | """ 176 | 177 | if len(self.input_s) >= self.max_width: 178 | left_width = self.max_width 179 | if right: 180 | list_iter = self.char_list_iter(self.input_char_list_end, True) 181 | else: 182 | list_iter = self.char_list_iter( 183 | self.input_char_list_index, False) 184 | 185 | new_i = 0 186 | for i, c in list_iter: 187 | 188 | if left_width - len(c) < 0: 189 | break 190 | left_width -= len(c) 191 | new_i = i 192 | 193 | if right: 194 | self.input_char_list_start = new_i 195 | else: 196 | self.input_char_list_end = new_i + 1 197 | 198 | self.cursor_index = 0 199 | show_s = '' 200 | for i in range(self.input_char_list_start, self.input_char_list_end): 201 | show_s += str(self.input_s.char_list[i]) 202 | if i < self.input_char_list_index: 203 | self.cursor_index += len(self.input_s.char_list[i]) 204 | 205 | self.move_cursor_to_btm() 206 | self.view.set_text(show_s) 207 | self.ctl.re_draw() 208 | self.move_cursor_to_view() 209 | 210 | def move_cursor_to_btm(self): 211 | s = '' 212 | if self.layout_height - self.x > 0: 213 | s += Cursor.DOWN(self.layout_height - self.x) 214 | if self.y + self.cursor_index > 0: 215 | s += Cursor.BACK(self.y + self.cursor_index + 2) 216 | if s: 217 | sys.stdout.write(s) 218 | sys.stdout.flush() 219 | 220 | def update_text(self): 221 | 222 | self.view.set_text(str(self.input_s)) 223 | self.ctl.update_width() 224 | 225 | def key_event(self, kl, event): 226 | c = event.key.code 227 | right = True 228 | 229 | if len(repr(c)) > 1 and (repr(c).startswith("'\\") or repr(c).startswith("u'\\")): 230 | if event.key == Key.LEFT and self.input_char_list_index > 0: 231 | 232 | self.input_char_list_index -= 1 233 | if self.input_char_list_index < self.input_char_list_start: 234 | self.input_char_list_start = self.input_char_list_index 235 | right = False 236 | 237 | elif event.key == Key.RIGHT and self.input_char_list_index < len(self.input_s.char_list): 238 | 239 | self.input_char_list_index += 1 240 | if self.input_char_list_index > self.input_char_list_end - 1: 241 | self.input_char_list_end = min( 242 | self.input_char_list_index + 1, len(self.input_s.char_list)) 243 | 244 | elif event.key == Key.BACKSPACE and self.input_char_list_index > 0: 245 | 246 | self.input_char_list_index -= 1 247 | self.input_s.pop(self.input_char_list_index) 248 | self.input_char_list_end -= 1 249 | 250 | elif event.key == Key.ENTER: 251 | kl.stop() 252 | return 253 | else: 254 | 255 | c_str = String(c) 256 | if self.max_length: 257 | left_length = self.max_length - len(self.input_s.char_list) 258 | if left_length == 0: 259 | return 260 | if len(c_str.char_list) > left_length: 261 | c_str.char_list_slice(stop=left_length) 262 | 263 | self.input_s.insert_into_char_list( 264 | self.input_char_list_index, c_str) 265 | self.input_char_list_index += len(c_str.char_list) 266 | self.input_char_list_end += len(c_str.char_list) 267 | 268 | self.update_index_and_show_s(right=right) 269 | -------------------------------------------------------------------------------- /terminal_layout/view/layout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | 5 | from terminal_layout.ansi import Cursor, clear_line, Style 6 | from terminal_layout.helper.helper import get_terminal_size 7 | from terminal_layout.view.base import View 8 | from terminal_layout.view.params import Visibility, Gravity, Orientation, Width, OverflowVertical 9 | from terminal_layout.view.text_view import TextView 10 | 11 | 12 | class Layout(object): 13 | def add_view(self, v): 14 | v.parent = self 15 | self.data.append(v) 16 | 17 | def add_views(self, *views): 18 | for v in views: 19 | v.parent = self 20 | self.data += views 21 | 22 | def add_view_list(self, views): 23 | for v in views: 24 | v.parent = self 25 | self.data += views 26 | 27 | 28 | class TableRow(View, Layout): 29 | __slots__ = ('back', 'child_width') 30 | 31 | def __init__(self, id, width=Width.fill, height=1, back=None, visibility=Visibility.visible, gravity=Gravity.left): 32 | """ 33 | 34 | :param id: 35 | :param width: 36 | :param height: no used 37 | 38 | :type id:str 39 | :type width: 40 | :type height:int 41 | :type visibility:str 42 | :type gravity:str 43 | """ 44 | 45 | super(TableRow, self).__init__(id, width, height, visibility, gravity) 46 | self.back = back or '' 47 | self.data = [] # type:list[TextView] 48 | 49 | @classmethod 50 | def quick_init(cls, id, data, width=Width.fill, height=1, back=None, visibility=Visibility.visible, 51 | gravity=Gravity.left): 52 | """ 53 | 54 | :param id: 55 | :param data: view or [view] 56 | :param width: 57 | :param height: 58 | :param back: 59 | :param visibility: 60 | :param gravity: 61 | :return: 62 | 63 | :rtype:TableRow 64 | """ 65 | 66 | row = cls(id, width, height, back, visibility, gravity) 67 | if isinstance(data, list): 68 | row.add_view_list(data) 69 | if isinstance(data, View): 70 | row.add_view(data) 71 | return row 72 | 73 | def add_view(self, v): 74 | if not isinstance(v, TextView): 75 | raise TypeError('only support add TextView') 76 | super(TableRow, self).add_view(v) 77 | 78 | def draw(self): 79 | if self.visibility == Visibility.visible: 80 | left = '' 81 | right = '' 82 | if self.real_width > self.child_width: 83 | if self.gravity == Gravity.left: 84 | right = ' ' * (self.real_width - self.child_width) 85 | elif self.gravity == Gravity.right: 86 | left = ' ' * (self.real_width - self.child_width) 87 | elif self.gravity == Gravity.center: 88 | p = (self.real_width - self.child_width) / 2.0 89 | left = ' ' * int(p) 90 | right = ' ' * (int(p) if int(p) == p else int(p) + 1) 91 | sys.stdout.write(str(self.back) + left + str(Style.reset_all)) 92 | for v in self.data: 93 | v.draw() 94 | sys.stdout.write(str(self.back) + right + str(Style.reset_all)) 95 | self.real_height = 1 96 | elif self.visibility == Visibility.invisible: 97 | sys.stdout.write(' ' * self.real_width) 98 | self.real_height = 1 99 | 100 | def clear(self): 101 | sys.stdout.write(Cursor.UP(self.real_height) + clear_line()) 102 | 103 | def update_width(self, parent_width): 104 | """ 105 | 106 | :param parent_width: 107 | :return: 108 | :rtype:int 109 | """ 110 | 111 | if self.width == Width.fill: 112 | self.real_width = parent_width 113 | elif self.width == Width.wrap: 114 | pass 115 | else: 116 | self.real_width = self.width 117 | 118 | weight_view = [] 119 | all_weight = 0 120 | 121 | if self.width == Width.wrap: 122 | _width = parent_width 123 | else: 124 | _width = self.real_width 125 | 126 | for v in self.data: 127 | if v.weight: 128 | weight_view.append(v) 129 | all_weight += v.weight 130 | continue 131 | child_width = v.update_width(_width) 132 | _width -= child_width 133 | _width = max(0, _width) 134 | 135 | if weight_view: 136 | per_width = _width / float(all_weight) 137 | for v in weight_view: 138 | v.real_width = int(v.weight * per_width) 139 | 140 | self.child_width = sum([v.real_width for v in self.data]) 141 | 142 | if self.width == Width.wrap: 143 | self.real_width = self.child_width 144 | 145 | return self.real_width 146 | 147 | 148 | class TableLayout(View, Layout): 149 | 150 | def __init__(self, id, width=Width.fill, height=1, visibility=Visibility.visible, 151 | overflow_vertical=OverflowVertical.none): 152 | """ 153 | 154 | :param id: 155 | :param width: 156 | :param height: no used 157 | 158 | :type id:str 159 | :type width: 160 | :type height:int 161 | :type visibility:str 162 | """ 163 | 164 | super(TableLayout, self).__init__( 165 | id, width, height, visibility, Gravity.left) 166 | self.data = [] # type: list[TableRow] 167 | self.overflow_vertical = overflow_vertical 168 | 169 | @classmethod 170 | def quick_init(cls, id, data, row_id_formatter='{table_id}_row_{index}', width=Width.fill, height=1, visibility=Visibility.visible, 171 | overflow_vertical=OverflowVertical.hidden_top): 172 | """ 173 | 174 | :param id: 175 | :param data: view or [view] or [[TextView]] 176 | :param row_id_formatter: data为 [[TextView]] 时,row的id。占位置只支持:table_id, index 177 | :param width: 178 | :param height: 179 | :param visibility: 180 | :return: 181 | 182 | :rtype:TableLayout 183 | """ 184 | 185 | table = cls(id, width, height, visibility, overflow_vertical) 186 | if isinstance(data, list): 187 | if isinstance(data[0], TableRow): 188 | table.add_view_list(data) 189 | else: 190 | for i, r in enumerate(data): 191 | row = TableRow.quick_init( 192 | row_id_formatter.format(table_id=id, index=i), r) 193 | table.add_view(row) 194 | if isinstance(data, View): 195 | table.add_view(data) 196 | return table 197 | 198 | def update_width(self, parent_width): 199 | """ 200 | 201 | :param parent_width: 202 | :return: 203 | :rtype: int 204 | """ 205 | 206 | if self.width == Width.fill: 207 | self.real_width = parent_width 208 | elif self.width == Width.wrap: 209 | self.real_width = 0 210 | else: 211 | self.real_width = self.width 212 | 213 | for row in self.data: 214 | if self.width == Width.wrap: 215 | child_width = row.update_width(parent_width) 216 | self.real_width = max(self.real_width, child_width) 217 | else: 218 | row.update_width(self.real_width) 219 | 220 | return self.real_width 221 | 222 | def hidden(self): 223 | h = self.terminal_h 224 | if not h: 225 | _, h = get_terminal_size() 226 | # 最后一行需要显示光标,因此-1 227 | h -= 1 228 | 229 | for r in self.data if self.overflow_vertical == OverflowVertical.hidden_btm else reversed(self.data): 230 | self.old_row_visibility.append(r.visibility) 231 | if h > 0: 232 | if not r.visibility == Visibility.gone: 233 | h -= 1 234 | r._set_is_show(True) 235 | else: 236 | r._set_is_show(False) 237 | else: 238 | r.visibility = Visibility.gone 239 | r._set_is_show(False) 240 | 241 | old_row_visibility = None 242 | 243 | def befor_draw(self): 244 | if self.overflow_vertical == OverflowVertical.none: 245 | return 246 | self.old_row_visibility = [] 247 | self.hidden() 248 | 249 | def draw(self): 250 | 251 | self.befor_draw() 252 | 253 | self.real_height = 0 254 | is_first = True 255 | for r in self.data: 256 | if r.visibility != Visibility.gone: 257 | if not is_first: 258 | sys.stdout.write('\n') 259 | else: 260 | is_first = False 261 | r.draw() 262 | self.real_height += 1 263 | 264 | self.after_draw() 265 | 266 | def after_draw(self): 267 | if not self.old_row_visibility: 268 | return 269 | # 注意,里面这个if只能是 overflow_vertical != hidden_top, 不能改成 overflow_vertical == hidden_top 270 | # scroll会复用after_draw恢复row的visibility,此时应该是正序的 271 | for i, v in enumerate( 272 | self.old_row_visibility if self.overflow_vertical != OverflowVertical.hidden_top else reversed( 273 | self.old_row_visibility)): 274 | self.data[i].visibility = v 275 | 276 | def clear(self): 277 | while self.real_height: 278 | sys.stdout.write(Cursor.UP(1) + clear_line()) 279 | self.real_height -= 1 280 | 281 | 282 | class LinearLayout(View): 283 | 284 | def __init__(self, id, width, height=1, visibility=Visibility.visible, gravity=Gravity.left, 285 | orientation=Orientation.vertical): 286 | """ 287 | 288 | :param id: 289 | :param width: 290 | :param height: no used 291 | 292 | :type id:str 293 | :type width: 294 | :type height:int 295 | :type visibility:str 296 | :type gravity:str 297 | """ 298 | 299 | super(LinearLayout, self).__init__( 300 | id, width, height, visibility, gravity) 301 | if orientation == Orientation.vertical: 302 | self.end_code = '\n' 303 | elif orientation == Orientation.horizon: 304 | self.end_code = '' 305 | -------------------------------------------------------------------------------- /terminal_layout/ctl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | import platform 5 | import threading 6 | import time 7 | from typing import Union 8 | 9 | 10 | from terminal_layout.helper.helper import get_terminal_size 11 | from terminal_layout.ansi import term_init 12 | from terminal_layout.view import * 13 | from terminal_layout.view.base import View 14 | 15 | 16 | class NULL: 17 | pass 18 | 19 | 20 | class BaseViewProxy(object): 21 | def __init__(self, ctl, view): 22 | self.ctl = ctl # type: LayoutCtl 23 | self.view = view 24 | 25 | def set(self, k, v, raise_error): 26 | try: 27 | setattr(self.view, k, v) 28 | except Exception as e: 29 | if raise_error: 30 | raise e 31 | 32 | def set_width(self, width, raise_error=False): 33 | self.set('width', width, raise_error) 34 | 35 | def set_visibility(self, visibility, raise_error=False): 36 | self.set('visibility', visibility, raise_error) 37 | 38 | def set_gravity(self, gravity, raise_error=False): 39 | self.set('gravity', gravity, raise_error) 40 | 41 | def set_text(self, text, raise_error=False): 42 | self.set('text', text, raise_error) 43 | 44 | def set_back(self, back, raise_error=False): 45 | self.set('back', back, raise_error) 46 | 47 | def set_style(self, style, raise_error=False): 48 | self.set('style', style, raise_error) 49 | 50 | def set_fore(self, fore, raise_error=False): 51 | self.set('fore', fore, raise_error) 52 | 53 | def set_weight(self, weight, raise_error=False): 54 | self.set('weight', weight, raise_error) 55 | 56 | def delay_set_text(self, text, delay=0.3): 57 | """ 58 | set text one by one 59 | """ 60 | s = '' 61 | for c in text: 62 | s += c 63 | self.set_text(s, raise_error=True) 64 | time.sleep(delay) 65 | 66 | def get(self, k, default): 67 | """ 68 | if default == NULL , it is raised an error when the attribute doesn't exist 69 | 如果default为NULL,当不存在变量时会抛错 70 | """ 71 | if default == NULL: 72 | return getattr(self.view, k) 73 | else: 74 | return getattr(self.view, k, default) 75 | 76 | def get_id(self, default=NULL): 77 | return self.get('id', default) 78 | 79 | def get_width(self, default=NULL): 80 | return self.get('width', default) 81 | 82 | def get_real_width(self, default=NULL): 83 | return self.get('real_width', default) 84 | 85 | def get_visibility(self, default=NULL): 86 | return self.get('visibility', default) 87 | 88 | def get_gravity(self, default=NULL): 89 | return self.get('gravity', default) 90 | 91 | def get_text(self, default=NULL): 92 | return self.get('text', default) 93 | 94 | def get_back(self, default=NULL): 95 | return self.get('back', default) 96 | 97 | def get_style(self, default=NULL): 98 | self.get('style', default) 99 | 100 | def get_fore(self, default=NULL): 101 | self.get('fore', default) 102 | 103 | def get_weight(self, default=NULL): 104 | self.get('weight', default) 105 | 106 | def get_parent(self, default=NULL): 107 | """ 108 | :rtype: View 109 | """ 110 | self.get('parent', default) 111 | 112 | def remove(self): 113 | """ 114 | :rtype: bool 115 | """ 116 | return self.view.remove() 117 | 118 | def is_show(self): 119 | return self.view.is_show() 120 | 121 | 122 | class TextViewProxy(BaseViewProxy): 123 | 124 | def set_text(self, text, raise_error=False): 125 | self.set('text', text, raise_error) 126 | 127 | def set_back(self, back, raise_error=False): 128 | self.set('back', back, raise_error) 129 | 130 | def set_style(self, style, raise_error=False): 131 | self.set('style', style, raise_error) 132 | 133 | def set_fore(self, fore, raise_error=False): 134 | self.set('fore', fore, raise_error) 135 | 136 | def set_weight(self, weight, raise_error=False): 137 | self.set('weight', weight, raise_error) 138 | 139 | def set_overflow(self, overflow, raise_error=False): 140 | self.set('overflow', overflow, raise_error) 141 | 142 | def delay_set_text(self, text, delay=0.3): 143 | """ 144 | set text one by one 145 | """ 146 | s = '' 147 | for c in text: 148 | s += c 149 | self.set_text(s, raise_error=True) 150 | time.sleep(delay) 151 | 152 | def get_text(self, default=NULL): 153 | return self.get('text', default) 154 | 155 | def get_back(self, default=NULL): 156 | return self.get('back', default) 157 | 158 | def get_style(self, default=NULL): 159 | self.get('style', default) 160 | 161 | def get_fore(self, default=NULL): 162 | self.get('fore', default) 163 | 164 | def get_weight(self, default=NULL): 165 | self.get('weight', default) 166 | 167 | def get_overflow(self, default=NULL): 168 | self.get('overflow', default) 169 | 170 | 171 | class LayoutProxy(BaseViewProxy): 172 | def add_view(self, v): 173 | self.view.add_view(v) 174 | 175 | def add_views(self, *views): 176 | self.view.add_views(*views) 177 | 178 | def add_view_list(self, views): 179 | self.view.add_view_list(views) 180 | 181 | def insert_view(self, i, view): 182 | return self.view.insert(i, view) 183 | 184 | def remove_view_by_id(self, id): 185 | """ 186 | :rtype: bool 187 | """ 188 | return self.view.remove_view_by_id(id) 189 | 190 | def set_overflow_vertical(self, overflow_vertical, raise_error=False): 191 | self.set('overflow_vertical', overflow_vertical, raise_error) 192 | 193 | def get_overflow_vertical(self, default=NULL): 194 | return self.get('overflow_vertical', default) 195 | 196 | 197 | class LayoutCtl(object): 198 | debug = False 199 | version = 0 200 | _drawing = False 201 | _stop_flag = False 202 | auto_re_draw = True 203 | 204 | def check(self): 205 | if not sys.stdout.isatty(): 206 | raise RuntimeError('terminal_layout can only run on Terminal') 207 | 208 | def __init__(self, layout=None, skip_check=False): 209 | if not skip_check: 210 | self.check() 211 | self.layout = layout # type:View 212 | self.refresh_lock = threading.Lock() 213 | self.init_refresh_thread() 214 | 215 | def init_refresh_thread(self): 216 | self.refresh_thread_stop = False 217 | self.refresh_thread = threading.Thread( 218 | target=LayoutCtl.refresh, 219 | args=(self,), 220 | ) 221 | self.refresh_thread.daemon = True 222 | 223 | def set_buffer_size(self, size): 224 | """ 225 | 当输出的文本太大会出现界面闪烁的情况,这时要调大sys.stdout的缓冲区 226 | (见 https://github.com/gojuukaze/terminal_layout/issues/3 ) 227 | 建议在draw之前调用 228 | """ 229 | self.buffering = size 230 | sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 231 | self.buffering, encoding='utf-8') 232 | 233 | def is_stop(self): 234 | return self._stop_flag 235 | 236 | @classmethod 237 | def quick(cls, layout_class, data, id='root', row_id_formatter='{table_id}_row_{index}'): 238 | """ 239 | 240 | :param layout_class: TableRow or TableLayout 241 | :param data: TableRow的data为 [textView, ...] ; 242 | TableLayout的data为 [ [textView], ] 243 | 244 | :param id: layout的id 245 | :param row_id_formatter: 创建TableLayout时设置row的id。 246 | 247 | :return: 248 | :rtype :LayoutCtl 249 | """ 250 | if layout_class is TableLayout: 251 | table_layout = TableLayout.quick_init(id, data, row_id_formatter) 252 | return cls(table_layout) 253 | elif layout_class is TableRow: 254 | row = TableRow.quick_init(id, data) 255 | return cls(row) 256 | else: 257 | raise TypeError("quick not support %s" % (str(layout_class, ))) 258 | 259 | def set_layout(self, layout): 260 | """ 261 | 262 | :return: 263 | """ 264 | 265 | self.layout = layout 266 | 267 | def get_layout(self) -> LayoutProxy: 268 | """ 269 | 270 | :return: 271 | :rtype:LayoutProxy 272 | """ 273 | return LayoutProxy(self, self.layout) 274 | 275 | def enable_debug(self, width=50, height=10): 276 | self.debug = True 277 | self.debug_width = width 278 | self.debug_height = height 279 | 280 | def get_terminal_size(self): 281 | if self.debug: 282 | self.height = self.debug_height 283 | self.width = self.debug_width 284 | else: 285 | size = get_terminal_size() 286 | self.height = size.lines 287 | self.width = size.columns 288 | 289 | if platform.system() == 'Windows': 290 | self.width -= 1 291 | 292 | return self.width, self.height 293 | 294 | def update_width(self): 295 | self.get_terminal_size() 296 | self.layout.update_width(self.width) 297 | 298 | def draw(self, auto_re_draw=True): 299 | term_init() 300 | self.auto_re_draw = auto_re_draw 301 | self.version += 1 302 | self.update_width() 303 | self.layout.set_terminal_size(*self.get_terminal_size()) 304 | self.layout.draw() 305 | 306 | sys.stdout.write('\n') 307 | sys.stdout.flush() 308 | if auto_re_draw: 309 | self.refresh_thread.start() 310 | 311 | def clear(self): 312 | self.layout.clear() 313 | 314 | def re_draw(self): 315 | self.refresh_lock.acquire() 316 | self.clear() 317 | 318 | self.update_width() 319 | self.layout.set_terminal_size(*self.get_terminal_size()) 320 | 321 | self.layout.draw() 322 | 323 | sys.stdout.write('\n') 324 | sys.stdout.flush() 325 | self.refresh_lock.release() 326 | 327 | @staticmethod 328 | def refresh(ctl): 329 | while True: 330 | if ctl.is_stop() or ctl.refresh_thread_stop: 331 | break 332 | time.sleep(0.1) 333 | ctl.re_draw() 334 | 335 | def find_view_by_id(self, id) -> Union[TextViewProxy, LayoutProxy, None]: 336 | """ 337 | :rtype: Union[TextViewProxy, LayoutProxy, None] 338 | """ 339 | v = self.layout.find_view_by_id(id) 340 | if not v: 341 | return None 342 | if isinstance(v, TextView): 343 | return TextViewProxy(self, v) 344 | else: 345 | return LayoutProxy(self, v) 346 | 347 | def stop(self): 348 | if not self.is_stop(): 349 | self._stop_flag = True 350 | self.refresh_thread.join() 351 | self.re_draw() 352 | -------------------------------------------------------------------------------- /terminal_layout/ansi/back.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from terminal_layout.ansi.font import Font 3 | 4 | 5 | class Back(object): 6 | black = Font('black', 'back', 40) 7 | red = Font('red', 'back', 41) 8 | green = Font('green', 'back', 42) 9 | yellow = Font('yellow', 'back', 43) 10 | blue = Font('blue', 'back', 44) 11 | magenta = Font('magenta', 'back', 45) 12 | cyan = Font('cyan', 'back', 46) 13 | white = Font('white', 'back', 47) 14 | reset = Font('reset', 'back', 49) 15 | 16 | # these are fairly well supported, but not part of the standard. 17 | lightblack = Font('lightblack', 'back', 100) 18 | lightred = Font('lightred', 'back', 101) 19 | lightgreen = Font('lightgreen', 'back', 102) 20 | lightyellow = Font('lightyellow', 'back', 103) 21 | lightblue = Font('lightblue', 'back', 104) 22 | lightmagenta = Font('lightmagenta', 'back', 105) 23 | lightcyan = Font('lightcyan', 'back', 106) 24 | lightwhite = Font('lightwhite', 'back', 107) 25 | 26 | # ex_xx only support linux, mac 27 | 28 | ex_black = Font('ex_black', 'back') 29 | ex_red = Font('ex_red', 'back') 30 | ex_green = Font('ex_green', 'back') 31 | ex_yellow = Font('ex_yellow', 'back') 32 | ex_blue = Font('ex_blue', 'back') 33 | ex_magenta = Font('ex_magenta', 'back') 34 | ex_cyan = Font('ex_cyan', 'back') 35 | ex_light_gray = Font('ex_light_gray', 'back') 36 | ex_dark_gray = Font('ex_dark_gray', 'back') 37 | ex_light_red = Font('ex_light_red', 'back') 38 | ex_light_green = Font('ex_light_green', 'back') 39 | ex_light_yellow = Font('ex_light_yellow', 'back') 40 | ex_light_blue = Font('ex_light_blue', 'back') 41 | ex_light_magenta = Font('ex_light_magenta', 'back') 42 | ex_light_cyan = Font('ex_light_cyan', 'back') 43 | ex_white = Font('ex_white', 'back') 44 | ex_grey_0 = Font('ex_grey_0', 'back') 45 | ex_navy_blue = Font('ex_navy_blue', 'back') 46 | ex_dark_blue = Font('ex_dark_blue', 'back') 47 | ex_blue_3a = Font('ex_blue_3a', 'back') 48 | ex_blue_3b = Font('ex_blue_3b', 'back') 49 | ex_blue_1 = Font('ex_blue_1', 'back') 50 | ex_dark_green = Font('ex_dark_green', 'back') 51 | ex_deep_sky_blue_4a = Font('ex_deep_sky_blue_4a', 'back') 52 | ex_deep_sky_blue_4b = Font('ex_deep_sky_blue_4b', 'back') 53 | ex_deep_sky_blue_4c = Font('ex_deep_sky_blue_4c', 'back') 54 | ex_dodger_blue_3 = Font('ex_dodger_blue_3', 'back') 55 | ex_dodger_blue_2 = Font('ex_dodger_blue_2', 'back') 56 | ex_green_4 = Font('ex_green_4', 'back') 57 | ex_spring_green_4 = Font('ex_spring_green_4', 'back') 58 | ex_turquoise_4 = Font('ex_turquoise_4', 'back') 59 | ex_deep_sky_blue_3a = Font('ex_deep_sky_blue_3a', 'back') 60 | ex_deep_sky_blue_3b = Font('ex_deep_sky_blue_3b', 'back') 61 | ex_dodger_blue_1 = Font('ex_dodger_blue_1', 'back') 62 | ex_green_3a = Font('ex_green_3a', 'back') 63 | ex_spring_green_3a = Font('ex_spring_green_3a', 'back') 64 | ex_dark_cyan = Font('ex_dark_cyan', 'back') 65 | ex_light_sea_green = Font('ex_light_sea_green', 'back') 66 | ex_deep_sky_blue_2 = Font('ex_deep_sky_blue_2', 'back') 67 | ex_deep_sky_blue_1 = Font('ex_deep_sky_blue_1', 'back') 68 | ex_green_3b = Font('ex_green_3b', 'back') 69 | ex_spring_green_3b = Font('ex_spring_green_3b', 'back') 70 | ex_spring_green_2a = Font('ex_spring_green_2a', 'back') 71 | ex_cyan_3 = Font('ex_cyan_3', 'back') 72 | ex_dark_turquoise = Font('ex_dark_turquoise', 'back') 73 | ex_turquoise_2 = Font('ex_turquoise_2', 'back') 74 | ex_green_1 = Font('ex_green_1', 'back') 75 | ex_spring_green_2b = Font('ex_spring_green_2b', 'back') 76 | ex_spring_green_1 = Font('ex_spring_green_1', 'back') 77 | ex_medium_spring_green = Font('ex_medium_spring_green', 'back') 78 | ex_cyan_2 = Font('ex_cyan_2', 'back') 79 | ex_cyan_1 = Font('ex_cyan_1', 'back') 80 | ex_dark_red_1 = Font('ex_dark_red_1', 'back') 81 | ex_deep_pink_4a = Font('ex_deep_pink_4a', 'back') 82 | ex_purple_4a = Font('ex_purple_4a', 'back') 83 | ex_purple_4b = Font('ex_purple_4b', 'back') 84 | ex_purple_3 = Font('ex_purple_3', 'back') 85 | ex_blue_violet = Font('ex_blue_violet', 'back') 86 | ex_orange_4a = Font('ex_orange_4a', 'back') 87 | ex_grey_37 = Font('ex_grey_37', 'back') 88 | ex_medium_purple_4 = Font('ex_medium_purple_4', 'back') 89 | ex_slate_blue_3a = Font('ex_slate_blue_3a', 'back') 90 | ex_slate_blue_3b = Font('ex_slate_blue_3b', 'back') 91 | ex_royal_blue_1 = Font('ex_royal_blue_1', 'back') 92 | ex_chartreuse_4 = Font('ex_chartreuse_4', 'back') 93 | ex_dark_sea_green_4a = Font('ex_dark_sea_green_4a', 'back') 94 | ex_pale_turquoise_4 = Font('ex_pale_turquoise_4', 'back') 95 | ex_steel_blue = Font('ex_steel_blue', 'back') 96 | ex_steel_blue_3 = Font('ex_steel_blue_3', 'back') 97 | ex_cornflower_blue = Font('ex_cornflower_blue', 'back') 98 | ex_chartreuse_3a = Font('ex_chartreuse_3a', 'back') 99 | ex_dark_sea_green_4b = Font('ex_dark_sea_green_4b', 'back') 100 | ex_cadet_blue_2 = Font('ex_cadet_blue_2', 'back') 101 | ex_cadet_blue_1 = Font('ex_cadet_blue_1', 'back') 102 | ex_sky_blue_3 = Font('ex_sky_blue_3', 'back') 103 | ex_steel_blue_1a = Font('ex_steel_blue_1a', 'back') 104 | ex_chartreuse_3b = Font('ex_chartreuse_3b', 'back') 105 | ex_pale_green_3a = Font('ex_pale_green_3a', 'back') 106 | ex_sea_green_3 = Font('ex_sea_green_3', 'back') 107 | ex_aquamarine_3 = Font('ex_aquamarine_3', 'back') 108 | ex_medium_turquoise = Font('ex_medium_turquoise', 'back') 109 | ex_steel_blue_1b = Font('ex_steel_blue_1b', 'back') 110 | ex_chartreuse_2a = Font('ex_chartreuse_2a', 'back') 111 | ex_sea_green_2 = Font('ex_sea_green_2', 'back') 112 | ex_sea_green_1a = Font('ex_sea_green_1a', 'back') 113 | ex_sea_green_1b = Font('ex_sea_green_1b', 'back') 114 | ex_aquamarine_1a = Font('ex_aquamarine_1a', 'back') 115 | ex_dark_slate_gray_2 = Font('ex_dark_slate_gray_2', 'back') 116 | ex_dark_red_2 = Font('ex_dark_red_2', 'back') 117 | ex_deep_pink_4b = Font('ex_deep_pink_4b', 'back') 118 | ex_dark_magenta_1 = Font('ex_dark_magenta_1', 'back') 119 | ex_dark_magenta_2 = Font('ex_dark_magenta_2', 'back') 120 | ex_dark_violet_1a = Font('ex_dark_violet_1a', 'back') 121 | ex_purple_1a = Font('ex_purple_1a', 'back') 122 | ex_orange_4b = Font('ex_orange_4b', 'back') 123 | ex_light_pink_4 = Font('ex_light_pink_4', 'back') 124 | ex_plum_4 = Font('ex_plum_4', 'back') 125 | ex_medium_purple_3a = Font('ex_medium_purple_3a', 'back') 126 | ex_medium_purple_3b = Font('ex_medium_purple_3b', 'back') 127 | ex_slate_blue_1 = Font('ex_slate_blue_1', 'back') 128 | ex_yellow_4a = Font('ex_yellow_4a', 'back') 129 | ex_wheat_4 = Font('ex_wheat_4', 'back') 130 | ex_grey_53 = Font('ex_grey_53', 'back') 131 | ex_light_slate_grey = Font('ex_light_slate_grey', 'back') 132 | ex_medium_purple = Font('ex_medium_purple', 'back') 133 | ex_light_slate_blue = Font('ex_light_slate_blue', 'back') 134 | ex_yellow_4b = Font('ex_yellow_4b', 'back') 135 | ex_dark_olive_green_3a = Font('ex_dark_olive_green_3a', 'back') 136 | ex_dark_green_sea = Font('ex_dark_green_sea', 'back') 137 | ex_light_sky_blue_3a = Font('ex_light_sky_blue_3a', 'back') 138 | ex_light_sky_blue_3b = Font('ex_light_sky_blue_3b', 'back') 139 | ex_sky_blue_2 = Font('ex_sky_blue_2', 'back') 140 | ex_chartreuse_2b = Font('ex_chartreuse_2b', 'back') 141 | ex_dark_olive_green_3b = Font('ex_dark_olive_green_3b', 'back') 142 | ex_pale_green_3b = Font('ex_pale_green_3b', 'back') 143 | ex_dark_sea_green_3a = Font('ex_dark_sea_green_3a', 'back') 144 | ex_dark_slate_gray_3 = Font('ex_dark_slate_gray_3', 'back') 145 | ex_sky_blue_1 = Font('ex_sky_blue_1', 'back') 146 | ex_chartreuse_1 = Font('ex_chartreuse_1', 'back') 147 | ex_light_green_2 = Font('ex_light_green_2', 'back') 148 | ex_light_green_3 = Font('ex_light_green_3', 'back') 149 | ex_pale_green_1a = Font('ex_pale_green_1a', 'back') 150 | ex_aquamarine_1b = Font('ex_aquamarine_1b', 'back') 151 | ex_dark_slate_gray_1 = Font('ex_dark_slate_gray_1', 'back') 152 | ex_red_3a = Font('ex_red_3a', 'back') 153 | ex_deep_pink_4c = Font('ex_deep_pink_4c', 'back') 154 | ex_medium_violet_red = Font('ex_medium_violet_red', 'back') 155 | ex_magenta_3a = Font('ex_magenta_3a', 'back') 156 | ex_dark_violet_1b = Font('ex_dark_violet_1b', 'back') 157 | ex_purple_1b = Font('ex_purple_1b', 'back') 158 | ex_dark_orange_3a = Font('ex_dark_orange_3a', 'back') 159 | ex_indian_red_1a = Font('ex_indian_red_1a', 'back') 160 | ex_hot_pink_3a = Font('ex_hot_pink_3a', 'back') 161 | ex_medium_orchid_3 = Font('ex_medium_orchid_3', 'back') 162 | ex_medium_orchid = Font('ex_medium_orchid', 'back') 163 | ex_medium_purple_2a = Font('ex_medium_purple_2a', 'back') 164 | ex_dark_goldenrod = Font('ex_dark_goldenrod', 'back') 165 | ex_light_salmon_3a = Font('ex_light_salmon_3a', 'back') 166 | ex_rosy_brown = Font('ex_rosy_brown', 'back') 167 | ex_grey_63 = Font('ex_grey_63', 'back') 168 | ex_medium_purple_2b = Font('ex_medium_purple_2b', 'back') 169 | ex_medium_purple_1 = Font('ex_medium_purple_1', 'back') 170 | ex_gold_3a = Font('ex_gold_3a', 'back') 171 | ex_dark_khaki = Font('ex_dark_khaki', 'back') 172 | ex_navajo_white_3 = Font('ex_navajo_white_3', 'back') 173 | ex_grey_69 = Font('ex_grey_69', 'back') 174 | ex_light_steel_blue_3 = Font('ex_light_steel_blue_3', 'back') 175 | ex_light_steel_blue = Font('ex_light_steel_blue', 'back') 176 | ex_yellow_3a = Font('ex_yellow_3a', 'back') 177 | ex_dark_olive_green_3 = Font('ex_dark_olive_green_3', 'back') 178 | ex_dark_sea_green_3b = Font('ex_dark_sea_green_3b', 'back') 179 | ex_dark_sea_green_2 = Font('ex_dark_sea_green_2', 'back') 180 | ex_light_cyan_3 = Font('ex_light_cyan_3', 'back') 181 | ex_light_sky_blue_1 = Font('ex_light_sky_blue_1', 'back') 182 | ex_green_yellow = Font('ex_green_yellow', 'back') 183 | ex_dark_olive_green_2 = Font('ex_dark_olive_green_2', 'back') 184 | ex_pale_green_1b = Font('ex_pale_green_1b', 'back') 185 | ex_dark_sea_green_5b = Font('ex_dark_sea_green_5b', 'back') 186 | ex_dark_sea_green_5a = Font('ex_dark_sea_green_5a', 'back') 187 | ex_pale_turquoise_1 = Font('ex_pale_turquoise_1', 'back') 188 | ex_red_3b = Font('ex_red_3b', 'back') 189 | ex_deep_pink_3a = Font('ex_deep_pink_3a', 'back') 190 | ex_deep_pink_3b = Font('ex_deep_pink_3b', 'back') 191 | ex_magenta_3b = Font('ex_magenta_3b', 'back') 192 | ex_magenta_3c = Font('ex_magenta_3c', 'back') 193 | ex_magenta_2a = Font('ex_magenta_2a', 'back') 194 | ex_dark_orange_3b = Font('ex_dark_orange_3b', 'back') 195 | ex_indian_red_1b = Font('ex_indian_red_1b', 'back') 196 | ex_hot_pink_3b = Font('ex_hot_pink_3b', 'back') 197 | ex_hot_pink_2 = Font('ex_hot_pink_2', 'back') 198 | ex_orchid = Font('ex_orchid', 'back') 199 | ex_medium_orchid_1a = Font('ex_medium_orchid_1a', 'back') 200 | ex_orange_3 = Font('ex_orange_3', 'back') 201 | ex_light_salmon_3b = Font('ex_light_salmon_3b', 'back') 202 | ex_light_pink_3 = Font('ex_light_pink_3', 'back') 203 | ex_pink_3 = Font('ex_pink_3', 'back') 204 | ex_plum_3 = Font('ex_plum_3', 'back') 205 | ex_violet = Font('ex_violet', 'back') 206 | ex_gold_3b = Font('ex_gold_3b', 'back') 207 | ex_light_goldenrod_3 = Font('ex_light_goldenrod_3', 'back') 208 | ex_tan = Font('ex_tan', 'back') 209 | ex_misty_rose_3 = Font('ex_misty_rose_3', 'back') 210 | ex_thistle_3 = Font('ex_thistle_3', 'back') 211 | ex_plum_2 = Font('ex_plum_2', 'back') 212 | ex_yellow_3b = Font('ex_yellow_3b', 'back') 213 | ex_khaki_3 = Font('ex_khaki_3', 'back') 214 | ex_light_goldenrod_2a = Font('ex_light_goldenrod_2a', 'back') 215 | ex_light_yellow_3 = Font('ex_light_yellow_3', 'back') 216 | ex_grey_84 = Font('ex_grey_84', 'back') 217 | ex_light_steel_blue_1 = Font('ex_light_steel_blue_1', 'back') 218 | ex_yellow_2 = Font('ex_yellow_2', 'back') 219 | ex_dark_olive_green_1a = Font('ex_dark_olive_green_1a', 'back') 220 | ex_dark_olive_green_1b = Font('ex_dark_olive_green_1b', 'back') 221 | ex_dark_sea_green_1 = Font('ex_dark_sea_green_1', 'back') 222 | ex_honeydew_2 = Font('ex_honeydew_2', 'back') 223 | ex_light_cyan_1 = Font('ex_light_cyan_1', 'back') 224 | ex_red_1 = Font('ex_red_1', 'back') 225 | ex_deep_pink_2 = Font('ex_deep_pink_2', 'back') 226 | ex_deep_pink_1a = Font('ex_deep_pink_1a', 'back') 227 | ex_deep_pink_1b = Font('ex_deep_pink_1b', 'back') 228 | ex_magenta_2b = Font('ex_magenta_2b', 'back') 229 | ex_magenta_1 = Font('ex_magenta_1', 'back') 230 | ex_orange_red_1 = Font('ex_orange_red_1', 'back') 231 | ex_indian_red_1c = Font('ex_indian_red_1c', 'back') 232 | ex_indian_red_1d = Font('ex_indian_red_1d', 'back') 233 | ex_hot_pink_1a = Font('ex_hot_pink_1a', 'back') 234 | ex_hot_pink_1b = Font('ex_hot_pink_1b', 'back') 235 | ex_medium_orchid_1b = Font('ex_medium_orchid_1b', 'back') 236 | ex_dark_orange = Font('ex_dark_orange', 'back') 237 | ex_salmon_1 = Font('ex_salmon_1', 'back') 238 | ex_light_coral = Font('ex_light_coral', 'back') 239 | ex_pale_violet_red_1 = Font('ex_pale_violet_red_1', 'back') 240 | ex_orchid_2 = Font('ex_orchid_2', 'back') 241 | ex_orchid_1 = Font('ex_orchid_1', 'back') 242 | ex_orange_1 = Font('ex_orange_1', 'back') 243 | ex_sandy_brown = Font('ex_sandy_brown', 'back') 244 | ex_light_salmon_1 = Font('ex_light_salmon_1', 'back') 245 | ex_light_pink_1 = Font('ex_light_pink_1', 'back') 246 | ex_pink_1 = Font('ex_pink_1', 'back') 247 | ex_plum_1 = Font('ex_plum_1', 'back') 248 | ex_gold_1 = Font('ex_gold_1', 'back') 249 | ex_light_goldenrod_2b = Font('ex_light_goldenrod_2b', 'back') 250 | ex_light_goldenrod_2c = Font('ex_light_goldenrod_2c', 'back') 251 | ex_navajo_white_1 = Font('ex_navajo_white_1', 'back') 252 | ex_misty_rose1 = Font('ex_misty_rose1', 'back') 253 | ex_thistle_1 = Font('ex_thistle_1', 'back') 254 | ex_yellow_1 = Font('ex_yellow_1', 'back') 255 | ex_light_goldenrod_1 = Font('ex_light_goldenrod_1', 'back') 256 | ex_khaki_1 = Font('ex_khaki_1', 'back') 257 | ex_wheat_1 = Font('ex_wheat_1', 'back') 258 | ex_cornsilk_1 = Font('ex_cornsilk_1', 'back') 259 | ex_grey_100 = Font('ex_grey_100', 'back') 260 | ex_grey_3 = Font('ex_grey_3', 'back') 261 | ex_grey_7 = Font('ex_grey_7', 'back') 262 | ex_grey_11 = Font('ex_grey_11', 'back') 263 | ex_grey_15 = Font('ex_grey_15', 'back') 264 | ex_grey_19 = Font('ex_grey_19', 'back') 265 | ex_grey_23 = Font('ex_grey_23', 'back') 266 | ex_grey_27 = Font('ex_grey_27', 'back') 267 | ex_grey_30 = Font('ex_grey_30', 'back') 268 | ex_grey_35 = Font('ex_grey_35', 'back') 269 | ex_grey_39 = Font('ex_grey_39', 'back') 270 | ex_grey_42 = Font('ex_grey_42', 'back') 271 | ex_grey_46 = Font('ex_grey_46', 'back') 272 | ex_grey_50 = Font('ex_grey_50', 'back') 273 | ex_grey_54 = Font('ex_grey_54', 'back') 274 | ex_grey_58 = Font('ex_grey_58', 'back') 275 | ex_grey_62 = Font('ex_grey_62', 'back') 276 | ex_grey_66 = Font('ex_grey_66', 'back') 277 | ex_grey_70 = Font('ex_grey_70', 'back') 278 | ex_grey_74 = Font('ex_grey_74', 'back') 279 | ex_grey_78 = Font('ex_grey_78', 'back') 280 | ex_grey_82 = Font('ex_grey_82', 'back') 281 | ex_grey_85 = Font('ex_grey_85', 'back') 282 | ex_grey_89 = Font('ex_grey_89', 'back') 283 | ex_grey_93 = Font('ex_grey_93', 'back') 284 | -------------------------------------------------------------------------------- /terminal_layout/ansi/fore.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from terminal_layout.ansi.font import Font 3 | 4 | 5 | class Fore(object): 6 | black = Font('black', 'fore', 30) 7 | red = Font('red', 'fore', 31) 8 | green = Font('green', 'fore', 32) 9 | yellow = Font('yellow', 'fore', 33) 10 | blue = Font('blue', 'fore', 34) 11 | magenta = Font('magenta', 'fore', 35) 12 | cyan = Font('cyan', 'fore', 36) 13 | white = Font('white', 'fore', 37) 14 | reset = Font('reset', 'fore', 39) 15 | 16 | # these are fairly well supported, but not part of the standard. 17 | lightblack = Font('lightblack', 'fore', 90) 18 | lightred = Font('lightred', 'fore', 91) 19 | lightgreen = Font('lightgreen', 'fore', 92) 20 | lightyellow = Font('lightyellow', 'fore', 93) 21 | lightblue = Font('lightblue', 'fore', 94) 22 | lightmagenta = Font('lightmagenta', 'fore', 95) 23 | lightcyan = Font('lightcyan', 'fore', 96) 24 | lightwhite = Font('lightwhite', 'fore', 97) 25 | 26 | # ex_xx only support linux, mac 27 | 28 | ex_black = Font('ex_black', 'fore') 29 | ex_red = Font('ex_red', 'fore') 30 | ex_green = Font('ex_green', 'fore') 31 | ex_yellow = Font('ex_yellow', 'fore') 32 | ex_blue = Font('ex_blue', 'fore') 33 | ex_magenta = Font('ex_magenta', 'fore') 34 | ex_cyan = Font('ex_cyan', 'fore') 35 | ex_light_gray = Font('ex_light_gray', 'fore') 36 | ex_dark_gray = Font('ex_dark_gray', 'fore') 37 | ex_light_red = Font('ex_light_red', 'fore') 38 | ex_light_green = Font('ex_light_green', 'fore') 39 | ex_light_yellow = Font('ex_light_yellow', 'fore') 40 | ex_light_blue = Font('ex_light_blue', 'fore') 41 | ex_light_magenta = Font('ex_light_magenta', 'fore') 42 | ex_light_cyan = Font('ex_light_cyan', 'fore') 43 | ex_white = Font('ex_white', 'fore') 44 | ex_grey_0 = Font('ex_grey_0', 'fore') 45 | ex_navy_blue = Font('ex_navy_blue', 'fore') 46 | ex_dark_blue = Font('ex_dark_blue', 'fore') 47 | ex_blue_3a = Font('ex_blue_3a', 'fore') 48 | ex_blue_3b = Font('ex_blue_3b', 'fore') 49 | ex_blue_1 = Font('ex_blue_1', 'fore') 50 | ex_dark_green = Font('ex_dark_green', 'fore') 51 | ex_deep_sky_blue_4a = Font('ex_deep_sky_blue_4a', 'fore') 52 | ex_deep_sky_blue_4b = Font('ex_deep_sky_blue_4b', 'fore') 53 | ex_deep_sky_blue_4c = Font('ex_deep_sky_blue_4c', 'fore') 54 | ex_dodger_blue_3 = Font('ex_dodger_blue_3', 'fore') 55 | ex_dodger_blue_2 = Font('ex_dodger_blue_2', 'fore') 56 | ex_green_4 = Font('ex_green_4', 'fore') 57 | ex_spring_green_4 = Font('ex_spring_green_4', 'fore') 58 | ex_turquoise_4 = Font('ex_turquoise_4', 'fore') 59 | ex_deep_sky_blue_3a = Font('ex_deep_sky_blue_3a', 'fore') 60 | ex_deep_sky_blue_3b = Font('ex_deep_sky_blue_3b', 'fore') 61 | ex_dodger_blue_1 = Font('ex_dodger_blue_1', 'fore') 62 | ex_green_3a = Font('ex_green_3a', 'fore') 63 | ex_spring_green_3a = Font('ex_spring_green_3a', 'fore') 64 | ex_dark_cyan = Font('ex_dark_cyan', 'fore') 65 | ex_light_sea_green = Font('ex_light_sea_green', 'fore') 66 | ex_deep_sky_blue_2 = Font('ex_deep_sky_blue_2', 'fore') 67 | ex_deep_sky_blue_1 = Font('ex_deep_sky_blue_1', 'fore') 68 | ex_green_3b = Font('ex_green_3b', 'fore') 69 | ex_spring_green_3b = Font('ex_spring_green_3b', 'fore') 70 | ex_spring_green_2a = Font('ex_spring_green_2a', 'fore') 71 | ex_cyan_3 = Font('ex_cyan_3', 'fore') 72 | ex_dark_turquoise = Font('ex_dark_turquoise', 'fore') 73 | ex_turquoise_2 = Font('ex_turquoise_2', 'fore') 74 | ex_green_1 = Font('ex_green_1', 'fore') 75 | ex_spring_green_2b = Font('ex_spring_green_2b', 'fore') 76 | ex_spring_green_1 = Font('ex_spring_green_1', 'fore') 77 | ex_medium_spring_green = Font('ex_medium_spring_green', 'fore') 78 | ex_cyan_2 = Font('ex_cyan_2', 'fore') 79 | ex_cyan_1 = Font('ex_cyan_1', 'fore') 80 | ex_dark_red_1 = Font('ex_dark_red_1', 'fore') 81 | ex_deep_pink_4a = Font('ex_deep_pink_4a', 'fore') 82 | ex_purple_4a = Font('ex_purple_4a', 'fore') 83 | ex_purple_4b = Font('ex_purple_4b', 'fore') 84 | ex_purple_3 = Font('ex_purple_3', 'fore') 85 | ex_blue_violet = Font('ex_blue_violet', 'fore') 86 | ex_orange_4a = Font('ex_orange_4a', 'fore') 87 | ex_grey_37 = Font('ex_grey_37', 'fore') 88 | ex_medium_purple_4 = Font('ex_medium_purple_4', 'fore') 89 | ex_slate_blue_3a = Font('ex_slate_blue_3a', 'fore') 90 | ex_slate_blue_3b = Font('ex_slate_blue_3b', 'fore') 91 | ex_royal_blue_1 = Font('ex_royal_blue_1', 'fore') 92 | ex_chartreuse_4 = Font('ex_chartreuse_4', 'fore') 93 | ex_dark_sea_green_4a = Font('ex_dark_sea_green_4a', 'fore') 94 | ex_pale_turquoise_4 = Font('ex_pale_turquoise_4', 'fore') 95 | ex_steel_blue = Font('ex_steel_blue', 'fore') 96 | ex_steel_blue_3 = Font('ex_steel_blue_3', 'fore') 97 | ex_cornflower_blue = Font('ex_cornflower_blue', 'fore') 98 | ex_chartreuse_3a = Font('ex_chartreuse_3a', 'fore') 99 | ex_dark_sea_green_4b = Font('ex_dark_sea_green_4b', 'fore') 100 | ex_cadet_blue_2 = Font('ex_cadet_blue_2', 'fore') 101 | ex_cadet_blue_1 = Font('ex_cadet_blue_1', 'fore') 102 | ex_sky_blue_3 = Font('ex_sky_blue_3', 'fore') 103 | ex_steel_blue_1a = Font('ex_steel_blue_1a', 'fore') 104 | ex_chartreuse_3b = Font('ex_chartreuse_3b', 'fore') 105 | ex_pale_green_3a = Font('ex_pale_green_3a', 'fore') 106 | ex_sea_green_3 = Font('ex_sea_green_3', 'fore') 107 | ex_aquamarine_3 = Font('ex_aquamarine_3', 'fore') 108 | ex_medium_turquoise = Font('ex_medium_turquoise', 'fore') 109 | ex_steel_blue_1b = Font('ex_steel_blue_1b', 'fore') 110 | ex_chartreuse_2a = Font('ex_chartreuse_2a', 'fore') 111 | ex_sea_green_2 = Font('ex_sea_green_2', 'fore') 112 | ex_sea_green_1a = Font('ex_sea_green_1a', 'fore') 113 | ex_sea_green_1b = Font('ex_sea_green_1b', 'fore') 114 | ex_aquamarine_1a = Font('ex_aquamarine_1a', 'fore') 115 | ex_dark_slate_gray_2 = Font('ex_dark_slate_gray_2', 'fore') 116 | ex_dark_red_2 = Font('ex_dark_red_2', 'fore') 117 | ex_deep_pink_4b = Font('ex_deep_pink_4b', 'fore') 118 | ex_dark_magenta_1 = Font('ex_dark_magenta_1', 'fore') 119 | ex_dark_magenta_2 = Font('ex_dark_magenta_2', 'fore') 120 | ex_dark_violet_1a = Font('ex_dark_violet_1a', 'fore') 121 | ex_purple_1a = Font('ex_purple_1a', 'fore') 122 | ex_orange_4b = Font('ex_orange_4b', 'fore') 123 | ex_light_pink_4 = Font('ex_light_pink_4', 'fore') 124 | ex_plum_4 = Font('ex_plum_4', 'fore') 125 | ex_medium_purple_3a = Font('ex_medium_purple_3a', 'fore') 126 | ex_medium_purple_3b = Font('ex_medium_purple_3b', 'fore') 127 | ex_slate_blue_1 = Font('ex_slate_blue_1', 'fore') 128 | ex_yellow_4a = Font('ex_yellow_4a', 'fore') 129 | ex_wheat_4 = Font('ex_wheat_4', 'fore') 130 | ex_grey_53 = Font('ex_grey_53', 'fore') 131 | ex_light_slate_grey = Font('ex_light_slate_grey', 'fore') 132 | ex_medium_purple = Font('ex_medium_purple', 'fore') 133 | ex_light_slate_blue = Font('ex_light_slate_blue', 'fore') 134 | ex_yellow_4b = Font('ex_yellow_4b', 'fore') 135 | ex_dark_olive_green_3a = Font('ex_dark_olive_green_3a', 'fore') 136 | ex_dark_green_sea = Font('ex_dark_green_sea', 'fore') 137 | ex_light_sky_blue_3a = Font('ex_light_sky_blue_3a', 'fore') 138 | ex_light_sky_blue_3b = Font('ex_light_sky_blue_3b', 'fore') 139 | ex_sky_blue_2 = Font('ex_sky_blue_2', 'fore') 140 | ex_chartreuse_2b = Font('ex_chartreuse_2b', 'fore') 141 | ex_dark_olive_green_3b = Font('ex_dark_olive_green_3b', 'fore') 142 | ex_pale_green_3b = Font('ex_pale_green_3b', 'fore') 143 | ex_dark_sea_green_3a = Font('ex_dark_sea_green_3a', 'fore') 144 | ex_dark_slate_gray_3 = Font('ex_dark_slate_gray_3', 'fore') 145 | ex_sky_blue_1 = Font('ex_sky_blue_1', 'fore') 146 | ex_chartreuse_1 = Font('ex_chartreuse_1', 'fore') 147 | ex_light_green_2 = Font('ex_light_green_2', 'fore') 148 | ex_light_green_3 = Font('ex_light_green_3', 'fore') 149 | ex_pale_green_1a = Font('ex_pale_green_1a', 'fore') 150 | ex_aquamarine_1b = Font('ex_aquamarine_1b', 'fore') 151 | ex_dark_slate_gray_1 = Font('ex_dark_slate_gray_1', 'fore') 152 | ex_red_3a = Font('ex_red_3a', 'fore') 153 | ex_deep_pink_4c = Font('ex_deep_pink_4c', 'fore') 154 | ex_medium_violet_red = Font('ex_medium_violet_red', 'fore') 155 | ex_magenta_3a = Font('ex_magenta_3a', 'fore') 156 | ex_dark_violet_1b = Font('ex_dark_violet_1b', 'fore') 157 | ex_purple_1b = Font('ex_purple_1b', 'fore') 158 | ex_dark_orange_3a = Font('ex_dark_orange_3a', 'fore') 159 | ex_indian_red_1a = Font('ex_indian_red_1a', 'fore') 160 | ex_hot_pink_3a = Font('ex_hot_pink_3a', 'fore') 161 | ex_medium_orchid_3 = Font('ex_medium_orchid_3', 'fore') 162 | ex_medium_orchid = Font('ex_medium_orchid', 'fore') 163 | ex_medium_purple_2a = Font('ex_medium_purple_2a', 'fore') 164 | ex_dark_goldenrod = Font('ex_dark_goldenrod', 'fore') 165 | ex_light_salmon_3a = Font('ex_light_salmon_3a', 'fore') 166 | ex_rosy_brown = Font('ex_rosy_brown', 'fore') 167 | ex_grey_63 = Font('ex_grey_63', 'fore') 168 | ex_medium_purple_2b = Font('ex_medium_purple_2b', 'fore') 169 | ex_medium_purple_1 = Font('ex_medium_purple_1', 'fore') 170 | ex_gold_3a = Font('ex_gold_3a', 'fore') 171 | ex_dark_khaki = Font('ex_dark_khaki', 'fore') 172 | ex_navajo_white_3 = Font('ex_navajo_white_3', 'fore') 173 | ex_grey_69 = Font('ex_grey_69', 'fore') 174 | ex_light_steel_blue_3 = Font('ex_light_steel_blue_3', 'fore') 175 | ex_light_steel_blue = Font('ex_light_steel_blue', 'fore') 176 | ex_yellow_3a = Font('ex_yellow_3a', 'fore') 177 | ex_dark_olive_green_3 = Font('ex_dark_olive_green_3', 'fore') 178 | ex_dark_sea_green_3b = Font('ex_dark_sea_green_3b', 'fore') 179 | ex_dark_sea_green_2 = Font('ex_dark_sea_green_2', 'fore') 180 | ex_light_cyan_3 = Font('ex_light_cyan_3', 'fore') 181 | ex_light_sky_blue_1 = Font('ex_light_sky_blue_1', 'fore') 182 | ex_green_yellow = Font('ex_green_yellow', 'fore') 183 | ex_dark_olive_green_2 = Font('ex_dark_olive_green_2', 'fore') 184 | ex_pale_green_1b = Font('ex_pale_green_1b', 'fore') 185 | ex_dark_sea_green_5b = Font('ex_dark_sea_green_5b', 'fore') 186 | ex_dark_sea_green_5a = Font('ex_dark_sea_green_5a', 'fore') 187 | ex_pale_turquoise_1 = Font('ex_pale_turquoise_1', 'fore') 188 | ex_red_3b = Font('ex_red_3b', 'fore') 189 | ex_deep_pink_3a = Font('ex_deep_pink_3a', 'fore') 190 | ex_deep_pink_3b = Font('ex_deep_pink_3b', 'fore') 191 | ex_magenta_3b = Font('ex_magenta_3b', 'fore') 192 | ex_magenta_3c = Font('ex_magenta_3c', 'fore') 193 | ex_magenta_2a = Font('ex_magenta_2a', 'fore') 194 | ex_dark_orange_3b = Font('ex_dark_orange_3b', 'fore') 195 | ex_indian_red_1b = Font('ex_indian_red_1b', 'fore') 196 | ex_hot_pink_3b = Font('ex_hot_pink_3b', 'fore') 197 | ex_hot_pink_2 = Font('ex_hot_pink_2', 'fore') 198 | ex_orchid = Font('ex_orchid', 'fore') 199 | ex_medium_orchid_1a = Font('ex_medium_orchid_1a', 'fore') 200 | ex_orange_3 = Font('ex_orange_3', 'fore') 201 | ex_light_salmon_3b = Font('ex_light_salmon_3b', 'fore') 202 | ex_light_pink_3 = Font('ex_light_pink_3', 'fore') 203 | ex_pink_3 = Font('ex_pink_3', 'fore') 204 | ex_plum_3 = Font('ex_plum_3', 'fore') 205 | ex_violet = Font('ex_violet', 'fore') 206 | ex_gold_3b = Font('ex_gold_3b', 'fore') 207 | ex_light_goldenrod_3 = Font('ex_light_goldenrod_3', 'fore') 208 | ex_tan = Font('ex_tan', 'fore') 209 | ex_misty_rose_3 = Font('ex_misty_rose_3', 'fore') 210 | ex_thistle_3 = Font('ex_thistle_3', 'fore') 211 | ex_plum_2 = Font('ex_plum_2', 'fore') 212 | ex_yellow_3b = Font('ex_yellow_3b', 'fore') 213 | ex_khaki_3 = Font('ex_khaki_3', 'fore') 214 | ex_light_goldenrod_2a = Font('ex_light_goldenrod_2a', 'fore') 215 | ex_light_yellow_3 = Font('ex_light_yellow_3', 'fore') 216 | ex_grey_84 = Font('ex_grey_84', 'fore') 217 | ex_light_steel_blue_1 = Font('ex_light_steel_blue_1', 'fore') 218 | ex_yellow_2 = Font('ex_yellow_2', 'fore') 219 | ex_dark_olive_green_1a = Font('ex_dark_olive_green_1a', 'fore') 220 | ex_dark_olive_green_1b = Font('ex_dark_olive_green_1b', 'fore') 221 | ex_dark_sea_green_1 = Font('ex_dark_sea_green_1', 'fore') 222 | ex_honeydew_2 = Font('ex_honeydew_2', 'fore') 223 | ex_light_cyan_1 = Font('ex_light_cyan_1', 'fore') 224 | ex_red_1 = Font('ex_red_1', 'fore') 225 | ex_deep_pink_2 = Font('ex_deep_pink_2', 'fore') 226 | ex_deep_pink_1a = Font('ex_deep_pink_1a', 'fore') 227 | ex_deep_pink_1b = Font('ex_deep_pink_1b', 'fore') 228 | ex_magenta_2b = Font('ex_magenta_2b', 'fore') 229 | ex_magenta_1 = Font('ex_magenta_1', 'fore') 230 | ex_orange_red_1 = Font('ex_orange_red_1', 'fore') 231 | ex_indian_red_1c = Font('ex_indian_red_1c', 'fore') 232 | ex_indian_red_1d = Font('ex_indian_red_1d', 'fore') 233 | ex_hot_pink_1a = Font('ex_hot_pink_1a', 'fore') 234 | ex_hot_pink_1b = Font('ex_hot_pink_1b', 'fore') 235 | ex_medium_orchid_1b = Font('ex_medium_orchid_1b', 'fore') 236 | ex_dark_orange = Font('ex_dark_orange', 'fore') 237 | ex_salmon_1 = Font('ex_salmon_1', 'fore') 238 | ex_light_coral = Font('ex_light_coral', 'fore') 239 | ex_pale_violet_red_1 = Font('ex_pale_violet_red_1', 'fore') 240 | ex_orchid_2 = Font('ex_orchid_2', 'fore') 241 | ex_orchid_1 = Font('ex_orchid_1', 'fore') 242 | ex_orange_1 = Font('ex_orange_1', 'fore') 243 | ex_sandy_brown = Font('ex_sandy_brown', 'fore') 244 | ex_light_salmon_1 = Font('ex_light_salmon_1', 'fore') 245 | ex_light_pink_1 = Font('ex_light_pink_1', 'fore') 246 | ex_pink_1 = Font('ex_pink_1', 'fore') 247 | ex_plum_1 = Font('ex_plum_1', 'fore') 248 | ex_gold_1 = Font('ex_gold_1', 'fore') 249 | ex_light_goldenrod_2b = Font('ex_light_goldenrod_2b', 'fore') 250 | ex_light_goldenrod_2c = Font('ex_light_goldenrod_2c', 'fore') 251 | ex_navajo_white_1 = Font('ex_navajo_white_1', 'fore') 252 | ex_misty_rose1 = Font('ex_misty_rose1', 'fore') 253 | ex_thistle_1 = Font('ex_thistle_1', 'fore') 254 | ex_yellow_1 = Font('ex_yellow_1', 'fore') 255 | ex_light_goldenrod_1 = Font('ex_light_goldenrod_1', 'fore') 256 | ex_khaki_1 = Font('ex_khaki_1', 'fore') 257 | ex_wheat_1 = Font('ex_wheat_1', 'fore') 258 | ex_cornsilk_1 = Font('ex_cornsilk_1', 'fore') 259 | ex_grey_100 = Font('ex_grey_100', 'fore') 260 | ex_grey_3 = Font('ex_grey_3', 'fore') 261 | ex_grey_7 = Font('ex_grey_7', 'fore') 262 | ex_grey_11 = Font('ex_grey_11', 'fore') 263 | ex_grey_15 = Font('ex_grey_15', 'fore') 264 | ex_grey_19 = Font('ex_grey_19', 'fore') 265 | ex_grey_23 = Font('ex_grey_23', 'fore') 266 | ex_grey_27 = Font('ex_grey_27', 'fore') 267 | ex_grey_30 = Font('ex_grey_30', 'fore') 268 | ex_grey_35 = Font('ex_grey_35', 'fore') 269 | ex_grey_39 = Font('ex_grey_39', 'fore') 270 | ex_grey_42 = Font('ex_grey_42', 'fore') 271 | ex_grey_46 = Font('ex_grey_46', 'fore') 272 | ex_grey_50 = Font('ex_grey_50', 'fore') 273 | ex_grey_54 = Font('ex_grey_54', 'fore') 274 | ex_grey_58 = Font('ex_grey_58', 'fore') 275 | ex_grey_62 = Font('ex_grey_62', 'fore') 276 | ex_grey_66 = Font('ex_grey_66', 'fore') 277 | ex_grey_70 = Font('ex_grey_70', 'fore') 278 | ex_grey_74 = Font('ex_grey_74', 'fore') 279 | ex_grey_78 = Font('ex_grey_78', 'fore') 280 | ex_grey_82 = Font('ex_grey_82', 'fore') 281 | ex_grey_85 = Font('ex_grey_85', 'fore') 282 | ex_grey_89 = Font('ex_grey_89', 'fore') 283 | ex_grey_93 = Font('ex_grey_93', 'fore') 284 | 285 | 286 | --------------------------------------------------------------------------------