ponysay/src/backend.py
Mattias Andrée cfb41b52c9 update copyright year
Signed-off-by: Mattias Andrée <maandree@operamail.com>
2014-02-03 20:39:18 +01:00

651 lines
25 KiB
Python
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 <http://www.gnu.org/licenses/>.
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