From 11cd50c66d14177f3c8c3d38478b59d7cc83a66d Mon Sep 17 00:00:00 2001 From: Mike Hiretsky Date: Fri, 14 Feb 2014 11:57:04 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=BD=D1=81=D0=BE=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Объекты вывода цветного текста в терминал256, терминал, span блоки --- calculate/lib/datavars.py | 4 +- calculate/lib/utils/color/__init__.py | 0 calculate/lib/utils/color/converter.py | 188 ++++++++++++ calculate/lib/utils/color/output.py | 396 +++++++++++++++++++++++++ calculate/lib/utils/color/palette.py | 318 ++++++++++++++++++++ calculate/lib/utils/text.py | 110 ------- calculate/lib/utils/tools.py | 36 +++ setup.py | 3 +- 8 files changed, 942 insertions(+), 113 deletions(-) create mode 100644 calculate/lib/utils/color/__init__.py create mode 100644 calculate/lib/utils/color/converter.py create mode 100644 calculate/lib/utils/color/output.py create mode 100644 calculate/lib/utils/color/palette.py create mode 100644 calculate/lib/utils/tools.py diff --git a/calculate/lib/datavars.py b/calculate/lib/datavars.py index 3c41ea3..82bfe16 100644 --- a/calculate/lib/datavars.py +++ b/calculate/lib/datavars.py @@ -814,7 +814,7 @@ class SimpleDataVars: """ Unserialize form string for varname """ - fixEmpty = lambda x:"" if x=="''" else x + fixEmpty = lambda x:"" if x=="''" or x=='""' else x def getList(delimeter=','): def wrapper(val): if val == "": @@ -825,7 +825,7 @@ class SimpleDataVars: return getList()(value) if "table" in varType: return map(getList(':'),value.split(',')) - return fixEmpty(value).strip("'") + return fixEmpty(value).strip("'").strip('"') class DataVars(SimpleDataVars): diff --git a/calculate/lib/utils/color/__init__.py b/calculate/lib/utils/color/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/calculate/lib/utils/color/converter.py b/calculate/lib/utils/color/converter.py new file mode 100644 index 0000000..81ae107 --- /dev/null +++ b/calculate/lib/utils/color/converter.py @@ -0,0 +1,188 @@ +#-*- 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. + +from output import BaseOutput +from palette import (TextState, BaseColorMapping, ConsoleCodesInfo, + ConsoleCodeMapping, LightColorMapping, ConsoleColor256) +from calculate.lib.utils.tools import SavableIterator +from itertools import chain,ifilter +import re + +class ConsoleCodesConverter(object): + """Преобразователь текста из цветного консольного вывода через объект + форматирования. + + Объект форматирования должен реализовывать BaseOutput + + >>> cct = ConsoleCodesConverter(BaseOutput()) + >>> outtext = "\033[32;1mHello\033[0;39m" + >>> cct.transform(outtext) + 'Hello' + >>> cct = ConsoleCodesConverter(SpanCssOutput()) + >>> outtext = "\033[32;1mHello\033[0;39m" + >>> cct.transform(outtext) + 'Hello' + """ + + class CodeElement: + """Элемент кода в ESC последовательности""" + def __init__(self,condition=lambda code:False,action=lambda :None): + self.action = action + self.condition = condition + + def tryParse(self,code): + """Обрабатывает ли экземпляр код""" + return self.condition(code) + + def parse(self,code,codes): + """Обработать код, вызвать действие""" + self.action() + + def _next_code(self,other): + """ + Получить следующий код + """ + try: + return int(other.next()) + except StopIteration: + return None + + class ColorElement: + """Элемент кода для указания стандартного цвета + + Проверка кода в интервале, запуск действия с передачей цвета + """ + # соответствие консольных цветов внутренним цветам + mapColors = BaseColorMapping.mapConsole_TS + + def __init__(self,action=lambda x:None, begin=None, end=None): + self.action = action + self.begin = begin + self.end = end + + def tryParse(self,code): + cci = ConsoleCodesInfo + return code >=self.begin and code <= self.end + + def parse(self,code,codes): + cci = ConsoleCodesInfo + return self.action(self.mapColors.get(code-self.begin, + TextState.Colors.DEFAULT)) + + def __init__(self,output=None,escSymb="\033"): + self.output = output or BaseOutput() + self.escSymb = escSymb + self.reParse = re.compile( + "({0}\[\d+(?:;\d+)*m)?(.*?)(?=$|{0}\[\d)".format(escSymb), + re.DOTALL) + resetBoldHalfbright = lambda : ( + (self.output.resetBold() or "") + + (self.output.resetHalfbright() or "")) + cci = ConsoleCodesInfo + element = self.CodeElement + + # набор правил обработки кодов + reset = element(lambda code:code==cci.RESET, self.output.reset) + bold = element(lambda code:code==cci.BOLD, self.output.setBold) + halfbright = element(lambda code:code==cci.HALFBRIGHT, + self.output.setHalfbright) + underline = element(lambda code:code==cci.UNDERLINE, + self.output.setUnderline) + nounderline = element(lambda code:code==cci.NOUNDERLINE, + self.output.resetUnderline) + invert = element(lambda code:code==cci.INVERT, + self.output.setInvert) + noinvert = element(lambda code:code==cci.NOINVERT, + self.output.resetInvert) + normal = element(lambda code:code==cci.NORMAL, + resetBoldHalfbright) + reset_foreground = element(lambda code:code==cci.FOREGROUND_DEFAULT, + self.output.resetForeground) + reset_background = element(lambda code:code==cci.BACKGROUND_DEFAULT, + self.output.resetBackground) + foreground = self.ColorElement(begin=cci.FOREGROUND, + end=cci.FOREGROUND_END,action=self.output.setForeground) + background = self.ColorElement(begin=cci.BACKGROUND, + end=cci.BACKGROUND_END,action=self.output.setBackground) + self.grams = [reset,bold,halfbright,underline,nounderline,normal, + invert,noinvert,foreground,background] + + def outputText(self,s): + return self.output.outputText(s) + + def transform(self,s): + """ + Запустить преобразование текста + """ + def generator(): + offset = len(self.escSymb)+1 + for ctrl,txt in self.reParse.findall(s): + if ctrl: + codes = SavableIterator(ctrl[offset:-1].split(';')) + for code in codes: + code = int(code) + res = "" + for gram in ifilter(lambda x:x.tryParse(code), + self.grams): + res = gram.parse(code,codes) + break + if res: + yield res + if txt: + yield self.outputText(txt) + yield self.output.endText() + return "".join(list(filter(None,generator()))) + +class ConsoleCodes256Converter(ConsoleCodesConverter): + """Расширяет возможность обработки 256 цветного терминала""" + + class Color256Element(ConsoleCodesConverter.CodeElement): + def __init__(self,action=lambda x:None, begin=None): + self.action = action + self.begin = begin + + def tryParse(self,code): + return code == self.begin + + def parse(self,code,codes): + """ + Тон: 38;5;0-255 + Фон: 48;5;0-255 + """ + colorMap = LightColorMapping(BaseColorMapping).mapConsole_TS + + codes.save() + if self._next_code(codes) == ConsoleCodesInfo.COLOR256: + code = self._next_code(codes) + if code is not None: + if code in colorMap: + self.action(colorMap[code]) + else: + self.action(ConsoleColor256.consoleToRgb(code)) + else: + # если после 38 не 5 - не обрабатываем этот код + codes.restore() + + def __init__(self,*args,**kwargs): + ConsoleCodesConverter.__init__(self,*args,**kwargs) + cci = ConsoleCodesInfo + # обработчики кодов для вывода в 256 + foreground256 = self.Color256Element(begin=cci.FOREGROUND256, + action=self.output.setForeground) + background256 = self.Color256Element(begin=cci.BACKGROUND256, + action=self.output.setBackground) + self.grams.insert(0,foreground256) + self.grams.insert(0,background256) diff --git a/calculate/lib/utils/color/output.py b/calculate/lib/utils/color/output.py new file mode 100644 index 0000000..dc5af2b --- /dev/null +++ b/calculate/lib/utils/color/output.py @@ -0,0 +1,396 @@ +#-*- 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. + +from calculate.lib.utils.tools import SavableIterator +from palette import (TextState, BaseColorMapping, + ConsoleCodeMapping, LightColorMapping, ConsoleColor256, + ConsoleCodesInfo, SpanPalette) + +class BaseOutput: + """ + Базовый вывод текста. + + Вывод просто текста без изменения шрифта + """ + 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,color): + """ + Использовать цвет шрифта по умолчанию + """ + pass + + def resetBackground(self,color): + """ + Использовать цвет фона по умолчанию + """ + pass + + def setInvert(self): + """ + Включить инверсию + """ + + def resetInvert(self): + """ + Выключить инверсию + """ + +class SaveAttrOutput(BaseOutput): + """ + Базовый класс с сохранением атрибутов + """ + def __init__(self,state=None): + self.prev_state = state.clone() if state else TextState() + self.current_state = self.prev_state.clone() + + 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 + +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() + SaveAttrOutput.setBold(self) + + def setHalfbright(self): + self.resetBold() + SaveAttrOutput.setHalfbright(self) + + def handleForeground(self,prevstate,curstate,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 curstate.bold == False: + attrs.append(cci.BOLD) + attrs.append(self.mapLightColors[color]) + else: + attrs.append(cci.FOREGROUND_DEFAULT) + self.resetForeground() + + def handleBackground(self,prevstate,curstate,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): + 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): + if curstate.underline: + attrs.append(ConsoleCodesInfo.UNDERLINE) + else: + attrs.append(ConsoleCodesInfo.NOUNDERLINE) + + def handleInvert(self,prevstate,curstate,attrs): + if curstate.invert: + attrs.append(ConsoleCodesInfo.INVERT) + else: + attrs.append(ConsoleCodesInfo.NOINVERT) + + def _createAttrs(self,prevstate,curstate): + """ + Создать ESC последовательность для установки параметров текста + """ + 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) + if prevstate.underline != curstate.underline: + self.handleUnderline(prevstate,curstate,attrs) + if prevstate.invert != curstate.invert: + self.handleInvert(prevstate,curstate,attrs) + if prevstate.foreground != curstate.foreground: + self.handleForeground(prevstate,curstate,attrs) + if prevstate.background != curstate.background: + self.handleBackground(prevstate,curstate,attrs) + return attrs + + def handlePostAttr(self,prevstate,curstate): + """ + Добавление аттрибутов после выведенного текста + """ + if prevstate.foreground != curstate.foreground: + color = curstate.foreground + if (color in self.mapLightColors and + prevstate.bold == curstate.bold and curstate.bold == False): + return [ConsoleCodesInfo.NORMAL] + + 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 = self._createAttrs(self.prev_state,self.current_state) + attr = self._createEscCode(attr) + postattr = self.handlePostAttr(self.prev_state,self.current_state) + if postattr: + postattr = self._createEscCode(postattr) + else: + postattr = "" + self.prev_state = self.current_state.clone() + else: + attr = "" + postattr = "" + return attr + s + postattr + + def endText(self): + self.reset() + return self.outputText("") + +class ColorTerminal256Output(ColorTerminalOutput): + """ + Вывод на 256 цветный терминал + """ + mapLightColors = LightColorMapping.mapTS_Console + def handleForeground(self,prevstate,curstate,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: + ColorTerminalOutput.handleForeground(self,prevstate,curstate,attrs) + + def handleBackground(self,prevstate,curstate,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: + ColorTerminalOutput.handleBackground(self,prevstate,curstate,attrs) + + def handlePostAttr(self,prevstate,curstate): + pass + +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): + """ + Создать ESC последовательность для установки параметров текста + """ + 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 ''%"".join(style),'' + + 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 + s + rattr + + def endText(self): + self.reset() + return "" diff --git a/calculate/lib/utils/color/palette.py b/calculate/lib/utils/color/palette.py new file mode 100644 index 0000000..bfdbcd8 --- /dev/null +++ b/calculate/lib/utils/color/palette.py @@ -0,0 +1,318 @@ +#-*- 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 os +import sys +import re +from itertools import chain,ifilter,tee +from os import path + +class ConsoleCodesInfo: + """ + Коды цветов + """ + COLORCOUNT = 8 + BLACK, RED, GREEN, BROWN, BLUE, PURPLE, CYAN, WHITE = range(0,COLORCOUNT) + DEFAULT = 9 + FOREGROUND = 30 + FOREGROUND_DEFAULT = 39 + FOREGROUND_END = 37 + BACKGROUND = 40 + BACKGROUND_DEFAULT = 49 + BACKGROUND_END = 47 + BOLD = 1 + HALFBRIGHT = 2 + UNDERLINE = 4 + NOUNDERLINE = 24 + NORMAL = 22 + RESET = 0 + FOREGROUND256 = 38 + COLOR256 = 5 + BACKGROUND256 = 48 + INVERT = 7 + NOINVERT = 27 + +class ConsoleColor256: + """Объект перевода RGB цветов в консоль 256color""" + + colorList = (0,95,135,175,215,255) + colorMatch = [colorList[x]+(colorList[x+1]-colorList[x])/2 + for x in range(0,5)] + [255] + colorHex = ["%02x"%val for val in colorList] + webMatch = re.compile("^#[0-9A-Fa-f]{6}$") + grayMatch = [6 + x * 11 for x in xrange(0,23)] + [255] + + @staticmethod + def rgbToConsole(color): + """Перевести RGB в 256 консоль + + >>> ConsoleColor256.rgbToConsole("#5f00ff") + 57 + >>> ConsoleColor256.rgbToConsole("not #rrggbb") is None + True + """ + if color and ConsoleColor256.webMatch.match(color): + color = [int(x,base=16) for x in (color[5:7],color[3:5],color[1:3])] + # grayscale match + if abs(color[0]-color[1]) + abs(color[0]-color[2]) < 5: + for j,matchGray in enumerate(ConsoleColor256.grayMatch): + if color[0] <= matchGray: + return 232+j + # color match + colorNumber = 16 + for i,_color in enumerate(color): + for j,halfColor in enumerate(ConsoleColor256.colorMatch): + if _color <= halfColor: + colorNumber += j * 6 ** i + break + return colorNumber + return None + + @staticmethod + def consoleToRgb(color): + """ + Перевести 256 консоль в #RGB + + + >>> ConsoleColor256.consoleToRgb(216) + '#ffaf87' + >>> ConsoleColor256.consoleToRgb(15) is None + True + """ + if color >= 255: + return "#ffffff" + if color >= 232: + return "#{0:02x}{0:02x}{0:02x}".format((color-232)*11) + elif color >=16: + color-= 16 + return "#%s"%"".join(ConsoleColor256.colorHex[color/x%6] + for x in (36,6,1)) + else: + return None + +class TextState(object): + """ + Параметры текста + + >>> ts = TextState() + >>> ts.attr = TextState.Attributes.BOLD | TextState.Attributes.UNDERLINE + >>> ts.bold + True + >>> ts.underline + True + >>> ts.halfbright + False + """ + class Attributes: + NONE = 0 + BOLD = 1 + HALFBRIGHT = 2 + UNDERLINE = 4 + INVERT = 8 + + class Colors: + # обычные цвета + BLACK = "black" + RED = "red" + GREEN = "green" + BROWN = "brown" + BLUE = "blue" + PURPLE = "purple" + CYAN = "cyan" + GRAY = "gray" + # яркие цвета + DARK = "dark" + LIGHT_RED = "lightred" + LIGHT_GREEN = "lightgreen" + YELLOW = "yellow" + LIGHT_BLUE = "lightblue" + LIGHT_PURPLE = "lightpurple" + LIGHT_CYAN = "lightcyan" + WHITE = "white" + # темные цвета (консоль пониженной яркости) + DARK_BLACK = "darkblack" + DARK_RED = "darkred" + DARK_GREEN = "darkgreen" + DARK_BROWN = "darkbrown" + DARK_BLUE = "darkblue" + DARK_PURPLE = "darkpurple" + DARK_CYAN = "darkcyan" + DARK_GRAY = "darkgray" + DEFAULT = None + + normalColors = [Colors.BLACK,Colors.RED,Colors.GREEN,Colors.BROWN, + Colors.BLUE,Colors.PURPLE,Colors.CYAN,Colors.GRAY] + lightColors = [Colors.DARK,Colors.LIGHT_RED,Colors.LIGHT_GREEN, + Colors.YELLOW,Colors.LIGHT_BLUE,Colors.LIGHT_PURPLE, + Colors.LIGHT_CYAN,Colors.WHITE] + darkColors = [Colors.DARK_BLACK,Colors.DARK_RED,Colors.DARK_GREEN, + Colors.DARK_BROWN,Colors.DARK_BLUE,Colors.DARK_PURPLE, + Colors.DARK_CYAN,Colors.DARK_GRAY] + + def bitProperty(bit): + def set(self,value): + self.attr&=~bit + self.attr|=bit if value else 0 + def get(self): + return bool(self.attr & bit) + return property(get,set) + + # текст с подчеркиванием + underline = bitProperty(Attributes.UNDERLINE) + # текст жирный + bold = bitProperty(Attributes.BOLD) + # пониженная яркость + halfbright = bitProperty(Attributes.HALFBRIGHT) + # инверсия + invert = bitProperty(Attributes.INVERT) + + def __init__(self,foreground=Colors.DEFAULT, + background=Colors.DEFAULT, + attr=Attributes.NONE): + self.foreground = foreground + self.background = background + self.attr = attr + + def clone(self): + return self.__class__(self.foreground,self.background, + self.attr) + + def __cmp__(self,other): + for i in ["foreground","background","attr"]: + cmp_res = cmp(getattr(self,i),getattr(other,i)) + if cmp_res: + return cmp_res + return 0 + + @classmethod + def colors(cls): + return chain(xrange(0,8),[None]) + +class BaseColorMapping: + # соответствие внутренних цветов консольным + mapConsole_TS = {ConsoleCodesInfo.BLACK: TextState.Colors.BLACK, + ConsoleCodesInfo.RED: TextState.Colors.RED, + ConsoleCodesInfo.GREEN: TextState.Colors.GREEN, + ConsoleCodesInfo.BROWN: TextState.Colors.BROWN, + ConsoleCodesInfo.BLUE: TextState.Colors.BLUE, + ConsoleCodesInfo.PURPLE:TextState.Colors.PURPLE, + ConsoleCodesInfo.CYAN: TextState.Colors.CYAN, + ConsoleCodesInfo.WHITE: TextState.Colors.GRAY} + + mapTS_Console = {v:k for k,v in mapConsole_TS.items()} + + def __init__(self,base): + self.mapConsole_TS = self.mapConsole_TS.copy() + self.mapConsole_TS.update(base.mapConsole_TS) + self.mapTS_Console = {v:k for k,v in self.mapConsole_TS.items()} + +class LightColorMapping(BaseColorMapping): + # соответствие внутренних цветов консольным + offset = ConsoleCodesInfo.COLORCOUNT + mapConsole_TS = {ConsoleCodesInfo.BLACK+offset: TextState.Colors.DARK, + ConsoleCodesInfo.RED+offset: TextState.Colors.LIGHT_RED, + ConsoleCodesInfo.GREEN+offset: TextState.Colors.LIGHT_GREEN, + ConsoleCodesInfo.BROWN+offset: TextState.Colors.YELLOW, + ConsoleCodesInfo.BLUE+offset: TextState.Colors.LIGHT_BLUE, + ConsoleCodesInfo.PURPLE+offset:TextState.Colors.LIGHT_PURPLE, + ConsoleCodesInfo.CYAN+offset: TextState.Colors.LIGHT_CYAN, + ConsoleCodesInfo.WHITE+offset: TextState.Colors.WHITE} + + mapTS_Console = {v:k for k,v in mapConsole_TS.items()} + +class ConsoleCodeMapping(BaseColorMapping): + """ + Декоратор для преобразования кодов цвета в код цвета тона или фона + + Добавляет смещение к коду (3 -> 3 + codeoffset) + """ + def __init__(self,codeoffset,base): + self.mapConsole_TS = {codeoffset+k:v for k,v in base.mapConsole_TS.items()} + self.mapTS_Console = {v:k for k,v in self.mapConsole_TS.items()} + +class SpanPalette: + """ + Палитра для SpanCssOutput (черное на белом) + """ + LOW_BRIGHT, NORMAL_BRIGHT, HIGH_BRIGHT = 0, 1, 2 + + defaultColor = ["Black","Black","DarkGrey"] + defaultBackground = "White" + + normalBright = dict(zip(TextState.normalColors, + ["Black","DarkRed","DarkGreen", + "Sienna","DarkBlue","DarkViolet", + "LightSeaGreen","Grey"])) + + highBright = dict(zip(TextState.lightColors, + ["DarkGrey","Red","Green", + "Yellow","RoyalBlue","Magenta", + "Cyan","White"])) + + lowBright = dict(zip(TextState.darkColors, + ["Black","Maroon","DarkGreen", + "SaddleBrown","DarkBlue","DarkViolet", + "LightSeaGreen","Grey"])) + + + mapHighNormal = dict(zip(TextState.normalColors, + TextState.lightColors)) + mapLowNormal = dict(zip(TextState.normalColors, + TextState.darkColors)) + + def __init__(self): + self.colorMap = dict(self.normalBright.items()+ + self.highBright.items()+ + self.lowBright.items()) + self.brightMap = {self.NORMAL_BRIGHT:{}, + self.HIGH_BRIGHT:self.mapHighNormal, + self.LOW_BRIGHT:self.mapLowNormal} + + def brightTransform(self,color,bright): + """ + Преобразовать основной цвет в зависимости от установленной яркости + """ + mapper = self.brightMap.get(bright,{}) + return mapper.get(color,color) + + def getBackgroundColor(self,color): + if not color: + return self.defaultBackground + return self.colorMap.get(color,color) + + def getTextColor(self,color,bright): + """ + Получить соответствие цвета строкой + """ + if not color: + return self.defaultColor[bright] + color = self.brightTransform(color,bright) + return self.colorMap.get(color,color) + +class DarkPastelsPalette(SpanPalette): + """ + Палитра идентичная Calculate в консоли (Dark Pastels) + """ + defaultColor = ["#DCDCDC","#DCDCDC","#709080"] + defaultBackground = "#2C2C2C" + + normalBright = dict(zip(TextState.normalColors, + ["#2C2C2C","#705050","#60B48A", "#DFAF8F", + "#9AB8D7","#DC8CC3","#8CD0D3","#DCDCDC"])) + highBright = dict(zip(TextState.lightColors, + ["#709080","#DCA3A3","#72D5A3", "#F0DFAF", + "#94BFF3","#EC93D3","#93E0E3","#FFFFFF"])) + diff --git a/calculate/lib/utils/text.py b/calculate/lib/utils/text.py index 882c294..50747b0 100644 --- a/calculate/lib/utils/text.py +++ b/calculate/lib/utils/text.py @@ -27,116 +27,6 @@ from re import search, compile, S from calculate.lib.cl_lang import setLocalTranslate setLocalTranslate('cl_lib3',sys.modules[__name__]) -def prettyColumnStr(*cols): - '''Функция преобразования строк в текстовые колонки. Если указанный текст - не помещается в колонку, то строка переносится на следующую этой же колонки - перенос текста идет по словам, и текст выравнивается по ширине колонки за - счет дополнительных пробелов между словами. Если в строке используется - перенос строки, то текст переносится не просто на следующую строку, а также - на следующую строку колонки, причем если используется \r текст выравнива- - ется по ширине, а если \n, то просто перевод строки. - - Параметры: - cols множестово пар: текст, ширина колонки, причем, если у последней - колонки не указывать ширину, то она будет выведена вся. - - Возвращаемые параметры: - строка, которую можно использовать для вывода на экран - - Пример: columnWrite( "Some text", 10, "Next column", 20 ) - ''' - # шаблон поиска переводов строк - wherenr = compile( '[\n\r]', S ) - retstr = "" - # перевести кортеж в список, т.к. изменяется - cols = list(cols) - # перевести текст в юникод, заодно перевести числа в строку - noconvert = False - space = u' ' - nospace = u'' - for i in xrange(0,len(cols),2): - cols[i] = _toUNICODE(cols[i]) - # флаг "есть еще текст для вывода" - repeat = True - while repeat: - # сбросить итератор на первый элемент - q = 0 - repeat = False - # пока не закончили перебирать параметры (перебираем по парам) - while q < len(cols): - # если это последний параметр, и для него не указана ширина - if q == len(cols)-1: - # выводим его полностью не смотря на ширину окна - retstr += cols[q] + " " - cols[q] = '' - else: - # вывести часть строки не больше указанной ширины колонки - partstr = cols[q][:cols[q+1]] - # искать перевод строки с полученной части - brfind = wherenr.search(partstr) - # если это не последняя колонка - if q + 2 < len(cols): - # добавить разделитель между колонками - cellspacing = space - else: - # разделитель не нужен - cellspacing = nospace - - # если перевод строки найден, то - if brfind != None: - # для текущего вывода в колонку - # берем часть строки до перевода - partstr = partstr[:brfind.start()] - # остальная часть идет в остаток (без перевода) - cols[q] = cols[q][brfind.start()+1:] -# # если используется перевод каретки -# if brfind.group() == '\r': -# # то выравниваем по ширине колонки -# partstr = partstr.ljust(cols[q+1], ' ') -# else: -# # добавить отступы чтобы закончить колонку - partstr = partstr.ljust(cols[q+1], ' ') - # если взята часть строки - elif len(partstr) == cols[q+1] and partstr != cols[q]: - # если взята часть строки (разрыв в слове) - if cols[q][cols[q+1]] != ' ': - # ищем ближайший пробел справа - spacepos = partstr.rfind(' ') - # если пробел найти не удалось - if spacepos == -1: - # то на вывод идет часть строки равной ширине - cols[q] = cols[q][cols[q+1]:] - # если пробел найден - else: - # обрезаем строку до найденного пробела - partstr = partstr[:spacepos] - cols[q] = cols[q][spacepos+1:] - # если взята часть строки (разрыв на пробеле) - else: - # ислючить переносной пробел - cols[q] = cols[q][cols[q+1]+1:] - # выровнить текст по ширине колонки - partstr = partstr.ljust(cols[q+1], ' ') - #partstr = justify(partstr, cols[q+1]) - # остатки строки - else: - # добавить отступы чтобы закончить колонку - partstr = partstr.ljust(cols[q+1], ' ') - cols[q] = '' - - retstr+= partstr + cellspacing - - # остальную часть строки оставить на следующую итерацию - # если от строки что то осаталось - if len(cols[q]) > 0: - # отметить запуск еще одной итерации по параметрам - repeat = True - # следующая пара - q += 2 - # колонки отображены - retstr += "\n" - return retstr.encode('utf8') - def columnMatrix(*cols): """ Split columns text to matrix diff --git a/calculate/lib/utils/tools.py b/calculate/lib/utils/tools.py new file mode 100644 index 0000000..1b2881f --- /dev/null +++ b/calculate/lib/utils/tools.py @@ -0,0 +1,36 @@ +#-*- 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. + +from itertools import tee + +class SavableIterator: + def __init__(self,seq): + self.seq = iter(seq) + self.back = None + + def __iter__(self): + return self + + def save(self): + self.seq, self.back = tee(self.seq) + + def restore(self): + if self.back: + self.seq, self.back = tee(self.back) + + def next(self): + return self.seq.next() + diff --git a/setup.py b/setup.py index 19f69a1..bd57c91 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,8 @@ setup( module_name + '.server', module_name + '.variables', module_name + '.mod', - module_name + '.utils'], + module_name + '.utils', + module_name + '.utils.color'], data_files = [("/etc/calculate", []), ("/var/calculate/remote", []), ("/var/log/calculate", [])]