使用特殊字符实现console UI
我们经常见到一些开源软件在安装或运行时,实现了某种程度上的基于Linux命令行终端的界面,使我们可以更直观的感受软件运行情况或进度, 例如下面的测试进度变化:
使用Python时,我们可以通过print()函数和某些特殊字符达到上面的效果。
先看一下Python的print()函数定义,从下面的帮助文档,我们可以看到print()有4个关键字参数: sep, end, file, flush, 其中sep表示当打印多个参数时,各参数间的分隔符,默认为一个空格;end表示一次打印结束时添加的结尾标识符,默认为换行符;file为类似文件的字节流对象,默认为sys.stdout; flush 指示是否强制将缓冲区数据写入输出设备,对于sys.stdout来讲,就是显示器。
print(...) print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False) Prints the values to a stream, or to sys.stdout by default. Optional keyword arguments: file: a file-like object (stream); defaults to the current sys.stdout. sep: string inserted between values, default a space. end: string appended after the last value, default a newline. flush: whether to forcibly flush the stream.
再看一下转义字符'\r', 该字符使的输入位置重新回到本行开头。利用print()的end参数和'\r', 我们可以实现一个的进度条:
代码如下:
import time for i in range(1, 11): percent = i * 10 if i < 10: bar = '[' + '=' * (i * 5) + '>]' print('{}% {}\r'.format(percent, bar), end='') else: bar = '[' + '=' * (i * 5) + ']' print('{}% {}\r'.format(percent, bar)) time.sleep(1)
我们也可以使用特定的字符组合控制输出的颜色, 例如
代码如下:
#utils/textcolor.py class TextColor: NONE = "\033[0m" RED = "\033[0;31m" BLACK = "\033[0;30m" WHITE = "\033[1;37m" GREEN = "\033[0;32m" BLUE = "\033[0;34m" YELLOW = "\033[1;33m" BROWN = "\033[0;33m" CYAN = "\033[0;36m" PURPLE = "\033[0;35m" DARK_GRAY = "\033[1;30m" LIGHT_BLUE = "\033[1;34m" LIGHT_GREEN = "\033[1;32m" LIGHT_CYAN = "\033[1;36m" LIGHT_RED = "\033[1;31m" LIGHT_PURPLE = "\033[1;35m" LIGHT_GRAY = "\033[0;37m" # colors.py from utils.textcolor import TextColor print(TextColor.RED + '红色' + TextColor.NONE, end=' ') print(TextColor.GREEN + '绿色' + TextColor.NONE, end=' ') print(TextColor.BLUE + '蓝色' + TextColor.NONE, end=' ') print(TextColor.YELLOW + '黄色' + TextColor.NONE, end=' ') print(TextColor.WHITE + '白色' + TextColor.NONE, end=' ') print(TextColor.LIGHT_GRAY + '浅灰色')
其实,添加颜色只是ANSI控制码中的一种, ANSI控制码可以帮我们实现很多功能,一个完整的列表如下:
\033[0m 关闭所有属性 \033[1m 设置高亮度 \033[4m 下划线 \033[5m 闪烁 \033[7m 反显 \033[8m 消隐 \033[30m -- \033[37m 设置前景色 \033[40m -- \033[47m 设置背景色 \033[nA 光标上移n行 \033[nB 光标下移n行 \033[nC 光标右移n行 \033[nD 光标左移n行 \033[y;xH设置光标位置 \033[2J 清屏 \033[K 清除从光标到行尾的内容 \033[s 保存光标位置 \033[u 恢复光标位置 \033[?25l 隐藏光标 \033[?25h 显示光标
现在回到本文的开始:如何实现类似test.py的效果?这其中最主要的难点是我们不仅要修改/覆盖当前行的输出内容,还要修改/覆盖非当前行的内容。一个最直接简单的思路是我们用python dict变量记录所有输出内容和它相对应的打印输出时的行号(需注意的是,如果运行过程中涉及到滚屏,所记录的的行号也应该相应递减: 每滚动一行,所记录的行号减1),在我们准备更新某条信息时,先查找它被输出到了哪一行,然后把光标定位到相应行,打印输出更新后的信息,最后把光标恢复到当前行。除此之外,我们可能还希望在整个过程中隐藏光标,屏蔽键盘输入回显。基于以上所述,下列特殊字符将会被用到:
\033[y;xH设置光标位置 \033[K 清除从光标到行尾的内容 \033[s 保存光标位置 \033[u 恢复光标位置 \033[?25l 隐藏光标 \033[?25h 显示光标
除此外,我们用打印”\x1b[6n“时的输出来得到当前光标的位置,源代码如下:
test.py:
1 import time 2 import os 3 from os import path 4 import sys 5 6 sys.path.append(path.dirname(__file__)) 7 import utils.termops_new as termops 8 from utils.termops_new import TextColor 9 10 script_status_list = [ 11 ('tests/test1.py', 'Not Started'), 12 ('tests/test1.py', 'Running'), 13 ('tests/test2.py', 'Not Started'), 14 ('tests/test1.py', 'Finished'), 15 ('tests/test2.py', 'Running'), 16 ('tests/test3.py', 'Not Started'), 17 ('tests/test2.py', 'Failed'), 18 ('tests/test3.py', 'Running'), 19 ('tests/test4.py', 'Not Started'), 20 ('tests/test4.py', 'Finished'), 21 ('tests/test3.py', 'Finished'), 22 ] 23 24 25 def colored_status(status): 26 if status == 'Not Started': 27 return TextColor.LIGHT_GRAY + status + TextColor.NONE 28 elif status == 'Running': 29 return TextColor.WHITE + status + TextColor.NONE 30 elif status == 'Finished': 31 return TextColor.GREEN + status + TextColor.NONE 32 elif status == 'Failed': 33 return TextColor.RED+ status + TextColor.NONE 34 35 36 def formatted_msg(test, status): 37 return '\t' + test + '-'*20 + '[' + colored_status(status) + ']' 38 39 40 if __name__ == '__main__': 41 termsize = os.get_terminal_size() 42 oldattr = termops.disable_term_echo() 43 termops.hide_cursor() 44 pos_map = dict() 45 46 try: 47 # firstly, output all inital status 48 for test, status in script_status_list: 49 time.sleep(1) 50 51 if test in pos_map.keys(): 52 # Update printed information 53 termops.save_cursor_pos() 54 x, y = pos_map[test] 55 termops.print_at(x, y, formatted_msg(test, status)) 56 time.sleep(1) 57 termops.restore_cursor_pos() 58 else: 59 # First print 60 x, y = termops.get_cursor_pos() 61 62 # save output position for later update 63 pos_map[test] = (x, y) 64 termops.print_at(x, y, formatted_msg(test, status), end='\n') 65 time.sleep(1) 66 67 # if scroll up, all printed lines should be moved 1 line up 68 if x == termsize.lines: 69 for k in pos_map.keys(): 70 oldx, oldy = pos_map[k] 71 pos_map[k] = (oldx-1, oldy) 72 finally: 73 termops.enable_term_echo(oldattr) 74 termops.show_cursor() 75 76 print('')
utils/termops.py
1 import re 2 import sys 3 import termios 4 import tty 5 6 7 class TextColor: 8 NONE = "\033[0m" 9 RED = "\033[0;31m" 10 BLACK = "\033[0;30m" 11 WHITE = "\033[1;37m" 12 GREEN = "\033[0;32m" 13 BLUE = "\033[0;34m" 14 YELLOW = "\033[1;33m" 15 BROWN = "\033[0;33m" 16 CYAN = "\033[0;36m" 17 PURPLE = "\033[0;35m" 18 DARK_GRAY = "\033[1;30m" 19 LIGHT_BLUE = "\033[1;34m" 20 LIGHT_GREEN = "\033[1;32m" 21 LIGHT_CYAN = "\033[1;36m" 22 LIGHT_RED = "\033[1;31m" 23 LIGHT_PURPLE = "\033[1;35m" 24 LIGHT_GRAY = "\033[0;37m" 25 26 27 def get_cursor_pos(): 28 29 buf = "" 30 stdin = sys.stdin.fileno() 31 tattr = termios.tcgetattr(stdin) 32 33 try: 34 tty.setcbreak(stdin, termios.TCSANOW) 35 sys.stdout.write("\x1b[6n") 36 sys.stdout.flush() 37 38 while True: 39 buf += sys.stdin.read(1) 40 if buf[-1] == "R": 41 break 42 finally: 43 termios.tcsetattr(stdin, termios.TCSANOW, tattr) 44 45 try: 46 matches = re.match(r"^\x1b\[(\d*);(\d*)R", buf) 47 groups = matches.groups() 48 except AttributeError: 49 return None, None 50 51 return (int(groups[0]), int(groups[1])) 52 53 54 def disable_term_echo(): 55 fd = sys.stdin.fileno() 56 oldattr = termios.tcgetattr(fd) 57 newattr = termios.tcgetattr(fd) 58 newattr[3] = newattr[3] & ~termios.ECHO 59 termios.tcsetattr(fd, termios.TCSADRAIN, newattr) 60 return oldattr 61 62 def enable_term_echo(attr): 63 fd = sys.stdin.fileno() 64 termios.tcsetattr(fd, termios.TCSADRAIN, attr) 65 66 def save_cursor_pos(): 67 sys.stdout.write('\033[s') 68 sys.stdout.flush() 69 70 def restore_cursor_pos(): 71 sys.stdout.write('\033[u') 72 sys.stdout.flush() 73 74 def print_at(x, y, msg, end=''): 75 pos_str = '\033[{};{}H'.format(x, y) 76 clean_line_str = '\033[K' 77 print(pos_str + clean_line_str + msg, end=end) 78 sys.stdout.flush() 79 80 def hide_cursor(): 81 sys.stdout.write("\033[?25l") 82 sys.stdout.flush() 83 84 def show_cursor(): 85 sys.stdout.write("\033[?25h") 86 sys.stdout.flush()
posted on 2019-04-20 20:45 wangwenzhi2019 阅读(594) 评论(0) 编辑 收藏 举报