You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
calculate-utils-3-lib/pym/calculate/lib/utils/colortext/output.py

693 lines
22 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#-*- coding: utf-8 -*-
# Copyright 2014 Calculate Ltd. http://www.calculate-linux.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from xml.etree import ElementTree as ET
from calculate.lib.utils.text import MultiReplace
from palette import (TextState, BaseColorMapping,
ConsoleCodeMapping, LightColorMapping, ConsoleColor256,
ConsoleCodesInfo, SpanPalette, XmlFormat)
class BaseOutput(object):
"""
Базовый вывод текста.
Вывод просто текста без изменения шрифта
"""
def __init__(self, state=None):
pass
def setBold(self):
"""
Выводимый текст будет жирным
"""
pass
def resetBold(self):
"""
Выводимый текст будет нежирным
"""
pass
def setUnderline(self):
"""
Выводимый текст будет подчеркнутым
"""
pass
def resetUnderline(self):
"""
Выводимый текст не будет подчеркнутым
"""
pass
def setHalfbright(self):
"""
Цвет выводимого текста использует полутона
"""
pass
def resetHalfbright(self):
"""
Цвет выводимого текста не использует полутона
"""
pass
def reset(self):
"""
Использовать шрифт по умолчанию
"""
pass
def endText(self):
"""
Обработка текста завершена
"""
return ""
def outputText(self, text):
"""
Вывести текст с установленными настройками
"""
return text
def setForeground(self, color):
"""
Установить цвет шрифта
"""
pass
def setBackground(self, color):
"""
Установить цвет фона
"""
pass
def resetForeground(self):
"""
Использовать цвет шрифта по умолчанию
"""
pass
def resetBackground(self):
"""
Использовать цвет фона по умолчанию
"""
pass
def setInvert(self):
"""
Включить инверсию
"""
def resetInvert(self):
"""
Выключить инверсию
"""
def pushState(self):
"""
Сохранить состояние текста
"""
def popState(self):
"""
Восстановить состояние текста из стека
"""
def newLine(self):
"""
Вывести текст на новой строке
"""
return "\n"
def tab(self):
"""
Вывести символ табуляции
"""
return "\t"
def clone(self):
"""
Создать копию объекта
"""
return self.__class__()
class SaveAttrOutput(BaseOutput):
"""
Базовый класс с сохранением атрибутов
"""
def __init__(self, state=None):
self.prev_state = state.clone() if state else TextState()
self.current_state = self.prev_state.clone()
self.state_stack = []
def clone(self):
obj = self.__class__()
obj.current_state = self.current_state.clone()
obj.state_stack = list(self.state_stack)
return obj
def setBold(self):
self.current_state.bold = True
def resetBold(self):
self.current_state.bold = False
def setUnderline(self):
self.current_state.underline = True
def resetUnderline(self):
self.current_state.underline = False
def setHalfbright(self):
self.current_state.halfbright = True
def resetHalfbright(self):
self.current_state.halfbright = False
def reset(self):
self.resetBold()
self.resetHalfbright()
self.resetUnderline()
self.resetForeground()
self.resetBackground()
self.resetInvert()
def setForeground(self, color):
self.current_state.foreground = color
def resetForeground(self):
self.current_state.foreground = TextState.Colors.DEFAULT
def setBackground(self, color):
self.current_state.background = color
def resetBackground(self):
self.current_state.background = TextState.Colors.DEFAULT
def resetInvert(self):
self.current_state.invert = False
def setInvert(self):
self.current_state.invert = True
def pushState(self):
self.state_stack.append(self.current_state.clone())
def popState(self):
self.current_state = self.state_stack.pop()
class ColorTerminalOutput(SaveAttrOutput):
"""
Форматирует текст для вывода в консоль
"""
mapColors = ConsoleCodeMapping(ConsoleCodesInfo.FOREGROUND,
BaseColorMapping).mapTS_Console
mapLightColors = ConsoleCodeMapping(ConsoleCodesInfo.FOREGROUND -
LightColorMapping.offset,
LightColorMapping).mapTS_Console
mapBackgroundColors = ConsoleCodeMapping(ConsoleCodesInfo.BACKGROUND,
BaseColorMapping).mapTS_Console
def setBold(self):
self.resetHalfbright()
super(ColorTerminalOutput, self).setBold()
def setHalfbright(self):
self.resetBold()
super(ColorTerminalOutput, self).setHalfbright()
def handleForeground(self, prevstate, curstate, attrs, tail_attrs):
"""Обработать изменение тона"""
cci = ConsoleCodesInfo
color = curstate.foreground
if color in self.mapColors:
attrs.append(self.mapColors[color])
elif color in self.mapLightColors:
if prevstate.bold == curstate.bold and not curstate.bold:
tail_attrs.append(cci.NORMAL)
attrs.append(cci.BOLD)
attrs.append(self.mapLightColors[color])
else:
attrs.append(cci.FOREGROUND_DEFAULT)
self.resetForeground()
def handleBackground(self, prevstate, curstate, attrs, tail_attrs):
"""Обработать изменение фона"""
color = curstate.background
if color in self.mapBackgroundColors:
attrs.append(self.mapBackgroundColors[color])
else:
attrs.append(ConsoleCodesInfo.BACKGROUND_DEFAULT)
def handleIntensity(self, prevstate, curstate, attrs, tail_attrs):
"""Обработать интенсивность"""
if curstate.bold and curstate.bold != prevstate.bold:
attrs.append(ConsoleCodesInfo.BOLD)
elif (curstate.halfbright and
prevstate.halfbright != curstate.halfbright):
attrs.append(ConsoleCodesInfo.HALFBRIGHT)
else:
attrs.append(ConsoleCodesInfo.NORMAL)
def handleUnderline(self, prevstate, curstate, attrs, tail_attrs):
"""Обработать подчеркивание"""
if curstate.underline:
attrs.append(ConsoleCodesInfo.UNDERLINE)
else:
attrs.append(ConsoleCodesInfo.NOUNDERLINE)
def handleInvert(self, prevstate, curstate, attrs, tail_attrs):
"""Обработать инверсию"""
if curstate.invert:
attrs.append(ConsoleCodesInfo.INVERT)
else:
attrs.append(ConsoleCodesInfo.NOINVERT)
def handleColor(self, prevstate, curstate, attrs, tail_attrs):
"""Обработать изменение цветов (фон и/или тон)"""
if prevstate.foreground != curstate.foreground:
self.handleForeground(prevstate, curstate, attrs, tail_attrs)
if prevstate.background != curstate.background:
self.handleBackground(prevstate, curstate, attrs, tail_attrs)
def _createAttrs(self, prevstate, curstate):
"""
Создать ESC последовательность для установки параметров текста
"""
attrs, tail_attrs = [], []
# получить интенсивность (полутон и жирность относятся к интенсивности)
intensity = lambda x: x & (TextState.Attributes.HALFBRIGHT |
TextState.Attributes.BOLD)
if (prevstate.attr != curstate.attr and
curstate.attr == TextState.Attributes.NONE and
curstate.foreground is TextState.Colors.DEFAULT and
curstate.background is TextState.Colors.DEFAULT):
attrs.append(ConsoleCodesInfo.RESET)
else:
if intensity(prevstate.attr) != intensity(curstate.attr):
self.handleIntensity(prevstate, curstate, attrs, tail_attrs)
if prevstate.underline != curstate.underline:
self.handleUnderline(prevstate, curstate, attrs, tail_attrs)
if prevstate.invert != curstate.invert:
self.handleInvert(prevstate, curstate, attrs, tail_attrs)
if (prevstate.foreground != curstate.foreground or
prevstate.background != curstate.background):
self.handleColor(prevstate, curstate, attrs, tail_attrs)
return attrs, tail_attrs
def _createEscCode(self, attrs):
"""
Создать ESC строку
"""
attrs = map(str, ['\033['] + attrs + ['m'])
return "%s%s%s" % (attrs[0], ";".join(attrs[1:-1]), attrs[-1])
def outputText(self, s):
"""
Задание параметров текста и вывод его
"""
if self.prev_state != self.current_state:
attr, tail_attrs = \
self._createAttrs(self.prev_state, self.current_state)
attr = self._createEscCode(attr)
if tail_attrs:
postattr = self._createEscCode(tail_attrs)
else:
postattr = ""
self.prev_state = self.current_state.clone()
else:
attr = ""
postattr = ""
return attr + s + postattr
def endText(self):
self.reset()
return self.outputText("")
def newLine(self):
return "\n"
def tab(self):
return "\t"
class ColorTerminal256Output(ColorTerminalOutput):
"""
Вывод на 256 цветный терминал
"""
mapLightColors = LightColorMapping.mapTS_Console
def handleForeground(self, prevstate, curstate, attrs, tail_attrs):
color = curstate.foreground
color256 = ConsoleColor256.rgbToConsole(color)
if not color256 and color in self.mapLightColors:
color256 = self.mapLightColors[color]
if color256:
attrs.extend([ConsoleCodesInfo.FOREGROUND256,
ConsoleCodesInfo.COLOR256,
color256])
else:
super(ColorTerminal256Output,
self).handleForeground(prevstate, curstate, attrs, tail_attrs)
def handleBackground(self, prevstate, curstate, attrs, tail_attrs):
color = curstate.background
color256 = ConsoleColor256.rgbToConsole(color)
if not color256 and color in self.mapLightColors:
color256 = self.mapLightColors[color]
if color256:
attrs.extend([ConsoleCodesInfo.BACKGROUND256,
ConsoleCodesInfo.COLOR256,
color256])
else:
super(ColorTerminal256Output,
self).handleBackground(prevstate, curstate, attrs, tail_attrs)
class ColorTerminal16Output(ColorTerminalOutput):
"""
Вывод на 16 цветный терминал с преобразованием RGB к ближайшему базовому
Bugs:
После преобразования текст и фон могут одинакового цвета
"""
def __init__(self, state=None, palette=None):
SaveAttrOutput.__init__(self, state=state)
self.palette = palette
def _handleNearestColors(self, color):
"""
Обработка преобразования к ближайшему цвету
"""
standardColors = TextState.normalColors + TextState.lightColors
if self.palette and color not in standardColors:
return self.palette.getBaseColorByRGB(color)
return color
def handleForeground(self, prevstate, curstate, attrs, tail_attrs):
"""
Добавить преобразование RGB к ближайшему базовому
"""
_curstate = curstate.clone()
_curstate.foreground = \
self._handleNearestColors(curstate.foreground)
super(ColorTerminal16Output, self).handleForeground(
prevstate, _curstate,
attrs, tail_attrs)
def handleBackground(self, prevstate, curstate, attrs, tail_attrs):
"""
Добавить преобразование RGB к ближайшему базовому
"""
mapHighNormal = dict(zip(TextState.lightColors,
TextState.normalColors))
_curstate = curstate.clone()
_curstate.background = \
self._handleNearestColors(curstate.background)
# преобразовать яркий цвет к обычному
_curstate.background = \
mapHighNormal.get(_curstate.background, _curstate.background)
super(ColorTerminal16Output, self).handleBackground(
prevstate, _curstate,
attrs, tail_attrs)
class SpanCssOutput(SaveAttrOutput):
"""
Форматирует текст для вывода в консоль
"""
def __init__(self, state=None, palette=SpanPalette()):
SaveAttrOutput.__init__(self, state=state)
self.palette = palette
def getStringColor(self, color, bold=False, halfbright=False,
background=False):
"""
Получить название цвета по номеру и состоянию текста
"""
if halfbright:
bright = SpanPalette.LOW_BRIGHT
elif bold:
bright = SpanPalette.HIGH_BRIGHT
else:
bright = SpanPalette.NORMAL_BRIGHT
if background:
return self.palette.getBackgroundColor(color)
else:
return self.palette.getTextColor(color, bright)
def getTags(self, prevstate, curstate):
"""
Создать tag span для указания параметров текста
"""
style = []
colorAttr = ["color", "background"]
if curstate.invert:
colorAttr = colorAttr[1], colorAttr[0]
if (prevstate.foreground != curstate.foreground or
prevstate.bold != curstate.bold or
curstate.invert or
prevstate.halfbright != curstate.halfbright):
sColor = self.getStringColor(curstate.foreground,
curstate.bold,
curstate.halfbright,
background=False)
style.append("%s:%s;" % (colorAttr[0], sColor))
if prevstate.background != curstate.background or curstate.invert:
sColor = self.getStringColor(curstate.background,
background=True)
style.append("%s:%s;" % (colorAttr[1], sColor))
if prevstate.underline != curstate.underline:
if curstate.underline:
style.append("text-decoration:underline;")
else:
style.append("text-decoration:none;")
if prevstate.bold != curstate.bold:
if curstate.bold:
style.append("font-weight:bold;")
else:
style.append("font-weight:normal;")
return '<span style="%s">' % "".join(style), '</span>'
def outputText(self, s):
if self.prev_state != self.current_state:
lattr, rattr = self.getTags(self.prev_state, self.current_state)
else:
lattr = rattr = ""
return lattr + XmlFormat.escaper(s) + rattr
def endText(self):
self.reset()
return ""
def newLine(self):
return "<br/>"
def tab(self):
return "&#9;"
class XmlOutput(SaveAttrOutput):
"""
Форматирует текст c описанием формата в XML для внутренней передачи
Bugs: игнорирует первоначальное состояние (state)
не экономное использование тэгов (например при выводе нескольких строк
"""
escaper = XmlFormat.escaper
def __init__(self, state=None):
super(XmlOutput, self).__init__(state=state)
self.clear_state = TextState()
def getXML(self, curstate, text):
"""
Создать управляющие тэги
:type curstate: TextState
:type text: str
:type rtype: ET.Element
"""
Tags, FontAttributes = XmlFormat.Tags, XmlFormat.FontAttributes
root = ET.Element("root")
tail = root
if (curstate.foreground != TextState.Colors.DEFAULT or
curstate.background != TextState.Colors.DEFAULT):
tail = ET.SubElement(tail, Tags.FONT)
sColor = str(curstate.foreground or "")
if sColor:
tail.attrib[FontAttributes.FOREGROUND] = sColor
sColor = str(curstate.background or "")
if sColor:
tail.attrib[FontAttributes.BACKGROUND] = sColor
if curstate.halfbright:
tail = ET.SubElement(tail, Tags.HALFBRIGHT)
if curstate.invert:
tail = ET.SubElement(tail, Tags.INVERT)
if curstate.underline:
tail = ET.SubElement(tail, Tags.UNDERLINE)
if curstate.bold:
tail = ET.SubElement(tail, Tags.BOLD)
tail.text = text
return root[0]
def xmlToString(self, xml):
"""
Получить строку xml не преобразовывая &, <, >
"""
def generator(root):
if root.attrib:
yield "<%s %s>" % (root.tag,
" ".join(['%s="%s"' % (k, v) for k, v in
root.attrib.items()]))
else:
yield "<%s>" % root.tag
if len(root):
for element in root:
for text in generator(element):
yield text
if root.text:
yield self.escaper(root.text)
yield "</%s>" % root.tag
return "".join(filter(None, list(generator(xml))))
def outputText(self, s):
if self.clear_state != self.current_state:
return self.xmlToString(self.getXML(self.current_state, s))
else:
return self.escaper(s)
def endText(self):
self.reset()
return ""
def newLine(self):
return "<br/>"
def tab(self):
return "<tab/>"
class BasePositionOutput(object):
"""
Объект составляющий ESC последовательности для управлением местом вывода
"""
def moveCursorUp(self, count=1):
"""
Переместить курсор вверх
"""
return ""
def moveCursorDown(self, count=1):
"""
Переместить курсор вниз
"""
return ""
def moveCursorRight(self, count=1):
"""
Переместить курсор вправо
"""
return ""
def moveCursorLeft(self, count=1):
"""
Переместить курсор влево
"""
return ""
def clearLine(self, whole_line=False):
"""
Очистить строку от курсора до конца или всю строку
"""
return ""
def savePosition(self):
"""
Сохранить положение курсора
"""
return ""
def restorePosition(self):
"""
Восстановить положение курсора
"""
return ""
class TerminalPositionOutput(BasePositionOutput):
"""
Управление позицией вывода текста в терминале
"""
class Codes:
UP = 'A'
DOWN = 'B'
RIGHT = 'C'
LEFT = 'D'
CLEAR_LINE = 'K'
CLEAR_FROM_CURSOR = '1'
CLEAR_WHOLE_LINE = '2'
SAVE_POSITION = 's'
RESTORE_POSITION = 'u'
def _createEscCode(self, attrs):
"""
Создать ESC строку
"""
return '\033[%s' % attrs
def _moveCursor(self, direct, count):
if int(count) > 1:
count = str(count)
else:
count = ""
return self._createEscCode("%s%s" % (count, direct))
def moveCursorDown(self, count=1):
return self._moveCursor(self.Codes.DOWN, count)
def moveCursorUp(self, count=1):
return self._moveCursor(self.Codes.UP, count)
def moveCursorRight(self, count=1):
return self._moveCursor(self.Codes.RIGHT, count)
def moveCursorLeft(self, count=1):
return self._moveCursor(self.Codes.LEFT, count)
def clearLine(self, whole_line=False):
if whole_line:
mode_code = self.Codes.CLEAR_WHOLE_LINE
else:
mode_code = self.Codes.CLEAR_FROM_CURSOR
return self._createEscCode("%s%s"%(mode_code, self.Codes.CLEAR_LINE))
def savePosition(self):
return self._createEscCode(self.Codes.SAVE_POSITION)
def restorePosition(self):
return self._createEscCode(self.Codes.RESTORE_POSITION)