#!/usr/bin/env python # -*- coding: utf-8 -*- ''' ponysay.py - Ponysay, a cowsay reimplementation for ponies Copyright (C) 2012 Erkin Batu Altunbaş Authors: Erkin Batu Altunbaş: Project leader, helped write the first implementation Mattias "maandree" Andrée: Major contributor of both implementions Elis "etu" Axelsson: Major contributor of current implemention and patcher of the first implementation Sven-Hendrik "svenstaro" Haase: Major contributor of the first implementation Jan Alexander "heftig" Steffens: Major contributor of the first implementation Kyah "L-four" Rindlisbacher: Patched the first implementation License: WTFPL ''' import os import shutil import sys import random from subprocess import Popen, PIPE ''' The version of ponysay ''' VERSION = '2.3' ''' The directory where ponysay is installed, this is modified when building with make ''' INSTALLDIR = '/usr' ''' This is the mane class of ponysay ''' class Ponysay(): ''' Starts the part of the program the arguments indicate ''' def __init__(self, args): if (args.argcount == 0) and not pipelinein: args.help() return if args.opts['-h'] is not None: args.help() elif args.opts['--quoters'] is not None: self.quoters() elif args.opts['--onelist'] is not None: self.onelist() elif args.opts['-v'] is not None: self.version() elif args.opts['-l'] is not None: self.list() elif args.opts['-L'] is not None: self.linklist() elif args.opts['-B'] is not None: self.balloonlist() elif args.opts['++onelist'] is not None: self.__extraponies(); self.onelist() elif args.opts['+l'] is not None: self.__extraponies(); self.list() elif args.opts['+L'] is not None: self.__extraponies(); self.linklist() else: self.__extraponies(args) self.__bestpony(args) self.__ucsremap(args) if args.opts['-q'] is not None: self.quote(args) else: self.print_pony(args) ## ## Methods that run before the mane methods ## ''' Use extra ponies ''' def __extraponies(self, args = None): if args is None: ponydirs[:] = extraponydirs elif args.opts['-F'] is not None: args.opts['-f'] = args.opts['-F'] ponydirs[:] = extraponydirs ''' Use best.pony if nothing else is set ''' def __bestpony(self, args): if (args.opts['-f'] is None) or (args.opts['-q'] is None) or (len(args.opts['-q']) == 0): for ponydir in ponydirs: if os.path.isfile(ponydir + 'best.pony') or os.path.islink(ponydir + 'best.pony'): pony = os.path.realpath(ponydir + 'best.pony') # Canonical path args.opts['-f' if args.opts['-q'] is None else '-q'] = [pony] break ''' Apply pony name remapping to args according to UCS settings ''' def __ucsremap(self, args): 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 if ucs_conf == 0: return maplines = [] for sharedir in sharedirs: if os.path.isfile(sharedir + 'ucsmap'): mapfile = None try: mapfile = open(sharedir + 'ucsmap', 'r') maplines += [line.replace('\n', '') for line in mapfile.readlines()] finally: if mapfile is not None: mapfile.close() 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 for flag in ('-f', '-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 ## ''' Apply USC:ise pony names according to UCS settings ''' def __ucsise(self, ponies, links = None): 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 if ucs_conf == 0: return maplines = [] for sharedir in sharedirs: if os.path.isfile(sharedir + 'ucsmap'): mapfile = None try: mapfile = open(sharedir + 'ucsmap', 'r') maplines += [line.replace('\n', '') for line in mapfile.readlines()] finally: if mapfile is not None: mapfile.close() 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[ascii] = ucs 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]] ''' Returns one file with full path, names is filter for names, also accepts filepaths ''' def __getponypath(self, names = None): ponies = {} for ponydir in ponydirs: for ponyfile in os.listdir(ponydir): pony = ponyfile[:-5] if pony not in ponies: ponies[pony] = ponydir + ponyfile if not names == None: for name in names: if os.path.exists(name): ponies[name] = name if names == None: names = list(ponies.keys()) pony = names[random.randrange(0, len(names))] if pony not in ponies: sys.stderr.write('I have never heared of any pony named %s\n' % (pony)); exit(1) else: return ponies[pony] ''' Returns a set with all ponies that have quotes and are displayable ''' def __quoters(self): quotes = [] quoteshash = set() _quotes = [] for quotedir in quotedirs: _quotes += [item[:item.index('.')] for item in os.listdir(INSTALLDIR + '/share/ponysay/quotes/')] for quote in _quotes: if not quote == '': if not quote in quoteshash: quoteshash.add(quote) quotes.append(quote) 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 ''' Returns a list with all (pony, quote file) pairs ''' def __quotes(self): quotes = [] for quotedir in quotedirs: quotes += [quotedir + item for item in os.listdir(quotedir)] rc = [] for ponydir in ponydirs: for pony in os.listdir(ponydir): if not pony[0] == '.': 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)) return rc ''' Gets the size of the terminal in (rows, columns) ''' def __gettermsize(self): termsize = Popen(['stty', 'size'], stdout=PIPE, stdin=sys.stderr).communicate()[0] termsize = termsize.decode('utf8', 'replace')[:-1].split(' ') # [:-1] removes a \n termsize = [int(item) for item in termsize] return termsize ## ## Listing methods ## ''' Columnise a list and prints it ''' def __columnise(self, ponies): termwidth = self.__gettermsize()[1] + 2 ponies.sort(key = lambda pony : pony[0]) widths = [UCS.dispLen(pony[0]) for pony in ponies] width = max(widths) + 2 # longest pony file name + space between columns cols = termwidth // width rows = (len(ponies) + cols - 1) // cols columns = [] for c in range(0, cols): columns.append([]) (y, x) = (0, 0) for j in range(0, len(ponies)): cell = ponies[j][1] + ' ' * (width - widths[j]); columns[x].append(cell) y += 1 if y == rows: x += 1 y = 0 diff = rows * cols - len(ponies) if diff > 2: c = cols - 1 diff -= 1 while diff > 0: columns[c] = columns[c - 1][-diff:] + columns[c] c -= 1 columns[c] = columns[c][:-diff] diff -= 1 pass lines = [] for r in range(0, rows): lines.append([]) for c in range(0, cols): if r < len(columns[c]): line = lines[r].append(columns[c][r]) print('\n'.join([''.join(line)[:-2] for line in lines])); print() ''' Lists the available ponies ''' def list(self): quoters = self.__quoters() for ponydir in ponydirs: # Loop ponydirs _ponies = os.listdir(ponydir) ponies = [] for pony in _ponies: if (len(pony) > 5) and (pony[-5:] == '.pony'): ponies.append(pony[:-5]) self.__ucsise(ponies) if len(ponies) == 0: continue print('\033[1mponies located in ' + ponydir + '\033[21m') self.__columnise([(pony, '\033[1m' + pony + '\033[21m' if pony in quoters else pony) for pony in ponies]) ''' Lists the available ponies with alternatives inside brackets ''' def linklist(self): termsize = self.__gettermsize() quoters = self.__quoters() for ponydir in ponydirs: # Loop ponydirs _ponies = os.listdir(ponydir) ponies = [] for pony in _ponies: if (len(pony) > 5) and (pony[-5:] == '.pony'): ponies.append(pony[:-5]) if len(ponies) == 0: continue print('\033[1mponies located in ' + ponydir + '\033[21m') pseudolinkmap = {} self.__ucsise(ponies, pseudolinkmap) pairs = [] for pony in ponies: if pony in pseudolinkmap: pairs.append((pony, pseudolinkmap[pony] + '.pony')); else: pairs.append((pony, os.path.realpath(ponydir + pony + '.pony') if os.path.islink(ponydir + pony + '.pony') else None)) ponymap = {} for pair in pairs: if (pair[1] is None) or (pair[1] == ''): if pair[0] not in ponymap: ponymap[pair[0]] = [] else: target = pair[1][:-5] if '/' in target: target = target[target.rindex('/') + 1:] if target in ponymap: ponymap[target].append(pair[0]) else: ponymap[target] = [pair[0]] width = 0 ponies = {} for pony in ponymap: w = UCS.dispLen(pony) item = '\033[1m' + pony + '\033[21m' if (pony in quoters) else pony syms = ponymap[pony] syms.sort() if len(syms) > 0: w += 2 + len(syms) item += ' (' first = True for sym in syms: w += UCS.dispLen(sym) if first: first = False else: item += ' ' item += '\033[1m' + sym + '\033[21m' if (sym in quoters) else sym item += ')' ponies[(item.replace('\033[1m', '').replace('\033[21m', ''), item)] = w if width < w: width = w self.__columnise(list(ponies)) ''' Lists with all ponies that have quotes and are displayable ''' def quoters(self): last = '' ponies = [] for pony in self.__quoters(): ponies.append(pony) self.__ucsise(ponies) ponies.sort() for pony in ponies: if not pony == last: last = pony print(pony) ''' Lists the available ponies one one column without anything bold ''' def onelist(self): last = '' _ponies = [] for ponydir in ponydirs: # Loop ponydirs _ponies += os.listdir(ponydir) ponies = [] for pony in _ponies: if (len(pony) > 5) and (pony[-5:] == '.pony'): ponies.append(pony[:-5]) self.__ucsise(ponies) ponies.sort() for pony in ponies: if not pony == last: last = pony print(pony) ## ## Balloon methods ## ''' Prints a list of all balloons ''' def balloonlist(self): termsize = self.__gettermsize() balloonset = set() for balloondir in balloondirs: for balloon in os.listdir(balloondir): if isthink and (len(balloon) > 6) and (balloon[-6:] == '.think'): balloon = balloon[:-6] elif (not isthink) and (len(balloon) > 4) and (balloon[-4:] == '.say'): balloon = balloon[:-4] else: continue if balloon not in balloonset: balloonset.add(balloon) self.__columnise([(balloon, balloon) for balloon in list(balloonset)]) ''' Returns one file with full path, names is filter for style names, also accepts filepaths ''' def __getballoonpath(self, names): if names is None: return None balloons = {} for balloondir in balloondirs: for balloon in os.listdir(balloondir): balloonfile = balloon if isthink and (len(balloon) > 6) and (balloon[-6:] == '.think'): balloon = balloon[:-6] elif (not isthink) and (len(balloon) > 4) and (balloon[-4:] == '.say'): balloon = balloon[:-4] else: continue if balloon not in balloons: balloons[balloon] = balloondir + balloonfile for name in names: if os.path.exists(name): balloons[name] = name if names == None: names = list(balloons.keys()) balloon = names[random.randrange(0, len(names))] if balloon not in balloons: sys.stderr.write('That balloon style %s does not exist\n' % (balloon)); exit(1) else: return balloons[balloon] ''' Creates the balloon style object ''' def __getballoon(self, balloonfile): if balloonfile is None: if isthink: return Balloon('o', 'o', '( ', ' )', [' _'], ['_'], ['_'], ['_'], ['_ '], ' )', ' )', ' )', ['- '], ['-'], ['-'], ['-'], [' -'], '( ', '( ', '( ') return Balloon('\\', '/', '< ', ' >', [' _'], ['_'], ['_'], ['_'], ['_ '], ' \\', ' |', ' /', ['- '], ['-'], ['-'], ['-'], [' -'], '\\ ', '| ', '/ ') map = {} for elem in ('\\', '/', 'ww', 'ee', 'nw', 'nnw', 'n', 'nne', 'ne', 'nee', 'e', 'see', 'se', 'sse', 's', 'ssw', 'sw', 'sww', 'w', 'nww'): map[elem] = [] balloonstream = None try: balloonstream = open(balloonfile, 'r') data = [line.replace('\n', '') for line in balloonstream.readlines()] finally: if balloonstream is not None: balloonstream.close() last = None for line in data: if len(line) > 0: if line[0] == ':': map[last].append(line[1:]) else: last = line[:line.index(':')] value = line[len(last) + 1:] map[last].append(value) return Balloon(map['\\'][0], map['/'][0], map['ww'][0], map['ee'][0], map['nw'], map['nnw'], map['n'], map['nne'], map['ne'], map['nee'][0], map['e'][0], map['see'][0], map['se'], map['sse'], map['s'], map['ssw'], map['sw'], map['sww'][0], map['w'][0], map['nww'][0]) ## ## Displaying methods ## ''' Prints the name of the program and the version of the program ''' def version(self): print('%s %s' % ('ponysay', VERSION)) ''' Print the pony with a speech or though bubble. message, pony and wrap from args are used. ''' def print_pony(self, args): if args.message == None: msg = ''.join(sys.stdin.readlines()).rstrip() else: msg = args.message if args.opts['-c'] is not None: ## This algorithm should give some result as cowsay's (according to tests) 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 msg = buf.replace('\n', '\n\n') pony = self.__getponypath(args.opts['-f']) if (len(pony) > 4) and (pony[-4:].lower() == '.png'): pony = '\'' + pony.replace('\'', '\'\\\'\'') + '\'' pngcmd = ('img2ponysay -p -- ' if linuxvt else 'img2ponysay -- ') + pony pngpipe = os.pipe() Popen(pngcmd, stdout=os.fdopen(pngpipe[1], 'w'), shell=True).wait() pony = '/proc/' + str(os.getpid()) + '/fd/' + str(pngpipe[0]) pony = self.__kms(pony) if linuxvt: print('\033[H\033[2J', end='') env_width = os.environ['PONYSAY_FULL_WIDTH'] if 'PONYSAY_FULL_WIDTH' in os.environ else None if env_width is None: env_width = '' widthtruncation = self.__gettermsize()[1] if env_width not in ('yes', 'y', '1') else None messagewrap = int(args.opts['-W'][0]) if args.opts['-W'] is not None else None balloon = self.__getballoon(self.__getballoonpath(args.opts['-b'])) backend = Backend(message = msg, ponyfile = pony, wrapcolumn = messagewrap if messagewrap is not None else 40, width = widthtruncation, balloon = balloon) backend.parse() output = backend.output if (len(output) > 0) and (output[-1] == '\n'): output = output[:-1] 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' lines = self.__gettermsize()[0] - int(env_lines) if linuxvt or (env_height is ('yes', 'y', '1')): if env_bottom is ('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); ''' Print the pony with a speech or though bubble and a self quote ''' def quote(self, args): pairs = self.__quotes() if len(args.opts['-q']) > 0: ponyset = {} for pony in args.opts['-q']: if (len(pony) > 5) and (pony[-5:] == '.pony'): ponyname = pony[:-5] if '/' in ponyname: ponyname = ponyname[ponyname.rindex('/') + 1:] ponyset[ponyname] = pony else: ponyset[pony] = pony alts = [] for pair in pairs: if pair[0] in ponyset: alts.append((ponyset[pair[0]], pair[1])) pairs = alts if not len(pairs) == 0: pair = pairs[random.randrange(0, len(pairs))] qfile = None try: qfile = open(pair[1], 'r') args.message = '\n'.join(qfile.readlines()).strip() finally: if qfile is not None: qfile.close() args.opts['-f'] = [pair[0]] elif len(args.opts['-q']) == 0: sys.stderr.write('Princess Celestia! All the ponies are mute!\n') exit(1) else: args.opts['-f'] = [args.opts['-q'][random.randrange(0, len(args.opts['-q']))]] args.message = 'Zecora! Help me, I am mute!' self.print_pony(args) ''' Indentifies whether KMS support is utilised ''' @staticmethod def isUsingKMS(): if not linuxvt: return False env_kms = os.environ['PONYSAY_KMS_PALETTE'] if 'PONYSAY_KMS_PALETTE' in os.environ else None if env_kms is None: env_kms = '' env_kms_cmd = os.environ['PONYSAY_KMS_PALETTE_CMD'] if 'PONYSAY_KMS_PALETTE_CMD' in os.environ else None if (env_kms_cmd is not None) and (not env_kms_cmd == ''): env_kms = Popen(shlex.split(env_kms_cmd), stdout=PIPE, stdin=sys.stderr).communicate()[0].decode('utf8', 'replace') if env_kms[-1] == '\n': env_kms = env_kms[:-1] return env_kms != '' ''' Returns the file name of the input pony converted to a KMS pony, or if KMS is not used, the input pony itself ''' def __kms(self, pony): if not linuxvt: return pony KMS_VERSION = '1' env_kms = os.environ['PONYSAY_KMS_PALETTE'] if 'PONYSAY_KMS_PALETTE' in os.environ else None if env_kms is None: env_kms = '' env_kms_cmd = os.environ['PONYSAY_KMS_PALETTE_CMD'] if 'PONYSAY_KMS_PALETTE_CMD' in os.environ else None if (env_kms_cmd is not None) and (not env_kms_cmd == ''): env_kms = Popen(shlex.split(env_kms_cmd), stdout=PIPE, stdin=sys.stderr).communicate()[0].decode('utf8', 'replace') if env_kms[-1] == '\n': env_kms = env_kms[:-1] if env_kms == '': return pony palette = env_kms palettefile = env_kms.replace('\033]P', '') cachedir = '/var/cache/ponysay' if not os.path.isdir(cachedir): cachedir = HOME + '/.cache/ponysay' if not os.path.isdir(cachedir): os.makedirs(cachedir) newversion = False if not os.path.isfile(cachedir + '/.version'): newversion = True else: cachev = None try: cachev = open(cachedir + '/.version', 'r') if ''.join(cachev.readlines()) == KMS_VERSION: newversion = True finally: if cachev is not None: cachev.close() if newversion: for cached in os.listdir(cachedir): cached = cachedir + '/' + cached if os.path.isdir(cached) and not os.path.islink(cached): shutil.rmtree(cached, False) else: os.remove(cached) cachev = None try: cachev = open(cachedir + '/.version', 'w+') cachev.write(KMS_VERSION) finally: if cachev is not None: cachev.close() kmsponies = cachedir + '/kmsponies/' + palettefile kmspony = (kmsponies + pony).replace('//', '/') if not os.path.isfile(kmspony): protokmsponies = cachedir + '/protokmsponies/' protokmspony = (protokmsponies + pony).replace('//', '/') protokmsponydir = protokmspony[:protokmspony.rindex('/')] kmsponydir = kmspony[: kmspony.rindex('/')] _protokmspony = '\'' + protokmspony.replace('\'', '\'\\\'\'') + '\'' _kmspony = '\'' + kmspony.replace('\'', '\'\\\'\'') + '\'' _pony = '\'' + pony.replace('\'', '\'\\\'\'') + '\'' if not os.path.isfile(protokmspony): if not os.path.isdir(protokmsponydir): os.makedirs(protokmsponydir) if not os.system('ponysay2ttyponysay < ' + _pony + ' > ' + _protokmspony) == 0: sys.stderr.write('Unable to run ponysay2ttyponysay successfully, you need util-say for KMS support\n') exit(1) if not os.path.isdir(kmsponydir): os.makedirs(kmsponydir) if not os.system('tty2colourfultty -p ' + palette + ' < ' + _protokmspony + ' > ' + _kmspony) == 0: sys.stderr.write('Unable to run tty2colourfultty successfully, you need util-say for KMS support\n') exit(1) return kmspony ARGUMENTLESS = 0 ARGUMENTED = 1 VARIADIC = 2 ''' Simple argument parser ''' class ArgParser(): ''' Constructor. The short description is printed on same line as the program name ''' def __init__(self, program, description, usage, longdescription = None): self.__program = program self.__description = description self.__usage = usage self.__longdescription = longdescription self.__arguments = [] self.opts = {} self.optmap = {} ''' Add option that takes no arguments ''' def add_argumentless(self, alternatives, help = None): ARGUMENTLESS 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 ''' 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 ''' 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 ''' 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 def unrecognised(arg): sys.stderr.write('%s: warning: unrecognised option %s\n' % (self.__program, arg)) 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] == '-') or (arg[0] == '+')): if (len(arg) > 2) and ((arg[:2] == '--') or (arg[:2] == '++')): 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(arg) else: self.files.append(arg) i = 0 n = len(optqueue) while i < n: opt = optqueue[i] arg = argqueue[i] i += 1 opt = self.optmap[opt][0] if (opt not in self.opts) or (self.opts[opt] is None): self.opts[opt] = [] 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 ''' Prints a colourful help message ''' def help(self): print('\033[1m%s\033[21m %s %s' % (self.__program, '-' if linuxvt else '—', self.__description)) print() if self.__longdescription is not None: print(self.__longdescription) print() 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() print('\033[1mSYNOPSIS:\033[21m') print() 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 for opt_alt in opt_alts: if opt_alt is opt_alts[-1]: print('\t' + opt_alt, end='') if opt_type == ARGUMENTED: print(' \033[4m%s\033[24m' % (opt_arg)) elif opt_type == VARIADIC: print(' [\033[4m%s\033[24m...]' % (opt_arg)) else: print() else: print('\t\033[2m' + opt_alt + '\033[22m') first = True for line in opt_help.split('\n'): if first: first = False print('\t\t\033[32;1m%s\033[21;39m' % (line)) else: print('\t\t%s' % (line)) print() print() ''' Balloon format class ''' class Balloon(): ''' Constructor ''' 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 ''' 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) ''' Replacement for cowsay ''' class Backend(): ''' Constructor Takes message [string], ponyfile [filename string], wrapcolumn [None or an int], width [None or an int] and balloon [Balloon object] ''' def __init__(self, message, ponyfile, wrapcolumn, width, balloon): self.message = message self.ponyfile = ponyfile self.wrapcolumn = wrapcolumn self.width = width self.balloon = balloon self.link = {'\\' : self.balloon.link, '/' : self.balloon.linkmirror} self.output = '' self.pony = None ''' Process all data ''' def parse(self): self.__expandMessage() self.__loadFile() self.__processPony() self.__truncate() ''' 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 = self.__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): ponystream = None try: ponystream = open(self.ponyfile, 'r') self.pony = ''.join(ponystream.readlines()) finally: if ponystream is not None: ponystream.close() ''' 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 = self.__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 += 8 - (indent & 7) ed = ' ' * (7 - (indent & 7)) c = ' ' self.pony = self.pony[:i] + ed + self.pony[i:] i += 1 if c == '$': if dollar is not None: if '=' in dollar: name = dollar[:find('=')] value = dollar[find('=') + 1:] variables[name] = value elif (len(dollar) < 7) or not (dollar[:7] == '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:] else: (w, h) = (0, 0) props = dollar[7:] if len(props) > 0: if ',' in props: if props[0] is not ',': w = int(props[:props.index(',')]) h = int(props[props.index(',') + 1:]) else: w = int(props) balloon = self.__getballoon(w, h, indent).split('\n') balloon = [AUTO_PUSH + 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 += self.__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 = self.__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 = self.__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, '') ''' Gets colour code att the currect offset in a buffer ''' def __getcolour(self, 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 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 ''' def __len(self, input): (rc, i, n) = (0, 0, len(input)) while i < n: c = input[i] if c == '\033': i += len(self.__getcolour(input, i)) else: i += 1 if not UCS.isCombining(c): rc += 1 return rc ''' Generates a balloon with the message ''' def __getballoon(self, width, height, 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) lines = msg.split('\n') return self.balloon.get(width, height, lines, self.__len) ''' Wraps the message ''' def __wrapMessage(self, message, wrap): lines = message.split('\n') buf = '' for line in lines: b = [None] * len(line) map = {} (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': # TODO this should use self.__getcolour() b[bi] = d bi += 1 b[bi] = line[i] d = line[i] bi += 1 i += 1 if d == '[': while True: b[bi] = line[i] d = line[i] bi += 1 i += 1 if (('a' <= d) and (d <= 'z')) or (('A' <= d) and (d <= 'Z')) or (d == '~'): break elif d == ']': b[bi] = line[i] d = line[i] bi += 1 i += 1 if d == 'P': for j in range(0, 7): b[bi] = line[i] bi += 1 i += 1 elif (d is not None) and (d != ' '): 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): cols += 1 map[cols] = bi else: mm = 0 while (w > 8) and (cols > w + 3): mm += w - 1 m = map[mm] for bb in b[:m]: buf += bb buf += '-\n' cols -= w - 1 m += w -1 bi -= m bb = b[m:] for j in range(0, bi): b[j] = bb[j] w = wrap if indent != -1: buf += line[:indent] w -= indentc if cols > w: buf += '\n' w = wrap if indent != -1: buf += line[:indent] w -= indentc for bb in b[:bi]: buf += bb w -= cols cols = 0 bi = 0 if d == -1: i += 1 else: if w > 0: buf += ' ' w -= 1 else: buf += '\n' w = wrap if indent != -1: buf + line[:indent] w -= indentc buf += '\n' return '\n'.join(line.rstrip() for line in buf[:-1].split('\n')) ''' ANSI colour stack ''' class ColourStack(): ''' Constructor ''' 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 def push(self): self.stack = [[self.bufproto, None, None, [False] * 9]] + self.stack if len(self.stack) == 1: return None old = self.stack[1] rc = '\033[' if old[1] is not None: rc += '39;' if old[2] is not None: rc += '49;' for i in range(0, 9): if old[3][i]: rc += '2%i;' % (i + 1) return '' if len(rc) == 2 else (rc[:-1] + 'm') def pop(self): old = self.stack[0] self.stack = self.stack[1:] rc = '\033[' if old[1] is not None: rc += '39;' if old[2] is not None: rc += '49;' for i in range(0, 9): if old[3][i]: rc += str(i + 21) + ';' 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 '' if len(rc) == 2 else (rc[:-1] + 'm') 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 ''' UCS utility class ''' class UCS(): ''' Checks whether a 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 ''' @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 ''' @staticmethod def dispLen(string): return len(string) - UCS.countCombining(string) ''' The user's home directory ''' HOME = os.environ['HOME'] if 'HOME' in os.environ else os.path.expanduser('~') ''' Whether the program is execute in Linux VT (TTY) ''' linuxvt = ('TERM' in os.environ) and (os.environ['TERM'] == 'linux') ''' Whether the script is executed as ponythink ''' isthink = (len(__file__) >= 5) and (__file__[-5:] == 'think') isthink = ((len(__file__) >= 8) and (__file__[-8:] == 'think.py')) or isthink ''' Whether stdin is piped ''' pipelinein = not sys.stdin.isatty() ''' Whether stdout is piped ''' pipelineout = not sys.stdout.isatty() ''' Whether stderr is piped ''' pipelineerr = not sys.stderr.isatty() ''' Whether KMS is used ''' usekms = Ponysay.isUsingKMS() ''' Root share/ directories ''' sharedirs = [] _sharedirs = [HOME + '/.local/share/ponysay/', INSTALLDIR + '/share/ponysay/'] for sharedir in _sharedirs: if os.path.isdir(sharedir): sharedirs.append(sharedir) ''' The directories where pony files are stored, ttyponies/ are used if the terminal is Linux VT (also known as TTY) and not with KMS ''' ponydirs = [] if linuxvt and not usekms: _ponydirs = [d + 'ttyponies/' for d in sharedirs] else: _ponydirs = [d + 'ponies/' for d in sharedirs] for ponydir in _ponydirs: if os.path.isdir(ponydir): ponydirs.append(ponydir) ''' The directories where pony files are stored, extrattyponies/ are used if the terminal is Linux VT (also known as TTY) and not with KMS ''' extraponydirs = [] if linuxvt and not usekms: _extraponydirs = [d + 'extrattyponies/' for d in sharedirs] else: _extraponydirs = [d + 'extraponies/' for d in sharedirs] for extraponydir in _extraponydirs: if os.path.isdir(extraponydir): extraponydirs.append(extraponydir) ''' The directories where quotes files are stored ''' quotedirs = [] _quotedirs = [d + 'quotes/' for d in sharedirs] for quotedir in _quotedirs: if os.path.isdir(quotedir): quotedirs.append(quotedir) ''' The directories where balloon style files are stored ''' balloondirs = [] _balloondirs = [d + 'balloons/' for d in sharedirs] for balloondir in _balloondirs: if os.path.isdir(balloondir): balloondirs.append(balloondir) usage_saythink = '\033[34;1m(ponysay | ponythink)\033[21;39m' usage_common = '[-c] [-W \033[4mCOLUMN\033[24m] [-b \033[4mSTYLE\033[24m]' usage_listhelp = '(-l | -L | -B | +l | +L | -v | -h)' usage_file = '[(-f | -F) \033[4mPONY\033[24m]... ([--] \033[4mmessage\033[24m | <<<\033[4mmessage\033[24m)' usage_quote = '-q [\033[4mPONY\033[24m...]' usage = '%s %s\n%s %s %s\n%s %s %s' % (usage_saythink, usage_listhelp, usage_saythink, usage_common, usage_file, usage_saythink, usage_common, usage_quote) usage = usage.replace('\033[', '\0') for sym in ('[', ']', '(', ')', '|', '...'): usage = usage.replace(sym, '\033[2m' + sym + '\033[22m') usage = usage.replace('\0', '\033[') ''' Argument parsing ''' opts = ArgParser(program = 'ponythink' if isthink else 'ponysay', description = 'cowsay reimplemention for ponies', usage = usage, longdescription = '''Ponysay displays an image of a pony saying some text provided by the user. If \033[4mmessage\033[24m is not provided, it accepts standard input. For an extensive documentation run `info ponysay`, or for just a little more help than this run `man ponysay`. Ponysay has so much more to offer than described here.''') opts.add_argumentless(['--quoters']) opts.add_argumentless(['--onelist']) opts.add_argumentless(['++onelist']) opts.add_argumentless(['-h', '--help'], help = 'Print this help message.') opts.add_argumentless(['-v', '--version'], help = 'Print the version of the program.') opts.add_argumentless(['-l', '--list'], help = 'List pony names.') opts.add_argumentless(['-L', '--altlist'], help = 'List pony names with alternatives.') opts.add_argumentless(['+l', '++list'], help = 'List non-MLP:FiM pony names.') opts.add_argumentless(['+L', '++altlist'], help = 'List non-MLP:FiM pony names with alternatives.') opts.add_argumentless(['-B', '--balloonlist'], help = 'List balloon styles.') opts.add_argumentless(['-c', '--compact'], help = 'Compress messages.') opts.add_argumented( ['-W', '--wrap'], arg = 'COLUMN', help = 'Specify the column when the message should be wrapped.') opts.add_argumented( ['-b', '--bubble', '--balloon'], arg = 'STYLE', help = 'Select a balloon style.') opts.add_argumented( ['-f', '--file', '--pony'], arg = 'PONY', help = 'Select a pony.\nEither a file name or a pony name.') opts.add_argumented( ['-F', '++file', '++pony'], arg = 'PONY', help = 'Select a non-MLP:FiM pony.') opts.add_variadic( ['-q', '--quote'], arg = 'PONY', help = 'Select a ponies which will quote themself.') opts.parse() ''' Start the program from ponysay.__init__ if this is the executed file ''' if __name__ == '__main__': Ponysay(opts)