ponysay/src/ponysay.py
Pablo Lezaeta f65be435ac Update copyright and copyleft year in the sourcecodes
Signed-off-by: Pablo Lezaeta <prflr88@gmail.com>
2016-05-31 00:53:02 -04:00

1140 lines
44 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

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-2016 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 backend import *
from balloon import *
from spellocorrecter import *
from ucs import *
from kms import *
import lists
from metadata import *
class Ponysay():
'''
This is the mane class of ponysay
'''
def __init__(self):
'''
Constructor
'''
# The user's home directory
self.HOME = os.environ['HOME'] if 'HOME' in os.environ else ''
if len(self.HOME) == 0:
os.environ['HOME'] = self.HOME = os.path.expanduser('~')
## Load extension and configurations via ponysayrc
for file in ('$XDG_CONFIG_HOME/ponysay/ponysayrc', '$HOME/.config/ponysay/ponysayrc', '$HOME/.ponysayrc', '/etc/ponysayrc'):
file = self.__parseFile(file)
if (file is not None) and os.path.exists(file):
with open(file, 'rb') as ponysayrc:
code = ponysayrc.read().decode('utf8', 'replace') + '\n'
env = os.environ
code = compile(code, file, 'exec')
exec(code)
break
self.HOME = os.environ['HOME'] if 'HOME' in os.environ else '' # in case ~/.ponysayrc changes it
if len(self.HOME) == 0:
os.environ['HOME'] = self.HOME = os.path.expanduser('~')
# Whether any unrecognised options was parsed, this should be set by the invoker before run()
self.unrecognised = False
# Whether the program is execute in Linux VT (TTY)
self.linuxvt = ('TERM' in os.environ) and (os.environ['TERM'] == 'linux')
# Whether the script is executed as ponythink
self.isthink = self.__isPonythink()
# Whether stdin is piped
self.pipelinein = not sys.stdin.isatty()
# Whether stdout is piped
self.pipelineout = not sys.stdout.isatty()
# Whether stderr is piped
self.pipelineerr = not sys.stderr.isatty()
# Whether KMS is used
self.usekms = KMS.usingKMS(self.linuxvt)
# Mode string that modifies or adds $ variables in the pony image
self.mode = ''
# The directories where pony files are stored, ttyponies/ are used if the terminal is Linux VT (also known as TTY) and not with KMS
self.xponydirs = self.__getShareDirectories('ponies/')
self.vtponydirs = self.__getShareDirectories('ttyponies/')
# The directories where pony files are stored, extrattyponies/ are used if the terminal is Linux VT (also known as TTY) and not with KMS
self.extraxponydirs = self.__getShareDirectories('extraponies/')
self.extravtponydirs = self.__getShareDirectories('extrattyponies/')
# The directories where quotes files are stored
self.quotedirs = self.__getShareDirectories('quotes/')
# The directories where balloon style files are stored
self.balloondirs = self.__getShareDirectories('balloons/')
# ucsmap files
self.ucsmaps = self.__getShareDirectories('ucsmap/')
@classmethod
def __parseFile(cls, file):
'''
Parse a file name encoded with environment variables
@param file The encoded file name
@return The target file name, None if the environment variables are not declared
'''
if '$' in file:
buf = ''
esc = False
var = None
for c in file:
if esc:
buf += c
esc = False
elif var is not None:
if c == '/':
var = os.environ[var] if var in os.environ else ''
if len(var) == 0:
return None
buf += var + c
var = None
else:
var += c
elif c == '$':
var = ''
elif c == '\\':
esc = True
else:
buf += c
return buf
return file
@classmethod
def __getShareDirectories(cls, directory):
'''
Gets existing unique /share directories
@param directory:str The directory base name
@return :list<str> Absolute directory names
'''
appendset = set()
rc = []
_ponydirs = cls.__share(directory)
for ponydir in _ponydirs:
if (ponydir is not None) and os.path.isdir(ponydir) and (ponydir not in appendset):
rc.append(ponydir)
appendset.add(ponydir)
return rc
@classmethod
def __share(cls, file):
'''
Gets /share files
@param file:str The file base name
@return :list<str> Absolute file names
'''
def cat(a, b):
if a is None:
return None
return a + b
# TODO use only ./ in development mode
return [cat(cls.__parseFile(item), file) for item in [
'$XDG_DATA_HOME/ponysay/',
'$HOME/.local/share/ponysay/',
'/usr/share/ponysay/'
]]
@classmethod
def __isPonythink(cls):
'''
Check if ponythink is executed
'''
isthink = sys.argv[0]
if os.sep in isthink:
isthink = isthink[isthink.rfind(os.sep) + 1:]
if os.extsep in isthink:
isthink = isthink[:isthink.find(os.extsep)]
isthink = isthink.endswith('think')
return isthink
def run(self, args):
'''
Starts the part of the program the arguments indicate
@param args:ArgParser Parsed command line arguments
'''
if (args.argcount == 0) and not self.pipelinein:
args.help()
exit(254)
return
self.args = args;
## Emulate termial capabilities
if self.__test_nfdnf('-X'): (self.linuxvt, self.usekms) = (False, False)
elif self.__test_nfdnf('-V'): (self.linuxvt, self.usekms) = (True, False)
elif self.__test_nfdnf('-K'): (self.linuxvt, self.usekms) = (True, True)
self.ponydirs = self.vtponydirs if self.linuxvt and not self.usekms else self.xponydirs
self.extraponydirs = self.extravtponydirs if self.linuxvt and not self.usekms else self.extraxponydirs
## Variadic variants of -f, -q &c
for sign in ('-', '+'):
for letter in ('f', 'F', 'q', 'Q'):
ssl = sign + sign + letter
sl = sign + letter
if (ssl in args.opts) and (args.opts[ssl] is not None):
if args.opts[sl] is not None: args.opts[sl] += args.opts[ssl]
else: args.opts[sl] = args.opts[ssl]
## Save whether standard or extra ponies are used
self.usingstandard = self.__test_nfdnf('-f', '-F', '-q') # -Q
self.usingextra = self.__test_nfdnf('+f', '-F') # +q -Q
## Run modes
if self.__test_nfdnf('-h'): args.help()
elif self.__test_nfdnf('+h'): args.help(True)
elif self.__test_nfdnf('-v'): self.version()
elif self.__test_nfdnf('--quoters'): self.quoters(True, False)
elif self.__test_nfdnf('--Onelist', ('--onelist', '++onelist')): self.onelist(True, True)
elif self.__test_nfdnf('--onelist'): self.onelist(True, False)
elif self.__test_nfdnf('++onelist'): self.onelist(False, True)
elif self.__test_nfdnf('+A', ('-L', '+L')): self.linklist(); self.__extraponies(); self.linklist()
elif self.__test_nfdnf('-A', ('-l', '+l')): self.list(); self.__extraponies(); self.list()
elif self.__test_nfdnf('-L'): self.linklist()
elif self.__test_nfdnf('-l'): self.list()
elif self.__test_nfdnf('+L'): self.__extraponies(); self.linklist()
elif self.__test_nfdnf('+l'): self.__extraponies(); self.list()
elif self.__test_nfdnf('-B'): self.balloonlist()
else:
self.__run()
def __test_nfdnf(self, *keys):
'''
Test arguments written in negation-free disjunctive normal form
@param keys:*(str|itr<str>) A list of keys and set of keys, any of which must exists, a set of keys only passes if all of those exists
@return :bool Whether the check passed
'''
for key in keys:
if isinstance(key, str):
if self.args.opts[key] is not None:
return True
else:
for skey in key:
if self.args.opts[skey] is None:
return False
return True
return False
def __run(self):
'''
Run the important part of the program, the pony
'''
## Colouring features
if self.__test_nfdnf('--colour-pony'):
self.mode += '\033[' + ';'.join(self.args.opts['--colour-pony']) + 'm'
else:
self.mode += '\033[0m'
if self.__test_nfdnf('+c'):
for part in ('msg', 'link', 'bubble'):
if self.args.opts['--colour-' + part] is None:
self.args.opts['--colour-' + part] = self.args.opts['+c']
## Other extra features
self.__bestpony(self.args)
self.__ucsremap(self.args)
if self.__test_nfdnf('-o'):
self.mode += '$/= $$\\= $'
self.args.message = ''
self.ponyonly = True
else:
self.ponyonly = False
if self.__test_nfdnf('-i', '+i'):
self.args.message = ''
self.restriction = self.args.opts['-r']
## The stuff
if not self.unrecognised:
self.printPony(self.args)
else:
self.args.help()
exit(255)
##############################################
## Methods that run before the mane methods ##
##############################################
def __extraponies(self):
'''
Use extra ponies
'''
## Change ponydir to extraponydir
self.ponydirs[:] = self.extraponydirs
self.quotedirs[:] = [] ## TODO +q
def __bestpony(self, args):
'''
Use best.pony if nothing else is set
@param args:ArgParser Parsed command line arguments
'''
## Set best.pony as the pony to display if none is selected
def test(keys, strict):
if strict:
for key in keys:
if (args.opts[key] is not None) and (len(args.opts[key]) != 0):
return False
else:
for key in keys:
if args.opts[key] is not None:
return False
return True
keys = ['-f', '+f', '-F', '-q'] ## TODO +q -Q
if test(keys, False):
for ponydir in self.ponydirs:
if os.path.isfile(ponydir + 'best.pony') or os.path.islink(ponydir + 'best.pony'):
pony = os.path.realpath(ponydir + 'best.pony') # Canonical path
if test(keys, True):
args.opts['-f'] = [pony]
else:
for key in keys:
if test(key, True):
args.opts[key] = [pony]
break
def __ucsremap(self, args):
'''
Apply pony name remapping to args according to UCS settings
@param args:ArgParser Parsed command line arguments
'''
## Read UCS configurations
env_ucs = os.environ['PONYSAY_UCS_ME'] if 'PONYSAY_UCS_ME' in os.environ else ''
ucs_conf = 0
if env_ucs in ('yes', 'y', '1'): ucs_conf = 1
elif env_ucs in ('harder', 'h', '2'): ucs_conf = 2
## Stop UCS is not used
if ucs_conf == 0:
return
## Read all lines in all UCS → ASCII map files
maplines = []
for ucsmap in self.ucsmaps:
if os.path.isfile(ucsmap):
with open(ucsmap, 'rb') as mapfile:
maplines += [line.replace('\n', '') for line in mapfile.read().decode('utf8', 'replace').split('\n')]
## Create UCS → ASCII mapping from read lines
map = {}
stripset = ' \t' # must be string, wtf! and way doesn't python's doc say so
for line in maplines:
if (len(line) > 0) and not (line[0] == '#'):
s = line.index('')
ucs = line[:s] .strip(stripset)
ascii = line[s + 1:].strip(stripset)
map[ucs] = ascii
## Apply UCS → ASCII mapping to -f, +f, -F and -q arguments
for flag in ('-f', '+f', '-F', '-q'): ## TODO +q -Q
if args.opts[flag] is not None:
for i in range(0, len(args.opts[flag])):
if args.opts[flag][i] in map:
args.opts[flag][i] = map[args.opts[flag][i]]
#######################
## Auxiliary methods ##
#######################
def __ucsise(self, ponies, links = None):
'''
Apply UCS:ise pony names according to UCS settings
@param ponies:list<str> List of all ponies (of interrest)
@param links:map<str, str>? Map to fill with simulated symlink ponies, may be `None`
'''
## Read UCS configurations
env_ucs = os.environ['PONYSAY_UCS_ME'] if 'PONYSAY_UCS_ME' in os.environ else ''
ucs_conf = 0
if env_ucs in ('yes', 'y', '1'): ucs_conf = 1
elif env_ucs in ('harder', 'h', '2'): ucs_conf = 2
## Stop UCS is not used
if ucs_conf == 0:
return
## Read all lines in all UCS → ASCII map files
maplines = []
for ucsmap in self.ucsmaps:
if os.path.isfile(ucsmap):
with open(ucsmap, 'rb') as mapfile:
maplines += [line.replace('\n', '') for line in mapfile.read().decode('utf8', 'replace').split('\n')]
## Create UCS → ASCII mapping from read lines
map = {}
stripset = ' \t' # must be string, wtf! and way doesn't python's doc say so
for line in maplines:
if not line.startswith('#'):
s = line.index('')
ucs = line[:s] .strip(stripset)
ascii = line[s + 1:].strip(stripset)
map[ascii] = ucs
## Apply UCS → ASCII mapping to ponies, by alias if weak settings
if ucs_conf == 1:
for pony in ponies:
if pony in map:
ponies.append(map[pony])
if links is not None:
links[map[pony]] = pony
else:
for j in range(0, len(ponies)):
if ponies[j] in map:
ponies[j] = map[ponies[j]]
def __getPony(self, selection, args, alt = False):
'''
Returns one file with full path and ponyquote that should be used, names is filter for names, also accepts filepaths
@param selection:(name:str, dirfiles:itr<str>, quote:bool)? Parsed command line arguments as namedirectoriesquoting tubles:
name: The pony name
dirfiles: Files, with the directory, in the pony directories
quote: Whether to use ponyquotes
@param args:ArgParser Parsed command line arguments
@param alt:bool For method internal use...
@return (path, quote):(str, str?) The file name of a pony, and the ponyquote that should be used if any
'''
## If there is no selected ponies, choose all of them
if (selection is None) or (len(selection) == 0):
selection = [self.__selectAnypony(args)]
## Select a random pony of the choosen ones
pony = selection[random.randrange(0, len(selection))]
if os.path.exists(pony[0]):
ponyname = pony[0].split(os.sep)[-1]
if os.extsep in ponyname:
ponyname = ponyname[:ponyname.rfind(os.extsep)]
return (pony[0], self.__getQuote(ponyname, pony[0]) if pony[2] else None)
else:
possibilities = [f.split(os.sep)[-1][:-5] for f in pony[1]]
if pony[0] not in possibilities:
if not alt:
autocorrect = SpelloCorrecter(possibilities)
(alternatives, dist) = autocorrect.correct(pony[0])
limit = os.environ['PONYSAY_TYPO_LIMIT'] if 'PONYSAY_TYPO_LIMIT' in os.environ else ''
limit = 5 if len(limit) == 0 else int(limit)
if (len(alternatives) > 0) and (dist <= limit):
(_, files, quote) = pony
return self.__getPony([(a, files, quote) for a in alternatives], True)
printerr('I have never heard of anypony named %s' % pony[0]);
if not self.usingstandard:
printerr('Use -f/-q or -F if it a MLP:FiM pony');
if not self.usingextra:
printerr('Have you tested +f or -F?');
exit(252)
else:
file = pony[1][possibilities.index(pony[0])]
return (file, self.__getQuote(pony[0], file) if pony[2] else None)
def __selectAnypony(self, args):
'''
Randomly select a pony from all installed ponies
@param args:ArgParser Parsed command line arguments
@return (name, dirfile, quote):(str, list<str>, bool) The pony name, pony file with the directory, and whether to use ponyquotes
'''
quote = args.opts['-q'] is not None ## TODO +q -Q
standard = (args.opts['-f'] is not None) or (args.opts['-F'] is not None) or (args.opts['-q'] is not None) ## TODO -Q
extra = (args.opts['+f'] is not None) or (args.opts['-F'] is not None) ## TODO +q -Q
if not (standard or extra):
standard = True
ponydirs = (self.ponydirs if standard else []) + (self.extraponydirs if extra else []);
quoters = self.__quoters() if standard and quote else None ## TODO +q -Q
if (quoters is not None) and (len(quoters) == 0):
printerr('Princess Celestia! All the ponies are mute!')
exit(250)
## Get all ponies, with quotes
oldponies = {}
self.__getAllPonies(standard, extra, oldponies, quoters)
## Apply restriction
ponies = self.__applyRestriction(oldponies, ponydirs)
## Select one pony and set all information
names = list(ponies.keys())
if len(names) == 0:
printerr('All the ponies are missing, call the Princess!')
exit(249)
pony = names[random.randrange(0, len(names))]
return (pony, [ponies[pony]], quote)
def __getAllPonies(self, standard, extra, collection, quoters):
'''
Get ponies for a set of directories
@param standard:bool Whether to include standard ponies
@parma extra:bool Whether to include extra ponies
@param collection:dict<str, str> Collection of already found ponies, and collection for new ponies, maps to the pony file
@param quoters:set<str>? Ponies to limit to, or `None` to include all ponies
'''
if standard:
self.__getPonies(self.ponydirs, collection, quoters)
if extra:
self.__getPonies(self.extraponydirs, collection, quoters)
def __getPonies(self, directories, collection, quoters):
'''
Get ponies for a set of directories
@param directories:list<str> Directories with ponies
@param collection:dict<str, str> Collection of already found ponies, and collection for new ponies, maps to the pony file
@param quoters:set<str>? Ponies to limit to, or `None` to include all ponies
'''
for ponydir in directories:
for ponyfile in os.listdir(ponydir):
if endswith(ponyfile, '.pony'):
pony = ponyfile[:-5]
if (pony not in collection) and ((quoters is None) or (pony in quoters)):
collection[pony] = ponydir + ponyfile
def __applyRestriction(self, oldponies, ponydirs):
'''
Restrict ponies
@param oldponies:dict<str, str> Collection of original ponies, maps to pony file
@param ponydirs:list<sr> List of pony directories
@return :dict<str, str> Map from restricted ponies to pony files
'''
## Apply metadata restriction
if self.restriction is not None:
ponies = {}
self.__applyMetadataRestriction(ponies, oldponies, ponydirs)
if len(ponies) > 0:
oldponies = ponies
## Apply dimension restriction
ponies = {}
self.__applyDimensionRestriction(ponies, oldponies, ponydirs)
if len(ponies) > 0:
oldponies = ponies
return oldponies
def __applyMetadataRestriction(self, ponies, oldponies, ponydirs):
'''
Restrict to ponies by metadata
@param ponies:dict<str, str> Collection to fill with restricted ponies, mapped to pony file
@param oldponies:dict<str, str> Collection of original ponies, maps to pony file
@param ponydirs:list<sr> List of pony directories
'''
logic = Metadata.makeRestrictionLogic(self.restriction)
for ponydir in ponydirs:
for pony in Metadata.restrictedPonies(ponydir, logic):
if (pony in oldponies) and not (pony in ponies):
ponies[pony] = ponydir + pony + '.pony'
def __applyDimensionRestriction(self, ponies, oldponies, ponydirs):
'''
Restrict to ponies by dimension
@param ponies:dict<str, str> Collection to fill with restricted ponies, mapped to pony file
@param oldponies:dict<str, str> Collection of original ponies, maps to pony file
@param ponydirs:list<sr> List of pony directories
'''
(termh, termw) = gettermsize()
for ponydir in ponydirs:
(fitw, fith) = (None, None)
if os.path.exists(ponydir + 'widths'):
fitw = set()
with open(ponydir + 'widths', 'rb') as file:
Metadata.getFitting(fitw, termw, file)
if os.path.exists(ponydir + ('onlyheights' if self.ponyonly else 'heights')):
fith = set()
with open(ponydir + ('onlyheights' if self.ponyonly else 'heights'), 'rb') as file:
Metadata.getFitting(fith, termh, file)
for ponyfile in oldponies.values():
if ponyfile.startswith(ponydir):
pony = ponyfile[len(ponydir) : -5]
if (fitw is None) or (pony in fitw):
if (fith is None) or (pony in fith):
ponies[pony] = ponyfile
def __getQuote(self, pony, file):
'''
Select a quote for a pony
@param pony:str The pony name
@param file:str The pony's file name
@return :str A quote from the pony, with a failure fall back message
'''
quote = []
if (os.path.dirname(file) + os.sep).replace(os.sep + os.sep, os.sep) in self.ponydirs:
realpony = pony
if os.path.islink(file):
realpony = os.path.basename(os.path.realpath(file))
if os.extsep in realpony:
realpony = realpony[:realpony.rfind(os.extsep)]
quote = self.__quotes(ponies = [realpony])
if len(quote) == 0:
quote = 'Zecora! Help me, I am mute!'
else:
quote = quote[random.randrange(0, len(quote))][1]
printinfo('quote file: ' + quote)
with open(quote, 'rb') as qfile:
quote = qfile.read().decode('utf8', 'replace').strip()
return quote
def __quoters(self, ponydirs = None, quotedirs = None):
'''
Returns a set with all ponies that have quotes and are displayable
@param ponydirs:itr<str>? The pony directories to use
@param quotedirs:itr<str>? The quote directories to use
@return :set<str> All ponies that have quotes and are displayable
'''
if ponydirs is None: ponydirs = self.ponydirs
if quotedirs is None: quotedirs = self.quotedirs
## List all unique quote files
quotes = []
quoteshash = set()
_quotes = []
for quotedir in quotedirs:
_quotes += [item[:item.index('.')] for item in os.listdir(quotedir)]
for quote in _quotes:
if not quote == '':
if not quote in quoteshash:
quoteshash.add(quote)
quotes.append(quote)
## Create a set of all ponies that have quotes
ponies = set()
for ponydir in ponydirs:
for pony in os.listdir(ponydir):
if not pony[0] == '.':
p = pony[:-5] # remove .pony
for quote in quotes:
if ('+' + p + '+') in ('+' + quote + '+'):
if not p in ponies:
ponies.add(p)
return ponies
def __quotes(self, ponydirs = None, quotedirs = None, ponies = None):
'''
Returns a list with all (pony, quote file) pairs
@param ponydirs:itr<str>? The pony directories to use
@param quotedirs:itr<str>? The quote directories to use
@param ponies:itr<str>? The ponies to use
@return (pony, quote):(str, str) All poniesquote file-pairs
'''
if ponydirs is None: ponydirs = self.ponydirs
if quotedirs is None: quotedirs = self.quotedirs
## Get all ponyquote files
quotes = []
for quotedir in quotedirs:
quotes += [quotedir + item for item in os.listdir(quotedir)]
## Create list of all ponyquote file-pairs
rc = []
if ponies is None:
for ponydir in ponydirs:
for pony in os.listdir(ponydir):
if endswith(pony, '.pony'):
p = pony[:-5] # remove .pony
for quote in quotes:
q = quote[quote.rindex('/') + 1:]
q = q[:q.rindex('.')]
if ('+' + p + '+') in ('+' + q + '+'):
rc.append((p, quote))
else:
for p in ponies:
for quote in quotes:
q = quote[quote.rindex('/') + 1:]
q = q[:q.rindex('.')]
if ('+' + p + '+') in ('+' + q + '+'):
rc.append((p, quote))
return rc
#####################
## Listing methods ##
#####################
def list(self, ponydirs = None):
'''
Lists the available ponies
@param ponydirs:itr<str>? The pony directories to use
'''
lists.simplelist(self.ponydirs if ponydirs is None else ponydirs,
self.__quoters(), lambda x : self.__ucsise(x))
def linklist(self, ponydirs = None):
'''
Lists the available ponies with alternatives inside brackets
@param ponydirs:itr<str> The pony directories to use
'''
lists.linklist(self.ponydirs if ponydirs is None else ponydirs,
self.__quoters(), lambda x, y : self.__ucsise(x, y))
def onelist(self, standard = True, extra = False):
'''
Lists the available ponies on one column without anything bold or otherwise formated
@param standard:bool Include standard ponies
@param extra:bool Include extra ponies
'''
pony_dirs = (self.ponydirs if standard else []) + (self.extraponydirs if extra else [])
lists.onelist(pony_dirs, self.__ucsise)
def quoters(self, standard = True, extra = False):
'''
Lists with all ponies that have quotes and are displayable, on one column without anything bold or otherwise formated
@param standard:bool Include standard ponies
@param extra:bool Include extra ponies
'''
## Get all quoters
ponies = list(self.__quoters()) if standard else []
## And now the extra ponies
if extra:
self.__extraponies()
ponies += list(self.__quoters())
## UCS:ise here
self.__ucsise(ponies)
ponies.sort()
## Print each one on a seperate line, but skip duplicates
last = ''
for pony in ponies:
if not pony == last:
last = pony
print(pony)
#####################
## Balloon methods ##
#####################
def balloonlist(self):
'''
Prints a list of all balloons
'''
lists.balloonlist(self.balloondirs, self.isthink)
def __getBalloonPath(self, names, alt = False):
'''
Returns one file with full path, names is filter for style names, also accepts filepaths
@param names:list<str> Balloons to choose from, may be `None`
@param alt:bool For method internal use
@param :str The file name of the balloon, will be `None` iff `names` is `None`
'''
## Stop if there is no choosen balloon
if names is None:
return None
## Get all balloons
balloons = {}
for balloondir in self.balloondirs:
for balloon in os.listdir(balloondir):
balloonfile = balloon
## Use .think if running ponythink, otherwise .say
if self.isthink and endswith(balloon, '.think'):
balloon = balloon[:-6]
elif (not self.isthink) and endswith(balloon, '.say'):
balloon = balloon[:-4]
else:
continue
## Add the balloon if there is none with the same name
if balloon not in balloons:
balloons[balloon] = balloondir + balloonfile
## Support for explicit balloon file names
for name in names:
if os.path.exists(name):
balloons[name] = name
## Select a random balloon of the choosen ones
balloon = names[random.randrange(0, len(names))]
if balloon not in balloons:
if not alt:
autocorrect = SpelloCorrecter(self.balloondirs, '.think' if self.isthink else '.say')
(alternatives, dist) = autocorrect.correct(balloon)
limit = os.environ['PONYSAY_TYPO_LIMIT'] if 'PONYSAY_TYPO_LIMIT' in os.environ else ''
limit = 5 if len(limit) == 0 else int(limit)
if (len(alternatives) > 0) and (dist <= limit):
return self.__getBalloonPath(alternatives, True)
printerr('That balloon style %s does not exist' % balloon)
exit(251)
else:
return balloons[balloon]
def __getBalloon(self, balloonfile):
'''
Creates the balloon style object
@param balloonfile:str The file with the balloon style, may be `None`
@return :Balloon Instance describing the balloon's style
'''
return Balloon.fromFile(balloonfile, self.isthink)
########################
## Displaying methods ##
########################
def version(self):
'''
Prints the name of the program and the version of the program
'''
## Prints the "ponysay $VERSION", if this is modified, ./dev/dist.sh must be modified accordingly
print('%s %s' % ('ponysay', VERSION))
def printPony(self, args):
'''
Print the pony with a speech or though bubble. message, pony and wrap from args are used.
@param args:ArgParser Parsed command line arguments
'''
## Get the pony
selection = []
self.__getSelectedPonies(args, selection)
(pony, quote) = self.__getPony(selection, args)
## Get message and manipulate it
msg = self.__getMessage(args, quote)
msg = self.__colouriseMessage(args, msg)
msg = self.__compressMessage(args, msg)
## Print info
printinfo('pony file: ' + pony)
## Use PNG file as pony file
pony = self.__useImage(pony)
## If KMS is utilies, select a KMS pony file and create it if necessary
pony = KMS.kms(pony, self.HOME, self.linuxvt)
## If in Linux VT clean the terminal (See info/pdf-manual [Printing in TTY with KMS])
if self.linuxvt:
print('\033[H\033[2J', end='')
## Get width truncation and wrapping
widthtruncation = self.__getWidthTruncation()
messagewrap = self.__getMessageWrap(args)
## Get balloon object
balloonfile = self.__getBalloonPath(args.opts['-b'] if args.opts['-b'] is not None else None)
printinfo('balloon style file: ' + str(balloonfile))
balloon = self.__getBalloon(balloonfile) if args.opts['-o'] is None else None
## Get hyphen style
hyphen = self.__getHyphen(args)
## Link and balloon colouring
linkcolour = self.__getLinkColour(args)
ballooncolour = self.__getBalloonColour(args)
## Determine --info/++info settings
minusinfo = args.opts['-i'] is not None
plusinfo = args.opts['+i'] is not None
## Run cowsay replacement
backend = Backend(message = msg, ponyfile = pony, wrapcolumn = messagewrap, width = widthtruncation, balloon = balloon,
hyphen = hyphen, linkcolour = linkcolour, ballooncolour = ballooncolour, mode = self.mode,
infolevel = 2 if plusinfo else (1 if minusinfo else 0))
backend.parse()
output = backend.output
if output.endswith('\n'):
output = output[:-1]
## Print the output, truncated on the height
self.__printOutput(output)
def __getSelectedPonies(self, args, selection):
'''
Get all selected ponies
@param args:ArgParser Command line options
@param selection:list<(name:str, file:str, quotes:bool)> List to fill with tuples of selected pony names, pony files and whether quotes are used
'''
(standard, extra) = ([], [])
for ponydir in self.ponydirs:
for pony in os.listdir(ponydir):
if endswith(pony, '.pony'):
standard.append(ponydir + pony)
for ponydir in self.extraponydirs:
for pony in os.listdir(ponydir):
if endswith(pony, '.pony'):
extra.append(ponydir + pony)
both = standard + extra
for (opt, ponies, quotes) in [('-f', standard, False), ('+f', extra, False), ('-F', both, False), ('-q', standard, True)]: ## TODO +q -Q
if args.opts[opt] is not None:
for pony in args.opts[opt]:
selection.append((pony, ponies, quotes))
def __getMessage(self, args, quote):
'''
Get message and remove tailing whitespace from stdin (but not for each line)
@param args:ArgParser Command line options
@param quote:str? The quote, or `None` if none
@return :str The message
'''
if quote is not None:
return quote
if args.message is None:
return ''.join(sys.stdin.readlines()).rstrip()
return args.message
def __colouriseMessage(self, args, msg):
'''
Colourise message if option is set
@param args:ArgParser Command line options
@param msg:str The message
@return :str The message colourised
'''
if args.opts['--colour-msg'] is not None:
msg = '\033[' + ';'.join(args.opts['--colour-msg']) + 'm' + msg
return msg
def __compressMessage(self, args, msg):
'''
This algorithm should give some result as cowsay's, if option is set
@param args:ArgParser Command line options
@param msg:str The message
@return :str The message compressed
'''
if args.opts['-c'] is None:
return msg
buf = ''
last = ' '
CHARS = '\t \n'
for c in msg:
if (c in CHARS) and (last in CHARS):
if last == '\n':
buf += last
last = c
else:
buf += c
last = c
msg = buf.strip(CHARS)
buf = ''
for c in msg:
if (c != '\n') or (last != '\n'):
buf += c
last = c
return buf.replace('\n', '\n\n')
def __useImage(self, pony):
'''
Convert image to the ponysay format if it is a regular image
@param pony:str The pony file
@return :str The new pony file, or the old if it was already in the ponysay format
'''
if endswith(pony.lower(), '.png'):
pony = '\'' + pony.replace('\'', '\'\\\'\'') + '\''
pngcmd = 'ponytool --import image --file %s --balloon n --export ponysay --platform %s --balloon y'
pngcmd %= (pony, ('linux' if self.linuxvt else 'xterm')) # XXX xterm should be haiku in Haiku
pngpipe = os.pipe()
Popen(pngcmd, stdout=os.fdopen(pngpipe[1], 'w'), shell=True).wait()
pony = '/proc/' + str(os.getpid()) + '/fd/' + str(pngpipe[0])
return pony
def __getWidthTruncation(self):
'''
Gets the width trunction setting
@return :int? The column the truncate the output at, or `None` to not truncate it
'''
env_width = os.environ['PONYSAY_FULL_WIDTH'] if 'PONYSAY_FULL_WIDTH' in os.environ else None
if env_width is None: env_width = 'auto'
return gettermsize()[1] if env_width not in ('yes', 'y', '1') else None
def __getMessageWrap(self, args):
'''
Gets the message balloon wrapping column
@param args:ArgParser Command line options
@return :int? The message balloon wrapping column, or `None` if disabled
'''
messagewrap = 65
if (args.opts['-W'] is not None) and (len(args.opts['-W'][0]) > 0):
messagewrap = args.opts['-W'][0]
if messagewrap[0] in 'nmsNMS': # m is left to n on QWERTY and s is left to n on Dvorak
messagewrap = None
elif messagewrap[0] in 'iouIOU': # o is left to i on QWERTY and u is right to i on Dvorak
messagewrap = gettermsize()[1]
else:
messagewrap = int(args.opts['-W'][0])
return messagewrap
def __getHyphen(self, args):
'''
Gets the hyphen to use a at hyphenation
@param args:ArgParser Command line options
@return :str The hyphen string to use at hyphenation
'''
hyphen = os.environ['PONYSAY_WRAP_HYPHEN'] if 'PONYSAY_WRAP_HYPHEN' in os.environ else None
if (hyphen is None) or (len(hyphen) == 0):
hyphen = '-'
hyphencolour = ''
if args.opts['--colour-wrap'] is not None:
hyphencolour = '\033[' + ';'.join(args.opts['--colour-wrap']) + 'm'
return '\033[31m' + hyphencolour + hyphen
def __getLinkColour(self, args):
'''
Gets the colour of balloon links
@param args:ArgParser Command line options
@return :str The colour of balloon links
'''
linkcolour = ''
if args.opts['--colour-link'] is not None:
linkcolour = '\033[' + ';'.join(args.opts['--colour-link']) + 'm'
return linkcolour
def __getBalloonColour(self, args):
'''
Gets the colour of balloons
@param args:ArgParser Command line options
@return :str The colour of balloons
'''
ballooncolour = ''
if args.opts['--colour-bubble'] is not None:
ballooncolour = '\033[' + ';'.join(args.opts['--colour-bubble']) + 'm'
return ballooncolour
def __printOutput(self, output):
'''
Print the output, but truncate it on the height
@param output:str The output truncated on the width but not on the height
'''
## Load height trunction settings
env_bottom = os.environ['PONYSAY_BOTTOM'] if 'PONYSAY_BOTTOM' in os.environ else None
if env_bottom is None: env_bottom = ''
env_height = os.environ['PONYSAY_TRUNCATE_HEIGHT'] if 'PONYSAY_TRUNCATE_HEIGHT' in os.environ else None
if env_height is None: env_height = ''
env_lines = os.environ['PONYSAY_SHELL_LINES'] if 'PONYSAY_SHELL_LINES' in os.environ else None
if (env_lines is None) or (env_lines == ''): env_lines = '2'
## Print the output, truncated on height if so set
lines = gettermsize()[0] - int(env_lines)
if self.linuxvt or (env_height in ('yes', 'y', '1')):
if env_bottom in ('yes', 'y', '1'):
for line in output.split('\n')[: -lines]:
print(line)
else:
for line in output.split('\n')[: lines]:
print(line)
else:
print(output)