#!/usr/bin/env python3 # -*- coding: utf-8 -*- ''' ponysay - Ponysay, cowsay reimplementation for ponies Copyright (C) 2012, 2013, 2014 Erkin Batu Altunbaş et al. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . If you intend to redistribute ponysay or a fork of it commercially, it contains aggregated images, some of which may not be commercially redistribute, you would be required to remove those. To determine whether or not you may commercially redistribute an image make use that line ‘FREE: yes’, is included inside the image between two ‘$$$’ lines and the ‘FREE’ is and upper case and directly followed by the colon. ''' from common import * from balloon import * from colourstack import * from ucs import * class Backend(): ''' Super-ultra-extreme-awesomazing replacement for cowsay ''' def __init__(self, message, ponyfile, wrapcolumn, width, balloon, hyphen, linkcolour, ballooncolour, mode, infolevel): ''' Constructor @param message:str The message spoken by the pony @param ponyfile:str The pony file @param wrapcolumn:int The column at where to wrap the message, `None` for no wrapping @param width:int The width of the screen, `None` if truncation should not be applied @param balloon:Balloon The balloon style object, `None` if only the pony should be printed @param hyphen:str How hyphens added by the wordwrapper should be printed @param linkcolour:str How to colour the link character, empty string if none @param ballooncolour:str How to colour the balloon, empty string if none @param mode:str Mode string for the pony @parma infolevel:int 2 if ++info is used, 1 if --info is used and 0 otherwise ''' self.message = message self.ponyfile = ponyfile self.wrapcolumn = None if wrapcolumn is None else wrapcolumn - (0 if balloon is None else balloon.minwidth) self.width = width self.balloon = balloon self.hyphen = hyphen self.ballooncolour = ballooncolour self.mode = mode self.balloontop = 0 self.balloonbottom = 0 self.infolevel = infolevel if self.balloon is not None: self.link = {'\\' : linkcolour + self.balloon.link, '/' : linkcolour + self.balloon.linkmirror, 'X' : linkcolour + self.balloon.linkcross} else: self.link = {} self.output = '' self.pony = None def parse(self): ''' Process all data ''' self.__loadFile() if self.pony.startswith('$$$\n'): self.pony = self.pony[4:] if self.pony.startswith('$$$\n'): infoend = 4 info = '' else: infoend = self.pony.index('\n$$$\n') info = self.pony[:infoend] infoend += 5 if self.infolevel == 2: self.message = Backend.formatInfo(info) self.pony = self.pony[infoend:] elif self.infolevel == 1: self.pony = Backend.formatInfo(info).replace('$', '$$') else: info = info.split('\n') for line in info: sep = line.find(':') if sep > 0: key = line[:sep].strip() if key == 'BALLOON TOP': value = line[sep + 1:].strip() if len(value) > 0: self.balloontop = int(value) if key == 'BALLOON BOTTOM': value = line[sep + 1:].strip() if len(value) > 0: self.balloonbottom = int(value) printinfo(info) self.pony = self.pony[infoend:] elif self.infolevel == 2: self.message = '\033[01;31mI am the mysterious mare...\033[21;39m' elif self.infolevel == 1: self.pony = 'There is not metadata for this pony file' self.pony = self.mode + self.pony self.__expandMessage() self.__unpadMessage() self.__processPony() self.__truncate() @staticmethod def formatInfo(info): ''' Format metadata to be nicely printed, this include bold keys @param info:str The metadata @return :str The metadata nicely formated ''' info = info.split('\n') tags = '' comment = '' for line in info: sep = line.find(':') if sep > 0: key = line[:sep] test = key for c in 'ABCDEFGHIJKLMN OPQRSTUVWXYZ': test = test.replace(c, '') if (len(test) == 0) and (len(key.replace(' ', '')) > 0): value = line[sep + 1:].strip() line = '\033[1m%s\033[21m: %s\n' % (key.strip(), value) tags += line continue comment += '\n' + line comment = comment.lstrip('\n') if len(comment) > 0: comment = '\n' + comment return tags + comment def __unpadMessage(self): ''' Remove padding spaces fortune cookies are padded with whitespace (damn featherbrains) ''' lines = self.message.split('\n') for spaces in (128, 64, 32, 16, 8, 4, 2, 1): padded = True for line in lines: if not line.startswith(' ' * spaces): padded = False break if padded: for i in range(0, len(lines)): line = lines[i] line = line[spaces:] lines[i] = line lines = [line.rstrip(' ') for line in lines] self.message = '\n'.join(lines) def __expandMessage(self): ''' Converts all tabs in the message to spaces by expanding ''' lines = self.message.split('\n') buf = '' for line in lines: (i, n, x) = (0, len(line), 0) while i < n: c = line[i] i += 1 if c == '\033': colour = Backend.getColour(line, i - 1) i += len(colour) - 1 buf += colour elif c == '\t': nx = 8 - (x & 7) buf += ' ' * nx x += nx else: buf += c if not UCS.isCombining(c): x += 1 buf += '\n' self.message = buf[:-1] def __loadFile(self): ''' Loads the pony file ''' with open(self.ponyfile, 'rb') as ponystream: self.pony = ponystream.read().decode('utf8', 'replace') def __truncate(self): ''' Truncate output to the width of the screen ''' if self.width is None: return lines = self.output.split('\n') self.output = '' for line in lines: (i, n, x) = (0, len(line), 0) while i < n: c = line[i] i += 1 if c == '\033': colour = Backend.getColour(line, i - 1) i += len(colour) - 1 self.output += colour else: if x < self.width: self.output += c if not UCS.isCombining(c): x += 1 self.output += '\n' self.output = self.output[:-1] def __processPony(self): ''' Process the pony file and generate output to self.output ''' self.output = '' AUTO_PUSH = '\033[01010~' AUTO_POP = '\033[10101~' variables = {'' : '$'} for key in self.link: variables[key] = AUTO_PUSH + self.link[key] + AUTO_POP indent = 0 dollar = None balloonLines = None colourstack = ColourStack(AUTO_PUSH, AUTO_POP) (i, n, lineindex, skip, nonskip) = (0, len(self.pony), 0, 0, 0) while i < n: c = self.pony[i] if c == '\t': n += 7 - (indent & 7) ed = ' ' * (8 - (indent & 7)) c = ' ' self.pony = self.pony[:i] + ed + self.pony[i + 1:] i += 1 if c == '$': if dollar is not None: if '=' in dollar: name = dollar[:dollar.find('=')] value = dollar[dollar.find('=') + 1:] variables[name] = value elif not dollar.startswith('balloon'): data = variables[dollar].replace('$', '$$') if data == '$$': # if not handled specially we will get an infinity loop if (skip == 0) or (nonskip > 0): if nonskip > 0: nonskip -= 1 self.output += '$' indent += 1 else: skip -= 1 else: n += len(data) self.pony = self.pony[:i] + data + self.pony[i:] elif self.balloon is not None: (w, h, x, justify) = ('0', 0, 0, None) props = dollar[7:] if len(props) > 0: if ',' in props: if props[0] is not ',': w = props[:props.index(',')] h = int(props[props.index(',') + 1:]) else: w = props if 'l' in w: (x, w) = (int(w[:w.find('l')]), int(w[w.find('l') + 1:])) justify = 'l' w -= x; elif 'c' in w: (x, w) = (int(w[:w.find('c')]), int(w[w.find('c') + 1:])) justify = 'c' w -= x; elif 'r' in w: (x, w) = (int(w[:w.find('r')]), int(w[w.find('r') + 1:])) justify = 'r' w -= x; else: w = int(w) balloon = self.__getBalloon(w, h, x, justify, indent) balloon = balloon.split('\n') balloon = [AUTO_PUSH + self.ballooncolour + item + AUTO_POP for item in balloon] for b in balloon[0]: self.output += b + colourstack.feed(b) if lineindex == 0: balloonpre = '\n' + (' ' * indent) for line in balloon[1:]: self.output += balloonpre; for b in line: self.output += b + colourstack.feed(b); indent = 0 elif len(balloon) > 1: balloonLines = balloon balloonLine = 0 balloonIndent = indent indent += Backend.len(balloonLines[0]) balloonLines[0] = None dollar = None else: dollar = '' elif dollar is not None: if c == '\033': c = self.pony[i] i += 1 dollar += c elif c == '\033': colour = Backend.getColour(self.pony, i - 1) for b in colour: self.output += b + colourstack.feed(b); i += len(colour) - 1 elif c == '\n': self.output += c indent = 0 (skip, nonskip) = (0, 0) lineindex += 1 if balloonLines is not None: balloonLine += 1 if balloonLine == len(balloonLines): balloonLines = None else: if (balloonLines is not None) and (balloonLines[balloonLine] is not None) and (balloonIndent == indent): data = balloonLines[balloonLine] datalen = Backend.len(data) skip += datalen nonskip += datalen data = data.replace('$', '$$') n += len(data) self.pony = self.pony[:i] + data + self.pony[i:] balloonLines[balloonLine] = None else: if (skip == 0) or (nonskip > 0): if nonskip > 0: nonskip -= 1 self.output += c + colourstack.feed(c); if not UCS.isCombining(c): indent += 1 else: skip -= 1 if balloonLines is not None: for line in balloonLines[balloonLine:]: data = ' ' * (balloonIndent - indent) + line + '\n' for b in data: self.output += b + colourstack.feed(b); indent = 0 self.output = self.output.replace(AUTO_PUSH, '').replace(AUTO_POP, '') if self.balloon is None: if (self.balloontop > 0) or (self.balloonbottom > 0): self.output = self.output.split('\n') self.output = self.output[self.balloontop : ~(self.balloonbottom)] self.output = '\n'.join(self.output) @staticmethod def getColour(input, offset): ''' Gets colour code att the currect offset in a buffer @param input:str The input buffer @param offset:int The offset at where to start reading, a escape must begin here @return :str The escape sequence ''' (i, n) = (offset, len(input)) rc = input[i] i += 1 if i == n: return rc c = input[i] i += 1 rc += c if c == ']': if i == n: return rc c = input[i] i += 1 rc += c if c == 'P': di = 0 while (di < 7) and (i < n): c = input[i] i += 1 di += 1 rc += c while c == '0': c = input[i] i += 1 rc += c if c == '4': c = input[i] i += 1 rc += c if c == ';': c = input[i] i += 1 rc += c while c != '\\': c = input[i] i += 1 rc += c elif c == '[': while i < n: c = input[i] i += 1 rc += c if (c == '~') or (('a' <= c) and (c <= 'z')) or (('A' <= c) and (c <= 'Z')): break return rc @staticmethod def len(input): ''' Calculates the number of visible characters in a text @param input:str The input buffer @return :int The number of visible characters ''' (rc, i, n) = (0, 0, len(input)) while i < n: c = input[i] if c == '\033': i += len(Backend.getColour(input, i)) else: i += 1 if not UCS.isCombining(c): rc += 1 return rc def __getBalloon(self, width, height, innerleft, justify, left): ''' Generates a balloon with the message @param width:int The minimum width of the balloon @param height:int The minimum height of the balloon @param innerleft:int The left column of the required span, excluding that of `left` @param justify:str Balloon placement justification, 'c' → centered, 'l' → left (expand to right), 'r' → right (expand to left) @param left:int The column where the balloon starts @return :str The balloon the the message as a string ''' wrap = None if self.wrapcolumn is not None: wrap = self.wrapcolumn - left if wrap < 8: wrap = 8 msg = self.message if wrap is not None: msg = self.__wrapMessage(msg, wrap) msg = msg.replace('\n', '\033[0m%s\n' % (self.ballooncolour)) + '\033[0m' + self.ballooncolour msg = msg.split('\n') extraleft = 0 if justify is not None: msgwidth = self.len(max(msg, key = self.len)) + self.balloon.minwidth extraleft = innerleft if msgwidth > width: if (justify == 'l') and (wrap is not None): if innerleft + msgwidth > wrap: extraleft -= msgwidth - wrap elif justify == 'r': extraleft -= msgwidth - width elif justify == 'c': extraleft -= (msgwidth - width) >> 1 if extraleft < 0: extraleft = 0 if wrap is not None: if extraleft + msgwidth > wrap: extraleft -= msgwidth - wrap rc = self.balloon.get(width, height, msg, Backend.len); if extraleft > 0: rc = ' ' * extraleft + rc.replace('\n', '\n' + ' ' * extraleft) return rc def __wrapMessage(self, message, wrap): ''' Wraps the message @param message:str The message to wrap @param wrap:int The width at where to force wrapping @return :str The message wrapped ''' wraplimit = os.environ['PONYSAY_WRAP_LIMIT'] if 'PONYSAY_WRAP_LIMIT' in os.environ else '' wraplimit = 8 if len(wraplimit) == 0 else int(wraplimit) wrapexceed = os.environ['PONYSAY_WRAP_EXCEED'] if 'PONYSAY_WRAP_EXCEED' in os.environ else '' wrapexceed = 5 if len(wrapexceed) == 0 else int(wrapexceed) buf = '' try: AUTO_PUSH = '\033[01010~' AUTO_POP = '\033[10101~' msg = message.replace('\n', AUTO_PUSH + '\n' + AUTO_POP) cstack = ColourStack(AUTO_PUSH, AUTO_POP) for c in msg: buf += c + cstack.feed(c) lines = buf.replace(AUTO_PUSH, '').replace(AUTO_POP, '').split('\n') buf = '' for line in lines: b = [None] * len(line) map = {0 : 0} (bi, cols, w) = (0, 0, wrap) (indent, indentc) = (-1, 0) (i, n) = (0, len(line)) while i <= n: d = None if i < n: d = line[i] i += 1 if d == '\033': ## Invisible stuff i -= 1 colourseq = Backend.getColour(line, i) b[bi : bi + len(colourseq)] = colourseq i += len(colourseq) bi += len(colourseq) elif (d is not None) and (d != ' '): ## Fetch word if indent == -1: indent = i - 1 for j in range(0, indent): if line[j] == ' ': indentc += 1 b[bi] = d bi += 1 if (not UCS.isCombining(d)) and (d != '­'): cols += 1 map[cols] = bi else: ## Wrap? mm = 0 bisub = 0 iwrap = wrap - (0 if indent == 1 else indentc) while ((w > wraplimit) and (cols > w + wrapexceed)) or (cols > iwrap): ## wrap x = w; if mm + x not in map: # Too much whitespace? cols = 0 break nbsp = b[map[mm + x]] == ' ' # nbsp m = map[mm + x] if ('­' in b[bisub : m]) and not nbsp: # soft hyphen hyphen = m - 1 while b[hyphen] != '­': # soft hyphen hyphen -= 1 while map[mm + x] > hyphen: ## Only looking backward, if forward is required the word is probabily not hyphenated correctly x -= 1 x += 1 m = map[mm + x] mm += x - (0 if nbsp else 1) ## − 1 so we have space for a hythen for bb in b[bisub : m]: buf += bb buf += '\n' if nbsp else '\0\n' cols -= x - (0 if nbsp else 1) bisub = m w = iwrap if indent != -1: buf += ' ' * indentc for j in range(bisub, bi): b[j - bisub] = b[j] bi -= bisub if cols > w: buf += '\n' w = wrap if indent != -1: buf += ' ' * indentc w -= indentc for bb in b[:bi]: if bb is not None: buf += bb w -= cols cols = 0 bi = 0 if d is None: i += 1 else: if w > 0: buf += ' ' w -= 1 else: buf += '\n' w = wrap if indent != -1: buf += ' ' * indentc w -= indentc buf += '\n' rc = '\n'.join(line.rstrip(' ') for line in buf[:-1].split('\n')); rc = rc.replace('­', ''); # remove soft hyphens rc = rc.replace('\0', '%s%s%s' % (AUTO_PUSH, self.hyphen, AUTO_POP)) return rc except Exception as err: import traceback errormessage = ''.join(traceback.format_exception(type(err), err, None)) rc = '\n'.join(line.rstrip(' ') for line in buf.split('\n')); rc = rc.replace('\0', '%s%s%s' % (AUTO_PUSH, self.hyphen, AUTO_POP)) errormessage += '\n---- WRAPPING BUFFER ----\n\n' + rc try: if os.readlink('/proc/self/fd/2') != os.readlink('/proc/self/fd/1'): printerr(errormessage, end='') return message except: pass return message + '\n\n\033[0;1;31m---- EXCEPTION IN PONYSAY WHILE WRAPPING ----\033[0m\n\n' + errormessage