splitting into multiple files

Signed-off-by: Mattias Andrée <maandree@operamail.com>
This commit is contained in:
Mattias Andrée 2013-04-02 11:21:33 +02:00
parent 9a8fa37ab8
commit 5df821d7cd
9 changed files with 2905 additions and 2784 deletions

File diff suppressed because it is too large Load diff

304
src/argparser.py Normal file
View file

@ -0,0 +1,304 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
ponysay - Ponysay, cowsay reimplementation for ponies
Copyright (C) 2012, 2013 Erkin Batu Altunbaş et al.
This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it
and/or modify it under the terms of the Do What The Fuck You Want
To Public License, Version 2, as published by Sam Hocevar. See
http://sam.zoy.org/wtfpl/COPYING for more details.
'''
from common import *
'''
Option takes no arguments
'''
ARGUMENTLESS = 0
'''
Option takes one argument per instance
'''
ARGUMENTED = 1
'''
Option consumes all following arguments
'''
VARIADIC = 2
'''
Simple argument parser
'''
class ArgParser():
'''
Constructor.
The short description is printed on same line as the program name
@param program:str The name of the program
@param description:str Short, single-line, description of the program
@param usage:str Formated, multi-line, usage text
@param longdescription:str Long, multi-line, description of the program, may be `None`
'''
def __init__(self, program, description, usage, longdescription = None):
self.linuxvt = ('TERM' in os.environ) and (os.environ['TERM'] == 'linux')
self.__program = program
self.__description = description
self.__usage = usage
self.__longdescription = longdescription
self.__arguments = []
self.opts = {}
self.optmap = {}
'''
Add option that takes no arguments
@param alternatives:list<str> Option names
@param help:str Short description, use `None` to hide the option
'''
def add_argumentless(self, alternatives, help = None):
self.__arguments.append((ARGUMENTLESS, alternatives, None, help))
stdalt = alternatives[0]
self.opts[stdalt] = None
for alt in alternatives:
self.optmap[alt] = (stdalt, ARGUMENTLESS)
'''
Add option that takes one argument
@param alternatives:list<str> Option names
@param arg:str The name of the takes argument, one word
@param help:str Short description, use `None` to hide the option
'''
def add_argumented(self, alternatives, arg, help = None):
self.__arguments.append((ARGUMENTED, alternatives, arg, help))
stdalt = alternatives[0]
self.opts[stdalt] = None
for alt in alternatives:
self.optmap[alt] = (stdalt, ARGUMENTED)
'''
Add option that takes all following argument
@param alternatives:list<str> Option names
@param arg:str The name of the takes arguments, one word
@param help:str Short description, use `None` to hide the option
'''
def add_variadic(self, alternatives, arg, help = None):
self.__arguments.append((VARIADIC, alternatives, arg, help))
stdalt = alternatives[0]
self.opts[stdalt] = None
for alt in alternatives:
self.optmap[alt] = (stdalt, VARIADIC)
'''
Parse arguments
@param args:list<str> The command line arguments, should include the execute file at index 0, `sys.argv` is default
@return :bool Whether no unrecognised option is used
'''
def parse(self, argv = sys.argv):
self.argcount = len(argv) - 1
self.files = []
argqueue = []
optqueue = []
deque = []
for arg in argv[1:]:
deque.append(arg)
dashed = False
tmpdashed = False
get = 0
dontget = 0
self.rc = True
self.unrecognisedCount = 0
def unrecognised(arg):
self.unrecognisedCount += 1
if self.unrecognisedCount <= 5:
sys.stderr.write('%s: warning: unrecognised option %s\n' % (self.__program, arg))
self.rc = False
while len(deque) != 0:
arg = deque[0]
deque = deque[1:]
if (get > 0) and (dontget == 0):
get -= 1
argqueue.append(arg)
elif tmpdashed:
self.files.append(arg)
tmpdashed = False
elif dashed: self.files.append(arg)
elif arg == '++': tmpdashed = True
elif arg == '--': dashed = True
elif (len(arg) > 1) and (arg[0] in ('-', '+')):
if (len(arg) > 2) and (arg[:2] in ('--', '++')):
if dontget > 0:
dontget -= 1
elif (arg in self.optmap) and (self.optmap[arg][1] == ARGUMENTLESS):
optqueue.append(arg)
argqueue.append(None)
elif '=' in arg:
arg_opt = arg[:arg.index('=')]
if (arg_opt in self.optmap) and (self.optmap[arg_opt][1] >= ARGUMENTED):
optqueue.append(arg_opt)
argqueue.append(arg[arg.index('=') + 1:])
if self.optmap[arg_opt][1] == VARIADIC:
dashed = True
else:
unrecognised(arg)
elif (arg in self.optmap) and (self.optmap[arg][1] == ARGUMENTED):
optqueue.append(arg)
get += 1
elif (arg in self.optmap) and (self.optmap[arg][1] == VARIADIC):
optqueue.append(arg)
argqueue.append(None)
dashed = True
else:
unrecognised(arg)
else:
sign = arg[0]
i = 1
n = len(arg)
while i < n:
narg = sign + arg[i]
i += 1
if (narg in self.optmap):
if self.optmap[narg][1] == ARGUMENTLESS:
optqueue.append(narg)
argqueue.append(None)
elif self.optmap[narg][1] == ARGUMENTED:
optqueue.append(narg)
nargarg = arg[i:]
if len(nargarg) == 0:
get += 1
else:
argqueue.append(nargarg)
break
elif self.optmap[narg][1] == VARIADIC:
optqueue.append(narg)
nargarg = arg[i:]
argqueue.append(nargarg if len(nargarg) > 0 else None)
dashed = True
break
else:
unrecognised(narg)
else:
self.files.append(arg)
i = 0
n = len(optqueue)
while i < n:
opt = optqueue[i]
arg = argqueue[i] if len(argqueue) > i else None
i += 1
opt = self.optmap[opt][0]
if (opt not in self.opts) or (self.opts[opt] is None):
self.opts[opt] = []
if len(argqueue) >= i:
self.opts[opt].append(arg)
for arg in self.__arguments:
if arg[0] == VARIADIC:
varopt = self.opts[arg[1][0]]
if varopt is not None:
additional = ','.join(self.files).split(',') if len(self.files) > 0 else []
if varopt[0] is None:
self.opts[arg[1][0]] = additional
else:
self.opts[arg[1][0]] = varopt[0].split(',') + additional
self.files = []
break
self.message = ' '.join(self.files) if len(self.files) > 0 else None
if self.unrecognisedCount > 5:
sys.stderr.write('%s: warning: %i more unrecognised %s\n' % (self.unrecognisedCount - 5, 'options' if self.unrecognisedCount == 6 else 'options'))
return self.rc
'''
Prints a colourful help message
'''
def help(self):
print('\033[1m%s\033[21m %s %s' % (self.__program, '-' if self.linuxvt else '', self.__description))
print()
if self.__longdescription is not None:
print(self.__longdescription)
print()
print('\033[1mUSAGE:\033[21m', end='')
first = True
for line in self.__usage.split('\n'):
if first:
first = False
else:
print(' or', end='')
print('\t%s' % (line))
print()
maxfirstlen = []
for opt in self.__arguments:
opt_alts = opt[1]
opt_help = opt[3]
if opt_help is None:
continue
first = opt_alts[0]
last = opt_alts[-1]
if first is not last:
maxfirstlen.append(first)
maxfirstlen = len(max(maxfirstlen, key = len))
print('\033[1mSYNOPSIS:\033[21m')
(lines, lens) = ([], [])
for opt in self.__arguments:
opt_type = opt[0]
opt_alts = opt[1]
opt_arg = opt[2]
opt_help = opt[3]
if opt_help is None:
continue
(line, l) = ('', 0)
first = opt_alts[0]
last = opt_alts[-1]
alts = ['', last] if first is last else [first, last]
alts[0] += ' ' * (maxfirstlen - len(alts[0]))
for opt_alt in alts:
if opt_alt is alts[-1]:
line += '%colour%' + opt_alt
l += len(opt_alt)
if opt_type == ARGUMENTED: line += ' \033[4m%s\033[24m' % (opt_arg); l += len(opt_arg) + 1
elif opt_type == VARIADIC: line += ' [\033[4m%s\033[24m...]' % (opt_arg); l += len(opt_arg) + 6
else:
line += ' \033[2m%s\033[22m ' % (opt_alt)
l += len(opt_alt) + 6
lines.append(line)
lens.append(l)
col = max(lens)
col += 8 - ((col - 4) & 7)
index = 0
for opt in self.__arguments:
opt_help = opt[3]
if opt_help is None:
continue
first = True
colour = '36' if (index & 1) == 0 else '34'
print(lines[index].replace('%colour%', '\033[%s;1m' % (colour)), end=' ' * (col - lens[index]))
for line in opt_help.split('\n'):
if first:
first = False
print('%s' % (line), end='\033[21;39m\n')
else:
print('%s\033[%sm%s\033[39m' % (' ' * col, colour, line))
index += 1
print()

628
src/backend.py Normal file
View file

@ -0,0 +1,628 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
ponysay - Ponysay, cowsay reimplementation for ponies
Copyright (C) 2012, 2013 Erkin Batu Altunbaş et al.
This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it
and/or modify it under the terms of the Do What The Fuck You Want
To Public License, Version 2, as published by Sam Hocevar. See
http://sam.zoy.org/wtfpl/COPYING for more details.
'''
from common import *
'''
Super-ultra-extreme-awesomazing replacement for cowsay
'''
class Backend():
'''
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
'''
def __init__(self, message, ponyfile, wrapcolumn, width, balloon, hyphen, linkcolour, ballooncolour, mode, infolevel):
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}
else:
self.link = {}
self.output = ''
self.pony = None
'''
Process all data
'''
def parse(self):
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)
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()
'''
Format metadata to be nicely printed, this include bold keys
@param info:str The metadata
@return :str The metadata nicely formated
'''
@staticmethod
def formatInfo(info):
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
'''
Remove padding spaces fortune cookies are padded with whitespace (damn featherbrains)
'''
def __unpadMessage(self):
lines = self.message.split('\n')
for spaces in (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]
while line.startswith(' ' * spaces):
line = line[spaces:]
lines[i] = line
lines = [line.rstrip(' ') for line in lines]
self.message = '\n'.join(lines)
'''
Converts all tabs in the message to spaces by expanding
'''
def __expandMessage(self):
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]
'''
Loads the pony file
'''
def __loadFile(self):
with open(self.ponyfile, 'rb') as ponystream:
self.pony = ponystream.read().decode('utf8', 'replace')
'''
Truncate output to the width of the screen
'''
def __truncate(self):
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]
'''
Process the pony file and generate output to self.output
'''
def __processPony(self):
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 not 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)
'''
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
'''
@staticmethod
def getcolour(input, offset):
(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
'''
Calculates the number of visible characters in a text
@param input:str The input buffer
@return :int The number of visible characters
'''
@staticmethod
def len(input):
(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
'''
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
'''
def __getballoon(self, width, height, innerleft, justify, left):
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
'''
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
'''
def __wrapMessage(self, message, wrap):
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: # sort hyphen
hyphen = m - 1
while b[hyphen] != '­': # sort hyphen
hyphen -= 1
while map[mm + x] > hyphen: ## Only looking backward, if foreward 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 += line[:indent]
for j in range(bisub, bi):
b[j - bisub] = b[j]
bi -= bisub
if cols > w:
buf += '\n'
w = wrap
if indent != -1:
buf += line[:indent]
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 + line[:indent]
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

114
src/balloon.py Normal file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
ponysay - Ponysay, cowsay reimplementation for ponies
Copyright (C) 2012, 2013 Erkin Batu Altunbaş et al.
This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it
and/or modify it under the terms of the Do What The Fuck You Want
To Public License, Version 2, as published by Sam Hocevar. See
http://sam.zoy.org/wtfpl/COPYING for more details.
'''
from common import *
'''
Balloon format class
'''
class Balloon():
'''
Constructor
@param link:str The \-directional balloon line character
@param linkmirror:str The /-directional balloon line character
@param ww:str See the info manual
@param ee:str See the info manual
@param nw:list<str> See the info manual
@param nnw:list<str> See the info manual
@param n:list<str> See the info manual
@param nne:list<str> See the info manual
@param ne:list<str> See the info manual
@param nee:str See the info manual
@param e:str See the info manual
@param see:str See the info manual
@param se:list<str> See the info manual
@param sse:list<str> See the info manual
@param s:list<str> See the info manual
@param ssw:list<str> See the info manual
@param sw:list<str> See the info manual
@param sww:str See the info manual
@param w:str See the info manual
@param nww:str See the info manual
'''
def __init__(self, link, linkmirror, ww, ee, nw, nnw, n, nne, ne, nee, e, see, se, sse, s, ssw, sw, sww, w, nww):
(self.link, self.linkmirror) = (link, linkmirror)
(self.ww, self.ee) = (ww, ee)
(self.nw, self.ne, self.se, self.sw) = (nw, ne, se, sw)
(self.nnw, self.n, self.nne) = (nnw, n, nne)
(self.nee, self.e, self.see) = (nee, e, see)
(self.sse, self.s, self.ssw) = (sse, s, ssw)
(self.sww, self.w, self.nww) = (sww, w, nww)
_ne = max(ne, key = UCS.dispLen)
_nw = max(nw, key = UCS.dispLen)
_se = max(se, key = UCS.dispLen)
_sw = max(sw, key = UCS.dispLen)
minE = UCS.dispLen(max([_ne, nee, e, see, _se, ee], key = UCS.dispLen))
minW = UCS.dispLen(max([_nw, nww, e, sww, _sw, ww], key = UCS.dispLen))
minN = len(max([ne, nne, n, nnw, nw], key = len))
minS = len(max([se, sse, s, ssw, sw], key = len))
self.minwidth = minE + minE
self.minheight = minN + minS
'''
Generates a balloon with a message
@param minw:int The minimum number of columns of the balloon
@param minh:int The minimum number of lines of the balloon
@param lines:list<str> The text lines to display
@param lencalc:int(str) Function used to compute the length of a text line
@return :str The balloon as a formated string
'''
def get(self, minw, minh, lines, lencalc):
h = self.minheight + len(lines)
w = self.minwidth + lencalc(max(lines, key = lencalc))
if w < minw: w = minw
if h < minh: h = minh
if len(lines) > 1:
(ws, es) = ({0 : self.nww, len(lines) - 1 : self.sww}, {0 : self.nee, len(lines) - 1 : self.see})
for j in range(1, len(lines) - 1):
ws[j] = self.w
es[j] = self.e
else:
(ws, es) = ({0 : self.ww}, {0 : self.ee})
rc = []
for j in range(0, len(self.n)):
outer = UCS.dispLen(self.nw[j]) + UCS.dispLen(self.ne[j])
inner = UCS.dispLen(self.nnw[j]) + UCS.dispLen(self.nne[j])
if outer + inner <= w:
rc.append(self.nw[j] + self.nnw[j] + self.n[j] * (w - outer - inner) + self.nne[j] + self.ne[j])
else:
rc.append(self.nw[j] + self.n[j] * (w - outer) + self.ne[j])
for j in range(0, len(lines)):
rc.append(ws[j] + lines[j] + ' ' * (w - lencalc(lines[j]) - UCS.dispLen(self.w) - UCS.dispLen(self.e)) + es[j])
for j in range(0, len(self.s)):
outer = UCS.dispLen(self.sw[j]) + UCS.dispLen(self.se[j])
inner = UCS.dispLen(self.ssw[j]) + UCS.dispLen(self.sse[j])
if outer + inner <= w:
rc.append(self.sw[j] + self.ssw[j] + self.s[j] * (w - outer - inner) + self.sse[j] + self.se[j])
else:
rc.append(self.sw[j] + self.s[j] * (w - outer) + self.se[j])
return '\n'.join(rc)

115
src/colourstack.py Normal file
View file

@ -0,0 +1,115 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
ponysay - Ponysay, cowsay reimplementation for ponies
Copyright (C) 2012, 2013 Erkin Batu Altunbaş et al.
This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it
and/or modify it under the terms of the Do What The Fuck You Want
To Public License, Version 2, as published by Sam Hocevar. See
http://sam.zoy.org/wtfpl/COPYING for more details.
'''
from common import *
'''
ANSI colour stack
This is used to make layers with independent coloursations
'''
class ColourStack():
'''
Constructor
@param autopush:str String that, when used, will create a new independently colourised layer
@param autopop:str String that, when used, will end the current layer and continue of the previous layer
'''
def __init__(self, autopush, autopop):
self.autopush = autopush
self.autopop = autopop
self.lenpush = len(autopush)
self.lenpop = len(autopop)
self.bufproto = ' ' * (self.lenpush if self.lenpush > self.lenpop else self.lenpop)
self.stack = []
self.push()
self.seq = None
'''
Create a new independently colourised layer
@return :str String that should be inserted into your buffer
'''
def push(self):
self.stack.insert(0, [self.bufproto, None, None, [False] * 9])
if len(self.stack) == 1:
return None
return '\033[0m'
'''
End the current layer and continue of the previous layer
@return :str String that should be inserted into your buffer
'''
def pop(self):
old = self.stack.pop(0)
rc = '\033[0;'
if len(self.stack) == 0: # last resort in case something made it pop too mush
push()
new = self.stack[0]
if new[1] is not None: rc += new[1] + ';'
if new[2] is not None: rc += new[2] + ';'
for i in range(0, 9):
if new[3][i]:
rc += str(i + 1) + ';'
return rc[:-1] + 'm'
'''
Use this, in sequence, for which character in your buffer that contains yor autopush and autopop
string, the automatically get push and pop string to insert after each character
@param :chr One character in your buffer
@return :str The text to insert after the input character
'''
def feed(self, char):
if self.seq is not None:
self.seq += char
if (char == '~') or (('a' <= char) and (char <= 'z')) or (('A' <= char) and (char <= 'Z')):
if (self.seq[0] == '[') and (self.seq[-1] == 'm'):
self.seq = self.seq[1:-1].split(';')
(i, n) = (0, len(self.seq))
while i < n:
part = self.seq[i]
p = 0 if part == '' else int(part)
i += 1
if p == 0: self.stack[0][1:] = [None, None, [False] * 9]
elif (1 <= p) and (p <= 9): self.stack[0][3][p - 1] = True
elif (21 <= p) and (p <= 29): self.stack[0][3][p - 21] = False
elif p == 39: self.stack[0][1] = None
elif p == 49: self.stack[0][2] = None
elif (30 <= p) and (p <= 37): self.stack[0][1] = part
elif (90 <= p) and (p <= 97): self.stack[0][1] = part
elif (40 <= p) and (p <= 47): self.stack[0][2] = part
elif (100 <= p) and (p <= 107): self.stack[0][2] = part
elif p == 38:
self.stack[0][1] = '%s;%s;%s' % (part, self.seq[i], self.seq[i + 1])
i += 2
elif p == 48:
self.stack[0][2] = '%s;%s;%s' % (part, self.seq[i], self.seq[i + 1])
i += 2
self.seq = None
elif char == '\033':
self.seq = ''
buf = self.stack[0][0]
buf = buf[1:] + char
rc = ''
if buf[-self.lenpush:] == self.autopush: rc = self.push()
elif buf[-self.lenpop:] == self.autopop: rc = self.pop()
self.stack[0][0] = buf
return rc

74
src/common.py Normal file
View file

@ -0,0 +1,74 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
ponysay - Ponysay, cowsay reimplementation for ponies
Copyright (C) 2012, 2013 Erkin Batu Altunbaş et al.
This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it
and/or modify it under the terms of the Do What The Fuck You Want
To Public License, Version 2, as published by Sam Hocevar. See
http://sam.zoy.org/wtfpl/COPYING for more details.
'''
import os
import shutil
import sys
import random
from subprocess import Popen, PIPE
'''
The version of ponysay
'''
VERSION = 'dev' # this line should not be edited, it is fixed by the build system
'''
Hack to enforce UTF-8 in output (in the future, if you see anypony not using utf-8 in
programs by default, report them to Princess Celestia so she can banish them to the moon)
@param text:str The text to print (empty string is default)
@param end:str The appendix to the text to print (line breaking is default)
'''
def print(text = '', end = '\n'):
sys.stdout.buffer.write((str(text) + end).encode('utf-8'))
'''
stderr equivalent to print()
@param text:str The text to print (empty string is default)
@param end:str The appendix to the text to print (line breaking is default)
'''
def printerr(text = '', end = '\n'):
sys.stderr.buffer.write((str(text) + end).encode('utf-8'))
fd3 = None
'''
/proc/self/fd/3 equivalent to print()
@param text:str The text to print (empty string is default)
@param end:str The appendix to the text to print (line breaking is default)
'''
def printinfo(text = '', end = '\n'):
global fd3
if os.path.exists('/proc/self/fd/3') and os.path.isfile('/proc/self/fd/3'):
if fd3 is None:
fd3 = os.fdopen(3, 'w')
if fd3 is not None:
fd3.write(str(text) + end)
'''
Checks whether a text ends with a specific text, but has more
@param text:str The text to test
@param ending:str The desired end of the text
@return :bool The result of the test
'''
def endswith(text, ending):
return text.endswith(ending) and not (text == ending)

1390
src/ponysay.py Normal file

File diff suppressed because it is too large Load diff

216
src/spellocorrecter.py Normal file
View file

@ -0,0 +1,216 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
ponysay - Ponysay, cowsay reimplementation for ponies
Copyright (C) 2012, 2013 Erkin Batu Altunbaş et al.
This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it
and/or modify it under the terms of the Do What The Fuck You Want
To Public License, Version 2, as published by Sam Hocevar. See
http://sam.zoy.org/wtfpl/COPYING for more details.
'''
from common import *
'''
Class used for correcting spellos and typos,
Note that this implementation will not find that correctly spelled word are correct faster than it corrects words.
It is also limited to words of size 0 to 127 (inclusive)
'''
class SpelloCorrecter(): # Naïvely and quickly ported and adapted from optimised Java, may not be the nicest, or even fast, Python code
'''
Constructor
@param directories:list<str> List of directories that contains the file names with the correct spelling
@param ending:str The file name ending of the correctly spelled file names, this is removed for the name
'''
def __init__(self, directories, ending):
self.weights = {'k' : {'c' : 0.25, 'g' : 0.75, 'q' : 0.125},
'c' : {'k' : 0.25, 'g' : 0.75, 's' : 0.5, 'z' : 0.5, 'q' : 0.125},
's' : {'z' : 0.25, 'c' : 0.5},
'z' : {'s' : 0.25, 'c' : 0.5},
'g' : {'k' : 0.75, 'c' : 0.75, 'q' : 0.9},
'o' : {'u' : 0.5},
'u' : {'o' : 0.5, 'v' : 0.75, 'w' : 0.5},
'b' : {'v' : 0.75},
'v' : {'b' : 0.75, 'w' : 0.5, 'u' : 0.7},
'w' : {'v' : 0.5, 'u' : 0.5},
'q' : {'c' : 0.125, 'k' : 0.125, 'g' : 0.9}}
self.corrections = None
self.dictionary = [None] * 513
self.reusable = [0] * 512
self.dictionaryEnd = 512
self.closestDistance = 0
self.M = [None] * 128
for y in range(0, 128):
self.M[y] = [0] * 128
self.M[y][0] = y
m0 = self.M[0]
x = 127
while x > -1:
m0[x] = x
x -= 1
previous = ''
self.dictionary[-1] = previous;
for directory in directories:
for filename in os.listdir(directory):
if (not endswith(filename, ending)) or (len(filename) - len(ending) > 127):
continue
proper = filename[:-len(ending)]
if self.dictionaryEnd == 0:
self.dictionaryEnd = len(self.dictionary)
self.reusable = [0] * self.dictionaryEnd + self.reusable
self.dictionary = [None] * self.dictionaryEnd + self.dictionary
self.dictionaryEnd -= 1
self.dictionary[self.dictionaryEnd] = proper
prevCommon = min(len(previous), len(proper))
for i in range(0, prevCommon):
if previous[i] != proper[i]:
prevCommon = i
break
previous = proper
self.reusable[self.dictionaryEnd] = prevCommon
#part = self.dictionary[self.dictionaryEnd : len(self.dictionary) - 1]
#part.sort()
#self.dictionary[self.dictionaryEnd : len(self.dictionary) - 1] = part
#
#index = len(self.dictionary) - 1
#while index >= self.dictionaryEnd:
# proper = self.dictionary[index]
# prevCommon = min(len(previous), len(proper))
# for i in range(0, prevCommon):
# if previous[i] != proper[i]:
# prevCommon = i
# break
# previous = proper
# self.reusable[self.dictionaryEnd] = prevCommon
# index -= 1;
'''
Finds the closests correct spelled word
@param used:str The word to correct
@return (words, distance):(list<string>, int) A list the closest spellings and the weighted distance
'''
def correct(self, used):
if len(used) > 127:
return ([used], 0)
self.__correct(used)
return (self.corrections, self.closestDistance)
'''
Finds the closests correct spelled word
@param used:str The word to correct, it must satisfy all restrictions
'''
def __correct(self, used):
self.closestDistance = 0x7FFFFFFF
previous = self.dictionary[-1]
prevLen = 0
usedLen = len(used)
proper = None
prevCommon = 0
d = len(self.dictionary) - 1
while d > self.dictionaryEnd:
d -= 1
proper = self.dictionary[d]
if abs(len(proper) - usedLen) <= self.closestDistance:
if previous == self.dictionary[d + 1]:
prevCommon = self.reusable[d];
else:
prevCommon = min(prevLen, len(proper))
for i in range(0, prevCommon):
if previous[i] != proper[i]:
prevCommon = i
break
skip = min(prevLen, len(proper))
i = prevCommon
while i < skip:
for u in range(0, usedLen):
if (used[u] == previous[i]) or (used[u] == proper[i]):
skip = i
break
i += 1
common = min(skip, min(usedLen, len(proper)))
for i in range(0, common):
if used[i] != proper[i]:
common = i
break
distance = self.__distance(proper, skip, len(proper), used, common, usedLen)
if self.closestDistance > distance:
self.closestDistance = distance
self.corrections = [proper]
elif self.closestDistance == distance:
self.corrections.append(proper)
previous = proper;
if distance >= 0x7FFFFF00:
prevLen = distance & 255
else:
prevLen = len(proper)
'''
Calculate the distance between a correct word and a incorrect word
@param proper:str The correct word
@param y0:int The offset for `proper`
@param yn:int The length, before applying `y0`, of `proper`
@param used:str The incorrect word
@param x0:int The offset for `used`
@param xn:int The length, before applying `x0`, of `used`
@return :float The distance between the words
'''
def __distance(self, proper, y0, yn, used, x0, xn):
my = self.M[y0]
for y in range(y0, yn):
best = 0x7FFFFFFF
p = proper[y]
myy = self.M[y + 1] # only one array bound check, and at most one + ☺
x = x0
while x < xn:
change = my[x]
u = used[x]
if p == u:
# commence black magick … twilight would be so disappointed
x += 1
myy[x] = change
best = min(best, change)
remove = myy[x]
add = my[x + 1]
cw = 1
if my[x] in self.weights:
if p in self.weights[u]:
cw = self.weights[u][p]
x += 1
myy[x] = min(cw + change, 1 + min(remove, add))
if best > myy[x]:
best = myy[x]
if best > self.closestDistance:
return 0x7FFFFF00 | y
my = myy
return my[xn]

62
src/ucs.py Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
ponysay - Ponysay, cowsay reimplementation for ponies
Copyright (C) 2012, 2013 Erkin Batu Altunbaş et al.
This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it
and/or modify it under the terms of the Do What The Fuck You Want
To Public License, Version 2, as published by Sam Hocevar. See
http://sam.zoy.org/wtfpl/COPYING for more details.
'''
from common import *
'''
UCS utility class
'''
class UCS():
'''
Checks whether a character is a combining character
@param char:chr The character to test
@return :bool Whether the character is a combining character
'''
@staticmethod
def isCombining(char):
o = ord(char)
if (0x0300 <= o) and (o <= 0x036F): return True
if (0x20D0 <= o) and (o <= 0x20FF): return True
if (0x1DC0 <= o) and (o <= 0x1DFF): return True
if (0xFE20 <= o) and (o <= 0xFE2F): return True
return False
'''
Gets the number of combining characters in a string
@param string:str A text to count combining characters in
@return :int The number of combining characters in the string
'''
@staticmethod
def countCombining(string):
rc = 0
for char in string:
if UCS.isCombining(char):
rc += 1
return rc
'''
Gets length of a string not counting combining characters
@param string:str The text of which to determine the monospaced width
@return The determine the monospaced width of the text, provided it does not have escape sequnces
'''
@staticmethod
def dispLen(string):
return len(string) - UCS.countCombining(string)