mirror of
https://github.com/erkin/ponysay.git
synced 2024-11-25 05:47:59 +01:00
f65be435ac
Signed-off-by: Pablo Lezaeta <prflr88@gmail.com>
1140 lines
44 KiB
Python
Executable file
1140 lines
44 KiB
Python
Executable file
#!/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 name–directories–quoting 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 ponies–quote 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 pony–quote 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)
|
||
|