From 45eb51550aefb41098fb72e90e2a8d96463ea021 Mon Sep 17 00:00:00 2001 From: Martinez Date: Thu, 12 May 2016 16:41:09 +0200 Subject: [PATCH] finished over.text --- Changes | 3 - over/text.py | 287 +++++++++++++++++++++++---------------------------- 2 files changed, 130 insertions(+), 160 deletions(-) delete mode 100644 Changes diff --git a/Changes b/Changes deleted file mode 100644 index 4f6e27c..0000000 --- a/Changes +++ /dev/null @@ -1,3 +0,0 @@ -replaced text.ProgressBar with text.ProgressBar2 -simplified text.paragraph -color tags changed from §g...§/ to g$...$ diff --git a/over/text.py b/over/text.py index 50bb52a..96244d0 100644 --- a/over/text.py +++ b/over/text.py @@ -101,7 +101,7 @@ class _ProgressBarChannel: return (A, amount - A) -class ProgressBar2: +class ProgressBar: """ A configurable text progressbar. @@ -375,84 +375,7 @@ class Unit: # -------------------------------------------------- -def paragraph(text, width=0, indent=0, prefix=None, stamp=None): - """ - str text text to format - int width required line length; if 0, current terminal width will be used - int indent how many spaces to indent the text with - str stamp placed over the first line's indent (stretching the indent if necessary) - - Formats text into an indented paragraph that fits the terminal and returns it. - Correctly handles color tags. - """ - - words = text.split() - - term_width = get_terminal_size()[1] - - if not width: - width = term_width - elif width < 0: - width += term_width - - lines = [] # individual lines go in this buffer - - first = True - while words: - if indent: - if first and stamp: - lines.append([stamp + " "*(indent-1-len(stamp))]) - else: - lines.append([" "*(indent-1)]) # first word = indent minus one space (that's gonna get back while joining) - - first = False - else: - lines.append([]) - - while words: - word_added = False - - if len(re.sub("§.", "", " ".join(lines[-1]))) + len(re.sub("§.", "", words[0])) + 1 <= width: - lines[-1].append(words.pop(0)) - word_added = True - - elif not word_added and len(lines[-1]) == 1 and indent: - # no word added and just the indent"s in here = word"s too long -> screw indent - # we might try to keep at least a part of the indent - if possible - len_word = len(re.sub("§.", "", words[0])) - if len_word < width: - lines[-1] = [" "*(width - len_word - 1), words.pop(0)] - else: - lines[-1] = [words.pop(0)] - word_added = True - break - - elif not word_added and not lines[-1] and not indent: - # no word added, empty line = word"s too long -> screw indent - lines[-1] = [words.pop(0)] - word_added = True - break - - else: - break - - lines_tmp = [] - for line in lines: - if prefix: - line.insert(0, prefix) - - lines_tmp.append(" ".join(line)) # put words together - - return "\n".join(lines_tmp) # put lines together - -class prefix: - info = (" ", "INFO") - debug = (" §b?§/", "§bDEBG§/") - start = ("§B>>>§/", "§BEXEC§/") - exec = start - warn = (" §y#§/", "§yWARN§/") - fail = ("§r!!!§/", "§rFAIL§/") - done = (" §g*§/", "§gDONE§/") +colortag_char = "§" colortags = { "r": "\x1b[31;01m", @@ -462,7 +385,7 @@ colortags = { "m": "\x1b[35;01m", "c": "\x1b[36;01m", "B": "\x1b[01m", - "": "\x1b[39;49;00m" # reset + ".": "\x1b[39;49;00m" # reset } def render(text, colors=True): @@ -470,37 +393,101 @@ def render(text, colors=True): Processes text with color tags and either removes them (with colors=False) or replaces them with terminal color codes. + + Color tags are §x§ where x is the color code from colortags. + §.§ resets the color. Use §§x§ for a literal §x§. """ text = str(text) - tags = set(colortags.keys()) output = [] + window = [] - for - - - - if colors: - tags = re.findall(r"[{:s}]\$".format("".join(tags)), text) + for c in text: + window.append(c) - for tag in tags: - try: - text = text.replace(tag, colortags[tag]) - except KeyError: - pass - else: - text = re.sub("§[^§]", "", text) + if len(window) < 3: + continue + elif len(window) == 4: + output.append(window.pop(0)) + + # the window now contains three chars + if window[0] == window[2] == colortag_char: + code = window[1] + + if code in colortags.keys() and (not output or output[-1] != colortag_char): + if colors: + output.append(colortags[code]) + + window.clear() - # unescape actual paragraphs - text = re.sub("§§", "§", text) - - return text + return "".join(output + window) # -------------------------------------------------- -def char_length(string): +def rfind(text, C, start, length): """ - Returns the length of a string minus all formatting tags. + Returns the highest index of C in text[start:] that is <= start+length. + Color tags aren't counted into length. + """ + + # find the index of the last C in colorless text + colorless = render(text[start:], False) + indices_colorless = [i for i, c in enumerate(colorless) if c == C and i < length] + + # get indices of all Cs in the original + indices = [i for i, c in enumerate(text[start:]) if c == C] + + # return the original index at the same position as the last in colorless + return start + indices[len(indices_colorless) - 1] + +def paragraph(text, width=0, indent=0, stamp=None): + """ + str text text to format + int width required line length; if 0, current terminal width will be used + int indent how many spaces to indent the text with + str stamp placed into the first line's indent (whether it fits is your problem) + + Formats text into an indented paragraph that fits the terminal and returns it. + Correctly handles colors. + """ + + fit_into_width = (width or get_terminal_size()[1]) - indent + + lines = [] + last = 0 + + # break into lines + while True: + idx = rfind(text, " ", last, fit_into_width) + adj_last = last + 1 if last else 0 + + if len(text[adj_last:]) < fit_into_width or idx == -1: + lines.append(text[adj_last:]) + break + elif idx == last: + return (stamp or "") + text + else: + line = text[adj_last:idx] + last = idx + lines.append(line) + + # indent + indent_str = " " * indent + + for i, line in enumerate(lines): + if stamp and i == 0: + lines[i] = stamp.ljust(indent) + line + else: + lines[i] = indent_str + line + + # join + return "\n".join(lines) + +# -------------------------------------------------- + +def strlen(string): + """ + Returns the length of a string minus all color tags. """ plain_string = render(string, colors=False) @@ -510,70 +497,59 @@ def char_length(string): # -------------------------------------------------- class Output: + class tag: + class short: + info = " " + debug = " §b§?§.§" + start = "§B§>>>§.§" + exec = start + warn = " §y§#§.§" + fail = "§r§!!!§.§" + done = " §g§*§.§" + class long: + info = "INFO" + debug = "§b§DEBG§.§" + start = "§B§EXEC§.§" + exec = start + warn = "§y§WARN§.§" + fail = "§r§FAIL§.§" + done = "§g§DONE§.§" + + ts = tag.short + tl = tag.long + """ Text UI output renderer. Prints messages to the stdout with optional eye candy like colors and timestamps. - Usage: - >>> from over import Output, prefix - >>> say = Output("test", timestamp=True) - >>> say("system initialized") - [2013-02-28 16:41:28] INFO -- test, system initialized - >>> say("system is FUBAR", prefix.fail) - [2013-02-28 16:41:46] FAIL -- test, system is FUBAR - >>> say("I just realized this will not work", prefix.fail, timestamp=False) - !!! I just realized this will not work - - TODO use a generic format string + Formatting + ========== + You can use all formatting characters supported by strftime, as well as: + §T§ - tag + §n§ - name + §t§ - supplied text """ - def __init__(self, name, timestamp=True, colors=True, default_prefix=prefix.info, default_suffix=".\n", stream=sys.stderr): + def __init__(self, name, format="[%Y-%m-%d %H:%M:%S] §T§ -- §n§, §t§", colors=True, end=".\n", stream=sys.stderr): self.name = name - self.timestamp = timestamp + self.format = format self.colors = colors - self.default_prefix = default_prefix - self.default_suffix = default_suffix + self.end = end self.stream = stream - def __call__(self, text, prefix=None, suffix=None, indent=0, timestamp=None, colors=None, display_name=True): - if prefix is None: - prefix = self.default_prefix + def __call__(self, text, tag=None, format=None, colors=None, end=None): + tag = tag or self.__class__.tag.long.info + format = format or self.format + colors = colors or self.colors + end = end or self.end - if type(prefix) is str: - prefix = (prefix, prefix) - - if suffix is None: - suffix = self.default_suffix - - if timestamp is None: - timestamp = self.timestamp - - if colors is None: - colors = self.colors - - output = [] - - # [2012-11-11 16:52:06] INFO -- ahoj - if timestamp: - output.append(time.strftime("[%Y-%m-%d %H:%M:%S] ")) - output.append(prefix[1]) - output.append(" -- ") - elif prefix: - output.append(prefix[0]) - output.append(" ") - - if display_name and self.name: - output.append("%s, " %(self.name)) - - #output.append(paragraph(str(text), indent=indent)) - output.append(str(text)) - - if suffix: - output.append(suffix) - - output = "".join(output) + output = datetime.datetime.now().strftime(format) + output = output.replace("§T§", tag) + output = output.replace("§n§", self.name) + output = output.replace("§t§", text) + output += end self.stream.write(render(output, colors)) self.stream.flush() @@ -582,10 +558,11 @@ class Output: def get_terminal_size(): """ - Returns current terminal"s (rows, cols). + Returns current terminal's (rows, cols). """ terminal = sys.stdout.fileno() + try: return struct.unpack("HHHH", fcntl.ioctl(terminal, termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0))) except IOError: @@ -593,12 +570,8 @@ def get_terminal_size(): # -------------------------------------------------- -_print = Output("over.core.text", stream=sys.stderr) - -# -------------------------------------------------- - if __name__ == "__main__": - pb = ProgressBar2( + pb = ProgressBar( "§%a (§ra/§za) [§=a>§ A] §sa [§rb/§zb] (ETA §TA)", { "a": {